import { CreateProjectInputs, PaymentInputs } from "@/app/api/projects/actions"; import { TaskGroup } from "@/app/api/tasks"; import { Add, Check, Close, Delete } from "@mui/icons-material"; import { Stack, Typography, Grid, FormControl, Box, Button, } from "@mui/material"; import { GridColDef, GridActionsCellItem, GridToolbarContainer, GridRowModesModel, GridRowModes, FooterPropsOverrides, GridRowId, GridEventListener, useGridApiRef, GridRowModel, } from "@mui/x-data-grid"; import { LocalizationProvider, DatePicker } from "@mui/x-date-pickers"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import dayjs from "dayjs"; import "dayjs/locale/zh-hk"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import { useFormContext } from "react-hook-form"; import { useTranslation } from "react-i18next"; import StyledDataGrid from "../StyledDataGrid"; import { moneyFormatter } from "@/app/utils/formatUtil"; import isDate from "lodash/isDate"; interface Props { taskGroupId: TaskGroup["id"]; } declare module "@mui/x-data-grid" { interface FooterPropsOverrides { onAdd: () => void; } } type PaymentRow = Partial; const MilestoneSection: React.FC = ({ taskGroupId }) => { const { t } = useTranslation(); const { getValues, setValue } = useFormContext(); const [payments, setPayments] = useState( getValues("milestones")[taskGroupId]?.payments || [], ); const [rowModesModel, setRowModesModel] = useState({}); // Change the payments table depending on the TaskGroupId useEffect(() => { setPayments(getValues("milestones")[taskGroupId]?.payments || []); setRowModesModel({}); }, [getValues, taskGroupId]); const apiRef = useGridApiRef(); const addRow = useCallback(() => { const id = Date.now(); setPayments((p) => [...p, { id, _isNew: true }]); setRowModesModel((model) => ({ ...model, [id]: { mode: GridRowModes.Edit, fieldToFocus: "description" }, })); }, []); const validateRow = useCallback( (id: GridRowId) => { const row = apiRef.current.getRowWithUpdatedValues(id, "") as PaymentRow; let error: keyof PaymentInputs | "" = ""; if (!row.description) { error = "description"; } else if (!(isDate(row.date) && row.date.getTime())) { error = "date"; } else if (!(row.amount && row.amount >= 0)) { error = "amount"; } apiRef.current.updateRows([{ id, _error: error }]); return !error; }, [apiRef], ); const handleCancel = useCallback( (id: GridRowId) => () => { setRowModesModel((model) => ({ ...model, [id]: { mode: GridRowModes.View, ignoreModifications: true }, })); const editedRow = payments.find((payment) => payment.id === id); if (editedRow?._isNew) { setPayments((ps) => ps.filter((p) => p.id !== id)); } }, [payments], ); const handleDelete = useCallback( (id: GridRowId) => () => { setPayments((ps) => ps.filter((p) => p.id !== id)); }, [], ); const handleSave = useCallback( (id: GridRowId) => () => { if (validateRow(id)) { setRowModesModel((model) => ({ ...model, [id]: { mode: GridRowModes.View }, })); } }, [validateRow], ); const handleEditStop = useCallback>( (params, event) => { if (!validateRow(params.id)) { event.defaultMuiPrevented = true; } }, [validateRow], ); const processRowUpdate = useCallback((newRow: GridRowModel) => { const updatedRow = { ...newRow, _isNew: false }; setPayments((ps) => ps.map((p) => (p.id === newRow.id ? updatedRow : p))); return updatedRow; }, []); const columns = useMemo( () => [ { type: "actions", field: "actions", headerName: t("Actions"), getActions: ({ id }) => { if (rowModesModel[id]?.mode === GridRowModes.Edit) { return [ } label={t("Save")} onClick={handleSave(id)} />, } label={t("Cancel")} onClick={handleCancel(id)} />, ]; } return [ } label={t("Remove")} onClick={handleDelete(id)} />, ]; }, }, { field: "description", headerName: t("Payment Milestone Description"), width: 300, editable: true, }, { field: "date", headerName: t("Payment Milestone Date"), width: 200, type: "date", editable: true, valueGetter(params) { return new Date(params.value); }, }, { field: "amount", headerName: t("Payment Milestone Amount"), width: 300, editable: true, type: "number", valueFormatter(params) { return moneyFormatter.format(params.value); }, }, ], [handleCancel, handleDelete, handleSave, rowModesModel, t], ); useEffect(() => { const milestones = getValues("milestones"); setValue("milestones", { ...milestones, [taskGroupId]: { ...milestones[taskGroupId], payments: payments .filter((p) => !p._isNew && !p._error) .map((p) => ({ description: p.description!, id: p.id!, amount: p.amount!, date: dayjs(p.date!).toISOString(), })), }, }); }, [getValues, payments, setValue, taskGroupId]); return ( {t("Task Stage Milestones")} { if (!date) return; const milestones = getValues("milestones"); setValue("milestones", { ...milestones, [taskGroupId]: { ...milestones[taskGroupId], startDate: date.toISOString(), }, }); }} /> { if (!date) return; const milestones = getValues("milestones"); setValue("milestones", { ...milestones, [taskGroupId]: { ...milestones[taskGroupId], endDate: date.toISOString(), }, }); }} /> ({ marginBlockStart: 1, marginInline: -3, borderBottom: `1px solid ${theme.palette.divider}`, })} > { return params.row._error === params.field ? "hasError" : ""; }} slots={{ footer: FooterToolbar, noRowsOverlay: NoRowsOverlay, }} slotProps={{ footer: { onAdd: addRow }, }} /> ); }; const NoRowsOverlay: React.FC = () => { const { t } = useTranslation(); return ( {t("Add some payment milestones!")} ); }; const FooterToolbar: React.FC = ({ onAdd }) => { const { t } = useTranslation(); return ( ); }; export default MilestoneSection;