build ok,
This commit is contained in:
202
002_source/cms/src/app/dashboard/invoices/[invoiceId]/page.tsx
Normal file
202
002_source/cms/src/app/dashboard/invoices/[invoiceId]/page.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
import * as React from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
import RouterLink from 'next/link';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Link from '@mui/material/Link';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Grid from '@mui/material/Unstable_Grid2';
|
||||
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
|
||||
|
||||
import { config } from '@/config';
|
||||
import { paths } from '@/paths';
|
||||
import { dayjs } from '@/lib/dayjs';
|
||||
import { DynamicLogo } from '@/components/core/logo';
|
||||
import { InvoicePDFLink } from '@/components/dashboard/invoice/invoice-pdf-link';
|
||||
import { LineItemsTable } from '@/components/dashboard/invoice/line-items-table';
|
||||
import type { LineItem } from '@/components/dashboard/invoice/line-items-table';
|
||||
|
||||
export const metadata = { title: `Details | Invoices | Dashboard | ${config.site.name}` } satisfies Metadata;
|
||||
|
||||
const lineItems = [
|
||||
{ id: 'LI-001', name: 'Pro Subscription', quantity: 1, currency: 'USD', unitAmount: 14.99, totalAmount: 14.99 },
|
||||
] satisfies LineItem[];
|
||||
|
||||
export default function Page(): React.JSX.Element {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: 'var(--Content-maxWidth)',
|
||||
m: 'var(--Content-margin)',
|
||||
p: 'var(--Content-padding)',
|
||||
width: 'var(--Content-width)',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={4}>
|
||||
<Stack spacing={3}>
|
||||
<div>
|
||||
<Link
|
||||
color="text.primary"
|
||||
component={RouterLink}
|
||||
href={paths.dashboard.invoices.list}
|
||||
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
||||
variant="subtitle2"
|
||||
>
|
||||
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
||||
Invoices
|
||||
</Link>
|
||||
</div>
|
||||
<Stack direction="row" spacing={3} sx={{ alignItems: 'flex-start', justifyContent: 'space-between' }}>
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="h4">INV-001</Typography>
|
||||
<div>
|
||||
<Chip color="warning" label="Pending" variant="soft" />
|
||||
</div>
|
||||
</Stack>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
||||
<InvoicePDFLink invoice={undefined}>
|
||||
<Button color="secondary">Download</Button>
|
||||
</InvoicePDFLink>
|
||||
<Button component="a" href={paths.pdf.invoice('1')} target="_blank" variant="contained">
|
||||
Preview
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Card sx={{ p: 6 }}>
|
||||
<Stack spacing={6}>
|
||||
<Stack direction="row" spacing={3} sx={{ alignItems: 'flex-start' }}>
|
||||
<Box sx={{ flex: '1 1 auto' }}>
|
||||
<Typography variant="h4">Invoice</Typography>
|
||||
</Box>
|
||||
<Box sx={{ flex: '0 0 auto' }}>
|
||||
<DynamicLogo colorDark="light" colorLight="dark" emblem height={60} width={60} />
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack spacing={1}>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
||||
<Box sx={{ flex: '0 1 150px' }}>
|
||||
<Typography variant="subtitle2">Number:</Typography>
|
||||
</Box>
|
||||
<div>
|
||||
<Typography variant="body2">INV-008</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
||||
<Box sx={{ flex: '0 1 150px' }}>
|
||||
<Typography variant="subtitle2">Due date:</Typography>
|
||||
</Box>
|
||||
<div>
|
||||
<Typography variant="body2">{dayjs().add(15, 'day').format('MMM D,YYYY')}</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
||||
<Box sx={{ flex: '0 1 150px' }}>
|
||||
<Typography variant="subtitle2">Issue date:</Typography>
|
||||
</Box>
|
||||
<div>
|
||||
<Typography variant="body2">{dayjs().subtract(1, 'hour').format('MMM D, YYYY')}</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
||||
<Box sx={{ flex: '0 1 150px' }}>
|
||||
<Typography variant="subtitle2">Issuer VAT No:</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2">RO4675933</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Grid container spacing={3}>
|
||||
<Grid md={6} xs={12}>
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="subtitle1">Devias IO</Typography>
|
||||
<Typography variant="body2">
|
||||
2674 Alfred Drive
|
||||
<br />
|
||||
Brooklyn, New York, United States
|
||||
<br />
|
||||
11206
|
||||
<br />
|
||||
accounts@devias.io
|
||||
<br />
|
||||
(+1) 757 737 1980
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid md={6} xs={12}>
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="subtitle1">Billed to</Typography>
|
||||
<Typography variant="body2">
|
||||
Miron Vitold
|
||||
<br />
|
||||
Acme Inc.
|
||||
<br />
|
||||
1721 Bartlett Avenue
|
||||
<br />
|
||||
Southfield, Michigan, United States
|
||||
<br />
|
||||
48034
|
||||
<br />
|
||||
RO8795621
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<div>
|
||||
<Typography variant="h5">
|
||||
{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(19.99)} due{' '}
|
||||
{dayjs().add(15, 'day').format('MMM D, YYYY')}
|
||||
</Typography>
|
||||
</div>
|
||||
<Stack spacing={2}>
|
||||
<Card sx={{ borderRadius: 1, overflowX: 'auto' }} variant="outlined">
|
||||
<LineItemsTable rows={lineItems} />
|
||||
</Card>
|
||||
<Stack spacing={2}>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', justifyContent: 'flex-end' }}>
|
||||
<Box sx={{ flex: '0 1 150px' }}>
|
||||
<Typography>Subtotal</Typography>
|
||||
</Box>
|
||||
<Box sx={{ flex: '0 1 100px', textAlign: 'right' }}>
|
||||
<Typography>
|
||||
{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(14.99)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', justifyContent: 'flex-end' }}>
|
||||
<Box sx={{ flex: '0 1 150px' }}>
|
||||
<Typography>Tax</Typography>
|
||||
</Box>
|
||||
<Box sx={{ flex: '0 1 100px', textAlign: 'right' }}>
|
||||
<Typography>
|
||||
{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(5)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', justifyContent: 'flex-end' }}>
|
||||
<Box sx={{ flex: '0 1 150px' }}>
|
||||
<Typography variant="h6">Total</Typography>
|
||||
</Box>
|
||||
<Box sx={{ flex: '0 1 100px', textAlign: 'right' }}>
|
||||
<Typography variant="h6">
|
||||
{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(19.99)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="h6">Notes</Typography>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
Please make sure you have the right bank registration number as I had issues before and make sure you
|
||||
cover transfer expenses.
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
48
002_source/cms/src/app/dashboard/invoices/create/page.tsx
Normal file
48
002_source/cms/src/app/dashboard/invoices/create/page.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import * as React from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
import RouterLink from 'next/link';
|
||||
import Box from '@mui/material/Box';
|
||||
import Link from '@mui/material/Link';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
|
||||
|
||||
import { config } from '@/config';
|
||||
import { paths } from '@/paths';
|
||||
import { InvoiceCreateForm } from '@/components/dashboard/invoice/invoice-create-form';
|
||||
|
||||
export const metadata = { title: `Create | Invoices | Dashboard | ${config.site.name}` } satisfies Metadata;
|
||||
|
||||
export default function Page(): React.JSX.Element {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: 'var(--Content-maxWidth)',
|
||||
m: 'var(--Content-margin)',
|
||||
p: 'var(--Content-padding)',
|
||||
width: 'var(--Content-width)',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={4}>
|
||||
<Stack spacing={3}>
|
||||
<div>
|
||||
<Link
|
||||
color="text.primary"
|
||||
component={RouterLink}
|
||||
href={paths.dashboard.invoices.list}
|
||||
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
||||
variant="subtitle2"
|
||||
>
|
||||
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
||||
Invoices
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="h4">Create invoice</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
<InvoiceCreateForm />
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
175
002_source/cms/src/app/dashboard/invoices/page.tsx
Normal file
175
002_source/cms/src/app/dashboard/invoices/page.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
import * as React from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
||||
|
||||
import { config } from '@/config';
|
||||
import { dayjs } from '@/lib/dayjs';
|
||||
import { InvoicesFiltersCard } from '@/components/dashboard/invoice//invoices-filters-card';
|
||||
import { InvoicesPagination } from '@/components/dashboard/invoice//invoices-pagination';
|
||||
import { InvoicesStats } from '@/components/dashboard/invoice//invoices-stats';
|
||||
import { InvoicesTable } from '@/components/dashboard/invoice//invoices-table';
|
||||
import { ViewModeButton } from '@/components/dashboard/invoice//view-mode-button';
|
||||
import type { Filters } from '@/components/dashboard/invoice/invoices-filters';
|
||||
import { InvoicesFiltersButton } from '@/components/dashboard/invoice/invoices-filters-button';
|
||||
import { InvoicesSort } from '@/components/dashboard/invoice/invoices-sort';
|
||||
import type { Invoice } from '@/components/dashboard/invoice/invoices-table';
|
||||
|
||||
export const metadata = { title: `List | Invoices | Dashboard | ${config.site.name}` } satisfies Metadata;
|
||||
|
||||
const invoices = [
|
||||
{
|
||||
id: 'INV-005',
|
||||
customer: { name: 'Jie Yan', avatar: '/assets/avatar-8.png' },
|
||||
currency: 'USD',
|
||||
totalAmount: 23.11,
|
||||
status: 'pending',
|
||||
issueDate: dayjs().subtract(1, 'hour').toDate(),
|
||||
dueDate: dayjs().add(25, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'INV-004',
|
||||
customer: { name: 'Omar Darobe', avatar: '/assets/avatar-11.png' },
|
||||
currency: 'USD',
|
||||
totalAmount: 253.76,
|
||||
status: 'paid',
|
||||
issueDate: dayjs().subtract(2, 'hour').subtract(4, 'day').toDate(),
|
||||
dueDate: dayjs().add(17, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'INV-003',
|
||||
customer: { name: 'Carson Darrin', avatar: '/assets/avatar-3.png' },
|
||||
currency: 'USD',
|
||||
totalAmount: 781.5,
|
||||
status: 'canceled',
|
||||
issueDate: dayjs().subtract(4, 'hour').subtract(6, 'day').toDate(),
|
||||
dueDate: dayjs().add(11, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'INV-002',
|
||||
customer: { name: 'Fran Perez', avatar: '/assets/avatar-5.png' },
|
||||
currency: 'USD',
|
||||
totalAmount: 96.64,
|
||||
status: 'paid',
|
||||
issueDate: dayjs().subtract(2, 'hour').subtract(15, 'day').toDate(),
|
||||
dueDate: dayjs().add(3, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'INV-001',
|
||||
customer: { name: 'Miron Vitold', avatar: '/assets/avatar-1.png' },
|
||||
currency: 'USD',
|
||||
totalAmount: 19.99,
|
||||
status: 'pending',
|
||||
issueDate: dayjs().subtract(2, 'hour').subtract(15, 'day').toDate(),
|
||||
dueDate: dayjs().add(1, 'day').toDate(),
|
||||
},
|
||||
] satisfies Invoice[];
|
||||
|
||||
interface PageProps {
|
||||
searchParams: {
|
||||
customer?: string;
|
||||
endDate?: string;
|
||||
id?: string;
|
||||
sortDir?: 'asc' | 'desc';
|
||||
startDate?: string;
|
||||
status?: string;
|
||||
view?: 'group' | 'list';
|
||||
};
|
||||
}
|
||||
|
||||
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
const { customer, endDate, id, sortDir, startDate, status, view = 'group' } = searchParams;
|
||||
|
||||
const filters = { customer, endDate, id, startDate, status };
|
||||
|
||||
const sortedInvoices = applySort(invoices, sortDir);
|
||||
const filteredInvoices = applyFilters(sortedInvoices, filters);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: 'var(--Content-maxWidth)',
|
||||
m: 'var(--Content-margin)',
|
||||
p: 'var(--Content-padding)',
|
||||
width: 'var(--Content-width)',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={4}>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
|
||||
<Box sx={{ flex: '1 1 auto' }}>
|
||||
<Typography variant="h4">Invoices</Typography>
|
||||
</Box>
|
||||
<div>
|
||||
<Button startIcon={<PlusIcon />} variant="contained">
|
||||
New
|
||||
</Button>
|
||||
</div>
|
||||
</Stack>
|
||||
<InvoicesStats />
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', justifyContent: 'flex-end' }}>
|
||||
<InvoicesFiltersButton filters={filters} sortDir={sortDir} view={view} />
|
||||
<InvoicesSort filters={filters} sortDir={sortDir} view={view} />
|
||||
<ViewModeButton view={view} />
|
||||
</Stack>
|
||||
<Stack direction="row" spacing={4} sx={{ alignItems: 'flex-start' }}>
|
||||
<InvoicesFiltersCard filters={filters} sortDir={sortDir} view={view} />
|
||||
<Stack spacing={4} sx={{ flex: '1 1 auto', minWidth: 0 }}>
|
||||
<InvoicesTable rows={filteredInvoices} view={view} />
|
||||
<InvoicesPagination count={filteredInvoices.length} page={0} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Sorting and filtering has to be done on the server.
|
||||
|
||||
function applySort(row: Invoice[], sortDir: 'asc' | 'desc' | undefined): Invoice[] {
|
||||
return row.sort((a, b) => {
|
||||
if (sortDir === 'asc') {
|
||||
return a.issueDate.getTime() - b.issueDate.getTime();
|
||||
}
|
||||
|
||||
return b.issueDate.getTime() - a.issueDate.getTime();
|
||||
});
|
||||
}
|
||||
|
||||
function applyFilters(row: Invoice[], { customer, id, endDate, status, startDate }: Filters): Invoice[] {
|
||||
return row.filter((item) => {
|
||||
if (id) {
|
||||
if (!item.id.toLowerCase().includes(id.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (status) {
|
||||
if (item.status !== status) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (customer) {
|
||||
if (!item.customer.name?.toLowerCase().includes(customer.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (startDate) {
|
||||
if (dayjs(item.issueDate).isBefore(dayjs(startDate))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
if (dayjs(item.issueDate).isAfter(dayjs(endDate).add(1, 'day'))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user