Files
lcbp3/specs/06-tasks/TASK-FE-009-dashboard-notifications.md
admin 047e1b88ce
Some checks failed
Spec Validation / validate-markdown (push) Has been cancelled
Spec Validation / validate-diagrams (push) Has been cancelled
Spec Validation / check-todos (push) Has been cancelled
Main: revise specs to 1.5.0 (completed)
2025-12-01 01:28:32 +07:00

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

  1. Create dashboard homepage with widgets
  2. Implement statistics cards (documents, pending approvals)
  3. Build recent activity feed
  4. Create notifications dropdown
  5. Add pending tasks section
  6. 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


Created: 2025-12-01 Status: Ready