import { Check, ExpandMore } from "@mui/icons-material"; import { Accordion, AccordionDetails, AccordionSummary, Alert, Box, Button, Divider, FormControl, FormHelperText, InputLabel, MenuItem, Modal, ModalProps, Paper, Select, SxProps, TextField, Typography, } from "@mui/material"; import React, { useCallback, useMemo } from "react"; import { Controller, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { INPUT_DATE_FORMAT, moneyFormatter, OUTPUT_DATE_FORMAT, truncateMoney, } from "@/app/utils/formatUtil"; import { PaymentInputs } from "@/app/api/projects/actions"; import dayjs from "dayjs"; import { DatePicker } from "@mui/x-date-pickers"; export interface BulkAddPaymentForm { numberOfEntries: number; amountToDivide: number; dateType: "monthly" | "weekly" | "fixed"; dateReference?: dayjs.Dayjs; description: string; } export interface Props extends Omit { onSave: (payments: PaymentInputs[]) => Promise; modalSx?: SxProps; } type DatetypeOption = { value: string; label: string; unit: 'week' | 'month' | 'year'; step: number; }; const datetypeOptions: DatetypeOption[] = [ { value: "weekly", label: "Weekly", unit: "week", step: 1 }, { value: "biweekly", label: "Bi-Weekly", unit: "week", step: 2 }, { value: "monthly", label: "Monthly", unit: "month", step: 1 }, // { value: "bimonthly", label: "Every two months from", unit: "month", step: 2 }, { value: "quarterly", label: "Quarterly", unit: "month", step: 3 }, { value: "half-year", label: "Half Year", unit: "month", step: 6 }, // { value: "yearly", label: "Yearly from", unit: "year", step: 1 }, { value: "fixed", label: "Fixed", unit: "month", step: 0 }, // unit/step not used for fixed ]; const modalSx: SxProps = { position: "absolute", top: "50%", left: "50%", transform: "translate(-50%, -50%)", width: "90%", maxWidth: "sm", maxHeight: "90%", padding: 3, display: "flex", flexDirection: "column", gap: 2, }; let idOffset = Date.now(); const getID = () => { return ++idOffset; }; const BulkAddPaymentModal: React.FC = ({ onSave, open, onClose, modalSx: mSx, }) => { const { t } = useTranslation(); const { register, reset, trigger, formState, watch, control } = useForm({ mode: "onTouched", defaultValues: { dateType: "monthly", dateReference: dayjs() }, }); const formValues = watch(); const newPayments = useMemo(() => { const { numberOfEntries, amountToDivide, dateType, dateReference = dayjs(), description, } = formValues; if ( Number.isInteger(numberOfEntries) && numberOfEntries > 0 && amountToDivide && description ) { const dividedAmount = truncateMoney(amountToDivide / numberOfEntries)!; const amountForLastItem = truncateMoney( amountToDivide - dividedAmount * (numberOfEntries - 1), )!; const datetypeOption = datetypeOptions.find(r => r.value === dateType); return Array(numberOfEntries) .fill(undefined) .map((_, index) => { const date = dateType === "fixed" ? dateReference : dateReference.add( index * (datetypeOption?.step ?? 0), datetypeOption?.unit ?? "month", ); return { id: getID(), amount: index === numberOfEntries - 1 ? amountForLastItem : dividedAmount, description: replaceTemplateString(description, { "{index}": (index + 1).toString(), "{date}": date.format(OUTPUT_DATE_FORMAT), }), date: date.format(INPUT_DATE_FORMAT), }; }); } else { return []; } }, [formValues]); const saveHandler = useCallback(async () => { const valid = await trigger(); if (valid) { onSave(newPayments); reset(); } }, [trigger, onSave, reset, newPayments]); const closeHandler = useCallback>( (...args) => { onClose?.(...args); reset(); }, [onClose, reset], ); return ( { if (!value) { return t("Required"); } else if (value < 0) { return t("Number must be positive"); } else if (!Number.isInteger(value)) { return t("Number must be an integer"); } else { return true; } }, })} error={Boolean(formState.errors.numberOfEntries)} helperText={formState.errors.numberOfEntries?.message} /> {t("Default Date Type")} ( )} rules={{ required: t("Required"), }} /> {"The type of date to use for each payment entry."} ( )} rules={{ required: t("Required"), }} /> { "The reference date for calculating the date for each payment entry." } }> {t("Milestone payments preview")} {newPayments.length > 0 ? ( ) : ( {t( "Please input the amount to divde, the number of payments, and the description.", )} )} ); }; const PaymentSummary: React.FC<{ paymentInputs: PaymentInputs[] }> = ({ paymentInputs, }) => { const { t } = useTranslation(); return ( {paymentInputs.map(({ id, date, description, amount }, index) => { return ( {t("Description")} {description} {t("Date")} {date} {t("Amount")} {moneyFormatter.format(amount)} {index !== paymentInputs.length - 1 && ( )} ); })} ); }; const replaceTemplateString = ( template: string, replacements: { [key: string]: string }, ): string => { let returnString = template; Object.entries(replacements).forEach(([key, replacement]) => { returnString = returnString.replaceAll(key, replacement); }); return returnString; }; export default BulkAddPaymentModal;