import React from 'react'; import { AppShell, Navbar, Header, Group, Text, MediaQuery, Burger, useMantineTheme, Box, Container, Menu, Avatar, Button, Stack, ActionIcon, Indicator, Loader, Anchor, Badge, Modal, Tooltip, } from '@mantine/core'; import Sidebar from '../components/sidebar'; import ThemeToggle from '../components/theme-toggle'; import { SidebarProvider } from '@/components/ui/sidebar'; import { usePage, Link, router } from '@inertiajs/react'; import { Alert } from '@mantine/core'; import { notifications } from "@mantine/notifications"; import { useEffect } from "react"; import { IconInfoCircle, IconCircleX, IconAlertCircle, IconSettings, IconLogout, IconBell, IconFileDollar, IconUserPlus } from "@tabler/icons-react"; import { logout } from '@/routes'; import { edit } from '@/routes/profile'; import { SharedData } from '@/types'; import { MantineReactTable, type MRT_ColumnDef, type MRT_Row } from 'mantine-react-table'; type PendingInvoiceNotification = { id: number; client_id: number | null; invoice_no: string; pending_sql_acc_code?: string | null; pending_client_name?: string | null; requires_client?: boolean; payment_no: string | null; management_fee: string | number | null; media_fee: string | number | null; created_at: string | null; previous_payments?: PreviousPaymentRecord[]; invoice_billing_totals?: { media_fee: string | number | null; management_fee: string | number | null; }; client?: { name: string | null; } | null; }; type PreviousPaymentRecord = { payment_number: string | null; status: string | null; sql_created_at: string | null; amount: string | number | null; media_fee: string | number | null; management_fee: string | number | null; invoice_media_fee: string | number | null; invoice_management_fee: string | number | null; invoice_number: string | null; }; type AppNotification = { type: 'pending_invoice' | string; title: string; description: string; count: number; }; function AppNotifications() { const [notifications, setNotifications] = React.useState([]); const [invoices, setInvoices] = React.useState([]); const [count, setCount] = React.useState(0); const [isLoadingNotifications, setIsLoadingNotifications] = React.useState(true); const [isLoadingInvoices, setIsLoadingInvoices] = React.useState(false); const [pendingInvoicesOpened, setPendingInvoicesOpened] = React.useState(false); const dateFormatter = React.useMemo( () => new Intl.DateTimeFormat('en-MY', { day: '2-digit', month: 'short', year: 'numeric', }), [], ); const formatAmount = (amount: string | number | null) => { const value = Number(amount ?? 0); return new Intl.NumberFormat('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2, }).format(Number.isFinite(value) ? value : 0); }; const formatDate = (date: string | null) => { if (!date) { return '-'; } const value = new Date(date); return Number.isNaN(value.getTime()) ? '-' : dateFormatter.format(value); }; const pendingInvoiceColumns = React.useMemo[]>( () => [ { accessorKey: 'invoice_no', header: 'Invoice', Cell: ({ row }) => ( {row.original.invoice_no} {row.original.requires_client ? 'Client setup required' : 'Ready for review'} ), }, { id: 'client', header: 'Client', accessorFn: (invoice) => invoice.client?.name ?? invoice.pending_client_name ?? '-', Cell: ({ cell }) => ( {cell.getValue()} ), }, { id: 'sql_acc_code', header: 'SQL Acc Code', accessorFn: (invoice) => invoice.pending_sql_acc_code ?? '-', Cell: ({ cell }) => {cell.getValue()}, }, // { // id: 'invoice_management_fee', // header: 'Invoice Management Fee', // accessorFn: (invoice) => invoice.invoice_billing_totals?.management_fee ?? 0, // Cell: ({ row }) => ( // // {formatAmount(row.original.invoice_billing_totals?.management_fee ?? 0)} // // ), // }, // { // id: 'invoice_media_fee', // header: 'Invoice Media Fee', // accessorFn: (invoice) => invoice.invoice_billing_totals?.media_fee ?? 0, // Cell: ({ row }) => ( // // {formatAmount(row.original.invoice_billing_totals?.media_fee ?? 0)} // // ), // }, { accessorKey: 'management_fee', header: 'Payment Management Fee (incl. tax)', Cell: ({ row }) => ( {formatAmount(row.original.management_fee_amount)} ), }, { accessorKey: 'management_fee_tax', header: 'Payment Management Tax', Cell: ({ row }) => ( {formatAmount(row.original.management_fee_tax)} ), }, { accessorKey: 'management_fee_nett', header: 'Payment Management Nett', Cell: ({ row }) => ( {formatAmount(row.original.management_fee)} ), }, { accessorKey: 'media_fee', header: 'Payment Media Fee (incl. tax)', Cell: ({ row }) => ( {formatAmount(row.original.media_fee_amount)} ), }, { accessorKey: 'media_fee_tax', header: 'Payment Media Tax', Cell: ({ row }) => ( {formatAmount(row.original.media_fee_tax)} ), }, { accessorKey: 'media_fee_nett', header: 'Payment Media Nett', Cell: ({ row }) => ( {formatAmount(row.original.media_fee)} ), }, { id: 'payment_date', header: 'Payment Date', accessorFn: (invoice) => invoice.created_at, Cell: ({ row }) => ( {formatDate(row.original.created_at)} ), }, { accessorKey: 'created_at', header: 'Created', Cell: ({ row }) => ( {formatDate(row.original.created_at)} ), }, ], [], ); const renderPreviousPaymentsPanel = ({ row }: { row: MRT_Row }) => { const records = row.original.previous_payments ?? []; if (records.length === 0) { return ( No previous payment records found for this invoice. ); } return ( Previous Payment Records {records.length} {records.map((record, index) => ( ))}
Payment No Status Payment Date Media Fee Management Fee Invoice Media Invoice Management Total
{record.payment_number ?? '-'} {record.status ?? '-'} {formatDate(record.sql_created_at)} {formatAmount(record.media_fee)} {formatAmount(record.management_fee)} {formatAmount(record.invoice_media_fee)} {formatAmount(record.invoice_management_fee)} {formatAmount(record.amount)}
); }; const renderPendingInvoiceActions = ({ row }: { row: MRT_Row }) => ( {row.original.requires_client ? ( ) : null} ); useEffect(() => { let isMounted = true; const loadNotifications = async () => { try { const response = await fetch('/api/notifications', { headers: { Accept: 'application/json', }, credentials: 'same-origin', }); if (!response.ok) { throw new Error('Unable to load pending invoices.'); } const data = await response.json(); if (isMounted) { setNotifications(data.notifications ?? []); setCount(data.count ?? 0); } } catch { if (isMounted) { setNotifications([]); setCount(0); } } finally { if (isMounted) { setIsLoadingNotifications(false); } } }; loadNotifications(); return () => { isMounted = false; }; }, []); const loadPendingInvoices = React.useCallback(async () => { setIsLoadingInvoices(true); try { const response = await fetch('/api/customer-invoices/pending', { headers: { Accept: 'application/json', }, credentials: 'same-origin', }); if (!response.ok) { throw new Error('Unable to load pending invoices.'); } const data = await response.json(); setInvoices(data.invoices ?? []); console.log(data.invoices); } catch { setInvoices([]); } finally { setIsLoadingInvoices(false); } }, []); const openNotification = (notification: AppNotification) => { if (notification.type === 'pending_invoice') { setPendingInvoicesOpened(true); loadPendingInvoices(); } }; return ( <> 99 ? '99+' : count} size={18} disabled={count === 0} color="red" > ({ borderBottom: `1px solid ${theme.colorScheme === 'dark' ? theme.colors.dark[4] : theme.colors.gray[2] }`, backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0], })} > Notifications Updates grouped by notification type 0 ? 'red' : 'gray'} variant="filled"> {count} pending {isLoadingNotifications ? ( Loading notifications... ) : notifications.length === 0 ? ( All caught up No notifications need your attention. ) : ( {notifications.map((notification) => ( openNotification(notification)} icon={} rightSection={ {notification.count} } > {notification.title} {notification.description} ))} )} setPendingInvoicesOpened(false)} title="Pending invoice approvals" size="xxl" > {isLoadingInvoices ? ( Loading pending invoices... ) : invoices.length === 0 ? ( All caught up No invoices are waiting for approval. ) : ( true} positionExpandColumn="first" enableRowActions positionActionsColumn="last" enablePagination={false} enableGlobalFilter={false} enableColumnFilters={false} enableTopToolbar={false} enableBottomToolbar={false} initialState={{ density: 'xs' }} /> )} ); } type Props = { children: React.ReactNode; breadcrumbs?: unknown; }; export default function AppLayout({ children }: Props) { const theme = useMantineTheme(); const [opened, setOpened] = React.useState(false); const isDark = theme.colorScheme === 'dark'; const page = usePage(); const { flash, auth } = page.props as any; useEffect(() => { if (flash['message-info']) { notifications.show({ title: 'Info', color: 'blue', icon: , message: flash['message-info'], }); } if (flash['message-warning']) { notifications.show({ title: 'Warning', color: 'yellow', icon: , message: flash['message-warning'], }); } if (flash['message-error']) { notifications.show({ title: 'Error', color: 'red', icon: , message: flash['message-error'], }); } }, [flash]); return ( ); }