import React from 'react'; import AppLayout from '../layouts/app-layout'; import { Badge, Box, Card, Container, Button, Divider, Group, Modal, Paper, ScrollArea, SimpleGrid, Stack, Table, Text, ThemeIcon, Title, } from '@mantine/core'; import { IconBuilding, IconChecklist, IconFileInvoice, IconLinkOff, } from '@tabler/icons-react'; type DashboardStats = { totalClients: number; pendingInvoices: number; unlinkedPendingInvoices: number; pendingActivities: number; clientsMissingSqlCode: number; }; type RecentInvoice = { id: number; invoice_no: string; approved_at: string | null; management_fee: string | number | null; media_fee: string | number | null; nett_amount: string | number | null; created_at: string | null; client: { name: string; customer_id: string; } | null; }; type PendingActivity = { id: number; activity_no: string; activity_type: string | null; task_description: string | null; estimated_completed_at: string | null; assigned_person: string | null; client: { name: string; customer_id: string; } | null; }; interface Props { stats: DashboardStats; clientStatusCounts: Record; recentInvoices: RecentInvoice[]; pendingActivities: PendingActivity[]; } const formatAmount = (value: string | number | null | undefined) => { const amount = Number(value ?? 0); return new Intl.NumberFormat('en-MY', { minimumFractionDigits: 2, maximumFractionDigits: 2, }).format(Number.isFinite(amount) ? amount : 0); }; const statusColor = (status: string) => { if (status === 'ENABLED') return 'green'; if (status === 'PAUSED') return 'yellow'; if (status === 'CANCELED' || status === 'ENDED') return 'red'; return 'gray'; }; const textPreview = (html: string | null) => { if (!html) return '-'; const text = html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim(); return text.length > 72 ? `${text.slice(0, 72)}...` : text || '-'; }; const daysUntil = (date: string | null) => { if (!date) { return { label: '-', color: 'gray' }; } const today = new Date(); today.setHours(0, 0, 0, 0); const dueDate = new Date(`${date}T00:00:00`); const diff = Math.ceil((dueDate.getTime() - today.getTime()) / 86400000); if (Number.isNaN(diff)) { return { label: '-', color: 'gray' }; } if (diff < 0) { return { label: `${Math.abs(diff)} day${Math.abs(diff) === 1 ? '' : 's'} overdue`, color: 'red' }; } if (diff === 0) { return { label: 'Due today', color: 'orange' }; } return { label: `${diff} day${diff === 1 ? '' : 's'} left`, color: diff <= 3 ? 'yellow' : 'green' }; }; export default function Dashboard({ stats, clientStatusCounts, recentInvoices, pendingActivities, }: Props) { const [selectedActivity, setSelectedActivity] = React.useState(null); const statCards = [ { label: 'Visible Clients', value: stats.totalClients.toLocaleString('en-MY'), detail: 'Based on your hierarchy access', icon: IconBuilding, color: 'blue', }, { label: 'Pending Invoices', value: stats.pendingInvoices.toLocaleString('en-MY'), detail: stats.unlinkedPendingInvoices > 0 ? `${stats.unlinkedPendingInvoices} need client linking` : 'Ready for review', icon: IconFileInvoice, color: 'orange', }, { label: 'Pending Activities', value: stats.pendingActivities.toLocaleString('en-MY'), detail: 'Open scheduled work', icon: IconChecklist, color: 'violet', }, { label: 'Missing SQL Code', value: stats.clientsMissingSqlCode.toLocaleString('en-MY'), detail: 'Synced clients not linked to SQL', icon: IconLinkOff, color: 'red', }, ]; return ( ({ background: theme.colorScheme === 'dark' ? theme.colors.dark[7] : theme.colors.gray[0], })} > Overview Dashboard Your client, invoice, and activity overview follows the same hierarchy access as the accounts page. Hierarchy filtered {statCards.map((item) => { const Icon = item.icon; return ( ({ position: 'relative', overflow: 'hidden', backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.white, })} > ({ position: 'absolute', top: 0, left: 0, right: 0, height: 3, backgroundColor: theme.colors[item.color][6], })} /> {item.label} {item.value} {item.detail} ); })} Client Status {stats.totalClients.toLocaleString('en-MY')} total {Object.keys(clientStatusCounts).length === 0 ? ( No visible clients. ) : ( {Object.entries(clientStatusCounts).map(([status, count]) => ( {status || 'Unknown'} {count} ))} )} Recent Invoices {recentInvoices.length} {recentInvoices.length === 0 ? ( ) : recentInvoices.map((invoice) => ( ))}
Invoice Client Nett Status
No invoices found.
{invoice.invoice_no} {invoice.client?.name ?? '-'} RM {formatAmount(invoice.nett_amount)} {invoice.approved_at ? 'Approved' : 'Pending'}
Upcoming Activities Sorted by nearest due date {pendingActivities.length} shown {pendingActivities.length === 0 ? ( ) : pendingActivities.map((activity) => { const due = daysUntil(activity.estimated_completed_at); return ( ); })}
No Client Type Assigned Person Task Due Days Left
No pending activities.
{activity.activity_no} {activity.client?.name ?? '-'} {activity.activity_type ?? '-'} {activity.assigned_person ?? '-'} {activity.estimated_completed_at ?? '-'} {due.label}
setSelectedActivity(null)} title={selectedActivity?.activity_no ?? 'Activity'} size="lg" > {selectedActivity?.client?.name ?? '-'} {selectedActivity?.assigned_person ?? 'Unassigned'} {daysUntil(selectedActivity?.estimated_completed_at ?? null).label}
); }