source: src/sections/invoice/invoice-new-edit-form.tsx@ 299af01

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

chore

  • Property mode set to 100644
File size: 13.1 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 createDate: 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.createDate.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 createDate: currentInvoice?.createDate ? new Date(currentInvoice.createDate) : 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 createDate =
178 data.createDate instanceof Date ? data.createDate : new Date(data.createDate);
179 const dueDate = data.dueDate instanceof Date ? data.dueDate : new Date(data.dueDate);
180
181 const currentTime = new Date();
182 createDate.setHours(
183 currentTime.getHours(),
184 currentTime.getMinutes(),
185 currentTime.getSeconds()
186 );
187
188 // attach serivce details
189 const items = data.items.map((item) => ({
190 ...item,
191 service: invoiceServices.find((service) => service.id === item.service) || null,
192 }));
193
194 // transform data
195 const writeData: CreateInvoice = {
196 ...data,
197 invoiceNumber: incrementInvoiceNumber(tenant?.lastInvoiceNumber),
198 status: 'draft',
199 createDate,
200 dueDate,
201 items: items.filter((item) => item.service !== null) as CreateInvoice['items'],
202 subTotal: data.totalAmount - (data.taxes || 0) - (data.discount || 0),
203 month: data.month as Invoice['month'],
204 invoiceFrom: data.invoiceFrom!,
205 invoiceTo: data.invoiceTo!,
206 };
207
208 // upload invoice PDF to storage
209 const invoicePdf = <InvoicePDF invoice={writeData as Invoice} currentStatus="pending" />;
210 const blob: Blob = await pdf(invoicePdf).toBlob();
211 const storagePath: string = `invoices/${writeData.invoiceTo.name}-${writeData.invoiceNumber}.pdf`;
212 await uploadToFirebaseStorage(blob, storagePath);
213
214 // write to DB
215 // await firestoreBatch([
216 // {
217 // docPath: `${collections.invoice}/${id}`,
218 // type: 'set',
219 // data: {
220 // ...writeData,
221 // pdfRef: storagePath,
222 // },
223 // },
224 // {
225 // docPath: `${collections.settings}/${documents.settingsInvoice}`,
226 // type: 'set',
227 // data: { lastInvoiceNumber: writeData.invoiceNumber },
228 // },
229 // ]);
230
231 await createInvoice({ ...writeData, pdfRef: storagePath });
232
233 loadingSave.onFalse();
234 router.push(paths.dashboard.invoice.root);
235 // console.info('DATA', JSON.stringify(writeData, null, 2));
236 } catch (error) {
237 console.error(error);
238 loadingSave.onFalse();
239 }
240 });
241
242 const handleCreateAndUpdate = handleSubmit(async (data) => {
243 loadingSend.onTrue();
244
245 try {
246 if (currentInvoice) {
247 // Ensure dates are valid Date objects
248 const createDate =
249 data.createDate instanceof Date ? data.createDate : new Date(data.createDate);
250 const dueDate = data.dueDate instanceof Date ? data.dueDate : new Date(data.dueDate);
251
252 // attach serivce details
253 const items = data.items.map((item) => ({
254 ...item,
255 service: invoiceServices.find((service) => service.id === item.service) || null,
256 }));
257
258 // transform data
259 const writeData = {
260 ...data,
261 createDate,
262 dueDate,
263 items,
264 invoiceFrom: data.invoiceFrom!,
265 invoiceTo: data.invoiceTo!,
266 };
267
268 // upload invoice PDF to storage
269 const invoicePdf = <InvoicePDF invoice={writeData as Invoice} currentStatus="pending" />;
270 const blob: Blob = await pdf(invoicePdf).toBlob();
271 const storagePath: string = `invoices/${data.invoiceTo.name}-${data.invoiceNumber}.pdf`;
272 await uploadToFirebaseStorage(blob, storagePath);
273
274 // update DB
275 // await updateDocument(collections.invoice, currentInvoice.id, {
276 // ...writeData,
277 // pdfRef: storagePath,
278 // });
279
280 await updateInvoice(currentInvoice.id, {
281 ...writeData,
282 pdfRef: storagePath,
283 status: data.status as 'draft' | 'processing' | 'pending' | 'overdue' | 'paid',
284 month: data.month as
285 | 'January'
286 | 'February'
287 | 'March'
288 | 'April'
289 | 'May'
290 | 'June'
291 | 'July'
292 | 'August'
293 | 'September'
294 | 'October'
295 | 'November'
296 | 'December',
297 items: items.filter((item) => item.service !== null) as {
298 service: { id: string; month: number; name: string; sprint: number; hour: number };
299 title: string;
300 price: number;
301 total: number;
302 quantity: number;
303 description: string;
304 }[],
305 });
306
307 // mutate current data
308 // mutate([collections.invoice, currentInvoice.id]);
309 } else {
310 // generate collection id
311 const id = generateId(collections.invoice);
312
313 // Ensure dates are valid Date objects
314 const createDate =
315 data.createDate instanceof Date ? data.createDate : new Date(data.createDate);
316 const dueDate = data.dueDate instanceof Date ? data.dueDate : new Date(data.dueDate);
317
318 // attach serivce details
319 const items = data.items.map((item) => ({
320 ...item,
321 service: invoiceServices?.find((service) => service.id === item.service) || null,
322 }));
323
324 // transform data
325 const writeData = {
326 ...data,
327 createDate,
328 dueDate,
329 items,
330 invoiceFrom: data.invoiceFrom!,
331 invoiceTo: data.invoiceTo!,
332 };
333
334 // upload invoice PDF to storage
335 const invoicePdf = <InvoicePDF invoice={writeData as Invoice} currentStatus="pending" />;
336 const blob: Blob = await pdf(invoicePdf).toBlob();
337 const storagePath: string = `invoices/${data.invoiceTo.name}-${data.invoiceNumber}.pdf`;
338 await uploadToFirebaseStorage(blob, storagePath);
339
340 // write to DB
341 // await firestoreBatch([
342 // {
343 // docPath: `${collections.invoice}/${id}`,
344 // type: 'set',
345 // data: {
346 // ...writeData,
347 // pdfRef: storagePath,
348 // },
349 // },
350 // {
351 // docPath: `${collections.settings}/${documents.settingsInvoice}`,
352 // type: 'set',
353 // data: { lastInvoiceNumber: writeData.invoiceNumber },
354 // },
355 // ]);
356 await createInvoice({ ...writeData, pdfRef: storagePath });
357
358 reset();
359 }
360
361 loadingSend.onFalse();
362 router.push(paths.dashboard.invoice.root);
363
364 // console.info('DATA', JSON.stringify(data, null, 2));
365 } catch (error) {
366 console.error(error);
367 loadingSend.onFalse();
368 }
369 });
370
371 return (
372 <FormProvider methods={methods}>
373 <Card>
374 {!!tenant && <InvoiceNewEditAddress />}
375
376 <InvoiceNewEditStatusDate isCopy={isCopy} />
377
378 <InvoiceNewEditDetails />
379 </Card>
380
381 <Stack justifyContent="flex-end" direction="row" spacing={2} sx={{ mt: 3 }}>
382 {currentInvoice && isCopy && (
383 <LoadingButton
384 color="inherit"
385 size="large"
386 variant="outlined"
387 loading={loadingSave.value && isSubmitting}
388 onClick={handleSaveAsDraft}
389 >
390 Save as Draft
391 </LoadingButton>
392 )}
393
394 {!isCopy && (
395 <LoadingButton
396 size="large"
397 variant="contained"
398 loading={loadingSend.value && isSubmitting}
399 onClick={handleCreateAndUpdate}
400 >
401 {currentInvoice ? 'Update' : 'Create'}
402 </LoadingButton>
403 )}
404 </Stack>
405 </FormProvider>
406 );
407}
Note: See TracBrowser for help on using the repository browser.