import React, { useMemo, useState, useEffect, useCallback } from "react"; import AppLayout from "../../layouts/app-layout"; import { Client, ClientInvoice, ClientInvoiceAdjustment } from "../../types"; import { FormStatus } from "@/types"; import axios from "axios"; import dayjs from "dayjs"; import { Link, router } from "@inertiajs/react"; import { Inertia } from "@inertiajs/inertia"; import { useDisclosure } from "@mantine/hooks"; import { useModals } from "@mantine/modals"; import ActivityForm from "@/forms/activities/activityForm"; import Table, { RowActionsProps } from "@/components/table"; import { Title, Group, Button, SimpleGrid, Card, Text, Paper, Badge, Divider, Stack, ThemeIcon, Tabs, LoadingOverlay, ActionIcon, Center, Flex, Tooltip, Switch, Modal, Portal, NumberInput, Select, Textarea, } from "@mantine/core"; import { IconBuilding, IconCurrencyDollar, IconClock, IconChartLine, IconCalendarStats, IconReportAnalytics, IconEdit, IconPlus, IconTrash, IconUsers, IconNotebook, IconEye, IconChecks, IconBriefcase, IconDeviceTv, IconWallet, IconCash, IconChartBar, IconFileDollar, IconCreditCard, } from "@tabler/icons-react"; import { DatePickerInput } from "@mantine/dates"; import { MantineReactTable } from "mantine-react-table"; import type { MRT_Cell, MRT_ColumnDef, MRT_Row, } from "mantine-react-table"; import { ResponsiveContainer, LineChart, Line, CartesianGrid, Tooltip as RechartTooltip, XAxis, YAxis, Legend, } from "recharts"; import { Tab } from "@headlessui/react"; 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[]; }; type InvoiceNoSummaryRow = { invoice_no: string; payments: InvoiceRow[]; depth: number; parentInvoiceNo?: string; }; 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 getInvoiceSpending = (invoice: ClientInvoice): number => invoice.total_spend !== undefined && invoice.total_spend !== null ? parseNumber(invoice.total_spend) : parseNumber(invoice.total_spending); const getBillableInvoiceSpending = (invoice: ClientInvoice): number => invoice.is_credit_card ? 0 : getInvoiceSpending(invoice); const getBillableNettAmount = (invoice: ClientInvoice): number => invoice.is_credit_card ? 0 : parseNumber(invoice.nett_amount); 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 getPaymentAmount = (invoice: InvoiceRow): number => parseNumber(invoice.management_fee) + parseNumber(invoice.media_fee); 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 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(), } ); }; 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) : -Infinity; const bDate = b.estimated_completed_at ? new Date(b.estimated_completed_at) : -Infinity; return bDate.getTime() - aDate.getTime(); }); }, [activities]); const completedActivities = useMemo(() => { return activities .filter((activity) => !!activity.completed_at) .sort((a, b) => { const aDate = a.completed_at ? new Date(a.completed_at) : -Infinity; const bDate = b.completed_at ? new Date(b.completed_at) : -Infinity; return bDate.getTime() - aDate.getTime(); }); }, [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: 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, }); }; 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) => { // Prefer computed total_spend (from metrics) when present; fall back to stored total_spending. const value = invoice.total_spend !== undefined && invoice.total_spend !== null ? parseNumber(invoice.total_spend as any) : parseNumber(invoice.total_spending as any); return sum + value; }, 0); const billableInvoiceSpending = invoicesData.reduce( (sum, invoice) => sum + getBillableInvoiceSpending(invoice), 0 ); const nettAmountBase = invoicesData.reduce( (sum, invoice) => sum + parseNumber(invoice.nett_amount ?? 0), 0 ); const nettAmount = nettAmountBase + adjustmentNet; const remainingAmount = Math.max(0, nettAmount - billableInvoiceSpending); return { managementFee: invoicesData.reduce( (sum, invoice) => sum + parseNumber(invoice.management_fee ?? 0), 0 ), mediaFee: invoicesData.reduce( (sum, invoice) => sum + parseNumber(invoice.media_fee ?? 0), 0 ), invoiceSpending, billableInvoiceSpending, adjustmentNet, nettAmount, remainingAmount, }; }, [clientInvoices, clientAdjustments]); const groupedInvoices = useMemo(() => buildInvoiceTree(clientInvoices ?? []), [clientInvoices]); const invoiceNoSummaries = useMemo(() => { const allInvoices: InvoiceRow[] = groupedInvoices.flatMap((invoice) => flattenInvoiceRows(invoice) ); const invoiceById = new Map(); allInvoices.forEach((invoice) => invoiceById.set(invoice.id, invoice)); // Group by invoice_no const grouped = new Map(); allInvoices.forEach((invoice) => { const key = invoice.invoice_no ?? "—"; if (!grouped.has(key)) { grouped.set(key, []); } grouped.get(key)!.push(invoice); }); // Create summary rows from grouped invoices const result: InvoiceNoSummaryRow[] = Array.from(grouped.entries()).map( ([invoice_no, payments]) => { // Check if any payment in this group is linked (child) const linkedPayment = payments.find( (payment) => payment.linked_invoice_id && invoiceById.has(payment.linked_invoice_id) ); return { invoice_no, payments: payments.sort((a, b) => (a.invoice_no ?? "").localeCompare(b.invoice_no ?? "", undefined, { numeric: true }) ), depth: linkedPayment ? 1 : 0, parentInvoiceNo: linkedPayment ? invoiceById.get(linkedPayment.linked_invoice_id!)?.invoice_no : undefined, }; } ); // Sort by parent, then by invoice_no return result.sort((a, b) => { const aParent = a.parentInvoiceNo ?? a.invoice_no; const bParent = b.parentInvoiceNo ?? b.invoice_no; const parentCompare = aParent.localeCompare(bParent, undefined, { numeric: true }); if (parentCompare !== 0) return parentCompare; return a.invoice_no.localeCompare(b.invoice_no, undefined, { numeric: true }); }); }, [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 (RM)", value: invoiceTotals.mediaFee, icon: IconDeviceTv, color: "grape", }, { label: "Net Amount (RM)", value: invoiceTotals.nettAmount, icon: IconWallet, color: "green", }, { label: "Billable Spend (Invoice)", value: invoiceTotals.billableInvoiceSpending, icon: IconCurrencyDollar, color: "indigo", }, { label: "Remaining Amount (RM)", value: invoiceTotals.remainingAmount, icon: IconChartBar, color: "teal", }, { label: "Adjustments (RM)", value: invoiceTotals.adjustmentNet, icon: IconChartBar, color: "cyan", }, { label: "Lifetime Spend (RM)", value: lifeTimeSpending, icon: IconCash, color: "orange", }, { label: "Total Management Fees (RM)", 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 = row.original.payments.some( (payment) => payment.is_credit_card ); return ( 0 ? "right" : "left"} noWrap > {cell.getValue()} {isCreditCard && ( )} ); }, }, { id: "billing_mode", header: "Billing", accessorFn: (row) => row.payments.every((payment) => payment.is_credit_card) ? "Credit Card" : row.payments.some((payment) => payment.is_credit_card) ? "Mixed" : "Pay to Us", Cell: ({ cell }) => { const value = cell.getValue(); return ( {value} ); }, }, { id: "budget_total", header: "Budget", accessorFn: (row) => row.payments.reduce((sum, payment) => sum + parseNumber(payment.media_fee), 0), Cell: ({ cell }) => `${currencyFormatter.format(parseNumber(cell.getValue() as number))}`, }, { id: "budget_after_tax", header: "Budget After Tax", accessorFn: (row) => row.payments.reduce((sum, payment) => sum + parseNumber(payment.nett_amount), 0), Cell: ({ cell }) => `${currencyFormatter.format(parseNumber(cell.getValue() as number))}`, }, { id: "management_total", header: "Management Fee", accessorFn: (row) => row.payments.reduce((sum, payment) => sum + parseNumber(payment.management_fee), 0), Cell: ({ cell }) => `${currencyFormatter.format(parseNumber(cell.getValue() as number))}`, }, { id: "grand_total_invoice", header: "Grand Total Invoice", accessorFn: (row) => row.payments.reduce((sum, payment) => getPaymentAmount(payment), 0), Cell: ({ cell }) => `${currencyFormatter.format(parseNumber(cell.getValue() as number))}`, }, { id: "paid_amount", header: "Paid Amount", accessorFn: (row) => row.payments.reduce((sum, payment) => payment.is_paid ? getPaymentAmount(payment) : 0, 0), Cell: ({ cell }) => `${currencyFormatter.format(parseNumber(cell.getValue() as number))}`, }, { id: "outstanding", header: "Outstanding", accessorFn: (row) => { const total = row.payments.reduce((sum, payment) => sum + getPaymentAmount(payment), 0); const paid = row.payments.reduce((sum, payment) => sum + (payment.is_paid ? getPaymentAmount(payment) : 0), 0); return Math.max(0, total - paid); }, Cell: ({ cell }) => `${currencyFormatter.format(parseNumber(cell.getValue() as number))}`, }, { id: "start_date", header: "Start Date", accessorFn: (row) => minDateString(row.payments.map(p => p.start_date)), Cell: ({ cell }) => cell.getValue() ?? "—", }, { id: "end_date", header: "End Date", accessorFn: (row) => maxDateString(row.payments.map(p => p.end_date)), Cell: ({ cell }) => cell.getValue() ?? "—", }, { id: "spending", header: "Spending", accessorFn: (row) => row.payments.reduce((sum, payment) => sum + getInvoiceSpending(payment), 0), Cell: ({ cell }) => `${currencyFormatter.format(parseNumber(cell.getValue() as number))}`, }, { id: "remaining_media", header: "Remaining", accessorFn: (row) => { const nettAmount = row.payments.reduce( (sum, payment) => sum + getBillableNettAmount(payment), 0 ); const spending = row.payments.reduce( (sum, payment) => sum + getBillableInvoiceSpending(payment), 0 ); return Math.max(0, nettAmount - spending); }, 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.payments[0] } as any }), }, ], [] ); const paymentDetailColumns = useMemo[]>( () => [ { accessorKey: "invoice_no", header: "Invoice No", }, { accessorKey: "payment_no", header: "Payment No", Cell: ({ cell }) => cell.getValue() ?? "—", }, { id: "paid_amount", header: "Paid Amount", accessorFn: (row) => getPaymentAmount(row), Cell: ({ cell }) => `${currencyFormatter.format(parseNumber(cell.getValue() as number))}`, }, // { // id: "payment_date", // header: "Payment Date", // accessorFn: (row) => row.start_date, // Cell: ({ cell }) => cell.getValue() ?? "—", // }, ], [] ); 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", }} /> Adjustments