inspiren-sem-tool/resources/js/layouts/app-layout.tsx

1278 lines
52 KiB
TypeScript

import {
ActionIcon,
Anchor,
AppShell,
Avatar,
Badge,
Box,
Burger,
Button,
Container,
Group,
Header,
Indicator,
Loader,
MediaQuery,
Menu,
Modal,
Navbar,
Stack,
Text,
Tooltip,
useMantineTheme,
} from '@mantine/core';
import React from 'react';
import { SidebarProvider } from '@/components/ui/sidebar';
import { logout } from '@/routes';
import { edit } from '@/routes/profile';
import {
ClientInvoicePayment,
ClientInvoicePaymentItem,
SharedData,
} from '@/types';
import { Link, router, usePage } from '@inertiajs/react';
import { notifications } from '@mantine/notifications';
import {
IconAlertCircle,
IconBell,
IconCircleX,
IconFileDollar,
IconInfoCircle,
IconLogout,
IconSettings,
IconUserPlus,
} from '@tabler/icons-react';
import {
MantineReactTable,
type MRT_ColumnDef,
type MRT_Row,
} from 'mantine-react-table';
import { useEffect } from 'react';
import Sidebar from '../components/sidebar';
import ThemeToggle from '../components/theme-toggle';
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;
is_credit_card?: boolean;
is_paid?: boolean;
payment_no: string | null;
amount: string | number | null;
management_fee: string | number | null;
management_fee_amount?: string | number | null;
management_fee_tax?: string | number | null;
media_fee: string | number | null;
media_fee_amount?: string | number | null;
media_fee_tax?: string | number | null;
tax_percent?: string | number | null;
nett_amount?: string | number | null;
total_net_amount?: string | number | null;
created_at: string | null;
payments?: ClientInvoicePayment[];
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 CurrentPaymentItemRecord = ClientInvoicePaymentItem & {
payment_no: 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 parseAmount = (amount: string | number | null | undefined) => {
const value = Number(amount ?? 0);
return Number.isFinite(value) ? value : 0;
};
const getPaymentRows = (
invoice: PendingInvoiceNotification,
): ClientInvoicePayment[] => {
if (invoice.payments?.length) {
return invoice.payments;
}
const invoiceAmount =
parseAmount(invoice.amount) ||
parseAmount(invoice.management_fee) +
parseAmount(invoice.media_fee);
const taxAmount = Math.max(
0,
parseAmount(invoice.management_fee_tax) +
parseAmount(invoice.media_fee_tax),
);
const netAmount =
parseAmount(invoice.total_net_amount) ||
parseAmount(invoice.management_fee_amount) +
parseAmount(invoice.media_fee_amount) ||
Math.max(0, invoiceAmount - taxAmount);
return [
{
payment_no: invoice.payment_no,
payment_total_amount: invoiceAmount,
payment_tax_percentage: parseAmount(invoice.tax_percent),
payment_nett_amount: netAmount,
items: [],
},
];
};
const getPaymentTaxAmount = (payment: ClientInvoicePayment) => {
const totalAmount = parseAmount(payment.payment_total_amount);
const nettAmount = parseAmount(payment.payment_nett_amount);
if (totalAmount > 0 || nettAmount > 0) {
return Math.max(0, totalAmount - nettAmount);
}
return (payment.items ?? []).reduce(
(sum, item) =>
sum +
Math.max(
0,
parseAmount(item.payment_item_amount) -
parseAmount(item.net_amount),
),
0,
);
};
const getInvoiceAmount = (invoice: PendingInvoiceNotification) => {
const directAmount = parseAmount(invoice.amount);
if (directAmount > 0) {
return directAmount;
}
const paymentTotal = getPaymentRows(invoice).reduce(
(sum, payment) => sum + parseAmount(payment.payment_total_amount),
0,
);
return (
paymentTotal ||
parseAmount(invoice.management_fee) + parseAmount(invoice.media_fee)
);
};
const getInvoiceTaxAmount = (invoice: PendingInvoiceNotification) => {
const paymentTaxTotal = getPaymentRows(invoice).reduce(
(sum, payment) => sum + getPaymentTaxAmount(payment),
0,
);
return (
paymentTaxTotal ||
Math.max(
0,
parseAmount(invoice.management_fee_tax) +
parseAmount(invoice.media_fee_tax),
)
);
};
const getInvoiceNetAmount = (invoice: PendingInvoiceNotification) => {
const directNetAmount =
parseAmount(invoice.total_net_amount) ||
parseAmount(invoice.nett_amount);
if (directNetAmount > 0) {
return directNetAmount;
}
const paymentNetTotal = getPaymentRows(invoice).reduce(
(sum, payment) => sum + parseAmount(payment.payment_nett_amount),
0,
);
return (
paymentNetTotal ||
parseAmount(invoice.management_fee_amount) +
parseAmount(invoice.media_fee_amount)
);
};
const getPaidAmount = (invoice: PendingInvoiceNotification) =>
getPaymentRows(invoice).reduce(
(sum, payment) => sum + parseAmount(payment.payment_total_amount),
0,
);
const getOutstandingAmount = (invoice: PendingInvoiceNotification) =>
Math.max(0, getInvoiceAmount(invoice) - getPaidAmount(invoice));
const getPaymentItemRows = (
invoice: PendingInvoiceNotification,
): CurrentPaymentItemRecord[] => {
const paymentItems = getPaymentRows(invoice).flatMap((payment) =>
(payment.items ?? []).map((item) => ({
...item,
payment_no: payment.payment_no,
})),
);
if (paymentItems.length > 0) {
return paymentItems;
}
return [
{
payment_no: invoice.payment_no,
billing_item_types_id: 0,
billing_item_type: {
id: 0,
name: 'Google Ads Search (Media Fee)',
sql_acc_code: 'G03',
nett_contribution: true,
fee_type: 'Media',
type: 'Google',
campaign_type: 'Search',
},
start_date: null,
end_date: null,
payment_item_amount: invoice.media_fee ?? 0,
tax_percentage: invoice.tax_percent ?? 0,
net_amount:
invoice.media_fee_amount ?? invoice.nett_amount ?? 0,
withholding_tax: 0,
final_net_amount:
invoice.total_net_amount ?? invoice.nett_amount ?? 0,
spending: 0,
is_creditcard: !!invoice.is_credit_card,
},
{
payment_no: invoice.payment_no,
billing_item_types_id: 0,
billing_item_type: {
id: 0,
name: 'Management Fee (Google Search Ads)',
sql_acc_code: 'GOOGLE',
nett_contribution: false,
fee_type: 'Management',
type: 'Google',
campaign_type: 'Search',
},
start_date: null,
end_date: null,
payment_item_amount: invoice.management_fee ?? 0,
tax_percentage: invoice.tax_percent ?? 0,
net_amount:
invoice.management_fee_amount ??
invoice.management_fee ??
0,
withholding_tax: 0,
final_net_amount:
invoice.management_fee_amount ??
invoice.management_fee ??
0,
spending: 0,
is_creditcard: false,
},
].filter((item) => parseAmount(item.payment_item_amount) > 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={`/client-invoices/${row.original.id}/edit`}
size="sm"
weight={700}
>
{row.original.invoice_no}
</Anchor>
<Text size="xs" color="dimmed">
{row.original.requires_client
? 'Link client before approval'
: '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>
// ),
// },
{
id: 'invoice_amount',
header: 'Invoice Amount',
accessorFn: (invoice) => getInvoiceAmount(invoice),
Cell: ({ row }) => (
<Text size="sm" weight={600} align="right">
{formatAmount(getInvoiceAmount(row.original))}
</Text>
),
},
{
id: 'tax_amount',
header: 'Tax Amount',
accessorFn: (invoice) => getInvoiceTaxAmount(invoice),
Cell: ({ row }) => (
<Text size="sm" weight={600} align="right">
{formatAmount(getInvoiceTaxAmount(row.original))}
</Text>
),
},
{
id: 'net_amount',
header: 'Net Amount',
accessorFn: (invoice) => getInvoiceNetAmount(invoice),
Cell: ({ row }) => (
<Text size="sm" weight={600} align="right">
{formatAmount(getInvoiceNetAmount(row.original))}
</Text>
),
},
{
id: 'paid_amount',
header: 'Paid Amount',
accessorFn: (invoice) => getPaidAmount(invoice),
Cell: ({ row }) => (
<Text size="sm" weight={600} align="right">
{formatAmount(getPaidAmount(row.original))}
</Text>
),
},
{
id: 'outstanding',
header: 'Outstanding',
accessorFn: (invoice) => getOutstandingAmount(invoice),
Cell: ({ row }) => (
<Text
size="sm"
weight={700}
align="right"
color={
getOutstandingAmount(row.original) > 0
? 'red'
: 'green'
}
>
{formatAmount(getOutstandingAmount(row.original))}
</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 renderPendingInvoiceDetailPanel = ({
row,
}: {
row: MRT_Row<PendingInvoiceNotification>;
}) => {
const records = row.original.previous_payments ?? [];
const paymentItems = getPaymentItemRows(row.original);
return (
<Box px="md" py="sm">
<Stack spacing="lg">
<Stack spacing="xs">
<Group position="apart">
<Text size="sm" weight={700}>
Payment Items
</Text>
<Badge variant="light">{paymentItems.length}</Badge>
</Group>
{paymentItems.length === 0 ? (
<Text size="sm" color="dimmed">
No payment items found for this invoice.
</Text>
) : (
<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',
}}
>
Item
</th>
<th
style={{
textAlign: 'left',
padding: '8px',
}}
>
SQL Code
</th>
<th
style={{
textAlign: 'left',
padding: '8px',
}}
>
Fee Type
</th>
<th
style={{
textAlign: 'right',
padding: '8px',
}}
>
Amount
</th>
<th
style={{
textAlign: 'right',
padding: '8px',
}}
>
Tax %
</th>
<th
style={{
textAlign: 'right',
padding: '8px',
}}
>
Net
</th>
</tr>
</thead>
<tbody>
{paymentItems.map((item, index) => (
<tr
key={`${item.payment_no ?? 'payment'}-${item.billing_item_types_id}-${index}`}
>
<td style={{ padding: '8px' }}>
{item.payment_no ?? '-'}
</td>
<td style={{ padding: '8px' }}>
{item.billing_item_type
?.name ?? '-'}
</td>
<td style={{ padding: '8px' }}>
{item.billing_item_type
?.sql_acc_code ?? '-'}
</td>
<td style={{ padding: '8px' }}>
<Badge
variant="light"
color={
item
.billing_item_type
?.fee_type ===
'Media'
? 'blue'
: 'grape'
}
>
{item.billing_item_type
?.fee_type ?? '-'}
</Badge>
</td>
<td
style={{
padding: '8px',
textAlign: 'right',
}}
>
{formatAmount(
item.payment_item_amount,
)}
</td>
<td
style={{
padding: '8px',
textAlign: 'right',
}}
>
{formatAmount(
item.tax_percentage,
)}
</td>
<td
style={{
padding: '8px',
textAlign: 'right',
}}
>
{formatAmount(
item.net_amount,
)}
</td>
</tr>
))}
</tbody>
</table>
</Box>
)}
</Stack>
<Stack spacing="xs">
<Group position="apart">
<Text size="sm" weight={700}>
Previous Payment Records
</Text>
<Badge variant="light">{records.length}</Badge>
</Group>
{records.length === 0 ? (
<Text size="sm" color="dimmed">
No previous payment records found for this
invoice.
</Text>
) : (
<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>
</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}/edit`}
color="blue"
variant="light"
>
<Tooltip
label="Edit invoice and 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 ?? []);
} 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={renderPendingInvoiceDetailPanel}
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>
);
}