import { useEffect, useMemo } from 'react'; import * as Yup from 'yup'; import { useForm } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; // @mui import LoadingButton from '@mui/lab/LoadingButton'; import Card from '@mui/material/Card'; import Stack from '@mui/material/Stack'; // routes import { paths } from 'src/routes/paths'; import { useRouter } from 'src/routes/hooks'; // types import { CreateInvoice, Invoice } from 'src/schemas'; // hooks import { useBoolean } from 'src/hooks/use-boolean'; // components import FormProvider from 'src/components/hook-form'; // import { incrementInvoiceNumber } from 'src/utils/increment-invoice-number'; import uploadToFirebaseStorage from 'src/utils/upload-to-firebase-storage'; import { pdf } from '@react-pdf/renderer'; import { useGetTenant } from 'src/api/tenant'; import { collections, generateId, updateDocument } from 'src/lib/firestore'; import { useGetServices } from 'src/api/service'; import { mutate } from 'swr'; import InvoiceNewEditStatusDate from './invoice-new-edit-status-date'; import InvoiceNewEditAddress from './invoice-new-edit-address'; import InvoiceNewEditDetails from './invoice-new-edit-details'; import InvoicePDF from './invoice-pdf'; import { createInvoice, updateInvoice } from 'src/api/invoice'; // ---------------------------------------------------------------------- type Props = { isCopy?: boolean; currentInvoice?: Invoice; }; export const monthNames = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', ]; interface InvoiceItem { service: string | null; title: string; price: number; total: number; quantity: number; description: string; } export default function InvoiceNewEditForm({ isCopy, currentInvoice }: Props) { const router = useRouter(); const loadingSave = useBoolean(); const loadingSend = useBoolean(); const itemSchema = Yup.object({ title: Yup.string().required('Title is required'), description: Yup.string().required('Description is required'), service: Yup.string().nullable(), quantity: Yup.number() .required('Quantity is required') .min(0.5, 'Quantity must be at least 0.5'), price: Yup.number().required('Price is required').min(0, 'Price must be at least 0'), total: Yup.number().required('Total is required').min(0, 'Total must be at least 0'), }); const NewInvoiceSchema = Yup.object().shape({ invoiceNumber: Yup.string().nullable().required('Invoice number is required'), issueDate: Yup.mixed().nullable().required('Create date is required'), dueDate: Yup.mixed() .required('Due date is required') .test( 'date-min', 'Due date must be later than create date', (value, { parent }) => value.getTime() > parent.issueDate.getTime() ), invoiceFrom: Yup.mixed().nullable().required('Invoice from is required'), invoiceTo: Yup.mixed().nullable().required('Invoice to is required'), currency: Yup.string().oneOf(['EUR', 'USD']).required('Currency is required'), quantityType: Yup.string() .oneOf(['Unit', 'Hour', 'Sprint', 'Month']) .required('Quantity type is required'), month: Yup.string().oneOf(monthNames).required('Month is required'), status: Yup.string().oneOf(['draft', 'processing', 'pending', 'overdue', 'paid']).required(), totalAmount: Yup.number().required(), // not required taxes: Yup.number(), discount: Yup.number(), items: Yup.array() .of(itemSchema) .required('At least one item is required') .min(1, 'At least one item must be provided'), }); const { services: invoiceServices } = useGetServices(); console.log('invoiceServices', invoiceServices); const { settings: tenant, settingsEmpty, settingsLoading } = useGetTenant(); const defaultValues = useMemo( () => ({ invoiceNumber: !isCopy && currentInvoice?.invoiceNumber ? currentInvoice?.invoiceNumber : incrementInvoiceNumber(tenant?.lastInvoiceNumber), issueDate: currentInvoice?.issueDate ? new Date(currentInvoice.issueDate) : new Date(), dueDate: currentInvoice?.dueDate ? new Date(currentInvoice.dueDate) : new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), invoiceFrom: currentInvoice?.invoiceFrom || null, invoiceTo: currentInvoice?.invoiceTo || null, currency: currentInvoice?.currency || 'EUR', quantityType: currentInvoice?.quantityType || 'Unit', month: currentInvoice?.month || monthNames[new Date().getMonth()], // not required taxes: currentInvoice?.taxes || 0, status: !isCopy && currentInvoice?.status ? currentInvoice?.status : 'draft', discount: currentInvoice?.discount || 0, items: currentInvoice?.items.map((item) => ({ ...item, service: item.service?.id || null, })) || [ { title: '', description: '', service: '', quantity: 1, price: 0, total: 0, }, ], subTotal: currentInvoice?.subTotal || 0, totalAmount: currentInvoice?.totalAmount || 0, }), [currentInvoice, isCopy, tenant] ); const methods = useForm({ resolver: yupResolver(NewInvoiceSchema), defaultValues, }); const { reset, handleSubmit, formState: { isSubmitting }, } = methods; useEffect(() => { if (!settingsEmpty && !settingsLoading) { // eslint-disable-next-line react-hooks/exhaustive-deps reset(defaultValues); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [settingsEmpty, settingsLoading]); const handleSaveAsDraft = handleSubmit(async (data) => { loadingSave.onTrue(); try { const id = generateId(collections.invoice); // Ensure dates are valid Date objects const issueDate = data.issueDate instanceof Date ? data.issueDate : new Date(data.issueDate); const dueDate = data.dueDate instanceof Date ? data.dueDate : new Date(data.dueDate); const currentTime = new Date(); issueDate.setHours( currentTime.getHours(), currentTime.getMinutes(), currentTime.getSeconds() ); // attach serivce details const items = data.items.map((item) => ({ ...item, service: invoiceServices.find((service) => service.id === item.service) || null, })); // transform data const writeData: CreateInvoice = { ...data, invoiceNumber: incrementInvoiceNumber(tenant?.lastInvoiceNumber), status: 'draft', issueDate, dueDate, items: items.filter((item) => item.service !== null) as CreateInvoice['items'], subTotal: data.totalAmount - (data.taxes || 0) - (data.discount || 0), month: data.month as Invoice['month'], invoiceFrom: data.invoiceFrom!, invoiceTo: data.invoiceTo!, }; // upload invoice PDF to storage const invoicePdf = ; const blob: Blob = await pdf(invoicePdf).toBlob(); const storagePath: string = `invoices/${writeData.invoiceTo.name}-${writeData.invoiceNumber}.pdf`; await uploadToFirebaseStorage(blob, storagePath); // write to DB // await firestoreBatch([ // { // docPath: `${collections.invoice}/${id}`, // type: 'set', // data: { // ...writeData, // pdfRef: storagePath, // }, // }, // { // docPath: `${collections.settings}/${documents.settingsInvoice}`, // type: 'set', // data: { lastInvoiceNumber: writeData.invoiceNumber }, // }, // ]); await createInvoice({ ...writeData, pdfRef: storagePath }); loadingSave.onFalse(); router.push(paths.dashboard.invoice.root); // console.info('DATA', JSON.stringify(writeData, null, 2)); } catch (error) { console.error(error); loadingSave.onFalse(); } }); const handleCreateAndUpdate = handleSubmit(async (data) => { loadingSend.onTrue(); try { if (currentInvoice) { // Ensure dates are valid Date objects const issueDate = data.issueDate instanceof Date ? data.issueDate : new Date(data.issueDate); const dueDate = data.dueDate instanceof Date ? data.dueDate : new Date(data.dueDate); // attach serivce details const items = data.items.map((item) => ({ ...item, service: invoiceServices.find((service) => service.id === item.service) || null, })); // transform data const writeData = { ...data, issueDate, dueDate, items, invoiceFrom: data.invoiceFrom!, invoiceTo: data.invoiceTo!, }; // upload invoice PDF to storage const invoicePdf = ; const blob: Blob = await pdf(invoicePdf).toBlob(); const storagePath: string = `invoices/${data.invoiceTo.name}-${data.invoiceNumber}.pdf`; await uploadToFirebaseStorage(blob, storagePath); // update DB // await updateDocument(collections.invoice, currentInvoice.id, { // ...writeData, // pdfRef: storagePath, // }); await updateInvoice(currentInvoice.id, { ...writeData, pdfRef: storagePath, status: data.status as 'draft' | 'processing' | 'pending' | 'overdue' | 'paid', month: data.month as | 'January' | 'February' | 'March' | 'April' | 'May' | 'June' | 'July' | 'August' | 'September' | 'October' | 'November' | 'December', items: items.filter((item) => item.service !== null) as { service: { id: string; month: number; name: string; sprint: number; hour: number }; title: string; price: number; total: number; quantity: number; description: string; }[], }); // mutate current data // mutate([collections.invoice, currentInvoice.id]); } else { // generate collection id const id = generateId(collections.invoice); // Ensure dates are valid Date objects const issueDate = data.issueDate instanceof Date ? data.issueDate : new Date(data.issueDate); const dueDate = data.dueDate instanceof Date ? data.dueDate : new Date(data.dueDate); // attach serivce details const items = data.items.map((item) => ({ ...item, service: invoiceServices?.find((service) => service.id === item.service) || null, })); // transform data const writeData = { ...data, issueDate, dueDate, items, invoiceFrom: data.invoiceFrom!, invoiceTo: data.invoiceTo!, }; // upload invoice PDF to storage const invoicePdf = ; const blob: Blob = await pdf(invoicePdf).toBlob(); const storagePath: string = `invoices/${data.invoiceTo.name}-${data.invoiceNumber}.pdf`; await uploadToFirebaseStorage(blob, storagePath); // write to DB // await firestoreBatch([ // { // docPath: `${collections.invoice}/${id}`, // type: 'set', // data: { // ...writeData, // pdfRef: storagePath, // }, // }, // { // docPath: `${collections.settings}/${documents.settingsInvoice}`, // type: 'set', // data: { lastInvoiceNumber: writeData.invoiceNumber }, // }, // ]); await createInvoice({ ...writeData, pdfRef: storagePath }); reset(); } loadingSend.onFalse(); router.push(paths.dashboard.invoice.root); // console.info('DATA', JSON.stringify(data, null, 2)); } catch (error) { console.error(error); loadingSend.onFalse(); } }); return ( {!!tenant && } {currentInvoice && isCopy && ( Save as Draft )} {!isCopy && ( {currentInvoice ? 'Update' : 'Create'} )} ); }