From 21d5526a8e3b31012348f0528ffd5973b388142f Mon Sep 17 00:00:00 2001 From: Aditya Kadam <2025.akadam@isu.ac.in> Date: Fri, 1 May 2026 22:29:39 +0530 Subject: [PATCH] feat: build Payroll Overview page UI (Closes #443) --- .../payroll/components/PayrollOverview.tsx | 581 ++++++++++++++++++ src/app/(dashboard)/payroll/page.tsx | 219 +------ 2 files changed, 583 insertions(+), 217 deletions(-) create mode 100644 src/app/(dashboard)/payroll/components/PayrollOverview.tsx diff --git a/src/app/(dashboard)/payroll/components/PayrollOverview.tsx b/src/app/(dashboard)/payroll/components/PayrollOverview.tsx new file mode 100644 index 00000000..62de500e --- /dev/null +++ b/src/app/(dashboard)/payroll/components/PayrollOverview.tsx @@ -0,0 +1,581 @@ +"use client"; + +import React, { useState } from "react"; +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts"; +import { AlertCircle, Calendar, Users, DollarSign, ChevronLeft, ChevronRight, ExternalLink } from "lucide-react"; + +// Mock data for stat cards +const statsData = { + totalMonthlyPayout: "₦2,450,000", + totalEmployees: 24, + nextPayoutDate: "May 15, 2026", +}; + +// Mock data for payout overview chart +const payoutChartData = [ + { month: "Jan", payout: 1800000 }, + { month: "Feb", payout: 2100000 }, + { month: "Mar", payout: 1950000 }, + { month: "Apr", payout: 2300000 }, + { month: "May", payout: 2450000 }, + { month: "Jun", payout: 2200000 }, +]; + +// Mock data for payout schedule table +const payoutScheduleData = [ + { + id: "1", + name: "Aditya Kadam", + role: "Software Engineer", + contractType: "Full-time", + frequency: "Monthly", + amount: "₦450,000", + paidIn: "USDT", + nextPayout: "May 15, 2026", + avatar: null, + }, + { + id: "2", + name: "Sarah Chen", + role: "Product Designer", + contractType: "Full-time", + frequency: "Monthly", + amount: "₦380,000", + paidIn: "NGN", + nextPayout: "May 15, 2026", + avatar: null, + }, + { + id: "3", + name: "Michael Okon", + role: "DevOps Engineer", + contractType: "Contract", + frequency: "Bi-weekly", + amount: "₦280,000", + paidIn: "USDT", + nextPayout: "May 8, 2026", + avatar: null, + }, + { + id: "4", + name: "Fatima Ibrahim", + role: "Frontend Developer", + contractType: "Full-time", + frequency: "Monthly", + amount: "₦350,000", + paidIn: "NGN", + nextPayout: "May 15, 2026", + avatar: null, + }, + { + id: "5", + name: "David Paul", + role: "Backend Developer", + contractType: "Full-time", + frequency: "Monthly", + amount: "₦400,000", + paidIn: "USDT", + nextPayout: "May 15, 2026", + avatar: null, + }, + { + id: "6", + name: "Grace Emen", + role: "QA Engineer", + contractType: "Part-time", + frequency: "Monthly", + amount: "₦180,000", + paidIn: "NGN", + nextPayout: "May 15, 2026", + avatar: null, + }, + { + id: "7", + name: "James Wilson", + role: "Tech Lead", + contractType: "Full-time", + frequency: "Monthly", + amount: "₦550,000", + paidIn: "USDT", + nextPayout: "May 15, 2026", + avatar: null, + }, + { + id: "8", + name: "Amina Yusuf", + role: "UI Designer", + contractType: "Contract", + frequency: "Monthly", + amount: "₦320,000", + paidIn: "NGN", + nextPayout: "May 15, 2026", + avatar: null, + }, +]; + +const ITEMS_PER_PAGE_OPTIONS = [5, 10, 20]; + +function getInitials(name: string): string { + const parts = name.split(" "); + return parts.map((p) => p[0]).join("").toUpperCase().slice(0, 2); +} + +function getTokenColor(token: string): string { + if (token === "USDT") return "bg-green-100 text-green-700 border-green-200"; + if (token === "NGN") return "bg-blue-100 text-blue-700 border-blue-200"; + return "bg-gray-100 text-gray-700 border-gray-200"; +} + +function getContractTypeBadge(type: string): string { + switch (type) { + case "Full-time": + return "bg-purple-100 text-purple-700 border-purple-200"; + case "Part-time": + return "bg-blue-100 text-blue-700 border-blue-200"; + case "Contract": + return "bg-orange-100 text-orange-700 border-orange-200"; + default: + return "bg-gray-100 text-gray-700 border-gray-200"; + } +} + +// Stat Card Component +interface StatCardProps { + icon: React.ReactNode; + label: string; + value: string | number; + iconBg: string; +} + +function StatCard({ icon, label, value, iconBg }: StatCardProps) { + return ( +
+
+
+ {icon} +
+
+

{label}

+

{value}

+
+
+
+ + This year + +
+
+ ); +} + +// Urgent Action Banner Component +interface UrgentActionBannerProps { + visible: boolean; +} + +function UrgentActionBanner({ visible }: UrgentActionBannerProps) { + if (!visible) return null; + + return ( +
+
+
+ +
+
+

Urgent: Pending Payroll Action Required

+

+ You have 3 payroll items that require immediate attention. Please review and process them to avoid delays. +

+ +
+
+
+ ); +} + +// Payout Schedule Table Component +interface PayoutScheduleTableProps { + data: typeof payoutScheduleData; +} + +function PayoutScheduleTable({ data }: PayoutScheduleTableProps) { + const [currentPage, setCurrentPage] = useState(1); + const [itemsPerPage, setItemsPerPage] = useState(5); + const [selectedIds, setSelectedIds] = useState>(new Set()); + + const totalPages = Math.ceil(data.length / itemsPerPage); + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const currentPageData = data.slice(startIndex, endIndex); + + const allSelected = currentPageData.length > 0 && currentPageData.every((item) => selectedIds.has(item.id)); + + const toggleAll = () => { + if (allSelected) { + setSelectedIds((prev) => { + const next = new Set(prev); + currentPageData.forEach((item) => next.delete(item.id)); + return next; + }); + } else { + setSelectedIds((prev) => { + const next = new Set(prev); + currentPageData.forEach((item) => next.add(item.id)); + return next; + }); + } + }; + + const toggleItem = (id: string) => { + setSelectedIds((prev) => { + const next = new Set(prev); + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + return next; + }); + }; + + return ( +
+ {/* Table Header */} +
+
+ +
+ Employee + Contract Type + Frequency + Amount + Paid In + Next Payout Date +
+ + {/* Table Body */} +
+ {currentPageData.map((item) => ( +
+ {/* Checkbox - Desktop */} +
e.stopPropagation()}> + toggleItem(item.id)} + className="w-4 h-4 rounded border-gray-300 text-[#5E2A8C] focus:ring-[#5E2A8C] cursor-pointer" + /> +
+ + {/* Employee */} +
+
e.stopPropagation()}> + toggleItem(item.id)} + className="w-4 h-4 rounded border-gray-300 text-[#5E2A8C] focus:ring-[#5E2A8C] cursor-pointer" + /> +
+ {item.avatar ? ( + {item.name} + ) : ( +
+ {getInitials(item.name)} +
+ )} +
+

{item.name}

+

{item.role}

+
+
+ + {/* Contract Type */} +
+ + {item.contractType} + +
+ + {/* Frequency */} +
+ {item.frequency} +
+ + {/* Amount */} +
+ {item.amount} +
+ + {/* Paid In */} +
+ + {item.paidIn === "USDT" ? "USDT" : "₦"} {item.paidIn} + +
+ + {/* Next Payout Date */} +
+ {item.nextPayout} +
+
+ ))} +
+ + {/* Pagination */} +
+
+ Results per page: + +
+ +
+ + +
+ {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => ( + + ))} +
+ + +
+
+
+ ); +} + +// Mobile Card View Component +interface MobilePayoutCardsProps { + data: typeof payoutScheduleData; +} + +function MobilePayoutCards({ data }: MobilePayoutCardsProps) { + const [currentPage, setCurrentPage] = useState(1); + const [itemsPerPage, setItemsPerPage] = useState(5); + + const totalPages = Math.ceil(data.length / itemsPerPage); + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const currentPageData = data.slice(startIndex, endIndex); + + return ( +
+ {currentPageData.map((item) => ( +
+
+
+ {item.avatar ? ( + {item.name} + ) : ( +
+ {getInitials(item.name)} +
+ )} +
+

{item.name}

+ + {item.contractType} + +
+
+
+

{item.amount}

+ + {item.paidIn} + +
+
+
+
+

Frequency

+

{item.frequency}

+
+
+

Next Payout

+

{item.nextPayout}

+
+
+
+ ))} + + {/* Mobile Pagination */} +
+ + + Page {currentPage} of {totalPages} + + +
+
+ ); +} + +// Main Payroll Overview Component +export default function PayrollOverview() { + const [showPendingAction] = useState(true); // Set to false to hide banner + + return ( +
+ {/* Stat Cards */} +
+ } + label="Total Monthly Payout" + value={statsData.totalMonthlyPayout} + iconBg="bg-purple-100" + /> + } + label="Total Employees" + value={statsData.totalEmployees} + iconBg="bg-blue-100" + /> + } + label="Next Payout Date" + value={statsData.nextPayoutDate} + iconBg="bg-green-100" + /> +
+ + {/* Urgent Action Banner */} + + + {/* Payout Overview Chart */} +
+

Payout Overview

+
+ + + + + `₦${(value / 1000000).toFixed(1)}M`} + /> + { + if (typeof value === "number") { + return [`₦${value.toLocaleString()}`, "Payout"]; + } + return [String(value), "Payout"]; + }} + /> + + + +
+
+ + {/* Payout Schedule */} +
+

Payout Schedule

+ {/* Desktop Table */} +
+ +
+ {/* Mobile Cards */} +
+ +
+
+
+ ); +} diff --git a/src/app/(dashboard)/payroll/page.tsx b/src/app/(dashboard)/payroll/page.tsx index a91f91c1..8d1cd071 100644 --- a/src/app/(dashboard)/payroll/page.tsx +++ b/src/app/(dashboard)/payroll/page.tsx @@ -11,6 +11,7 @@ import { RequestError } from "@/lib/api-client"; import useModal from "@/hooks/useModal"; import PayoutHistory from "@/app/(dashboard)/payroll/components/PayoutHistory"; +import PayrollOverview from "@/app/(dashboard)/payroll/components/PayrollOverview"; type ModalState = | { type: "none" } @@ -181,223 +182,7 @@ export default function PayrollPage() { exit={{ opacity: 0, y: -15 }} transition={{ duration: 0.3 }} > - {/* Banner */} -
-

- Set up payroll for your employees -

-

- Let's make things easier! Automate payroll disbursement for your employees. -

- -
- - {/* Payout Schedule */} -
-
-

- Payout Schedule -

-

- Payroll -

- -
-
- setSearch(e.target.value)} - className="w-full sm:w-80 pl-4 pr-10 py-2.5 bg-white border border-[#DCE0E5] rounded-lg text-sm text-[#111827] placeholder-[#9CA3AF] focus:outline-none focus:ring-2 focus:ring-[#5E2A8C] focus:border-[#5E2A8C] transition-colors duration-200 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-200 dark:placeholder-gray-500" - /> -
- -
-
-
- - {/* Payroll Table */} -
- {isLoading ? ( -
- -
- ) : fetchError ? ( -
- -

{fetchError}

- -
- ) : filtered.length === 0 ? ( -
-
- No payroll -
-

- {search ? "No employees match your search." : "You haven't set up any payrolls."} -

-

- {search ? "Try a different search term." : "Employees you put on payroll will be displayed here"} -

-
- ) : ( - <> - {/* Run Payroll bar */} - {selectedIds.size > 0 && ( -
- - {selectedIds.size} employee{selectedIds.size > 1 ? "s" : ""} selected -  ·  - Total: {formatAmount(totalAmount, "NGN")} - - -
- )} - - {/* Table header */} -
-
- -
- Employee - Role - Invoice - Amount - Currency -
- - {/* Rows */} -
- {filtered.map((item) => ( -
toggleItem(item.id)} - className={`grid md:grid-cols-[40px_1fr_1fr_140px_120px_100px] gap-4 px-6 py-4 cursor-pointer transition-colors ${ - selectedIds.has(item.id) - ? "bg-[#F5F0FF] dark:bg-purple-950/30" - : "hover:bg-[#F9FAFB] dark:hover:bg-gray-800/50" - }`} - > - {/* Checkbox */} -
e.stopPropagation()}> - toggleItem(item.id)} - className="w-4 h-4 rounded border-gray-300 text-[#5E2A8C] focus:ring-[#5E2A8C]" - /> -
- - {/* Employee */} -
-
e.stopPropagation()}> - toggleItem(item.id)} - className="w-4 h-4 rounded border-gray-300 text-[#5E2A8C] focus:ring-[#5E2A8C]" - /> -
- {item.employee.avatarUrl ? ( - {item.employee.firstName} - ) : ( -
- {getInitials(item.employee.firstName, item.employee.lastName)} -
- )} -
-

- {item.employee.firstName} {item.employee.lastName} -

-

- {item.employee.email} -

-
-
- - {/* Role */} -
- - {item.employee.role} - -
- - {/* Invoice */} -
- - #{item.invoiceNo} - -
- - {/* Amount */} -
- - {formatAmount(item.amount, item.paidIn)} - -
- - {/* Currency */} -
- - {item.paidIn} - -
-
- ))} -
- - {/* Footer: select all + run payroll */} -
- - -
- - )} -
+ )}