734 lines
29 KiB
TypeScript
734 lines
29 KiB
TypeScript
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<AppNotification[]>([]);
|
|
const [invoices, setInvoices] = React.useState<PendingInvoiceNotification[]>([]);
|
|
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<MRT_ColumnDef<PendingInvoiceNotification>[]>(
|
|
() => [
|
|
{
|
|
accessorKey: 'invoice_no',
|
|
header: 'Invoice',
|
|
Cell: ({ row }) => (
|
|
<Stack spacing={1}>
|
|
<Anchor
|
|
component={Link}
|
|
href={
|
|
row.original.requires_client
|
|
? `/client-invoices/${row.original.id}/client/create`
|
|
: `/client-invoices/${row.original.id}/edit`
|
|
}
|
|
size="sm"
|
|
weight={700}
|
|
>
|
|
{row.original.invoice_no}
|
|
</Anchor>
|
|
<Text size="xs" color="dimmed">
|
|
{row.original.requires_client ? 'Client setup required' : 'Ready for review'}
|
|
</Text>
|
|
</Stack>
|
|
),
|
|
},
|
|
{
|
|
id: 'client',
|
|
header: 'Client',
|
|
accessorFn: (invoice) => invoice.client?.name ?? invoice.pending_client_name ?? '-',
|
|
Cell: ({ cell }) => (
|
|
<Text size="sm" lineClamp={1}>
|
|
{cell.getValue<string>()}
|
|
</Text>
|
|
),
|
|
},
|
|
{
|
|
id: 'sql_acc_code',
|
|
header: 'SQL Acc Code',
|
|
accessorFn: (invoice) => invoice.pending_sql_acc_code ?? '-',
|
|
Cell: ({ cell }) => <Text size="sm">{cell.getValue<string>()}</Text>,
|
|
},
|
|
// {
|
|
// id: 'invoice_management_fee',
|
|
// header: 'Invoice Management Fee',
|
|
// accessorFn: (invoice) => invoice.invoice_billing_totals?.management_fee ?? 0,
|
|
// Cell: ({ row }) => (
|
|
// <Text size="sm" weight={600} align="right">
|
|
// {formatAmount(row.original.invoice_billing_totals?.management_fee ?? 0)}
|
|
// </Text>
|
|
// ),
|
|
// },
|
|
// {
|
|
// id: 'invoice_media_fee',
|
|
// header: 'Invoice Media Fee',
|
|
// accessorFn: (invoice) => invoice.invoice_billing_totals?.media_fee ?? 0,
|
|
// Cell: ({ row }) => (
|
|
// <Text size="sm" weight={600} align="right">
|
|
// {formatAmount(row.original.invoice_billing_totals?.media_fee ?? 0)}
|
|
// </Text>
|
|
// ),
|
|
// },
|
|
{
|
|
accessorKey: 'management_fee',
|
|
header: 'Payment Management Fee (incl. tax)',
|
|
Cell: ({ row }) => (
|
|
<Text size="sm" weight={600} align="right">
|
|
{formatAmount(row.original.management_fee_amount)}
|
|
</Text>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'management_fee_tax',
|
|
header: 'Payment Management Tax',
|
|
Cell: ({ row }) => (
|
|
<Text size="sm" weight={600} align="right">
|
|
{formatAmount(row.original.management_fee_tax)}
|
|
</Text>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'management_fee_nett',
|
|
header: 'Payment Management Nett',
|
|
Cell: ({ row }) => (
|
|
<Text size="sm" weight={600} align="right">
|
|
{formatAmount(row.original.management_fee)}
|
|
</Text>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'media_fee',
|
|
header: 'Payment Media Fee (incl. tax)',
|
|
Cell: ({ row }) => (
|
|
<Text size="sm" weight={600} align="right">
|
|
{formatAmount(row.original.media_fee_amount)}
|
|
</Text>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'media_fee_tax',
|
|
header: 'Payment Media Tax',
|
|
Cell: ({ row }) => (
|
|
<Text size="sm" weight={600} align="right">
|
|
{formatAmount(row.original.media_fee_tax)}
|
|
</Text>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'media_fee_nett',
|
|
header: 'Payment Media Nett',
|
|
Cell: ({ row }) => (
|
|
<Text size="sm" weight={600} align="right">
|
|
{formatAmount(row.original.media_fee)}
|
|
</Text>
|
|
),
|
|
},
|
|
{
|
|
id: 'payment_date',
|
|
header: 'Payment Date',
|
|
accessorFn: (invoice) => invoice.created_at,
|
|
Cell: ({ row }) => (
|
|
<Text size="sm">
|
|
{formatDate(row.original.created_at)}
|
|
</Text>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'created_at',
|
|
header: 'Created',
|
|
Cell: ({ row }) => (
|
|
<Text size="sm">
|
|
{formatDate(row.original.created_at)}
|
|
</Text>
|
|
),
|
|
},
|
|
],
|
|
[],
|
|
);
|
|
|
|
const renderPreviousPaymentsPanel = ({ row }: { row: MRT_Row<PendingInvoiceNotification> }) => {
|
|
const records = row.original.previous_payments ?? [];
|
|
|
|
if (records.length === 0) {
|
|
return (
|
|
<Box px="md" py="sm">
|
|
<Text size="sm" color="dimmed">
|
|
No previous payment records found for this invoice.
|
|
</Text>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Box px="md" py="sm">
|
|
<Stack spacing="xs">
|
|
<Group position="apart">
|
|
<Text size="sm" weight={700}>
|
|
Previous Payment Records
|
|
</Text>
|
|
<Badge variant="light">{records.length}</Badge>
|
|
</Group>
|
|
<Box sx={{ overflowX: 'auto' }}>
|
|
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
|
<thead>
|
|
<tr>
|
|
<th style={{ textAlign: 'left', padding: '8px' }}>Payment No</th>
|
|
<th style={{ textAlign: 'left', padding: '8px' }}>Status</th>
|
|
<th style={{ textAlign: 'left', padding: '8px' }}>Payment Date</th>
|
|
<th style={{ textAlign: 'right', padding: '8px' }}>Media Fee</th>
|
|
<th style={{ textAlign: 'right', padding: '8px' }}>Management Fee</th>
|
|
<th style={{ textAlign: 'right', padding: '8px' }}>Invoice Media</th>
|
|
<th style={{ textAlign: 'right', padding: '8px' }}>Invoice Management</th>
|
|
<th style={{ textAlign: 'right', padding: '8px' }}>Total</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{records.map((record, index) => (
|
|
<tr key={`${record.payment_number ?? 'payment'}-${index}`}>
|
|
<td style={{ padding: '8px' }}>{record.payment_number ?? '-'}</td>
|
|
<td style={{ padding: '8px' }}>
|
|
<Badge color={record.status === 'Processed' ? 'green' : 'gray'} variant="light">
|
|
{record.status ?? '-'}
|
|
</Badge>
|
|
</td>
|
|
<td style={{ padding: '8px' }}>{formatDate(record.sql_created_at)}</td>
|
|
<td style={{ padding: '8px', textAlign: 'right' }}>
|
|
{formatAmount(record.media_fee)}
|
|
</td>
|
|
<td style={{ padding: '8px', textAlign: 'right' }}>
|
|
{formatAmount(record.management_fee)}
|
|
</td>
|
|
<td style={{ padding: '8px', textAlign: 'right' }}>
|
|
{formatAmount(record.invoice_media_fee)}
|
|
</td>
|
|
<td style={{ padding: '8px', textAlign: 'right' }}>
|
|
{formatAmount(record.invoice_management_fee)}
|
|
</td>
|
|
<td style={{ padding: '8px', textAlign: 'right', fontWeight: 700 }}>
|
|
{formatAmount(record.amount)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</Box>
|
|
</Stack>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const renderPendingInvoiceActions = ({ row }: { row: MRT_Row<PendingInvoiceNotification> }) => (
|
|
<Group spacing="xs" noWrap>
|
|
{row.original.requires_client ? (
|
|
<ActionIcon
|
|
component={Link}
|
|
href={`/client-invoices/${row.original.id}/client/create`}
|
|
color="blue"
|
|
variant="light"
|
|
>
|
|
<Tooltip label="Link client" withArrow withinPortal>
|
|
<IconUserPlus size={18} />
|
|
</Tooltip>
|
|
</ActionIcon>
|
|
) : null}
|
|
<ActionIcon
|
|
component="a"
|
|
href={`/client-invoices/pdf/invoice/${row.original.id}`}
|
|
target="_blank"
|
|
>
|
|
<Tooltip label="View Invoice" withArrow withinPortal>
|
|
<IconFileDollar color="green" />
|
|
</Tooltip>
|
|
</ActionIcon>
|
|
</Group>
|
|
);
|
|
|
|
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 (
|
|
<>
|
|
<Menu shadow="xl" width={360} position="bottom-end" withinPortal>
|
|
<Menu.Target>
|
|
<Indicator
|
|
label={count > 99 ? '99+' : count}
|
|
size={18}
|
|
disabled={count === 0}
|
|
color="red"
|
|
>
|
|
<ActionIcon
|
|
variant="light"
|
|
color="blue"
|
|
size="lg"
|
|
radius="xl"
|
|
aria-label="Notifications"
|
|
>
|
|
<IconBell size={20} />
|
|
</ActionIcon>
|
|
</Indicator>
|
|
</Menu.Target>
|
|
<Menu.Dropdown p={0}>
|
|
<Box
|
|
px="md"
|
|
py="sm"
|
|
sx={(theme) => ({
|
|
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],
|
|
})}
|
|
>
|
|
<Group position="apart" align="center">
|
|
<Stack spacing={2}>
|
|
<Text size="sm" weight={700}>
|
|
Notifications
|
|
</Text>
|
|
<Text size="xs" color="dimmed">
|
|
Updates grouped by notification type
|
|
</Text>
|
|
</Stack>
|
|
<Badge color={count > 0 ? 'red' : 'gray'} variant="filled">
|
|
{count} pending
|
|
</Badge>
|
|
</Group>
|
|
</Box>
|
|
{isLoadingNotifications ? (
|
|
<Group position="center" py="xl">
|
|
<Stack spacing="xs" align="center">
|
|
<Loader size="sm" />
|
|
<Text color="dimmed" size="sm">
|
|
Loading notifications...
|
|
</Text>
|
|
</Stack>
|
|
</Group>
|
|
) : notifications.length === 0 ? (
|
|
<Stack spacing={4} align="center" py="xl">
|
|
<Text weight={600} size="sm">
|
|
All caught up
|
|
</Text>
|
|
<Text color="dimmed" size="sm">
|
|
No notifications need your attention.
|
|
</Text>
|
|
</Stack>
|
|
) : (
|
|
<Stack spacing={0}>
|
|
{notifications.map((notification) => (
|
|
<Menu.Item
|
|
key={notification.type}
|
|
onClick={() => openNotification(notification)}
|
|
icon={<IconAlertCircle size={18} />}
|
|
rightSection={
|
|
<Badge color="red" variant="light">
|
|
{notification.count}
|
|
</Badge>
|
|
}
|
|
>
|
|
<Stack spacing={2}>
|
|
<Text size="sm" weight={700}>
|
|
{notification.title}
|
|
</Text>
|
|
<Text size="xs" color="dimmed">
|
|
{notification.description}
|
|
</Text>
|
|
</Stack>
|
|
</Menu.Item>
|
|
))}
|
|
</Stack>
|
|
)}
|
|
</Menu.Dropdown>
|
|
</Menu>
|
|
|
|
<Modal
|
|
opened={pendingInvoicesOpened}
|
|
onClose={() => setPendingInvoicesOpened(false)}
|
|
title="Pending invoice approvals"
|
|
size="xxl"
|
|
>
|
|
{isLoadingInvoices ? (
|
|
<Group position="center" py="xl">
|
|
<Stack spacing="xs" align="center">
|
|
<Loader size="sm" />
|
|
<Text color="dimmed" size="sm">
|
|
Loading pending invoices...
|
|
</Text>
|
|
</Stack>
|
|
</Group>
|
|
) : invoices.length === 0 ? (
|
|
<Stack spacing={4} align="center" py="xl">
|
|
<Text weight={600} size="sm">
|
|
All caught up
|
|
</Text>
|
|
<Text color="dimmed" size="sm">
|
|
No invoices are waiting for approval.
|
|
</Text>
|
|
</Stack>
|
|
) : (
|
|
<MantineReactTable
|
|
columns={pendingInvoiceColumns}
|
|
data={invoices}
|
|
renderRowActions={renderPendingInvoiceActions}
|
|
renderDetailPanel={renderPreviousPaymentsPanel}
|
|
enableExpanding
|
|
getRowCanExpand={() => true}
|
|
positionExpandColumn="first"
|
|
enableRowActions
|
|
positionActionsColumn="last"
|
|
enablePagination={false}
|
|
enableGlobalFilter={false}
|
|
enableColumnFilters={false}
|
|
enableTopToolbar={false}
|
|
enableBottomToolbar={false}
|
|
initialState={{ density: 'xs' }}
|
|
/>
|
|
)}
|
|
</Modal>
|
|
</>
|
|
);
|
|
}
|
|
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<SharedData>();
|
|
const { flash, auth } = page.props as any;
|
|
useEffect(() => {
|
|
|
|
if (flash['message-info']) {
|
|
notifications.show({
|
|
title: 'Info',
|
|
color: 'blue',
|
|
icon: <IconInfoCircle />,
|
|
message: flash['message-info'],
|
|
});
|
|
}
|
|
|
|
if (flash['message-warning']) {
|
|
notifications.show({
|
|
title: 'Warning',
|
|
color: 'yellow',
|
|
icon: <IconAlertCircle />,
|
|
message: flash['message-warning'],
|
|
});
|
|
}
|
|
|
|
if (flash['message-error']) {
|
|
notifications.show({
|
|
title: 'Error',
|
|
color: 'red',
|
|
icon: <IconCircleX />,
|
|
message: flash['message-error'],
|
|
});
|
|
}
|
|
|
|
}, [flash]);
|
|
return (
|
|
<SidebarProvider open={opened} onOpenChange={setOpened}>
|
|
<AppShell
|
|
padding="lg"
|
|
navbarOffsetBreakpoint="sm"
|
|
navbar={
|
|
<Navbar
|
|
width={{ base: 260 }}
|
|
hiddenBreakpoint="sm"
|
|
hidden={!opened}
|
|
p="md"
|
|
style={{
|
|
borderRight: `1px solid ${isDark ? theme.colors.dark[4] : theme.colors.gray[3]
|
|
}`,
|
|
}}
|
|
>
|
|
<Sidebar />
|
|
</Navbar>
|
|
}
|
|
header={
|
|
<Header
|
|
height={60}
|
|
px="lg"
|
|
style={{
|
|
backgroundColor: isDark ? theme.colors.dark[6] : theme.white,
|
|
borderBottom: `1px solid ${isDark ? theme.colors.dark[4] : theme.colors.gray[3]
|
|
}`,
|
|
}}
|
|
>
|
|
<Group position="apart" align="center" style={{ height: '100%' }}>
|
|
<Group spacing="sm">
|
|
<MediaQuery largerThan="sm" styles={{ display: 'none' }}>
|
|
<Burger
|
|
opened={opened}
|
|
onClick={() => setOpened((o) => !o)}
|
|
size="sm"
|
|
/>
|
|
</MediaQuery>
|
|
|
|
<Text weight={700} size="lg">
|
|
Inspiren SEM Tool
|
|
</Text>
|
|
</Group>
|
|
|
|
<Group spacing="sm">
|
|
<AppNotifications />
|
|
<ThemeToggle />
|
|
<Menu shadow="md" width={220}>
|
|
<Menu.Target>
|
|
<Button variant="subtle">
|
|
<Avatar color="gray" radius="xl" size="sm">
|
|
{auth.user.name
|
|
.split(' ')
|
|
.map((part: string) => part[0])
|
|
.join('')
|
|
.toUpperCase()}
|
|
</Avatar>
|
|
</Button>
|
|
</Menu.Target>
|
|
<Menu.Dropdown>
|
|
<Menu.Label>
|
|
<Stack spacing={0}>
|
|
<Text size="sm">{auth.user.name}</Text>
|
|
<Text size="xs" color="dimmed">
|
|
{auth.user.email}
|
|
</Text>
|
|
</Stack>
|
|
</Menu.Label>
|
|
<Menu.Item
|
|
icon={<IconSettings size={16} />}
|
|
component={Link}
|
|
href={edit()}
|
|
as="button"
|
|
prefetch
|
|
>
|
|
Settings
|
|
</Menu.Item>
|
|
<Menu.Item
|
|
icon={<IconLogout size={16} />}
|
|
onClick={() =>
|
|
router.post(logout(), {
|
|
preserveState: true,
|
|
})
|
|
}
|
|
>
|
|
Log out
|
|
</Menu.Item>
|
|
</Menu.Dropdown>
|
|
</Menu>
|
|
</Group>
|
|
</Group>
|
|
</Header>
|
|
}
|
|
styles={{
|
|
main: {
|
|
backgroundColor: isDark
|
|
? theme.colors.dark[7]
|
|
: theme.colors.gray[0],
|
|
minHeight: '100vh',
|
|
maxWidth: '100vw',
|
|
overflowX: 'hidden',
|
|
},
|
|
}}
|
|
>
|
|
<Container fluid px={{ base: 'xs', sm: 'md', lg: 'lg' }} style={{ maxWidth: '100%', overflowX: 'hidden' }}>
|
|
<Box
|
|
style={{
|
|
backgroundColor: isDark ? theme.colors.dark[6] : theme.white,
|
|
borderRadius: theme.radius.md,
|
|
padding: theme.spacing.lg,
|
|
boxShadow: theme.shadows.sm,
|
|
maxWidth: '100%',
|
|
minWidth: 0,
|
|
overflowX: 'auto',
|
|
}}
|
|
>
|
|
{children}
|
|
</Box>
|
|
</Container>
|
|
</AppShell>
|
|
</SidebarProvider>
|
|
);
|
|
}
|