source: src/sections/invoice/invoice-new-edit-form.tsx@ 87c9f1e

main
Last change on this file since 87c9f1e was 87c9f1e, checked in by Naum Shapkarovski <naumshapkarovski@…>, 5 weeks ago

update the seed script. update the prisma schema, use mapping

  • Property mode set to 100644
File size: 13.0 KB
Line 
1import { useEffect, useMemo } from 'react';
2import * as Yup from 'yup';
3import { useForm } from 'react-hook-form';
4import { yupResolver } from '@hookform/resolvers/yup';
5// @mui
6import LoadingButton from '@mui/lab/LoadingButton';
7import Card from '@mui/material/Card';
8import Stack from '@mui/material/Stack';
9// routes
10import { paths } from 'src/routes/paths';
11import { useRouter } from 'src/routes/hooks';
12// types
13import { CreateInvoice, Invoice } from 'src/schemas';
14// hooks
15import { useBoolean } from 'src/hooks/use-boolean';
16// components
17import FormProvider from 'src/components/hook-form';
18//
19import { incrementInvoiceNumber } from 'src/utils/increment-invoice-number';
20import uploadToFirebaseStorage from 'src/utils/upload-to-firebase-storage';
21import { pdf } from '@react-pdf/renderer';
22import { useGetTenant } from 'src/api/tenant';
23import { collections, generateId, updateDocument } from 'src/lib/firestore';
24import { useGetServices } from 'src/api/service';
25import { mutate } from 'swr';
26import InvoiceNewEditStatusDate from './invoice-new-edit-status-date';
27import InvoiceNewEditAddress from './invoice-new-edit-address';
28import InvoiceNewEditDetails from './invoice-new-edit-details';
29import InvoicePDF from './invoice-pdf';
30import { createInvoice, updateInvoice } from 'src/api/invoice';
31
32// ----------------------------------------------------------------------
33
34type Props = {
35 isCopy?: boolean;
36 currentInvoice?: Invoice;
37};
38
39export const monthNames = [
40 'January',
41 'February',
42 'March',
43 'April',
44 'May',
45 'June',
46 'July',
47 'August',
48 'September',
49 'October',
50 'November',
51 'December',
52];
53
54interface InvoiceItem {
55 service: string | null;
56 title: string;
57 price: number;
58 total: number;
59 quantity: number;
60 description: string;
61}
62
63export default function InvoiceNewEditForm({ isCopy, currentInvoice }: Props) {
64 const router = useRouter();
65
66 const loadingSave = useBoolean();
67
68 const loadingSend = useBoolean();
69
70 const itemSchema = Yup.object({
71 title: Yup.string().required('Title is required'),
72 description: Yup.string().required('Description is required'),
73 service: Yup.string().nullable(),
74 quantity: Yup.number()
75 .required('Quantity is required')
76 .min(0.5, 'Quantity must be at least 0.5'),
77 price: Yup.number().required('Price is required').min(0, 'Price must be at least 0'),
78 total: Yup.number().required('Total is required').min(0, 'Total must be at least 0'),
79 });
80
81 const NewInvoiceSchema = Yup.object().shape({
82 invoiceNumber: Yup.string().nullable().required('Invoice number is required'),
83 issueDate: Yup.mixed<any>().nullable().required('Create date is required'),
84 dueDate: Yup.mixed<any>()
85 .required('Due date is required')
86 .test(
87 'date-min',
88 'Due date must be later than create date',
89 (value, { parent }) => value.getTime() > parent.issueDate.getTime()
90 ),
91 invoiceFrom: Yup.mixed<any>().nullable().required('Invoice from is required'),
92 invoiceTo: Yup.mixed<any>().nullable().required('Invoice to is required'),
93 currency: Yup.string().oneOf(['EUR', 'USD']).required('Currency is required'),
94 quantityType: Yup.string()
95 .oneOf(['Unit', 'Hour', 'Sprint', 'Month'])
96 .required('Quantity type is required'),
97 month: Yup.string().oneOf(monthNames).required('Month is required'),
98 status: Yup.string().oneOf(['draft', 'processing', 'pending', 'overdue', 'paid']).required(),
99 totalAmount: Yup.number().required(),
100 // not required
101 taxes: Yup.number(),
102 discount: Yup.number(),
103 items: Yup.array()
104 .of(itemSchema)
105 .required('At least one item is required')
106 .min(1, 'At least one item must be provided'),
107 });
108
109 const { services: invoiceServices } = useGetServices();
110 console.log('invoiceServices', invoiceServices);
111 const { settings: tenant, settingsEmpty, settingsLoading } = useGetTenant();
112
113 const defaultValues = useMemo(
114 () => ({
115 invoiceNumber:
116 !isCopy && currentInvoice?.invoiceNumber
117 ? currentInvoice?.invoiceNumber
118 : incrementInvoiceNumber(tenant?.lastInvoiceNumber),
119 issueDate: currentInvoice?.issueDate ? new Date(currentInvoice.issueDate) : new Date(),
120 dueDate: currentInvoice?.dueDate
121 ? new Date(currentInvoice.dueDate)
122 : new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
123 invoiceFrom: currentInvoice?.invoiceFrom || null,
124 invoiceTo: currentInvoice?.invoiceTo || null,
125 currency: currentInvoice?.currency || 'EUR',
126 quantityType: currentInvoice?.quantityType || 'Unit',
127 month: currentInvoice?.month || monthNames[new Date().getMonth()],
128 // not required
129 taxes: currentInvoice?.taxes || 0,
130 status: !isCopy && currentInvoice?.status ? currentInvoice?.status : 'draft',
131 discount: currentInvoice?.discount || 0,
132 items: currentInvoice?.items.map((item) => ({
133 ...item,
134 service: item.service?.id || null,
135 })) || [
136 {
137 title: '',
138 description: '',
139 service: '',
140 quantity: 1,
141 price: 0,
142 total: 0,
143 },
144 ],
145 subTotal: currentInvoice?.subTotal || 0,
146 totalAmount: currentInvoice?.totalAmount || 0,
147 }),
148 [currentInvoice, isCopy, tenant]
149 );
150
151 const methods = useForm({
152 resolver: yupResolver(NewInvoiceSchema),
153 defaultValues,
154 });
155
156 const {
157 reset,
158 handleSubmit,
159 formState: { isSubmitting },
160 } = methods;
161
162 useEffect(() => {
163 if (!settingsEmpty && !settingsLoading) {
164 // eslint-disable-next-line react-hooks/exhaustive-deps
165 reset(defaultValues);
166 }
167 // eslint-disable-next-line react-hooks/exhaustive-deps
168 }, [settingsEmpty, settingsLoading]);
169
170 const handleSaveAsDraft = handleSubmit(async (data) => {
171 loadingSave.onTrue();
172
173 try {
174 const id = generateId(collections.invoice);
175
176 // Ensure dates are valid Date objects
177 const issueDate = data.issueDate instanceof Date ? data.issueDate : new Date(data.issueDate);
178 const dueDate = data.dueDate instanceof Date ? data.dueDate : new Date(data.dueDate);
179
180 const currentTime = new Date();
181 issueDate.setHours(
182 currentTime.getHours(),
183 currentTime.getMinutes(),
184 currentTime.getSeconds()
185 );
186
187 // attach serivce details
188 const items = data.items.map((item) => ({
189 ...item,
190 service: invoiceServices.find((service) => service.id === item.service) || null,
191 }));
192
193 // transform data
194 const writeData: CreateInvoice = {
195 ...data,
196 invoiceNumber: incrementInvoiceNumber(tenant?.lastInvoiceNumber),
197 status: 'draft',
198 issueDate,
199 dueDate,
200 items: items.filter((item) => item.service !== null) as CreateInvoice['items'],
201 subTotal: data.totalAmount - (data.taxes || 0) - (data.discount || 0),
202 month: data.month as Invoice['month'],
203 invoiceFrom: data.invoiceFrom!,
204 invoiceTo: data.invoiceTo!,
205 };
206
207 // upload invoice PDF to storage
208 const invoicePdf = <InvoicePDF invoice={writeData as Invoice} currentStatus="pending" />;
209 const blob: Blob = await pdf(invoicePdf).toBlob();
210 const storagePath: string = `invoices/${writeData.invoiceTo.name}-${writeData.invoiceNumber}.pdf`;
211 await uploadToFirebaseStorage(blob, storagePath);
212
213 // write to DB
214 // await firestoreBatch([
215 // {
216 // docPath: `${collections.invoice}/${id}`,
217 // type: 'set',
218 // data: {
219 // ...writeData,
220 // pdfRef: storagePath,
221 // },
222 // },
223 // {
224 // docPath: `${collections.settings}/${documents.settingsInvoice}`,
225 // type: 'set',
226 // data: { lastInvoiceNumber: writeData.invoiceNumber },
227 // },
228 // ]);
229
230 await createInvoice({ ...writeData, pdfRef: storagePath });
231
232 loadingSave.onFalse();
233 router.push(paths.dashboard.invoice.root);
234 // console.info('DATA', JSON.stringify(writeData, null, 2));
235 } catch (error) {
236 console.error(error);
237 loadingSave.onFalse();
238 }
239 });
240
241 const handleCreateAndUpdate = handleSubmit(async (data) => {
242 loadingSend.onTrue();
243
244 try {
245 if (currentInvoice) {
246 // Ensure dates are valid Date objects
247 const issueDate =
248 data.issueDate instanceof Date ? data.issueDate : new Date(data.issueDate);
249 const dueDate = data.dueDate instanceof Date ? data.dueDate : new Date(data.dueDate);
250
251 // attach serivce details
252 const items = data.items.map((item) => ({
253 ...item,
254 service: invoiceServices.find((service) => service.id === item.service) || null,
255 }));
256
257 // transform data
258 const writeData = {
259 ...data,
260 issueDate,
261 dueDate,
262 items,
263 invoiceFrom: data.invoiceFrom!,
264 invoiceTo: data.invoiceTo!,
265 };
266
267 // upload invoice PDF to storage
268 const invoicePdf = <InvoicePDF invoice={writeData as Invoice} currentStatus="pending" />;
269 const blob: Blob = await pdf(invoicePdf).toBlob();
270 const storagePath: string = `invoices/${data.invoiceTo.name}-${data.invoiceNumber}.pdf`;
271 await uploadToFirebaseStorage(blob, storagePath);
272
273 // update DB
274 // await updateDocument(collections.invoice, currentInvoice.id, {
275 // ...writeData,
276 // pdfRef: storagePath,
277 // });
278
279 await updateInvoice(currentInvoice.id, {
280 ...writeData,
281 pdfRef: storagePath,
282 status: data.status as 'draft' | 'processing' | 'pending' | 'overdue' | 'paid',
283 month: data.month as
284 | 'January'
285 | 'February'
286 | 'March'
287 | 'April'
288 | 'May'
289 | 'June'
290 | 'July'
291 | 'August'
292 | 'September'
293 | 'October'
294 | 'November'
295 | 'December',
296 items: items.filter((item) => item.service !== null) as {
297 service: { id: string; month: number; name: string; sprint: number; hour: number };
298 title: string;
299 price: number;
300 total: number;
301 quantity: number;
302 description: string;
303 }[],
304 });
305
306 // mutate current data
307 // mutate([collections.invoice, currentInvoice.id]);
308 } else {
309 // generate collection id
310 const id = generateId(collections.invoice);
311
312 // Ensure dates are valid Date objects
313 const issueDate =
314 data.issueDate instanceof Date ? data.issueDate : new Date(data.issueDate);
315 const dueDate = data.dueDate instanceof Date ? data.dueDate : new Date(data.dueDate);
316
317 // attach serivce details
318 const items = data.items.map((item) => ({
319 ...item,
320 service: invoiceServices?.find((service) => service.id === item.service) || null,
321 }));
322
323 // transform data
324 const writeData = {
325 ...data,
326 issueDate,
327 dueDate,
328 items,
329 invoiceFrom: data.invoiceFrom!,
330 invoiceTo: data.invoiceTo!,
331 };
332
333 // upload invoice PDF to storage
334 const invoicePdf = <InvoicePDF invoice={writeData as Invoice} currentStatus="pending" />;
335 const blob: Blob = await pdf(invoicePdf).toBlob();
336 const storagePath: string = `invoices/${data.invoiceTo.name}-${data.invoiceNumber}.pdf`;
337 await uploadToFirebaseStorage(blob, storagePath);
338
339 // write to DB
340 // await firestoreBatch([
341 // {
342 // docPath: `${collections.invoice}/${id}`,
343 // type: 'set',
344 // data: {
345 // ...writeData,
346 // pdfRef: storagePath,
347 // },
348 // },
349 // {
350 // docPath: `${collections.settings}/${documents.settingsInvoice}`,
351 // type: 'set',
352 // data: { lastInvoiceNumber: writeData.invoiceNumber },
353 // },
354 // ]);
355 await createInvoice({ ...writeData, pdfRef: storagePath });
356
357 reset();
358 }
359
360 loadingSend.onFalse();
361 router.push(paths.dashboard.invoice.root);
362
363 // console.info('DATA', JSON.stringify(data, null, 2));
364 } catch (error) {
365 console.error(error);
366 loadingSend.onFalse();
367 }
368 });
369
370 return (
371 <FormProvider methods={methods}>
372 <Card>
373 {!!tenant && <InvoiceNewEditAddress />}
374
375 <InvoiceNewEditStatusDate isCopy={isCopy} />
376
377 <InvoiceNewEditDetails />
378 </Card>
379
380 <Stack justifyContent="flex-end" direction="row" spacing={2} sx={{ mt: 3 }}>
381 {currentInvoice && isCopy && (
382 <LoadingButton
383 color="inherit"
384 size="large"
385 variant="outlined"
386 loading={loadingSave.value && isSubmitting}
387 onClick={handleSaveAsDraft}
388 >
389 Save as Draft
390 </LoadingButton>
391 )}
392
393 {!isCopy && (
394 <LoadingButton
395 size="large"
396 variant="contained"
397 loading={loadingSend.value && isSubmitting}
398 onClick={handleCreateAndUpdate}
399 >
400 {currentInvoice ? 'Update' : 'Create'}
401 </LoadingButton>
402 )}
403 </Stack>
404 </FormProvider>
405 );
406}
Note: See TracBrowser for help on using the repository browser.