Files
HKSingleParty/03_source/frontend/src/sections/invoice/invoice-new-edit-details.tsx

304 lines
8.3 KiB
TypeScript

import type { IInvoiceItem, IInvoiceItemItem } from 'src/types/invoice';
import { sumBy } from 'es-toolkit';
import { useEffect, useCallback } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Button from '@mui/material/Button';
import Divider from '@mui/material/Divider';
import MenuItem from '@mui/material/MenuItem';
import Typography from '@mui/material/Typography';
import InputAdornment from '@mui/material/InputAdornment';
import { inputBaseClasses } from '@mui/material/InputBase';
import { INVOICE_SERVICE_OPTIONS } from 'src/_mock';
import { Field } from 'src/components/hook-form';
import { Iconify } from 'src/components/iconify';
import { InvoiceTotalSummary } from './invoice-total-summary';
// ----------------------------------------------------------------------
export const defaultItem: Omit<IInvoiceItemItem, 'id'> = {
title: '',
description: '',
service: INVOICE_SERVICE_OPTIONS[0].name,
price: INVOICE_SERVICE_OPTIONS[0].price,
quantity: 1,
total: 0,
};
const getFieldNames = (index: number): Record<keyof typeof defaultItem, string> => ({
title: `items[${index}].title`,
description: `items[${index}].description`,
service: `items[${index}].service`,
quantity: `items[${index}].quantity`,
price: `items[${index}].price`,
total: `items[${index}].total`,
});
export function InvoiceNewEditDetails() {
const { control, setValue, getValues } = useFormContext();
const { fields, append, remove } = useFieldArray({ control, name: 'items' });
const items = getValues('items');
const taxes = getValues('taxes');
const discount = getValues('discount');
const shipping = getValues('shipping');
const subtotal = sumBy(items, (item: IInvoiceItemItem) => item.quantity * item.price);
const subtotalWithTax = subtotal + subtotal * (taxes / 100);
const totalAmount = subtotalWithTax - discount - shipping;
useEffect(() => {
setValue('subtotal', subtotal);
setValue('totalAmount', totalAmount);
}, [setValue, subtotal, totalAmount]);
return (
<Box sx={{ p: 3 }}>
<Typography variant="h6" sx={{ color: 'text.disabled', mb: 3 }}>
Details:
</Typography>
<Stack divider={<Divider flexItem sx={{ borderStyle: 'dashed' }} />} spacing={3}>
{fields.map((item, index) => (
<InvoiceItem
key={item.id}
fieldNames={getFieldNames(index)}
onRemoveItem={() => remove(index)}
/>
))}
</Stack>
<Divider sx={{ my: 3, borderStyle: 'dashed' }} />
<Box
sx={{
gap: 3,
display: 'flex',
flexDirection: { xs: 'column', md: 'row' },
alignItems: { xs: 'flex-end', md: 'center' },
}}
>
<Button
size="small"
color="primary"
startIcon={<Iconify icon="mingcute:add-line" />}
onClick={() => append(defaultItem)}
sx={{ flexShrink: 0 }}
>
Add Item
</Button>
<Box
sx={{
gap: 2,
width: 1,
display: 'flex',
justifyContent: 'flex-end',
flexDirection: { xs: 'column', md: 'row' },
}}
>
<Field.Text
size="small"
label="Shipping($)"
name="shipping"
type="number"
sx={{ maxWidth: { md: 120 } }}
slotProps={{ inputLabel: { shrink: true } }}
/>
<Field.Text
size="small"
label="Discount($)"
name="discount"
type="number"
sx={{ maxWidth: { md: 120 } }}
slotProps={{ inputLabel: { shrink: true } }}
/>
<Field.Text
size="small"
label="Taxes(%)"
name="taxes"
type="number"
sx={{ maxWidth: { md: 120 } }}
slotProps={{ inputLabel: { shrink: true } }}
/>
</Box>
</Box>
<InvoiceTotalSummary
taxes={taxes}
shipping={shipping}
subtotal={subtotal}
discount={discount}
totalAmount={totalAmount}
/>
</Box>
);
}
// ----------------------------------------------------------------------
type InvoiceItemProps = {
onRemoveItem: () => void;
fieldNames: Record<keyof typeof defaultItem, string>;
};
export function InvoiceItem({ onRemoveItem, fieldNames }: InvoiceItemProps) {
const { getValues, setValue } = useFormContext();
const priceInput = getValues(fieldNames.price);
const quantityInput = getValues(fieldNames.quantity);
useEffect(() => {
const totalValue = Number((priceInput * quantityInput).toFixed(2));
setValue(fieldNames.total, totalValue);
}, [fieldNames.total, priceInput, quantityInput, setValue]);
const handleSelectService = useCallback(
(option: string) => {
const selectedService = INVOICE_SERVICE_OPTIONS.find((service) => service.name === option);
setValue(fieldNames.price, selectedService?.price);
},
[fieldNames.price, setValue]
);
const handleClearService = useCallback(() => {
setValue(fieldNames.quantity, defaultItem.quantity);
setValue(fieldNames.price, defaultItem.price);
setValue(fieldNames.total, defaultItem.total);
}, [fieldNames.price, fieldNames.quantity, fieldNames.total, setValue]);
return (
<Box
sx={{
gap: 1.5,
display: 'flex',
alignItems: 'flex-end',
flexDirection: 'column',
}}
>
<Box
sx={{
gap: 2,
width: 1,
display: 'flex',
flexDirection: { xs: 'column', md: 'row' },
}}
>
<Field.Text
size="small"
name={fieldNames.title}
label="Title"
slotProps={{ inputLabel: { shrink: true } }}
/>
<Field.Text
multiline
maxRows={3}
size="small"
name={fieldNames.description}
label="Description"
slotProps={{ inputLabel: { shrink: true } }}
/>
<Field.Select
size="small"
name={fieldNames.service}
label="Service"
slotProps={{ inputLabel: { shrink: true } }}
sx={{ maxWidth: { md: 160 } }}
>
<MenuItem
value=""
onClick={handleClearService}
sx={{ fontStyle: 'italic', color: 'text.secondary' }}
>
None
</MenuItem>
<Divider sx={{ borderStyle: 'dashed' }} />
{INVOICE_SERVICE_OPTIONS.map((service) => (
<MenuItem
key={service.id}
value={service.name}
onClick={() => handleSelectService(service.name)}
>
{service.name}
</MenuItem>
))}
</Field.Select>
<Field.Text
size="small"
type="number"
name={fieldNames.quantity}
label="Quantity"
placeholder="0"
slotProps={{ inputLabel: { shrink: true } }}
sx={{ maxWidth: { md: 96 } }}
/>
<Field.Text
size="small"
type="number"
name={fieldNames.price}
label="Price"
placeholder="0.00"
slotProps={{
input: {
startAdornment: (
<InputAdornment position="start">
<Box sx={{ typography: 'subtitle2', color: 'text.disabled' }}>$</Box>
</InputAdornment>
),
},
}}
sx={{ maxWidth: { md: 96 } }}
/>
<Field.Text
disabled
size="small"
name={fieldNames.total}
type="number"
label="Total"
placeholder="0.00"
slotProps={{
input: {
startAdornment: (
<InputAdornment position="start">
<Box sx={{ typography: 'subtitle2', color: 'text.disabled' }}>$</Box>
</InputAdornment>
),
},
}}
sx={{
maxWidth: { md: 128 },
[`& .${inputBaseClasses.input}`]: { textAlign: { md: 'right' } },
}}
/>
</Box>
<Button
size="small"
color="error"
startIcon={<Iconify icon="solar:trash-bin-trash-bold" />}
onClick={onRemoveItem}
>
Remove
</Button>
</Box>
);
}