import Table from '@/components/table'; import ActivityForm from '@/forms/activities/activityForm'; import { FormStatus } from '@/types'; import { Inertia } from '@inertiajs/inertia'; import { Link, router } from '@inertiajs/react'; import { useDisclosure } from '@mantine/hooks'; import { useModals } from '@mantine/modals'; import axios from 'axios'; import dayjs from 'dayjs'; import React, { useCallback, useMemo, useState } from 'react'; import AppLayout from '../../layouts/app-layout'; import { Client, ClientInvoice, ClientInvoiceAdjustment, ClientInvoicePayment, ClientInvoicePaymentItem, } from '../../types'; import { ActionIcon, Badge, Button, Card, Center, Divider, Flex, Group, LoadingOverlay, Modal, NumberInput, Paper, Portal, Select, SimpleGrid, Stack, Tabs, Text, Textarea, ThemeIcon, Title, Tooltip, } from '@mantine/core'; import { IconBriefcase, IconBuilding, IconCash, IconChartBar, IconChecks, IconClock, IconCreditCard, IconCurrencyDollar, IconDeviceTv, IconEdit, IconEye, IconFileDollar, IconLink, IconNotebook, IconPlus, IconReportAnalytics, IconTrash, IconUsers, IconWallet, } from '@tabler/icons-react'; import type { MRT_Cell, MRT_ColumnDef, MRT_Row } from 'mantine-react-table'; import { MantineReactTable } from 'mantine-react-table'; import { CartesianGrid, Legend, Line, LineChart, Tooltip as RechartTooltip, ResponsiveContainer, XAxis, YAxis, } from 'recharts'; interface Props { id: string; client: Client & { activities_list?: ProjectActivity[]; can_modify?: boolean; }; clientAssignmentRoles: AssignmentRole[]; clientAssignments: Record; assignmentUsers: SelectOption[]; clientInvoices: ClientInvoice[]; clientAdjustments: ClientInvoiceAdjustment[]; localClientId: number; lifeTimeSpending: number; } interface AssignmentRole { id: number; label: string; field: string; } interface SelectOption { value: string; label: string; } interface CampaignMetric { date: string; impressions: number; clicks: number; actual_spend: number; conversions: number; cost_per_conversion: number; interactions: number; interaction_rate: number; } interface CampaignDetail { id: number; name: string; status: string; total_impressions: number; total_clicks: number; total_actual_spend: string; cpc?: number; metrics: CampaignMetric[]; } interface CampaignSummary { total_actual_spend: string; total_impressions: number; total_clicks: number; conversions: number; cpc: number; } interface CampaignDetailResponse { id: number; name: string; status: string; total_impressions?: number | string; total_clicks?: number | string; total_actual_spend?: number | string; metrics?: Array>; } interface ProjectActivity { id: number; activity_no?: string | null; activity_type?: string | null; task_description?: string | null; estimated_completed_at?: string | null; completed_at?: string | null; notification_sent_at?: string | null; notification_status?: number | null; assignation?: { ticket?: { ticket_no?: string | null; } | null; } | null; note_to_customer?: boolean; } type AdjustmentRow = { row_kind: 'adjustment'; id: number; client_id: number | null; entry_type: 'debit' | 'credit'; amount: number; remark: string | null; created_at?: string | null; }; type InvoiceRow = ClientInvoice & { row_kind: 'invoice'; linked_invoices?: InvoiceRow[]; }; const summaryDefaults: CampaignSummary = { total_actual_spend: '0.00', total_impressions: 0, total_clicks: 0, conversions: 0, cpc: 0, }; const currencyFormatter = new Intl.NumberFormat('en-MY', { minimumFractionDigits: 2, maximumFractionDigits: 2, }); const parseNumber = (value?: number | string | null): number => { if (value === undefined || value === null) { return 0; } const normalized = typeof value === 'string' ? Number(value.replace(/,/g, '')) : Number(value); return Number.isNaN(normalized) ? 0 : normalized; }; const getInvoicePayments = (invoice: ClientInvoice): ClientInvoicePayment[] => invoice.payments ?? []; const getInvoiceItems = (invoice: ClientInvoice): ClientInvoicePaymentItem[] => getInvoicePayments(invoice).flatMap((payment) => payment.items ?? []); const isMediaItem = (item: ClientInvoicePaymentItem): boolean => item.billing_item_type?.fee_type?.toLowerCase() === 'media'; const isManagementItem = (item: ClientInvoicePaymentItem): boolean => item.billing_item_type?.fee_type?.toLowerCase() === 'management'; const contributesToNet = (item: ClientInvoicePaymentItem): boolean => item.billing_item_type?.nett_contribution ?? false; const getInvoiceSpending = (invoice: ClientInvoice): number => getInvoiceItems(invoice).reduce( (sum, item) => sum + parseNumber(item.spending), 0, ) || parseNumber(invoice.total_sem_amount) || parseNumber(invoice.total_spend); const getBillableInvoiceSpending = (invoice: ClientInvoice): number => getInvoiceItems(invoice).reduce( (sum, item) => item.is_creditcard ? sum : sum + parseNumber(item.spending), 0, ); const getBillableNettAmount = (invoice: ClientInvoice): number => getInvoiceItems(invoice).reduce((sum, item) => { if (item.is_creditcard || !contributesToNet(item)) { return sum; } return ( sum + (parseNumber(item.final_net_amount) || parseNumber(item.net_amount)) ); }, 0); const getInvoiceMediaFee = (invoice: ClientInvoice): number => getInvoiceItems(invoice) .filter(isMediaItem) .reduce((sum, item) => sum + parseNumber(item.net_amount), 0); const getInvoiceManagementFee = (invoice: ClientInvoice): number => getInvoiceItems(invoice) .filter(isManagementItem) .reduce((sum, item) => sum + parseNumber(item.net_amount), 0); const getInvoiceIsCreditCard = (invoice: ClientInvoice): boolean => { const items = getInvoiceItems(invoice); return items.length > 0 && items.every((item) => item.is_creditcard); }; const buildInvoiceTree = (invoices: ClientInvoice[]): InvoiceRow[] => { const nodes: InvoiceRow[] = invoices.map((invoice) => ({ ...invoice, row_kind: 'invoice', linked_invoices: [] as InvoiceRow[], })); const nodeById = new Map(nodes.map((node) => [node.id, node])); const childrenByParent = new Map(); nodes.forEach((node) => { const parentId = node.linked_invoice_id; if (!parentId || !nodeById.has(parentId)) { return; } const children = childrenByParent.get(parentId) ?? []; children.push(node); childrenByParent.set(parentId, children); }); const visited = new Set(); const attachChildren = (node: InvoiceRow): InvoiceRow => { if (visited.has(node.id)) { return node; } visited.add(node.id); node.linked_invoices = (childrenByParent.get(node.id) ?? []).map( (child) => attachChildren(child), ); visited.delete(node.id); return node; }; const roots = nodes.filter( (node) => !node.linked_invoice_id || !nodeById.has(node.linked_invoice_id), ); if (roots.length === 0 && nodes.length > 0) { return nodes; } return roots.map((node) => attachChildren(node)); }; const flattenInvoiceRows = (invoice: InvoiceRow): InvoiceRow[] => [ invoice, ...(invoice.linked_invoices ?? []).flatMap(flattenInvoiceRows), ]; const getPaymentTaxAmount = (payment: ClientInvoicePayment): number => { const itemTaxAmount = (payment.items ?? []).reduce( (sum, item) => sum + Math.max( 0, parseNumber(item.payment_item_amount) - parseNumber(item.net_amount), ), 0, ); if (itemTaxAmount > 0) { return itemTaxAmount; } return Math.max( 0, parseNumber(payment.payment_total_amount) - parseNumber(payment.payment_nett_amount), ); }; const getPaymentTaxPercentage = (payment: ClientInvoicePayment): number => { const items = payment.items ?? []; const taxableGross = items.reduce( (sum, item) => sum + parseNumber(item.payment_item_amount), 0, ); if (taxableGross > 0) { return (getPaymentTaxAmount(payment) / taxableGross) * 100; } return 0; }; const getInvoiceAmount = (invoice: ClientInvoice): number => { return parseNumber(invoice.total_sem_amount); }; const getInvoiceNetAmount = (invoice: ClientInvoice): number => { return parseNumber(invoice.total_net_amount); }; const getInvoicePaidAmount = (invoice: ClientInvoice): number => { return getInvoicePayments(invoice).reduce( (sum, payment) => sum + parseNumber(payment.payment_nett_amount), 0, ); }; const getInvoiceOutstandingAmount = (invoice: ClientInvoice): number => Math.max(0, getInvoiceNetAmount(invoice) - getInvoicePaidAmount(invoice)); const getPaymentStartDate = (payment: ClientInvoicePayment): string | null => minDateString((payment.items ?? []).map((item) => item.start_date)); const getPaymentEndDate = (payment: ClientInvoicePayment): string | null => maxDateString((payment.items ?? []).map((item) => item.end_date)); const getPaymentSpending = (payment: ClientInvoicePayment): number => (payment.items ?? []).reduce( (sum, item) => sum + parseNumber(item.spending), 0, ); const minDateString = ( values: Array, ): string | null => { const filtered = values.filter((value): value is string => !!value); if (filtered.length === 0) return null; return filtered.reduce( (min, value) => (value < min ? value : min), filtered[0], ); }; const maxDateString = ( values: Array, ): string | null => { const filtered = values.filter((value): value is string => !!value); if (filtered.length === 0) return null; return filtered.reduce( (max, value) => (value > max ? value : max), filtered[0], ); }; const normalizeCampaignMetrics = ( metrics: Array> = [], ): CampaignMetric[] => metrics.map((metric) => ({ date: (metric['date'] as string) ?? '', impressions: parseNumber(metric['impressions'] as number | string), clicks: parseNumber(metric['clicks'] as number | string), actual_spend: parseNumber(metric['actual_spend'] as number | string), conversions: parseNumber(metric['conversions'] as number | string), cost_per_conversion: parseNumber( metric['cost_per_conversion'] as number | string, ), interactions: parseNumber(metric['interactions'] as number | string), interaction_rate: parseNumber( metric['interaction_rate'] as number | string, ), })); const buildSummary = (summary?: Record): CampaignSummary => { const totalClicks = parseNumber( summary?.['total_clicks'] as number | string, ); const totalImpressions = parseNumber( summary?.['total_impressions'] as number | string, ); const totalConversions = parseNumber( summary?.['conversions'] as number | string | undefined, ); const totalSpendValue = parseNumber( summary?.['total_actual_spend'] as number | string | undefined, ); const totalSpendString = summary?.['total_actual_spend'] ?? summaryDefaults.total_actual_spend; return { total_actual_spend: typeof totalSpendString === 'string' ? totalSpendString : totalSpendValue.toFixed(2), total_impressions: totalImpressions, total_clicks: totalClicks, conversions: totalConversions, cpc: totalClicks > 0 ? totalSpendValue / totalClicks : 0, }; }; const dateDisplay = (date: Date) => dayjs(date).format('DD MMM YYYY'); const isHtml = (value: string) => /<[^>]+>/i.test(value); export default function TicketDetails({ id, client, clientAssignmentRoles, clientAssignments, assignmentUsers, clientInvoices, clientAdjustments, localClientId, lifeTimeSpending, }: Props) { const [dateRange, setDateRange] = useState<[Date | null, Date | null]>([ dayjs().startOf('month').toDate(), dayjs().endOf('month').toDate(), ]); const [loading, setLoading] = useState(false); const [campaigns, setCampaigns] = useState([]); const [metrics, setMetrics] = useState(summaryDefaults); const [description, setDescription] = useState(''); const [isDescriptionModalOpen, setIsDescriptionModalOpen] = useState(false); const handleOpenDescriptionModal = useCallback((value: string) => { setDescription(value); setIsDescriptionModalOpen(true); }, []); const handleCloseDescriptionModal = useCallback(() => { setIsDescriptionModalOpen(false); }, []); const [ activityFormOpened, { open: openActivityForm, close: closeActivityForm }, ] = useDisclosure(false); const [activityFormMode, setActivityFormMode] = useState('create'); const [selectedActivity, setSelectedActivity] = useState(null); const handleOpenActivityForm = useCallback( (activity?: ProjectActivity) => { setSelectedActivity(activity ?? null); setActivityFormMode(activity ? 'update' : 'create'); openActivityForm(); }, [openActivityForm], ); const handleCloseActivityForm = useCallback(() => { closeActivityForm(); setSelectedActivity(null); setActivityFormMode('create'); }, [closeActivityForm]); const modals = useModals(); const [adjustmentModalOpened, setAdjustmentModalOpened] = useState(false); const [adjustmentEntryType, setAdjustmentEntryType] = useState< 'debit' | 'credit' >('debit'); const [adjustmentAmount, setAdjustmentAmount] = useState(''); const [adjustmentRemark, setAdjustmentRemark] = useState(''); const openAdjustmentModal = () => { setAdjustmentEntryType('debit'); setAdjustmentAmount(''); setAdjustmentRemark(''); setAdjustmentModalOpened(true); }; const closeAdjustmentModal = () => setAdjustmentModalOpened(false); const refreshAccount = () => { router.visit(route('google-ads.accounts.show', { id }), { preserveScroll: true, preserveState: false, replace: true, }); }; const submitAdjustment = () => { router.post( route('clients.adjustments.store', { client: localClientId }), { entry_type: adjustmentEntryType, amount: typeof adjustmentAmount === 'number' ? adjustmentAmount : 0, remark: adjustmentRemark.trim() ? adjustmentRemark.trim() : null, }, { preserveScroll: true, onSuccess: () => { closeAdjustmentModal(); refreshAccount(); }, }, ); }; const activities = useMemo( () => client.activities_list ?? [], [client.activities_list], ); const canModify = client.can_modify ?? true; const pendingActivities = useMemo(() => { return activities .filter( (activity) => !activity.completed_at && activity.estimated_completed_at, ) .sort((a, b) => { const aDate = a.estimated_completed_at ? new Date(a.estimated_completed_at).getTime() : -Infinity; const bDate = b.estimated_completed_at ? new Date(b.estimated_completed_at).getTime() : -Infinity; return bDate - aDate; }); }, [activities]); const completedActivities = useMemo(() => { return activities .filter((activity) => !!activity.completed_at) .sort((a, b) => { const aDate = a.completed_at ? new Date(a.completed_at).getTime() : -Infinity; const bDate = b.completed_at ? new Date(b.completed_at).getTime() : -Infinity; return bDate - aDate; }); }, [activities]); const notesActivities = useMemo(() => { return activities.filter((activity) => activity.note_to_customer); }, [activities]); const lookupAssignmentLabel = (roleId: number) => { const userId = clientAssignments?.[roleId]; if (!userId) { return '—'; } return ( assignmentUsers.find((option) => option.value === String(userId)) ?.label ?? '—' ); }; const accountSummary = [ { label: 'Industry', value: client.industry, icon: , }, { label: 'Customer Code', value: client.sql_acc_code, icon: , }, { label: 'Timezone', value: client.time_zone, icon: , }, ]; const fetchCampaigns = useCallback( async (rangeParam?: [Date | null, Date | null]) => { const rangeToUse = rangeParam ?? dateRange; if (!rangeToUse[0] || !rangeToUse[1]) { return; } try { setLoading(true); const startDate = dayjs(rangeToUse[0]).format('YYYY-MM-DD'); const endDate = dayjs(rangeToUse[1]).format('YYYY-MM-DD'); const res = await axios.post( route('google.getCampaignsDetails'), { clientCustomerId: id, startDate, endDate, }, ); const rawCampaigns: CampaignDetailResponse[] = res.data.campaigns ?? []; const normalizedCampaigns: CampaignDetail[] = rawCampaigns.map( (campaign) => ({ ...campaign, total_clicks: parseNumber(campaign.total_clicks), total_impressions: parseNumber( campaign.total_impressions, ), total_actual_spend: String( campaign.total_actual_spend ?? summaryDefaults.total_actual_spend, ), metrics: normalizeCampaignMetrics(campaign.metrics), }), ); const summaryData = buildSummary(res.data.summary); setCampaigns(normalizedCampaigns); setMetrics(summaryData); } catch (error) { console.error(error); } finally { setLoading(false); } }, [dateRange, id], ); const handleDeleteInvoice = (invoiceId: number) => { if ( !window.confirm( 'This will permanently delete the invoice. Continue?', ) ) { return; } Inertia.delete( route('client-invoices.destroy', { invoice: invoiceId }), { preserveState: true, }, ); }; const handleDeleteAdjustment = (adjustmentId: number) => { if ( !window.confirm( 'This will permanently delete the adjustment. Continue?', ) ) { return; } Inertia.delete( route('clients.adjustments.destroy', { adjustment: adjustmentId }), { preserveState: true, onSuccess: () => { refreshAccount(); }, }, ); }; const renderLinkedInvoiceActions = ({ row, }: { row: MRT_Row; }) => { const item = row.original; return ( {item.invoice_no !== null && ( )} handleDeleteInvoice(item.id)} > ); }; const renderAdjustmentActions = ({ row, }: { row: MRT_Row; }) => ( handleDeleteAdjustment(row.original.id)} > ); const columns = useMemo[]>( () => [ { accessorKey: 'name', header: 'Campaign' }, { accessorKey: 'status', header: 'Status', Cell: ({ cell }: { cell: MRT_Cell }) => { const value = cell.getValue() as string; const color = value === 'ENABLED' ? 'green' : value === 'PAUSED' ? 'yellow' : value === 'ENDED' ? 'red' : 'gray'; return {value}; }, }, { accessorKey: 'total_impressions', header: 'Impressions' }, { accessorKey: 'total_clicks', header: 'Clicks' }, { accessorKey: 'total_actual_spend', header: 'Spend (RM)' }, ], [], ); const invoiceTotals = useMemo(() => { const invoicesData = clientInvoices ?? []; const adjustmentsData = clientAdjustments ?? []; const adjustmentNet = adjustmentsData.reduce((sum, adj) => { const value = parseNumber(adj.amount as any); return sum + (adj.entry_type === 'credit' ? value : -value); }, 0); const invoiceSpending = invoicesData.reduce((sum, invoice) => { return sum + getInvoiceSpending(invoice); }, 0); const billableInvoiceSpending = invoicesData.reduce( (sum, invoice) => sum + getBillableInvoiceSpending(invoice), 0, ); const nettAmountBase = invoicesData.reduce( (sum, invoice) => sum + getBillableNettAmount(invoice), 0, ); const nettAmount = nettAmountBase + adjustmentNet; const remainingAmount = Math.max( 0, nettAmount - billableInvoiceSpending, ); return { managementFee: invoicesData.reduce( (sum, invoice) => sum + getInvoiceManagementFee(invoice), 0, ), mediaFee: invoicesData.reduce( (sum, invoice) => sum + getInvoiceMediaFee(invoice), 0, ), invoiceSpending, billableInvoiceSpending, adjustmentNet, nettAmount, remainingAmount, }; }, [clientInvoices, clientAdjustments]); const groupedInvoices = useMemo( () => buildInvoiceTree(clientInvoices ?? []), [clientInvoices], ); const invoiceRows = useMemo( () => groupedInvoices.flatMap((invoice) => flattenInvoiceRows(invoice)), [groupedInvoices], ); const adjustmentRows = useMemo( () => (clientAdjustments ?? []).map((adj) => ({ row_kind: 'adjustment', id: adj.id, client_id: adj.client_id ?? null, entry_type: adj.entry_type, amount: parseNumber(adj.amount as any), remark: adj.remark ?? null, created_at: adj.created_at ?? null, })), [clientAdjustments], ); const summaryItems = [ { label: 'Total Media Fees (Invoice)', value: invoiceTotals.mediaFee, icon: IconDeviceTv, color: 'grape', }, { label: 'Net Amount (Invoice)', value: invoiceTotals.nettAmount, icon: IconWallet, color: 'green', }, { label: 'Spending Media Fee (Google Live)', value: invoiceTotals.billableInvoiceSpending, icon: IconCurrencyDollar, color: 'indigo', }, { label: 'Remaining Amount', value: invoiceTotals.remainingAmount, icon: IconChartBar, color: 'teal', }, { label: 'Adjustments', value: invoiceTotals.adjustmentNet, icon: IconChartBar, color: 'cyan', }, { label: 'Lifetime Spend (All)', value: lifeTimeSpending, icon: IconCash, color: 'orange', }, { label: 'Total Management Fees (Invoice)', value: invoiceTotals.managementFee, icon: IconBriefcase, color: 'blue', }, ]; const metricColumns = useMemo[]>( () => [ { accessorFn: (row) => row.date ? dayjs(row.date).startOf('day').toDate() : null, id: 'date', header: 'Date', sortingFn: 'datetime', Cell: ({ cell }) => cell.getValue() ? dateDisplay(cell.getValue()) : '-', }, { accessorKey: 'impressions', header: 'Impressions' }, { accessorKey: 'clicks', header: 'Clicks' }, { accessorKey: 'actual_spend', header: 'Spend (RM)' }, { accessorKey: 'conversions', header: 'Conversions' }, { accessorKey: 'cost_per_conversion', header: 'Cost/Conversion %' }, { accessorKey: 'interactions', header: 'Interactions' }, { accessorKey: 'interaction_rate', header: 'Interaction %' }, ], [], ); const invoiceColumns = useMemo[]>( () => [ { accessorKey: 'invoice_no', header: 'Invoice No', size: 120, Cell: ({ cell, row }) => { const isCreditCard = getInvoiceIsCreditCard(row.original); return ( {cell.getValue()} {row.original.linked_invoice_id && ( )} {isCreditCard && ( )} ); }, }, { id: 'invoice_amount', header: 'Invoice Amount', accessorFn: (row) => getInvoiceAmount(row), Cell: ({ cell }) => `${currencyFormatter.format(parseNumber(cell.getValue() as number))}`, }, { id: 'net_amount', header: 'Net Amount', accessorFn: (row) => getInvoiceNetAmount(row), Cell: ({ cell }) => `${currencyFormatter.format(parseNumber(cell.getValue() as number))}`, }, { id: 'paid_amount', header: 'Paid Amount', accessorFn: (row) => getInvoicePaidAmount(row), Cell: ({ cell }) => `${currencyFormatter.format(parseNumber(cell.getValue() as number))}`, }, { id: 'outstanding', header: 'Outstanding', accessorFn: (row) => getInvoiceOutstandingAmount(row), Cell: ({ cell }) => `${currencyFormatter.format(parseNumber(cell.getValue() as number))}`, }, // { // id: 'spending', // header: 'Spending', // accessorFn: (row) => getInvoiceSpending(row), // Cell: ({ cell }) => // `${currencyFormatter.format(parseNumber(cell.getValue() as number))}`, // }, // { // id: 'remark', // header: 'Remark', // accessorFn: () => '—', // }, { id: 'actions', header: 'Actions', Cell: ({ row }) => renderLinkedInvoiceActions({ row: { original: row.original } as any, }), }, ], [], ); const paymentDetailColumns = useMemo[]>( () => [ { accessorKey: 'payment_no', header: 'Payment No', Cell: ({ cell }) => cell.getValue() ?? '—', }, { id: 'payment_amount', header: 'Payment Amount', accessorFn: (row) => (row.items ?? []).reduce( (sum, item) => sum + parseNumber(item.payment_item_amount), 0, ) || parseNumber(row.payment_total_amount), Cell: ({ cell }) => `${currencyFormatter.format(parseNumber(cell.getValue() as number))}`, }, { id: 'tax_amount', header: 'Tax Amount', accessorFn: (row) => getPaymentTaxAmount(row), Cell: ({ cell }) => `${currencyFormatter.format(parseNumber(cell.getValue() as number))}`, }, { id: 'net_amount', header: 'Net Amount', accessorFn: (row) => (row.items ?? []).reduce( (sum, item) => sum + (parseNumber(item.final_net_amount) || parseNumber(item.net_amount)), 0, ) || parseNumber(row.payment_nett_amount), Cell: ({ cell }) => `${currencyFormatter.format(parseNumber(cell.getValue() as number))}`, }, { id: 'spending', header: 'Spending', accessorFn: (row) => getPaymentSpending(row), Cell: ({ cell }) => `${currencyFormatter.format(parseNumber(cell.getValue() as number))}`, }, ], [], ); const paymentItemColumns = useMemo< MRT_ColumnDef[] >( () => [ { id: 'billing_item', header: 'Billing Item', accessorFn: (row) => row.billing_item_type?.name ?? 'Billing item', }, { id: 'billing_mode', header: 'Billing', accessorFn: (row) => row.is_creditcard ? 'Credit Card' : 'Pay to Us', Cell: ({ cell }) => ( () === 'Credit Card' ? 'teal' : 'green' } variant="light" > {cell.getValue()} ), }, { id: 'payment_item_amount', header: 'Amount', accessorFn: (row) => parseNumber(row.payment_item_amount), Cell: ({ cell }) => `${currencyFormatter.format(parseNumber(cell.getValue() as number))}`, }, { id: 'tax_percentage', header: 'Tax %', accessorFn: (row) => parseNumber(row.tax_percentage), Cell: ({ cell }) => `${currencyFormatter.format(parseNumber(cell.getValue() as number))}%`, }, { id: 'net_amount', header: 'Net Amount', accessorFn: (row) => parseNumber(row.net_amount), Cell: ({ cell }) => `${currencyFormatter.format(parseNumber(cell.getValue() as number))}`, }, { id: 'withholding_tax', header: 'Withholding Tax', accessorFn: (row) => parseNumber(row.withholding_tax), Cell: ({ cell }) => `${currencyFormatter.format(parseNumber(cell.getValue() as number))}%`, }, { id: 'final_net_amount', header: 'Final Net Amount', accessorFn: (row) => parseNumber(row.final_net_amount), Cell: ({ cell }) => `${currencyFormatter.format(parseNumber(cell.getValue() as number))}`, }, { accessorKey: 'start_date', header: 'Start Date', Cell: ({ cell }) => cell.getValue() ?? '—', }, { accessorKey: 'end_date', header: 'End Date', Cell: ({ cell }) => cell.getValue() ?? '—', }, { id: 'spending', header: 'Spending', accessorFn: (row) => parseNumber(row.spending), Cell: ({ cell }) => `${currencyFormatter.format(parseNumber(cell.getValue() as number))}`, }, ], [], ); const adjustmentColumns = useMemo[]>( () => [ { accessorKey: 'entry_type', header: 'Entry Type', Cell: ({ row }) => row.original.entry_type === 'credit' ? 'Credit' : 'Debit', }, { accessorKey: 'amount', header: 'Amount', Cell: ({ row }) => { const amount = row.original.amount ?? 0; const isCredit = row.original.entry_type === 'credit'; const signed = isCredit ? amount : -amount; const isNegative = signed < 0; const color = isNegative ? 'red' : 'green'; const sign = isNegative ? '-' : '+'; return ( {sign} {currencyFormatter.format(Math.abs(signed))} ); }, }, { accessorKey: 'remark', header: 'Remark', Cell: ({ cell }) => cell.getValue() ?? '—', }, { accessorKey: 'created_at', header: 'Created At', Cell: ({ cell }) => cell.getValue() ?? '—', }, ], [], ); const pendingColumns = useMemo[]>( () => [ { header: 'Activity No', accessorKey: 'activity_no' }, { header: 'Category', accessorKey: 'activity_type' }, { header: 'Task Description', accessorKey: 'task_description', muiTableBodyCellProps: { align: 'center', }, muiTableHeadCellProps: { align: 'center', }, Cell: ({ cell }) => { const value = cell.getValue(); if (!value) { return ( - ); } return ( handleOpenDescriptionModal(value)} > ); }, }, { header: 'Estimated Completion', accessorKey: 'estimated_completed_at', size: 180, accessorFn: (activity) => activity.estimated_completed_at ? dayjs(activity.estimated_completed_at) .startOf('day') .toDate() : null, Cell: ({ cell }) => { const date = cell.getValue(); if (!date) return ( - ); return ( {dateDisplay(date)} ); }, filterVariant: 'date-range', filterFn: (row, columnId, filterValue) => { const [start, end] = filterValue ?? []; const value = row.getValue(columnId) as Date | null; if (!start && !end) return true; if (!value) return false; return ( (!start || value >= dayjs(start).startOf('day').toDate()) && (!end || value <= dayjs(end).endOf('day').toDate()) ); }, sortingFn: 'datetime', }, ], [handleOpenDescriptionModal], ); const completedColumns = useMemo[]>( () => [ { header: 'Activity No', accessorKey: 'activity_no', }, { header: 'Category', accessorKey: 'activity_type' }, { header: 'Task Description', accessorKey: 'task_description', muiTableBodyCellProps: { align: 'center', }, muiTableHeadCellProps: { align: 'center', }, Cell: ({ cell }) => { const value = cell.getValue(); if (!value) { return ( - ); } return ( handleOpenDescriptionModal(value)} > ); }, }, { header: 'Estimated Completion', accessorKey: 'estimated_completed_at', size: 180, accessorFn: (activity) => activity.estimated_completed_at ? dayjs(activity.estimated_completed_at) .startOf('day') .toDate() : null, Cell: ({ cell }) => { const date = cell.getValue(); if (!date) return ( - ); return ( {dateDisplay(date)} ); }, filterVariant: 'date-range', filterFn: (row, columnId, filterValue) => { const [start, end] = filterValue ?? []; const value = row.getValue(columnId) as Date | null; if (!start && !end) return true; if (!value) return false; return ( (!start || value >= dayjs(start).startOf('day').toDate()) && (!end || value <= dayjs(end).endOf('day').toDate()) ); }, sortingFn: 'datetime', }, { header: 'Completed Date', accessorKey: 'completed_at', size: 180, Cell: ({ cell }) => { const date = cell.getValue(); if (!date) return ( - ); return ( {dateDisplay(date)} ); }, filterVariant: 'date-range', filterFn: (row, columnId, filterValue) => { const [start, end] = filterValue ?? []; const value = row.getValue(columnId) as Date | null; if (!start && !end) return true; if (!value) return false; return ( (!start || value >= dayjs(start).startOf('day').toDate()) && (!end || value <= dayjs(end).endOf('day').toDate()) ); }, sortingFn: 'datetime', }, ], [handleOpenDescriptionModal], ); const notesColumns = useMemo[]>( () => [ { header: 'Activity No', accessorKey: 'activity_no' }, { header: 'Task Description', accessorKey: 'task_description', Cell: ({ cell }) => { const value = cell.getValue(); if (!value) { return (
-
); } return (
handleOpenDescriptionModal(value) } >
); }, }, { header: 'Ticket No', accessorFn: (activity) => activity.assignation?.ticket?.ticket_no ?? '-', }, { header: 'Status', accessorFn: (activity) => ({ sent_at: activity.notification_sent_at, status: activity.notification_status, }), Cell: ({ cell }) => { const value = cell.getValue<{ sent_at?: string | null; status?: number; }>(); if (value?.sent_at) { return Notified; } if (!value?.sent_at && value?.status === 1) { return Schedule to notify; } return Not schedule; }, }, { header: 'Sent Date', accessorFn: (activity) => activity.notification_sent_at ? dayjs(activity.notification_sent_at).toDate() : null, Cell: ({ cell }) => cell.getValue() ? dateDisplay(cell.getValue()) : '-', maxSize: 50, }, ], [handleOpenDescriptionModal], ); const renderActivityActions = ( activity: ProjectActivity, allowComplete: boolean, ) => ( {allowComplete && !activity.completed_at && activity.estimated_completed_at ? ( completedButtonClicked(activity)} > ) : null} handleOpenActivityForm(activity)}> discardButtonClicked(activity)} > ); const rowOptions = ( row: MRT_Row, allowComplete: boolean, ) => renderActivityActions(row.original, allowComplete); function completedButtonClicked(activity: ProjectActivity) { let status = !!activity.notification_status; const ModalContent = () => { const [checked, setChecked] = useState(status); status = checked; return ( <> Are you sure you want to complete this activity? {/* setChecked(event.currentTarget.checked)} /> */} ); }; modals.openConfirmModal({ title: 'Activity Completion Confirmation', centered: true, children: , labels: { confirm: 'Confirm', cancel: 'Cancel' }, confirmProps: { color: 'green' }, onConfirm: () => { router.visit( route('google-ads.accounts.activity.completeActivity', { id: activity.id, }), { method: 'patch', data: { status }, }, ); }, }); } function discardButtonClicked(activity: ProjectActivity) { modals.openConfirmModal({ title: 'Activity Discard Confirmation', centered: true, children: Are you sure want to discard this activity?, labels: { confirm: 'Confirm', cancel: 'Cancel' }, confirmProps: { color: 'red' }, onConfirm: () => { router.visit( route('google-ads.accounts.activity.deleteActivity', { id: activity.id, }), { method: 'delete', }, ); }, }); } // useEffect(() => { // if (dateRange[0] && dateRange[1]) { // fetchCampaigns(dateRange); // } // }, [dateRange, fetchCampaigns]); const Chart = ({ data }: { data: CampaignMetric[] }) => ( ); return ( Client Dashboard {/* CLIENT INFO */} {client.name} {client.status} Customer ID: {client.id} Account Details {accountSummary.map((item) => ( ))} Assignments {clientAssignmentRoles.length} roles {clientAssignmentRoles.map((role) => { const assignee = lookupAssignmentLabel(role.id); return ( {role.label} {assignee !== '—' ? 'Assigned' : 'Unassigned'} {assignee} ); })} {/* } value="campaigns"> Campaigns */} } value="invoice" > Invoices } value="tasks"> Tasks {/* } /> } /> } /> } /> } /> Campaign Overview {campaigns.length > 0 && ( Campaign Analytics {campaigns.map((c) => ( {c.name} ))} {campaigns.map((c) => ( Performance Trend ))} )} */} Invoices {summaryItems.map((item) => { const Icon = item.icon; return ( {item.label} RM{' '} {item.value.toLocaleString( 'en-MY', { minimumFractionDigits: 2, maximumFractionDigits: 2, }, )} ); })} ( ( )} initialState={{ density: 'xs', }} /> )} initialState={{ density: 'xs', }} /> Adjustments