diff --git a/routes/__pycache__/auth.cpython-313.pyc b/routes/__pycache__/auth.cpython-313.pyc index c44381b..608081d 100644 Binary files a/routes/__pycache__/auth.cpython-313.pyc and b/routes/__pycache__/auth.cpython-313.pyc differ diff --git a/routes/__pycache__/contacts.cpython-313.pyc b/routes/__pycache__/contacts.cpython-313.pyc index 07b2291..e9f119d 100644 Binary files a/routes/__pycache__/contacts.cpython-313.pyc and b/routes/__pycache__/contacts.cpython-313.pyc differ diff --git a/routes/__pycache__/conversations.cpython-313.pyc b/routes/__pycache__/conversations.cpython-313.pyc index 5f57099..2950c62 100644 Binary files a/routes/__pycache__/conversations.cpython-313.pyc and b/routes/__pycache__/conversations.cpython-313.pyc differ diff --git a/routes/__pycache__/main.cpython-313.pyc b/routes/__pycache__/main.cpython-313.pyc index 1bbc838..9d5e64f 100644 Binary files a/routes/__pycache__/main.cpython-313.pyc and b/routes/__pycache__/main.cpython-313.pyc differ diff --git a/routes/__pycache__/room_members.cpython-313.pyc b/routes/__pycache__/room_members.cpython-313.pyc index 84896b2..70dda33 100644 Binary files a/routes/__pycache__/room_members.cpython-313.pyc and b/routes/__pycache__/room_members.cpython-313.pyc differ diff --git a/routes/auth.py b/routes/auth.py index 37170ed..940b815 100644 --- a/routes/auth.py +++ b/routes/auth.py @@ -3,7 +3,7 @@ from flask_login import login_user, logout_user, login_required, current_user from models import db, User from functools import wraps from datetime import datetime -from utils import log_event +from utils import log_event, create_notification def require_password_change(f): @wraps(f) @@ -94,6 +94,18 @@ def init_routes(auth_bp): db.session.add(new_user) db.session.commit() + # Create notification for the new user + create_notification( + notif_type='account_created', + user_id=new_user.id, + details={ + 'message': 'Welcome to DocuPulse! Your account has been created successfully.', + 'username': new_user.username, + 'email': new_user.email, + 'timestamp': datetime.utcnow().isoformat() + } + ) + # Log successful registration log_event( event_type='user_create', @@ -169,6 +181,16 @@ def init_routes(auth_bp): current_user.set_password(new_password) + # Create password change notification + create_notification( + notif_type='password_changed', + user_id=current_user.id, + details={ + 'message': 'Your password has been changed successfully.', + 'timestamp': datetime.utcnow().isoformat() + } + ) + # Log successful password change log_event( event_type='user_update', diff --git a/routes/contacts.py b/routes/contacts.py index 3b18bcc..1c8c7c1 100644 --- a/routes/contacts.py +++ b/routes/contacts.py @@ -5,10 +5,11 @@ from forms import UserForm from flask import abort from sqlalchemy import or_ from routes.auth import require_password_change -from utils import log_event +from utils import log_event, create_notification import json import os from werkzeug.utils import secure_filename +from datetime import datetime contacts_bp = Blueprint('contacts', __name__, url_prefix='/contacts') @@ -122,6 +123,20 @@ def new_contact(): db.session.add(user) db.session.commit() + # Create notification for the new user + create_notification( + notif_type='account_created', + user_id=user.id, + sender_id=current_user.id, # Admin who created the account + details={ + 'message': 'Your DocuPulse account has been created by an administrator.', + 'username': user.username, + 'email': user.email, + 'created_by': f"{current_user.username} {current_user.last_name}", + 'timestamp': datetime.utcnow().isoformat() + } + ) + # Log user creation event log_event( event_type='user_create', @@ -304,6 +319,26 @@ def edit_contact(id): db.session.commit() + # Create notification for the user being updated + create_notification( + notif_type='account_updated', + user_id=user.id, + sender_id=current_user.id, + details={ + 'message': 'Your account has been updated by an administrator.', + 'updated_by': f"{current_user.username} {current_user.last_name}", + 'changes': { + 'name': f"{user.username} {user.last_name}", + 'email': user.email, + 'phone': user.phone, + 'company': user.company, + 'position': user.position, + 'password_changed': password_changed + }, + 'timestamp': datetime.utcnow().isoformat() + } + ) + # Log user update event log_event( event_type='user_update', @@ -342,6 +377,18 @@ def delete_contact(id): flash('You cannot delete your own account.', 'error') return redirect(url_for('contacts.contacts_list')) + # Create notification for the user being deleted + create_notification( + notif_type='account_deleted', + user_id=user.id, + sender_id=current_user.id, + details={ + 'message': 'Your account has been deleted by an administrator.', + 'deleted_by': f"{current_user.username} {current_user.last_name}", + 'timestamp': datetime.utcnow().isoformat() + } + ) + # Log user deletion event log_event( event_type='user_delete', diff --git a/routes/conversations.py b/routes/conversations.py index 4f6acea..1070e23 100644 --- a/routes/conversations.py +++ b/routes/conversations.py @@ -3,7 +3,7 @@ from flask_login import login_required, current_user from models import db, Conversation, User, Message, MessageAttachment from forms import ConversationForm from routes.auth import require_password_change -from utils import log_event +from utils import log_event, create_notification import os from werkzeug.utils import secure_filename from datetime import datetime @@ -177,6 +177,20 @@ def add_member(conversation_id): conversation.members.append(user) db.session.commit() + # Create notification for the invited user + create_notification( + notif_type='conversation_invite', + user_id=user.id, + sender_id=current_user.id, + details={ + 'message': f'You have been invited to join conversation "{conversation.name}"', + 'conversation_id': conversation.id, + 'conversation_name': conversation.name, + 'invited_by': f"{current_user.username} {current_user.last_name}", + 'timestamp': datetime.utcnow().isoformat() + } + ) + # Log member addition log_event( event_type='conversation_member_add', @@ -186,7 +200,7 @@ def add_member(conversation_id): 'added_by': current_user.id, 'added_by_name': f"{current_user.username} {current_user.last_name}", 'added_user_id': user.id, - 'added_by_name': f"{current_user.username} {current_user.last_name}", + 'added_user_name': f"{user.username} {user.last_name}", 'added_user_email': user.email } ) @@ -215,6 +229,20 @@ def remove_member(conversation_id, user_id): conversation.members.remove(user) db.session.commit() + # Create notification for the removed user + create_notification( + notif_type='conversation_invite_removed', + user_id=user.id, + sender_id=current_user.id, + details={ + 'message': f'You have been removed from conversation "{conversation.name}"', + 'conversation_id': conversation.id, + 'conversation_name': conversation.name, + 'removed_by': f"{current_user.username} {current_user.last_name}", + 'timestamp': datetime.utcnow().isoformat() + } + ) + # Log member removal log_event( event_type='conversation_member_remove', @@ -437,6 +465,25 @@ def send_message(conversation_id): db.session.commit() + # Create notifications for all conversation members except the sender + for member in conversation.members: + if member.id != current_user.id: + create_notification( + notif_type='conversation_message', + user_id=member.id, + sender_id=current_user.id, + details={ + 'message': f'New message in conversation "{conversation.name}"', + 'conversation_id': conversation.id, + 'conversation_name': conversation.name, + 'sender': f"{current_user.username} {current_user.last_name}", + 'message_preview': message_content[:100] + ('...' if len(message_content) > 100 else ''), + 'has_attachments': len(attachments) > 0, + 'attachment_count': len(attachments), + 'timestamp': datetime.utcnow().isoformat() + } + ) + # Log message creation log_event( event_type='message_create', diff --git a/routes/main.py b/routes/main.py index 69d6d55..c945934 100644 --- a/routes/main.py +++ b/routes/main.py @@ -10,7 +10,7 @@ import logging import sys import time from forms import CompanySettingsForm -from utils import log_event +from utils import log_event, create_notification # Set up logging to show in console logging.basicConfig( @@ -55,7 +55,7 @@ def init_routes(main_bp): Event.user_id == current_user.id, # User's own actions db.and_( Event.event_type.in_(['conversation_create', 'message_create']), # Conversation-related events - Event.details['conversation_id'].astext.in_( + Event.details['conversation_id'].cast(db.Integer).in_( db.session.query(Conversation.id) .join(Conversation.members) .filter(User.id == current_user.id) @@ -370,6 +370,17 @@ def init_routes(main_bp): flash('Passwords do not match.', 'error') return render_template('profile/profile.html') current_user.set_password(new_password) + + # Create password change notification + create_notification( + notif_type='password_changed', + user_id=current_user.id, + details={ + 'message': 'Your password has been changed successfully.', + 'timestamp': datetime.utcnow().isoformat() + } + ) + flash('Password updated successfully.', 'success') elif confirm_password: flash('Please enter a new password.', 'error') @@ -522,7 +533,8 @@ def init_routes(main_bp): 'details': notif.details, 'sender': { 'id': notif.sender.id, - 'username': notif.sender.username + 'username': notif.sender.username, + 'last_name': notif.sender.last_name } if notif.sender else None } for notif in notifications.items], 'total_pages': total_pages, diff --git a/routes/room_members.py b/routes/room_members.py index 91eec30..7a10312 100644 --- a/routes/room_members.py +++ b/routes/room_members.py @@ -1,7 +1,8 @@ from flask import Blueprint, jsonify, request, abort from flask_login import login_required, current_user from models import db, Room, User, RoomMemberPermission -from utils import user_has_permission, log_event +from utils import user_has_permission, log_event, create_notification +from datetime import datetime room_members_bp = Blueprint('room_members', __name__) @@ -69,6 +70,21 @@ def add_room_member(room_id): permission.can_share = permissions.get('can_share', False) db.session.commit() + + # Create notification for the invited user + create_notification( + notif_type='room_invite', + user_id=user_id, + sender_id=current_user.id, + details={ + 'message': f'You have been invited to join room "{room.name}"', + 'room_id': room_id, + 'room_name': room.name, + 'invited_by': f"{current_user.username} {current_user.last_name}", + 'permissions': permissions, + 'timestamp': datetime.utcnow().isoformat() + } + ) log_event( event_type='room_member_add', @@ -77,8 +93,7 @@ def add_room_member(room_id): 'room_name': room.name, 'added_user_id': user_id, 'added_user_name': f"{user.username} {user.last_name}", - 'added_by': f"{current_user.username} {current_user.last_name}", - 'permissions': permissions + 'added_by': f"{current_user.username} {current_user.last_name}" }, user_id=current_user.id ) @@ -104,6 +119,20 @@ def remove_room_member(room_id, user_id): db.session.delete(permission) db.session.commit() + + # Create notification for the removed user + create_notification( + notif_type='room_invite_removed', + user_id=user_id, + sender_id=current_user.id, + details={ + 'message': f'You have been removed from room "{room.name}"', + 'room_id': room_id, + 'room_name': room.name, + 'removed_by': f"{current_user.username} {current_user.last_name}", + 'timestamp': datetime.utcnow().isoformat() + } + ) log_event( event_type='room_member_remove', diff --git a/static/js/notifications.js b/static/js/notifications.js index 1b65785..933eeee 100644 --- a/static/js/notifications.js +++ b/static/js/notifications.js @@ -21,6 +21,9 @@ document.addEventListener('DOMContentLoaded', function() { const notifDetailsModal = document.getElementById('notifDetailsModal'); const notifDetailsContent = document.getElementById('notifDetailsContent'); + // Get CSRF token from meta tag + const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); + // Function to update URL with current filters function updateURL() { const params = new URLSearchParams(window.location.search); @@ -31,7 +34,7 @@ document.addEventListener('DOMContentLoaded', function() { } // Function to fetch notifications - function fetchNotifications() { + async function fetchNotifications() { if (isFetching) return; isFetching = true; @@ -45,54 +48,26 @@ document.addEventListener('DOMContentLoaded', function() { ajax: 'true' }); - fetch(`${window.location.pathname}?${params.toString()}`, { - headers: { - 'X-Requested-With': 'XMLHttpRequest' - } - }) - .then(response => { + try { + const response = await fetch(`${window.location.pathname}?${params.toString()}`, { + headers: { + 'X-Requested-With': 'XMLHttpRequest' + } + }); + if (!response.ok) { throw new Error('Network response was not ok'); } - return response.text(); - }) - .then(html => { - const parser = new DOMParser(); - const doc = parser.parseFromString(html, 'text/html'); - const newTableBody = doc.getElementById('notifsTableBody'); - if (newTableBody) { - notifsTableBody.innerHTML = newTableBody.innerHTML; - - // Update pagination - const newCurrentPage = parseInt(doc.getElementById('currentPage').textContent); - const newTotalPages = parseInt(doc.getElementById('totalPages').textContent); - currentPage = newCurrentPage; - totalPages = newTotalPages; - currentPageSpan.textContent = currentPage; - totalPagesSpan.textContent = totalPages; - - // Update pagination buttons - prevPageBtn.disabled = currentPage === 1; - nextPageBtn.disabled = currentPage === totalPages; - - // Update URL - updateURL(); - - // Reattach event listeners - attachEventListeners(); - } else { - console.error('Could not find notifications table in response'); - notifsTableBody.innerHTML = 'Error loading notifications'; - } - }) - .catch(error => { + const data = await response.json(); + updateNotificationsTable(data.notifications); + updatePagination(data.total_pages, data.current_page); + } catch (error) { console.error('Error fetching notifications:', error); notifsTableBody.innerHTML = 'Error loading notifications'; - }) - .finally(() => { + } finally { isFetching = false; - }); + } } // Function to get notification type badge @@ -130,80 +105,123 @@ document.addEventListener('DOMContentLoaded', function() { } // Function to mark notification as read - function markAsRead(notifId) { - fetch(`/api/notifications/${notifId}/read`, { - method: 'POST', - headers: { - 'X-Requested-With': 'XMLHttpRequest', - 'Content-Type': 'application/json' - } - }) - .then(response => { + async function markAsRead(notifId) { + try { + const response = await fetch(`/api/notifications/${notifId}/read`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken, + 'X-Requested-With': 'XMLHttpRequest' + } + }); + if (!response.ok) { throw new Error('Network response was not ok'); } - return response.json(); - }) - .then(data => { + + const data = await response.json(); if (data.success) { - fetchNotifications(); + // Update the UI to show the notification as read + const notifRow = document.querySelector(`tr[data-notif-id="${notifId}"]`); + if (notifRow) { + notifRow.classList.remove('table-warning'); + const statusCell = notifRow.querySelector('td:nth-last-child(2)'); + if (statusCell) { + statusCell.innerHTML = 'Read'; + } + const actionsCell = notifRow.querySelector('td:last-child'); + if (actionsCell) { + const markReadBtn = actionsCell.querySelector('.mark-read'); + if (markReadBtn) { + markReadBtn.remove(); + } + } + } } - }) - .catch(error => { + } catch (error) { console.error('Error marking notification as read:', error); - }); + } } // Function to delete notification - function deleteNotification(notifId) { - if (!confirm('Are you sure you want to delete this notification?')) { - return; - } - - fetch(`/api/notifications/${notifId}`, { - method: 'DELETE', - headers: { - 'X-Requested-With': 'XMLHttpRequest' - } - }) - .then(response => { + async function deleteNotification(notifId) { + try { + const response = await fetch(`/api/notifications/${notifId}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken, + 'X-Requested-With': 'XMLHttpRequest' + } + }); + if (!response.ok) { throw new Error('Network response was not ok'); } - return response.json(); - }) - .then(data => { + + const data = await response.json(); if (data.success) { - fetchNotifications(); + // Remove the notification row from the table + const notifRow = document.querySelector(`tr[data-notif-id="${notifId}"]`); + if (notifRow) { + notifRow.remove(); + } } - }) - .catch(error => { + } catch (error) { console.error('Error deleting notification:', error); - }); + } } // Function to mark all notifications as read - function markAllAsRead() { - fetch('/api/notifications/mark-all-read', { - method: 'POST', - headers: { - 'X-Requested-With': 'XMLHttpRequest' - } - }) - .then(response => { + async function markAllAsRead() { + try { + const response = await fetch('/api/notifications/mark-all-read', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken, + 'X-Requested-With': 'XMLHttpRequest' + } + }); + if (!response.ok) { throw new Error('Network response was not ok'); } - return response.json(); - }) - .then(data => { + + const data = await response.json(); if (data.success) { - fetchNotifications(); + // Update all notifications to show as read + document.querySelectorAll('tr[data-notif-id]').forEach(row => { + row.classList.remove('table-warning'); + const statusCell = row.querySelector('td:nth-last-child(2)'); + if (statusCell) { + statusCell.innerHTML = 'Read'; + } + const actionsCell = row.querySelector('td:last-child'); + if (actionsCell) { + const markReadBtn = actionsCell.querySelector('.mark-read'); + if (markReadBtn) { + markReadBtn.remove(); + } + } + }); } - }) - .catch(error => { + } catch (error) { console.error('Error marking all notifications as read:', error); - }); + } + } + + // Function to handle notification action clicks + function handleNotificationAction(notifId, actionType) { + // Mark the notification as read when an action is taken + markAsRead(notifId); + + // Additional handling for specific action types can be added here + if (actionType === 'view_room' || actionType === 'view_conversation') { + // The link will handle the navigation automatically + return true; + } } // Function to attach event listeners @@ -231,6 +249,15 @@ document.addEventListener('DOMContentLoaded', function() { loadNotifDetails(notifId); }); }); + + // Action buttons (View Room, View Conversation) + document.querySelectorAll('.btn-group a').forEach(btn => { + btn.addEventListener('click', (e) => { + const notifId = e.target.closest('tr').dataset.notifId; + const actionType = btn.classList.contains('fa-door-open') ? 'view_room' : 'view_conversation'; + handleNotificationAction(notifId, actionType); + }); + }); } // Add event listeners for filters with debounce diff --git a/templates/notifications/notifications.html b/templates/notifications/notifications.html index f2a3018..031ecf8 100644 --- a/templates/notifications/notifications.html +++ b/templates/notifications/notifications.html @@ -58,7 +58,7 @@ {% if notifications %} {% for notif in notifications %} - + {{ notif.timestamp.strftime('%Y-%m-%d %H:%M:%S') }} {% if notif.notif_type == 'account_created' %} @@ -83,7 +83,7 @@ {{ notif.notif_type }} {% endif %} - {{ notif.sender.username if notif.sender else 'System' }} + {{ notif.sender.username + ' ' + notif.sender.last_name if notif.sender else 'System' }} - {% endif %} - +
+ {% if notif.notif_type in ['room_invite', 'room_invite_removed'] and notif.details and notif.details.room_id %} + + View Room + + {% elif notif.notif_type in ['conversation_invite', 'conversation_invite_removed', 'conversation_message'] and notif.details and notif.details.conversation_id %} + + View Conversation + + {% endif %} + {% if not notif.read %} + + {% endif %} + +
{% endfor %}