inspiren-sem-tool/resources/js/pages/campaigns/index.tsx

285 lines
9.1 KiB
TypeScript

import { Client } from '@/types';
import { dateDisplay } from '@/utils/datetime';
import { Link, router } from '@inertiajs/react';
import {
ActionIcon,
Badge,
Button,
Container,
Group,
Stack,
Tabs,
Text,
Title,
} from '@mantine/core';
import { IconEye, IconRefresh } from '@tabler/icons-react';
import type { MRT_Row } from 'mantine-react-table';
import { MantineReactTable } from 'mantine-react-table';
import React, { useMemo } from 'react';
import AppLayout from '../../layouts/app-layout';
interface Props {
clients: Client[];
googleCompanySyncRunning: boolean;
}
const currencyFormatter = new Intl.NumberFormat('en-MY', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
const parseAmount = (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 formatDate = (value?: string | null): string =>
value ? dateDisplay(value) : '—';
const statusOrder = ['ENABLED', 'PAUSED', 'REMOVED', 'ENDED', 'CANCELED'];
const getStatusColor = (status: string) => {
if (status === 'ENABLED') {
return 'green';
}
if (status === 'PAUSED') {
return 'yellow';
}
if (status === 'ENDED' || status === 'CANCELED' || status === 'REMOVED') {
return 'red';
}
return 'gray';
};
export default function TicketDetails({
clients,
googleCompanySyncRunning,
}: Props) {
const [syncing, setSyncing] = React.useState(false);
const [activeStatus, setActiveStatus] = React.useState<string | null>(
'all',
);
const campaignsData = clients ?? [];
const statusTabs = useMemo(() => {
const counts = campaignsData.reduce<Record<string, number>>(
(acc, client) => {
const status = client.status || 'UNKNOWN';
acc[status] = (acc[status] ?? 0) + 1;
return acc;
},
{},
);
return Object.entries(counts).sort(([a], [b]) => {
const aIndex = statusOrder.indexOf(a);
const bIndex = statusOrder.indexOf(b);
if (aIndex !== -1 || bIndex !== -1) {
return (
(aIndex === -1 ? Number.MAX_SAFE_INTEGER : aIndex) -
(bIndex === -1 ? Number.MAX_SAFE_INTEGER : bIndex)
);
}
return a.localeCompare(b);
});
}, [campaignsData]);
const filteredClients = useMemo(() => {
if (!activeStatus || activeStatus === 'all') {
return campaignsData;
}
return campaignsData.filter(
(client) => (client.status || 'UNKNOWN') === activeStatus,
);
}, [activeStatus, campaignsData]);
const columns = useMemo(
() => [
{
accessorKey: 'customer_id',
header: 'Google ID',
size: 150,
},
{
accessorKey: 'sql_acc_code',
header: 'Customer Code',
size: 170,
},
{
accessorKey: 'name',
header: 'Google Account Name',
size: 320,
},
{
accessorKey: 'status',
header: 'Status',
size: 130,
Cell: ({ cell }: any) => {
const value = cell.getValue() as string;
return <Badge color={getStatusColor(value)}>{value}</Badge>;
},
},
{
accessorKey: 'latest_remaining_amount',
header: 'Remaining Amount',
size: 180,
Cell: ({ cell }: any) => {
const amount = parseAmount(cell.getValue());
return (
<Text color={amount < 0 ? 'red' : 'green'} weight={700}>
{currencyFormatter.format(amount)}
</Text>
);
},
},
{
accessorKey: 'latest_start_date',
header: 'Latest Start Date',
size: 180,
Cell: ({ cell }: any) => formatDate(cell.getValue()),
},
{
accessorKey: 'latest_end_date',
header: 'Latest End Date',
size: 180,
Cell: ({ cell }: any) => formatDate(cell.getValue()),
},
{
accessorKey: 'latest_remaining_days',
header: 'Remaining Days',
size: 160,
Cell: ({ cell }: any) => cell.getValue() ?? '—',
},
{
accessorKey: 'industry',
header: 'Industry',
size: 180,
},
{
accessorKey: 'assigned_person',
header: 'Assigned Person',
size: 180,
Cell: ({ cell }: any) => cell.getValue() ?? '—',
},
{
accessorKey: 'sales_person',
header: 'Sales Person',
size: 180,
Cell: ({ cell }: any) => cell.getValue() ?? '—',
},
],
[],
);
const renderRowActions = ({ row }: { row: MRT_Row<Client> }) => {
const client = row.original;
return (
<Group spacing="xs">
<ActionIcon
color="blue"
component={Link}
href={`/google-ads/accounts/${client.customer_id}`}
>
<IconEye size={18} />
</ActionIcon>
</Group>
);
};
const syncGoogleRecords = () => {
if (syncing || googleCompanySyncRunning) {
return;
}
setSyncing(true);
router.post(
route('google-ads.accounts.sync-google-company-details'),
{},
{
preserveScroll: true,
onFinish: () => setSyncing(false),
},
);
};
return (
<AppLayout>
<Container fluid px="xs">
<Group position="apart" mb="md">
<Stack spacing={2}>
<Title order={2}>Clients</Title>
<Text size="sm" color="dimmed">
Sync Google account records before linking pending
invoices to new clients.
</Text>
</Stack>
<Button
leftIcon={<IconRefresh size={16} />}
onClick={syncGoogleRecords}
loading={syncing}
disabled={googleCompanySyncRunning}
>
{googleCompanySyncRunning
? 'Sync running'
: 'Sync Google records'}
</Button>
</Group>
<MantineReactTable
columns={columns}
data={filteredClients}
enableRowActions // REQUIRED
positionActionsColumn="first" // optional but recommended
renderRowActions={renderRowActions}
mantinePaperProps={{
style: { width: '100%' },
}}
mantineTableContainerProps={{
style: { width: '100%' },
}}
renderTopToolbarCustomActions={() => (
<Tabs
value={activeStatus}
onTabChange={setActiveStatus}
>
<Tabs.List>
<Tabs.Tab value="all">
All ({campaignsData.length})
</Tabs.Tab>
{statusTabs.map(([status, count]) => (
<Tabs.Tab key={status} value={status}>
<Group spacing={6}>
<Badge
color={getStatusColor(status)}
variant="dot"
>
{status}
</Badge>
<Text size="sm" color="dimmed">
{count}
</Text>
</Group>
</Tabs.Tab>
))}
</Tabs.List>
</Tabs>
)}
/>
</Container>
</AppLayout>
);
}