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 (
+
+ );
+}
+
+// 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 ? (
+

+ ) : (
+
+ {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 ? (
+

+ ) : (
+
+ {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 ? (
-
-
-
-
-
- {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 ? (
-
- ) : (
-
- {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 */}
-
-
-
-
- >
- )}
-
+
)}