diff --git a/app/api/notifications/[id]/read/route.ts b/app/api/notifications/[id]/read/route.ts new file mode 100644 index 00000000..a97c3e2d --- /dev/null +++ b/app/api/notifications/[id]/read/route.ts @@ -0,0 +1,50 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import { NotificationService } from '@/lib/services/notifications/notification-service'; + +// POST /api/notifications/{id}/read +export async function POST( + request: Request, + context: { params: { id: string } } +) { + try { + // Authenticate user + const session = await getServerSession(authOptions); + if (!session || !session.user?.id) { + return NextResponse.json( + { error: "Not authenticated" }, + { status: 401 } + ); + } + + // Await params as per Next.js requirements + const params = await context.params; + const id = params?.id; + if (!id) { + return NextResponse.json( + { error: "Missing notification ID" }, + { status: 400 } + ); + } + + const userId = session.user.id; + const notificationService = NotificationService.getInstance(); + const success = await notificationService.markAsRead(userId, id); + + if (!success) { + return NextResponse.json( + { error: "Failed to mark notification as read" }, + { status: 400 } + ); + } + + return NextResponse.json({ success: true }); + } catch (error: any) { + console.error('Error marking notification as read:', error); + return NextResponse.json( + { error: "Internal server error", message: error.message }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/notifications/count/route.ts b/app/api/notifications/count/route.ts new file mode 100644 index 00000000..f769d359 --- /dev/null +++ b/app/api/notifications/count/route.ts @@ -0,0 +1,30 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import { NotificationService } from '@/lib/services/notifications/notification-service'; + +// GET /api/notifications/count +export async function GET(request: Request) { + try { + // Authenticate user + const session = await getServerSession(authOptions); + if (!session || !session.user?.id) { + return NextResponse.json( + { error: "Not authenticated" }, + { status: 401 } + ); + } + + const userId = session.user.id; + const notificationService = NotificationService.getInstance(); + const counts = await notificationService.getNotificationCount(userId); + + return NextResponse.json(counts); + } catch (error: any) { + console.error('Error in notification count API:', error); + return NextResponse.json( + { error: "Internal server error", message: error.message }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/notifications/read-all/route.ts b/app/api/notifications/read-all/route.ts new file mode 100644 index 00000000..bcee46cf --- /dev/null +++ b/app/api/notifications/read-all/route.ts @@ -0,0 +1,37 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import { NotificationService } from '@/lib/services/notifications/notification-service'; + +// POST /api/notifications/read-all +export async function POST(request: Request) { + try { + // Authenticate user + const session = await getServerSession(authOptions); + if (!session || !session.user?.id) { + return NextResponse.json( + { error: "Not authenticated" }, + { status: 401 } + ); + } + + const userId = session.user.id; + const notificationService = NotificationService.getInstance(); + const success = await notificationService.markAllAsRead(userId); + + if (!success) { + return NextResponse.json( + { error: "Failed to mark all notifications as read" }, + { status: 400 } + ); + } + + return NextResponse.json({ success: true }); + } catch (error: any) { + console.error('Error marking all notifications as read:', error); + return NextResponse.json( + { error: "Internal server error", message: error.message }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/notifications/route.ts b/app/api/notifications/route.ts new file mode 100644 index 00000000..a9b405ad --- /dev/null +++ b/app/api/notifications/route.ts @@ -0,0 +1,54 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import { NotificationService } from '@/lib/services/notifications/notification-service'; + +// GET /api/notifications?page=1&limit=20 +export async function GET(request: Request) { + try { + // Authenticate user + const session = await getServerSession(authOptions); + if (!session || !session.user?.id) { + return NextResponse.json( + { error: "Not authenticated" }, + { status: 401 } + ); + } + + const userId = session.user.id; + const { searchParams } = new URL(request.url); + const page = parseInt(searchParams.get('page') || '1', 10); + const limit = parseInt(searchParams.get('limit') || '20', 10); + + // Validate parameters + if (isNaN(page) || page < 1) { + return NextResponse.json( + { error: "Invalid page parameter" }, + { status: 400 } + ); + } + + if (isNaN(limit) || limit < 1 || limit > 100) { + return NextResponse.json( + { error: "Invalid limit parameter, must be between 1 and 100" }, + { status: 400 } + ); + } + + const notificationService = NotificationService.getInstance(); + const notifications = await notificationService.getNotifications(userId, page, limit); + + return NextResponse.json({ + notifications, + page, + limit, + total: notifications.length + }); + } catch (error: any) { + console.error('Error in notifications API:', error); + return NextResponse.json( + { error: "Internal server error", message: error.message }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/notifications/page.tsx b/app/notifications/page.tsx index 01128ab9..d6084b2b 100644 --- a/app/notifications/page.tsx +++ b/app/notifications/page.tsx @@ -1,13 +1,186 @@ +"use client"; + +import { useEffect, useState } from 'react'; +import { useNotifications } from '@/hooks/use-notifications'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Notification } from '@/lib/types/notification'; +import { Badge } from '@/components/ui/badge'; +import { formatDistanceToNow } from 'date-fns'; +import { Bell, Check, CheckCheck, ExternalLink, Trash2 } from 'lucide-react'; +import Link from 'next/link'; + +// Source icon mapping +const SourceIcons: Record = { + leantime:
L
, + nextcloud:
N
, + gitea:
G
, + dolibarr:
D
, + moodle:
M
, +}; + export default function NotificationsPage() { + const { notifications, notificationCount, loading, error, fetchNotifications, markAsRead, markAllAsRead } = useNotifications(); + const [activeTab, setActiveTab] = useState('all'); + + // Filter notifications based on active tab + const filteredNotifications = activeTab === 'all' + ? notifications + : notifications.filter(notification => notification.source === activeTab); + + // Group notifications by source for counts + const sourceCounts = Object.entries(notificationCount.sources).map(([source, count]) => ({ + source, + count: count.total, + unread: count.unread + })); + + const handleMarkAsRead = async (notification: Notification) => { + if (!notification.isRead) { + await markAsRead(notification.id); + } + }; + + const handleMarkAllAsRead = async () => { + await markAllAsRead(); + }; + return ( -
-