source: src/sections/invoice/invoice-new-edit-form.tsx@ 5d6f37a

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

add customer

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