[5d6f37a] | 1 | import sum from 'lodash/sum';
|
---|
| 2 | import { useCallback, useEffect } from 'react';
|
---|
| 3 | import { useFormContext, useFieldArray } from 'react-hook-form';
|
---|
| 4 | // @mui
|
---|
| 5 | import Box from '@mui/material/Box';
|
---|
| 6 | import Stack from '@mui/material/Stack';
|
---|
| 7 | import Button from '@mui/material/Button';
|
---|
| 8 | import Divider from '@mui/material/Divider';
|
---|
| 9 | import MenuItem from '@mui/material/MenuItem';
|
---|
| 10 | import Typography from '@mui/material/Typography';
|
---|
| 11 | import { inputBaseClasses } from '@mui/material/InputBase';
|
---|
| 12 | import InputAdornment from '@mui/material/InputAdornment';
|
---|
| 13 | // utils
|
---|
| 14 | import { fCurrency } from 'src/utils/format-number';
|
---|
| 15 | // types
|
---|
| 16 | import { InvoiceItem } from 'mvpmasters-shared';
|
---|
| 17 | // components
|
---|
| 18 | import Iconify from 'src/components/iconify';
|
---|
| 19 | import { RHFSelect, RHFTextField } from 'src/components/hook-form';
|
---|
| 20 | import { useGetServices } from 'src/api/service';
|
---|
| 21 |
|
---|
| 22 | // ----------------------------------------------------------------------
|
---|
| 23 |
|
---|
| 24 | export default function InvoiceNewEditDetails() {
|
---|
| 25 | const { control, setValue, watch, resetField } = useFormContext();
|
---|
| 26 |
|
---|
| 27 | const { fields, append, remove } = useFieldArray({
|
---|
| 28 | control,
|
---|
| 29 | name: 'items',
|
---|
| 30 | });
|
---|
| 31 |
|
---|
| 32 | const { services: invoiceServices } = useGetServices();
|
---|
| 33 |
|
---|
| 34 | const values = watch();
|
---|
| 35 |
|
---|
| 36 | const currencySign = values.currency === 'EUR' ? '€' : '$';
|
---|
| 37 |
|
---|
| 38 | const totalOnRow = values.items.map((item: InvoiceItem) => item.quantity * item.price);
|
---|
| 39 | const subTotal = sum(totalOnRow);
|
---|
| 40 | const taxAmount = (values.taxes / 100) * subTotal;
|
---|
| 41 | const totalAmount = subTotal - values.discount + taxAmount;
|
---|
| 42 |
|
---|
| 43 | useEffect(() => {
|
---|
| 44 | setValue('subTotal', subTotal);
|
---|
| 45 | }, [setValue, subTotal]);
|
---|
| 46 |
|
---|
| 47 | useEffect(() => {
|
---|
| 48 | setValue('totalAmount', totalAmount);
|
---|
| 49 | }, [setValue, totalAmount]);
|
---|
| 50 |
|
---|
| 51 | const handleAdd = () => {
|
---|
| 52 | append({
|
---|
| 53 | title: '',
|
---|
| 54 | description: '',
|
---|
| 55 | service: '',
|
---|
| 56 | quantity: 1,
|
---|
| 57 | price: 0,
|
---|
| 58 | total: 0,
|
---|
| 59 | });
|
---|
| 60 | };
|
---|
| 61 |
|
---|
| 62 | const handleRemove = (index: number) => {
|
---|
| 63 | remove(index);
|
---|
| 64 | };
|
---|
| 65 |
|
---|
| 66 | const handleClearService = useCallback(
|
---|
| 67 | (index: number) => {
|
---|
| 68 | resetField(`items[${index}].quantity`);
|
---|
| 69 | resetField(`items[${index}]`);
|
---|
| 70 | resetField(`items[${index}].total`);
|
---|
| 71 | },
|
---|
| 72 | [resetField]
|
---|
| 73 | );
|
---|
| 74 |
|
---|
| 75 | const handleSelectService = useCallback(
|
---|
| 76 | (index: number, option: string) => {
|
---|
| 77 | setValue(
|
---|
| 78 | `items[${index}].price`,
|
---|
| 79 | invoiceServices.find((service) => service.id === option)?.price?.[
|
---|
| 80 | values.quantityType?.toLowerCase() as 'sprint' | 'hour' | 'month'
|
---|
| 81 | ]
|
---|
| 82 | );
|
---|
| 83 | setValue(
|
---|
| 84 | `items[${index}].total`,
|
---|
| 85 | values.items.map((item: InvoiceItem) => item.quantity * item.price)[index]
|
---|
| 86 | );
|
---|
| 87 | },
|
---|
| 88 | [setValue, values.items, values.quantityType, invoiceServices]
|
---|
| 89 | );
|
---|
| 90 |
|
---|
| 91 | const handleChangeQuantity = useCallback(
|
---|
| 92 | (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, index: number) => {
|
---|
| 93 | setValue(`items[${index}].quantity`, Number(event.target.value));
|
---|
| 94 | setValue(
|
---|
| 95 | `items[${index}].total`,
|
---|
| 96 | values.items.map((item: InvoiceItem) => item.quantity * item.price)[index]
|
---|
| 97 | );
|
---|
| 98 | },
|
---|
| 99 | [setValue, values.items]
|
---|
| 100 | );
|
---|
| 101 |
|
---|
| 102 | const handleChangePrice = useCallback(
|
---|
| 103 | (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, index: number) => {
|
---|
| 104 | setValue(`items[${index}].price`, Number(event.target.value));
|
---|
| 105 | setValue(
|
---|
| 106 | `items[${index}].total`,
|
---|
| 107 | values.items.map((item: InvoiceItem) => item.quantity * item.price)[index]
|
---|
| 108 | );
|
---|
| 109 | },
|
---|
| 110 | [setValue, values.items]
|
---|
| 111 | );
|
---|
| 112 |
|
---|
| 113 | const renderTotal = (
|
---|
| 114 | <Stack
|
---|
| 115 | spacing={2}
|
---|
| 116 | alignItems="flex-end"
|
---|
| 117 | sx={{ mt: 3, textAlign: 'right', typography: 'body2' }}
|
---|
| 118 | >
|
---|
| 119 | <Stack direction="row">
|
---|
| 120 | <Box sx={{ color: 'text.secondary' }}>Subtotal</Box>
|
---|
| 121 | <Box sx={{ width: 160, typography: 'subtitle2' }}>
|
---|
| 122 | {fCurrency(subTotal, values.currency) || '-'}
|
---|
| 123 | </Box>
|
---|
| 124 | </Stack>
|
---|
| 125 |
|
---|
| 126 | {/* <Stack direction="row">
|
---|
| 127 | <Box sx={{ color: 'text.secondary' }}>Discount</Box>
|
---|
| 128 | <Box
|
---|
| 129 | sx={{
|
---|
| 130 | width: 160,
|
---|
| 131 | ...(values.discount && { color: 'error.main' }),
|
---|
| 132 | }}
|
---|
| 133 | >
|
---|
| 134 | {values.discount ? `- ${fCurrency(values.discount, values.currency)}` : '-'}
|
---|
| 135 | </Box>
|
---|
| 136 | </Stack>
|
---|
| 137 |
|
---|
| 138 | <Stack direction="row">
|
---|
| 139 | <Box sx={{ color: 'text.secondary' }}>Taxes</Box>
|
---|
| 140 | <Box sx={{ width: 160 }}>
|
---|
| 141 | {values.taxes ? fCurrency(values.taxes, values.currency) : '-'}
|
---|
| 142 | </Box>
|
---|
| 143 | </Stack> */}
|
---|
| 144 |
|
---|
| 145 | <Stack direction="row" sx={{ typography: 'subtitle1' }}>
|
---|
| 146 | <Box>Total</Box>
|
---|
| 147 | <Box sx={{ width: 160 }}>{fCurrency(totalAmount, values.currency) || '-'}</Box>
|
---|
| 148 | </Stack>
|
---|
| 149 | </Stack>
|
---|
| 150 | );
|
---|
| 151 |
|
---|
| 152 | return (
|
---|
| 153 | <Box sx={{ p: 3 }}>
|
---|
| 154 | <Typography variant="h6" sx={{ color: 'text.disabled', mb: 3 }}>
|
---|
| 155 | Details:
|
---|
| 156 | </Typography>
|
---|
| 157 |
|
---|
| 158 | <Stack divider={<Divider flexItem sx={{ borderStyle: 'dashed' }} />} spacing={3}>
|
---|
| 159 | {fields.map((item, index) => (
|
---|
| 160 | <Stack key={item.id} alignItems="flex-end" spacing={1.5}>
|
---|
| 161 | <Stack direction={{ xs: 'column', md: 'row' }} spacing={2} sx={{ width: 1 }}>
|
---|
| 162 | <RHFTextField
|
---|
| 163 | size="small"
|
---|
| 164 | name={`items[${index}].title`}
|
---|
| 165 | label="Title"
|
---|
| 166 | InputLabelProps={{ shrink: true }}
|
---|
| 167 | />
|
---|
| 168 |
|
---|
| 169 | <RHFTextField
|
---|
| 170 | size="small"
|
---|
| 171 | name={`items[${index}].description`}
|
---|
| 172 | label="Description"
|
---|
| 173 | InputLabelProps={{ shrink: true }}
|
---|
| 174 | />
|
---|
| 175 |
|
---|
| 176 | {invoiceServices?.length && (
|
---|
| 177 | <RHFSelect
|
---|
| 178 | name={`items[${index}].service`}
|
---|
| 179 | size="small"
|
---|
| 180 | label="Service"
|
---|
| 181 | InputLabelProps={{ shrink: true }}
|
---|
| 182 | sx={{
|
---|
| 183 | maxWidth: { md: 160 },
|
---|
| 184 | }}
|
---|
| 185 | >
|
---|
| 186 | <MenuItem
|
---|
| 187 | value=""
|
---|
| 188 | onClick={() => handleClearService(index)}
|
---|
| 189 | sx={{ fontStyle: 'italic', color: 'text.secondary' }}
|
---|
| 190 | >
|
---|
| 191 | None
|
---|
| 192 | </MenuItem>
|
---|
| 193 |
|
---|
| 194 | <Divider sx={{ borderStyle: 'dashed' }} />
|
---|
| 195 |
|
---|
| 196 | {invoiceServices.map((service) => (
|
---|
| 197 | <MenuItem
|
---|
| 198 | key={service.id}
|
---|
| 199 | value={service.id}
|
---|
| 200 | onClick={() => handleSelectService(index, service.id)}
|
---|
| 201 | >
|
---|
| 202 | {service.name}
|
---|
| 203 | </MenuItem>
|
---|
| 204 | ))}
|
---|
| 205 | </RHFSelect>
|
---|
| 206 | )}
|
---|
| 207 |
|
---|
| 208 | <RHFTextField
|
---|
| 209 | size="small"
|
---|
| 210 | type="number"
|
---|
| 211 | name={`items[${index}].quantity`}
|
---|
| 212 | label="Quantity"
|
---|
| 213 | placeholder="0"
|
---|
| 214 | onChange={(event) => handleChangeQuantity(event, index)}
|
---|
| 215 | InputLabelProps={{ shrink: true }}
|
---|
| 216 | sx={{ maxWidth: { md: 96 } }}
|
---|
| 217 | />
|
---|
| 218 |
|
---|
| 219 | <RHFTextField
|
---|
| 220 | size="small"
|
---|
| 221 | type="number"
|
---|
| 222 | name={`items[${index}].price`}
|
---|
| 223 | label="Price"
|
---|
| 224 | placeholder="0.00"
|
---|
| 225 | onChange={(event) => handleChangePrice(event, index)}
|
---|
| 226 | InputProps={{
|
---|
| 227 | startAdornment: (
|
---|
| 228 | <InputAdornment position="start">
|
---|
| 229 | <Box sx={{ typography: 'subtitle2', color: 'text.disabled' }}>
|
---|
| 230 | {currencySign}
|
---|
| 231 | </Box>
|
---|
| 232 | </InputAdornment>
|
---|
| 233 | ),
|
---|
| 234 | }}
|
---|
| 235 | sx={{ maxWidth: { md: 96 } }}
|
---|
| 236 | />
|
---|
| 237 |
|
---|
| 238 | <RHFTextField
|
---|
| 239 | disabled
|
---|
| 240 | size="small"
|
---|
| 241 | type="number"
|
---|
| 242 | name={`items[${index}].total`}
|
---|
| 243 | label="Total"
|
---|
| 244 | placeholder="0.00"
|
---|
| 245 | value={values.items[index].total === 0 ? '' : values.items[index].total.toFixed(2)}
|
---|
| 246 | onChange={(event) => handleChangePrice(event, index)}
|
---|
| 247 | InputProps={{
|
---|
| 248 | startAdornment: (
|
---|
| 249 | <InputAdornment position="start">
|
---|
| 250 | <Box sx={{ typography: 'subtitle2', color: 'text.disabled' }}>
|
---|
| 251 | {currencySign}
|
---|
| 252 | </Box>
|
---|
| 253 | </InputAdornment>
|
---|
| 254 | ),
|
---|
| 255 | }}
|
---|
| 256 | sx={{
|
---|
| 257 | maxWidth: { md: 104 },
|
---|
| 258 | [`& .${inputBaseClasses.input}`]: {
|
---|
| 259 | textAlign: { md: 'right' },
|
---|
| 260 | },
|
---|
| 261 | }}
|
---|
| 262 | />
|
---|
| 263 | </Stack>
|
---|
| 264 |
|
---|
| 265 | <Button
|
---|
| 266 | size="small"
|
---|
| 267 | color="error"
|
---|
| 268 | startIcon={<Iconify icon="solar:trash-bin-trash-bold" />}
|
---|
| 269 | onClick={() => handleRemove(index)}
|
---|
| 270 | >
|
---|
| 271 | Remove
|
---|
| 272 | </Button>
|
---|
| 273 | </Stack>
|
---|
| 274 | ))}
|
---|
| 275 | </Stack>
|
---|
| 276 |
|
---|
| 277 | <Divider sx={{ my: 3, borderStyle: 'dashed' }} />
|
---|
| 278 |
|
---|
| 279 | <Stack
|
---|
| 280 | spacing={3}
|
---|
| 281 | direction={{ xs: 'column', md: 'row' }}
|
---|
| 282 | alignItems={{ xs: 'flex-end', md: 'center' }}
|
---|
| 283 | >
|
---|
| 284 | <Button
|
---|
| 285 | size="small"
|
---|
| 286 | color="primary"
|
---|
| 287 | startIcon={<Iconify icon="mingcute:add-line" />}
|
---|
| 288 | onClick={handleAdd}
|
---|
| 289 | sx={{ flexShrink: 0 }}
|
---|
| 290 | >
|
---|
| 291 | Add Item
|
---|
| 292 | </Button>
|
---|
| 293 |
|
---|
| 294 | <Stack
|
---|
| 295 | spacing={2}
|
---|
| 296 | justifyContent="flex-end"
|
---|
| 297 | direction={{ xs: 'column', md: 'row' }}
|
---|
| 298 | sx={{ width: 1 }}
|
---|
| 299 | >
|
---|
| 300 | <RHFTextField
|
---|
| 301 | size="small"
|
---|
| 302 | label={`Discount(${currencySign})`}
|
---|
| 303 | name="discount"
|
---|
| 304 | type="number"
|
---|
| 305 | sx={{ maxWidth: { md: 120 } }}
|
---|
| 306 | />
|
---|
| 307 |
|
---|
| 308 | <RHFTextField
|
---|
| 309 | size="small"
|
---|
| 310 | label="Taxes(%)"
|
---|
| 311 | name="taxes"
|
---|
| 312 | type="number"
|
---|
| 313 | sx={{ maxWidth: { md: 120 } }}
|
---|
| 314 | />
|
---|
| 315 | </Stack>
|
---|
| 316 | </Stack>
|
---|
| 317 |
|
---|
| 318 | {renderTotal}
|
---|
| 319 | </Box>
|
---|
| 320 | );
|
---|
| 321 | }
|
---|