9.3 KiB
9.3 KiB
TASK-FE-009: Dashboard & Notifications UI
ID: TASK-FE-009 Title: Dashboard, Notifications & Activity Feed UI Category: Supporting Features Priority: P3 (Low) Effort: 3-4 days Dependencies: TASK-FE-003, TASK-BE-011 Assigned To: Frontend Developer
📋 Overview
Build dashboard homepage with statistics widgets, recent activity, pending approvals, and real-time notifications system.
🎯 Objectives
- Create dashboard homepage with widgets
- Implement statistics cards (documents, pending approvals)
- Build recent activity feed
- Create notifications dropdown
- Add pending tasks section
- Implement real-time updates (optional)
✅ Acceptance Criteria
- Dashboard displays key statistics
- Recent activity feed working
- Notifications dropdown functional
- Pending tasks visible
- Charts/graphs display data
- Real-time updates (if WebSocket implemented)
🔧 Implementation Steps
Step 1: Dashboard Page
// File: src/app/(dashboard)/page.tsx
import { StatsCards } from '@/components/dashboard/stats-cards';
import { RecentActivity } from '@/components/dashboard/recent-activity';
import { PendingTasks } from '@/components/dashboard/pending-tasks';
import { QuickActions } from '@/components/dashboard/quick-actions';
export default async function DashboardPage() {
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold">Dashboard</h1>
<p className="text-gray-600 mt-1">
Welcome back! Here's what's happening.
</p>
</div>
<QuickActions />
<StatsCards />
<div className="grid grid-cols-3 gap-6">
<div className="col-span-2">
<RecentActivity />
</div>
<div className="col-span-1">
<PendingTasks />
</div>
</div>
</div>
);
}
Step 2: Statistics Cards
// File: src/components/dashboard/stats-cards.tsx
import { Card } from '@/components/ui/card';
import { FileText, Clipboard, CheckCircle, Clock } from 'lucide-react';
export async function StatsCards() {
const stats = await getStats(); // Fetch from API
const cards = [
{
title: 'Total Correspondences',
value: stats.correspondences,
icon: FileText,
color: 'text-blue-600',
bgColor: 'bg-blue-50',
},
{
title: 'Active RFAs',
value: stats.rfas,
icon: Clipboard,
color: 'text-purple-600',
bgColor: 'bg-purple-50',
},
{
title: 'Approved Documents',
value: stats.approved,
icon: CheckCircle,
color: 'text-green-600',
bgColor: 'bg-green-50',
},
{
title: 'Pending Approvals',
value: stats.pending,
icon: Clock,
color: 'text-orange-600',
bgColor: 'bg-orange-50',
},
];
return (
<div className="grid grid-cols-4 gap-6">
{cards.map((card) => {
const Icon = card.icon;
return (
<Card key={card.title} className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">{card.title}</p>
<p className="text-3xl font-bold mt-2">{card.value}</p>
</div>
<div className={`p-3 rounded-lg ${card.bgColor}`}>
<Icon className={`h-6 w-6 ${card.color}`} />
</div>
</div>
</Card>
);
})}
</div>
);
}
Step 3: Recent Activity Feed
// File: src/components/dashboard/recent-activity.tsx
import { Card } from '@/components/ui/card';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Badge } from '@/components/ui/badge';
import { formatDistanceToNow } from 'date-fns';
export async function RecentActivity() {
const activities = await getRecentActivities();
return (
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4">Recent Activity</h3>
<div className="space-y-4">
{activities.map((activity) => (
<div
key={activity.id}
className="flex gap-3 pb-4 border-b last:border-0"
>
<Avatar className="h-10 w-10">
<AvatarFallback>{activity.user.initials}</AvatarFallback>
</Avatar>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="font-medium">{activity.user.name}</span>
<Badge variant="outline" className="text-xs">
{activity.action}
</Badge>
</div>
<p className="text-sm text-gray-600">{activity.description}</p>
<p className="text-xs text-gray-500 mt-1">
{formatDistanceToNow(new Date(activity.createdAt), {
addSuffix: true,
})}
</p>
</div>
</div>
))}
</div>
</Card>
);
}
Step 4: Notifications Dropdown
// File: src/components/layout/notifications-dropdown.tsx
'use client';
import { useState, useEffect } from 'react';
import { Bell } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Badge } from '@/components/ui/badge';
import { notificationApi } from '@/lib/api/notifications';
export function NotificationsDropdown() {
const [notifications, setNotifications] = useState([]);
const [unreadCount, setUnreadCount] = useState(0);
useEffect(() => {
// Fetch notifications
notificationApi.getUnread().then((data) => {
setNotifications(data.items);
setUnreadCount(data.unreadCount);
});
}, []);
const markAsRead = async (id: number) => {
await notificationApi.markAsRead(id);
setNotifications((prev) => prev.filter((n) => n.notification_id !== id));
setUnreadCount((prev) => prev - 1);
};
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="relative">
<Bell className="h-5 w-5" />
{unreadCount > 0 && (
<Badge
variant="destructive"
className="absolute -top-1 -right-1 h-5 w-5 flex items-center justify-center p-0 text-xs"
>
{unreadCount}
</Badge>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-80">
<DropdownMenuLabel>Notifications</DropdownMenuLabel>
<DropdownMenuSeparator />
{notifications.length === 0 ? (
<div className="p-4 text-center text-sm text-gray-500">
No new notifications
</div>
) : (
<div className="max-h-96 overflow-y-auto">
{notifications.map((notification) => (
<DropdownMenuItem
key={notification.notification_id}
className="flex flex-col items-start p-3 cursor-pointer"
onClick={() => markAsRead(notification.notification_id)}
>
<div className="font-medium text-sm">{notification.title}</div>
<div className="text-xs text-gray-600 mt-1">
{notification.message}
</div>
<div className="text-xs text-gray-400 mt-1">
{formatDistanceToNow(new Date(notification.created_at), {
addSuffix: true,
})}
</div>
</DropdownMenuItem>
))}
</div>
)}
<DropdownMenuSeparator />
<DropdownMenuItem className="text-center justify-center">
View All
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
Step 5: Pending Tasks Widget
// File: src/components/dashboard/pending-tasks.tsx
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import Link from 'next/link';
export async function PendingTasks() {
const tasks = await getPendingTasks();
return (
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4">Pending Tasks</h3>
<div className="space-y-3">
{tasks.map((task) => (
<Link
key={task.id}
href={task.url}
className="block p-3 bg-gray-50 rounded hover:bg-gray-100 transition-colors"
>
<div className="flex items-start justify-between mb-1">
<span className="text-sm font-medium">{task.title}</span>
<Badge variant="warning" className="text-xs">
{task.daysOverdue > 0 ? `${task.daysOverdue}d overdue` : 'Due'}
</Badge>
</div>
<p className="text-xs text-gray-600">{task.description}</p>
</Link>
))}
</div>
</Card>
);
}
📦 Deliverables
- Dashboard page with widgets
- Statistics cards
- Recent activity feed
- Notifications dropdown
- Pending tasks section
- Quick actions buttons
🔗 Related Documents
Created: 2025-12-01 Status: Ready