Kaynağa Gözat

Milestone section

tags/Baseline_30082024_FRONTEND_UAT
Wayne 1 yıl önce
ebeveyn
işleme
7953684942
2 değiştirilmiş dosya ile 215 ekleme ve 24 silme
  1. +8
    -1
      src/app/api/projects/actions.ts
  2. +207
    -23
      src/components/CreateProject/MilestoneSection.tsx

+ 8
- 1
src/app/api/projects/actions.ts Dosyayı Görüntüle

@@ -36,11 +36,18 @@ export interface CreateProjectInputs {
[taskGroupId: TaskGroup["id"]]: {
startDate: string;
endDate: string;
payments: [];
payments: PaymentInputs[];
};
};
}

export interface PaymentInputs {
id: number;
description: string;
date: string;
amount: number;
}

export const saveProject = async (data: CreateProjectInputs) => {
return serverFetchJson(`${BASE_API_URL}/projects/new`, {
method: "POST",


+ 207
- 23
src/components/CreateProject/MilestoneSection.tsx Dosyayı Görüntüle

@@ -1,6 +1,6 @@
import { CreateProjectInputs } from "@/app/api/projects/actions";
import { CreateProjectInputs, PaymentInputs } from "@/app/api/projects/actions";
import { TaskGroup } from "@/app/api/tasks";
import { Add, Delete } from "@mui/icons-material";
import { Add, Check, Close, Delete } from "@mui/icons-material";
import {
Stack,
Typography,
@@ -13,41 +13,159 @@ 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, { useMemo } from "react";
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"];
}

interface FooterToolbarProps {
onAdd: () => void;
declare module "@mui/x-data-grid" {
interface FooterPropsOverrides {
onAdd: () => void;
}
}

type PaymentRow = Partial<PaymentInputs & { _isNew: boolean; _error: string }>;

const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => {
const { t } = useTranslation();
const {} = useFormContext<CreateProjectInputs>();
const { getValues, setValue } = useFormContext<CreateProjectInputs>();
const [payments, setPayments] = useState<PaymentRow[]>(
getValues("milestones")[taskGroupId]?.payments || [],
);
const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({});
// 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<GridEventListener<"rowEditStop">>(
(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<GridColDef[]>(
() => [
{
type: "actions",
field: "actions",
headerName: t("Actions"),
getActions: () => [
<GridActionsCellItem
key="delete-action"
icon={<Delete />}
label={t("Remove")}
onClick={() => {}}
/>,
],
getActions: ({ id }) => {
if (rowModesModel[id]?.mode === GridRowModes.Edit) {
return [
<GridActionsCellItem
key="accpet-action"
icon={<Check />}
label={t("Save")}
onClick={handleSave(id)}
/>,
<GridActionsCellItem
key="cancel-action"
icon={<Close />}
label={t("Cancel")}
onClick={handleCancel(id)}
/>,
];
}

return [
<GridActionsCellItem
key="delete-action"
icon={<Delete />}
label={t("Remove")}
onClick={handleDelete(id)}
/>,
];
},
},
{
field: "description",
@@ -61,6 +179,9 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => {
width: 200,
type: "date",
editable: true,
valueGetter(params) {
return new Date(params.value);
},
},
{
field: "amount",
@@ -68,11 +189,32 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => {
width: 300,
editable: true,
type: "number",
valueFormatter(params) {
return moneyFormatter.format(params.value);
},
},
],
[t],
[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 (
<Stack gap={1}>
<Typography variant="overline" display="block" marginBlockEnd={1}>
@@ -84,13 +226,38 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => {
<FormControl fullWidth>
<DatePicker
label={t("Stage Start Date")}
defaultValue={dayjs()}
value={dayjs(getValues("milestones")[taskGroupId]?.startDate)}
onChange={(date) => {
if (!date) return;
const milestones = getValues("milestones");
setValue("milestones", {
...milestones,
[taskGroupId]: {
...milestones[taskGroupId],
startDate: date.toISOString(),
},
});
}}
/>
</FormControl>
</Grid>
<Grid item xs>
<FormControl fullWidth>
<DatePicker label={t("Stage End Date")} defaultValue={dayjs()} />
<DatePicker
label={t("Stage End Date")}
value={dayjs(getValues("milestones")[taskGroupId]?.endDate)}
onChange={(date) => {
if (!date) return;
const milestones = getValues("milestones");
setValue("milestones", {
...milestones,
[taskGroupId]: {
...milestones[taskGroupId],
endDate: date.toISOString(),
},
});
}}
/>
</FormControl>
</Grid>
</Grid>
@@ -103,17 +270,32 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => {
})}
>
<StyledDataGrid
apiRef={apiRef}
autoHeight
sx={{ "--DataGrid-overlayHeight": "100px" }}
sx={{
"--DataGrid-overlayHeight": "100px",
".MuiDataGrid-row .MuiDataGrid-cell.hasError": {
border: "1px solid",
borderColor: "error.main",
},
}}
disableColumnMenu
rows={[]}
editMode="row"
rows={payments}
rowModesModel={rowModesModel}
onRowModesModelChange={setRowModesModel}
onRowEditStop={handleEditStop}
processRowUpdate={processRowUpdate}
columns={columns}
getCellClassName={(params) => {
return params.row._error === params.field ? "hasError" : "";
}}
slots={{
footer: FooterToolbar,
noRowsOverlay: NoRowsOverlay,
}}
slotProps={{
footer: undefined,
footer: { onAdd: addRow },
}}
/>
</Box>
@@ -130,12 +312,14 @@ const NoRowsOverlay: React.FC = () => {
alignItems="center"
height="100%"
>
<Typography variant="caption">{t("Add some milestones!")}</Typography>
<Typography variant="caption">
{t("Add some payment milestones!")}
</Typography>
</Box>
);
};

const FooterToolbar: React.FC<FooterToolbarProps> = ({ onAdd }) => {
const FooterToolbar: React.FC<FooterPropsOverrides> = ({ onAdd }) => {
const { t } = useTranslation();
return (
<GridToolbarContainer sx={{ p: 2 }}>
@@ -145,7 +329,7 @@ const FooterToolbar: React.FC<FooterToolbarProps> = ({ onAdd }) => {
onClick={onAdd}
size="small"
>
{t("Add Milestone")}
{t("Add Payment Milestone")}
</Button>
</GridToolbarContainer>
);


Yükleniyor…
İptal
Kaydet