diff --git a/backend/src/index.ts b/backend/src/index.ts index 3fba19d..b824baf 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -122,6 +122,9 @@ async function runMigrations() { IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'ai_verification_enabled') THEN ALTER TABLE users ADD COLUMN ai_verification_enabled BOOLEAN DEFAULT false; END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'notifications_cleared_at') THEN + ALTER TABLE users ADD COLUMN notifications_cleared_at TIMESTAMP; + END IF; END $$; `); diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index 414f8d0..6064ffd 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -856,28 +856,41 @@ export const notificationHistoryQueries = { return result.rows; }, - // Get recent notifications (for bell dropdown) + // Get recent notifications (for bell dropdown) - respects cleared_at getRecent: async (userId: number, limit: number = 10): Promise => { const result = await pool.query( - `SELECT * FROM notification_history - WHERE user_id = $1 - ORDER BY triggered_at DESC + `SELECT nh.* FROM notification_history nh + JOIN users u ON u.id = nh.user_id + WHERE nh.user_id = $1 + AND (u.notifications_cleared_at IS NULL OR nh.triggered_at > u.notifications_cleared_at) + ORDER BY nh.triggered_at DESC LIMIT $2`, [userId, limit] ); return result.rows; }, - // Count notifications in last 24 hours (for badge) + // Count notifications since last clear (for badge) countRecent: async (userId: number, hours: number = 24): Promise => { const result = await pool.query( - `SELECT COUNT(*) FROM notification_history - WHERE user_id = $1 AND triggered_at > NOW() - INTERVAL '1 hour' * $2`, + `SELECT COUNT(*) FROM notification_history nh + JOIN users u ON u.id = nh.user_id + WHERE nh.user_id = $1 + AND nh.triggered_at > NOW() - INTERVAL '1 hour' * $2 + AND (u.notifications_cleared_at IS NULL OR nh.triggered_at > u.notifications_cleared_at)`, [userId, hours] ); return parseInt(result.rows[0].count, 10); }, + // Clear notifications (sets timestamp, doesn't delete) + clear: async (userId: number): Promise => { + await pool.query( + `UPDATE users SET notifications_cleared_at = NOW() WHERE id = $1`, + [userId] + ); + }, + // Get total count for pagination getTotalCount: async (userId: number): Promise => { const result = await pool.query( diff --git a/backend/src/routes/notifications.ts b/backend/src/routes/notifications.ts index 6bbeea8..4a4c3aa 100644 --- a/backend/src/routes/notifications.ts +++ b/backend/src/routes/notifications.ts @@ -69,4 +69,16 @@ router.get('/count', async (req: AuthRequest, res: Response) => { } }); +// Clear notifications (marks as seen, doesn't delete history) +router.post('/clear', async (req: AuthRequest, res: Response) => { + try { + const userId = req.userId!; + await notificationHistoryQueries.clear(userId); + res.json({ message: 'Notifications cleared' }); + } catch (error) { + console.error('Error clearing notifications:', error); + res.status(500).json({ error: 'Failed to clear notifications' }); + } +}); + export default router; diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 7731671..9eb1db2 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -313,6 +313,9 @@ export const notificationsApi = { api.get<{ count: number }>('/notifications/count', { params: hours ? { hours } : undefined, }), + + clear: () => + api.post<{ message: string }>('/notifications/clear'), }; // Admin API diff --git a/frontend/src/components/NotificationBell.tsx b/frontend/src/components/NotificationBell.tsx index de2c6ce..90ecf0e 100644 --- a/frontend/src/components/NotificationBell.tsx +++ b/frontend/src/components/NotificationBell.tsx @@ -99,6 +99,16 @@ export default function NotificationBell() { } }; + const handleClear = async () => { + try { + await notificationsApi.clear(); + setNotifications([]); + setRecentCount(0); + } catch (error) { + console.error('Failed to clear notifications:', error); + } + }; + return (