diff --git a/src/app/api/claims/actions.ts b/src/app/api/claims/actions.ts index b4d99ad..d607c48 100644 --- a/src/app/api/claims/actions.ts +++ b/src/app/api/claims/actions.ts @@ -12,6 +12,9 @@ export interface ClaimInputFormByStaff { status: string; addClaimDetails: ClaimDetailTable[] + + // is grid editing + isGridEditing: boolean | null; } export interface ClaimDetailTable { diff --git a/src/app/api/claims/index.ts b/src/app/api/claims/index.ts index ceb9887..eb095f1 100644 --- a/src/app/api/claims/index.ts +++ b/src/app/api/claims/index.ts @@ -5,9 +5,10 @@ import "server-only"; export interface Claim { id: number; - created: string; - name: string; - cost: number; + code: string; + created: number[]; + project: ProjectCombo; + amount: number; type: "Expense" | "Petty Cash"; status: "Not Submitted" | "Waiting for Approval" | "Approved" | "Rejected"; remarks: string; @@ -41,8 +42,8 @@ export const preloadClaims = () => { }; export const fetchClaims = cache(async () => { - return mockClaims; - // return serverFetchJson(`${BASE_API_URL}/claim`); + // return mockClaims; + return serverFetchJson(`${BASE_API_URL}/claim`); }); export const fetchProjectCombo = cache(async () => { @@ -54,7 +55,7 @@ export const fetchProjectCombo = cache(async () => { // export const fetchAllCustomers = cache(async () => { // return serverFetchJson(`${BASE_API_URL}/customer`); // }); - +/* const mockClaims: Claim[] = [ { id: 1, @@ -83,4 +84,4 @@ const mockClaims: Claim[] = [ status: "Rejected", remarks: "Duplicate Claim Form", }, -]; +];*/ diff --git a/src/app/utils/formatUtil.ts b/src/app/utils/formatUtil.ts index 5f3e3e6..918b0ca 100644 --- a/src/app/utils/formatUtil.ts +++ b/src/app/utils/formatUtil.ts @@ -23,6 +23,15 @@ export const convertDateToString = (date: Date, format: string = OUTPUT_DATE_FOR return dayjs(date).format(format) } +export const convertDateArrayToString = (dateArray: number[], format: string = OUTPUT_DATE_FORMAT, needTime: boolean = false) => { + if (dateArray.length === 6) { + if (!needTime) { + const dateString = `${dateArray[0]}-${dateArray[1]}-${dateArray[2]}` + return dayjs(dateString).format(format) + } + } +} + const shortDateFormatter_en = new Intl.DateTimeFormat("en-HK", { weekday: "short", year: "numeric", diff --git a/src/components/ClaimDetail/ClaimDetail.tsx b/src/components/ClaimDetail/ClaimDetail.tsx index ad25cfa..db74447 100644 --- a/src/components/ClaimDetail/ClaimDetail.tsx +++ b/src/components/ClaimDetail/ClaimDetail.tsx @@ -15,6 +15,7 @@ import { ClaimInputFormByStaff, saveClaim } from "@/app/api/claims/actions"; import { DoneAll } from "@mui/icons-material"; import { expenseTypeCombo } from "@/app/utils/comboUtil"; import { convertDateToString } from "@/app/utils/formatUtil"; +import { errorDialog, submitDialog, successDialog, warningDialog } from "../Swal/CustomAlerts"; export interface Props { projectCombo: ProjectCombo[] @@ -40,17 +41,49 @@ const ClaimDetail: React.FC = ({ projectCombo }) => { const onSubmit = useCallback>( async (data, event) => { try { - console.log(data.addClaimDetails[0].newSupportingDocument); - console.log((event?.nativeEvent as any).submitter.name); + if (data.isGridEditing) { + warningDialog(t("Please save all the rows before submitting"), t) + return false + } + + let haveError = false + if (data.addClaimDetails.length === 0 || data.addClaimDetails.filter(row => String(row.description).trim().length === 0 || String(row.amount).trim().length === 0 || row.project === null || row.project === undefined || ((row.oldSupportingDocument === null || row.oldSupportingDocument === undefined) && (row.newSupportingDocument === null || row.newSupportingDocument === undefined))).length > 0) { + haveError = true + formProps.setError("addClaimDetails", { message: "Claim details include empty fields", type: "required" }) + } + + if (data.addClaimDetails.length > 0 && data.addClaimDetails.filter(row => row.invoiceDate.getTime() > new Date().getTime()).length > 0) { + haveError = true + formProps.setError("addClaimDetails", { message: "Claim details include invalid invoice date", type: "invalid_date" }) + } + + if (data.addClaimDetails.length > 0 && data.addClaimDetails.filter(row => row.project === null || row.project === undefined).length > 0) { + haveError = true + formProps.setError("addClaimDetails", { message: "Claim details include empty project", type: "invalid_project" }) + } + + if (data.addClaimDetails.length > 0 && data.addClaimDetails.filter(row => row.amount <= 0).length > 0) { + haveError = true + formProps.setError("addClaimDetails", { message: "Claim details include invalid amount", type: "invalid_amount" }) + } + + if (haveError) { + return false + } + const buttonName = (event?.nativeEvent as any).submitter.name - console.log(JSON.stringify(data)) const formData = new FormData() - // formData.append("expenseType", data.expenseType) + formData.append("expenseType", data.expenseType) + data.addClaimDetails.forEach((claimDetail) => { + formData.append("addClaimDetailIds", JSON.stringify(claimDetail.id)) + formData.append("addClaimDetailInvoiceDates", convertDateToString(claimDetail.invoiceDate, "YYYY-MM-DD")) + formData.append("addClaimDetailProjectIds", JSON.stringify(claimDetail.project.id)) + formData.append("addClaimDetailDescriptions", claimDetail.description) + formData.append("addClaimDetailAmounts", JSON.stringify(claimDetail.amount)) + formData.append("addClaimDetailNewSupportingDocuments", claimDetail.newSupportingDocument) + formData.append("addClaimDetailOldSupportingDocumentIds", JSON.stringify(claimDetail?.oldSupportingDocument?.id ?? -1)) + }) // for (let i = 0; i < data.addClaimDetails.length; i++) { - // // formData.append("addClaimDetails[]", JSON.stringify(data.addClaimDetails[i])) - // // formData.append("addClaimDetailsFiles[]", data.addClaimDetails[i].newSupportingDocument) - // console.log(data.addClaimDetails[i].invoiceDate) - // // data.addClaimDetails[i].invoiceDate = convertDateToString(data.addClaimDetails[i].invoiceDate) // const updatedData = { // id: data.addClaimDetails[i].id, // // project: data.addClaimDetails[i].project, @@ -63,22 +96,24 @@ const ClaimDetail: React.FC = ({ projectCombo }) => { // formData.append("addClaimDetailsFiles", data.addClaimDetails[i].newSupportingDocument) // formData.append("testFiles", data.addClaimDetails[i].newSupportingDocument) // } - // if (buttonName === "submit") { - // formData.append("status", "Not Submitted") - // } else if (buttonName === "save") { - // formData.append("status", "Waiting for Approval") - // } - // formData.append("id", "-1") + if (buttonName === "submit") { + formData.append("status", "Waiting for Approval") + } else if (buttonName === "save") { + formData.append("status", "Not Submitted") + } + formData.append("id", "-1") - // for (let i = 0; i < data.addClaimDetails.length; i++) { - // // const formData = new FormData(); - // // formData.append("newSupportingDocument", data.addClaimDetails[i].oldSupportingDocument); - // data.addClaimDetails[i].oldSupportingDocument = new Blob([data.addClaimDetails[i].oldSupportingDocument], {type: data.addClaimDetails[i].oldSupportingDocument.type}) - // } - await saveClaim(formData) setServerError(""); - // await saveProject(data); - // router.replace("/projects"); + + submitDialog(async () => { + const response = await saveClaim(formData); + + if (response.message === "Success") { + successDialog(t("Submit Success"), t).then(() => { + router.replace("/staffReimbursement"); + }) + } + }, t) } catch (e) { setServerError(t("An error has occurred. Please try again later.")); } @@ -107,10 +142,10 @@ const ClaimDetail: React.FC = ({ projectCombo }) => { - - diff --git a/src/components/ClaimDetail/ClaimFormInputGrid.tsx b/src/components/ClaimDetail/ClaimFormInputGrid.tsx index b6c2fde..9a98179 100644 --- a/src/components/ClaimDetail/ClaimFormInputGrid.tsx +++ b/src/components/ClaimDetail/ClaimFormInputGrid.tsx @@ -157,7 +157,7 @@ const ClaimFormInputGrid: React.FC = ({ projectCombo, }) => { const { t } = useTranslation() - const { control, setValue, getValues, formState: { errors } } = useFormContext(); + const { control, setValue, getValues, formState: { errors }, clearErrors, setError } = useFormContext(); const { fields } = useFieldArray({ control, name: "addClaimDetails" @@ -459,6 +459,56 @@ const ClaimFormInputGrid: React.FC = ({ }, ], [rows, rowModesModel, t],); + // check error + useEffect(() => { + if (getValues("addClaimDetails") === undefined || getValues("addClaimDetails") === null) { + return; + } + + if (getValues("addClaimDetails").length === 0) { + clearErrors("addClaimDetails") + } else { + + console.log(rows) + if (rows.filter(row => String(row.description).trim().length === 0 || String(row.amount).trim().length === 0 || row.project === null || row.project === undefined || ((row.oldSupportingDocument === null || row.oldSupportingDocument === undefined) && (row.newSupportingDocument === null || row.newSupportingDocument === undefined))).length > 0) { + setError("addClaimDetails", { message: "Claim details include empty fields", type: "required" }) + } else { + let haveError = false + + if (rows.filter(row => row.invoiceDate.getTime() > new Date().getTime()).length > 0) { + haveError = true + setError("addClaimDetails", { message: "Claim details include invalid invoice date", type: "invalid_date" }) + } + + if (rows.filter(row => row.project === null || row.project === undefined).length > 0) { + haveError = true + setError("addClaimDetails", { message: "Claim details include empty project", type: "invalid_project" }) + } + + if (rows.filter(row => row.amount <= 0).length > 0) { + haveError = true + setError("addClaimDetails", { message: "Claim details include invalid amount", type: "invalid_amount" }) + } + + if (!haveError) { + clearErrors("addClaimDetails") + } + } + } + }, [rows, rowModesModel]) + + // check editing + useEffect(() => { + const filteredByKey = Object.fromEntries( + Object.entries(rowModesModel).filter(([key, value]) => rowModesModel[key].mode === 'edit')) + + if (Object.keys(filteredByKey).length > 0) { + setValue("isGridEditing", true) + } else { + setValue("isGridEditing", false) + } + }, [rowModesModel]) + return ( = ({ }, }} > - {Boolean(errors.addClaimDetails?.type === "required") && ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> + {Boolean(errors.addClaimDetails?.type === "required") && ({ color: theme.palette.error.main, ml: 3, mt: 1 })} variant="overline" display='inline-block' noWrap> {t("Please ensure at least one row is created, and all the fields are inputted and saved")} } - {Boolean(errors.addClaimDetails?.type === "format") && ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> - {t("Please ensure the date formats are correct")} + {Boolean(errors.addClaimDetails?.type === "invalid_date") && ({ color: theme.palette.error.main, ml: 3, mt: 1 })} variant="overline" display='inline-block' noWrap> + {t("Please ensure the date are correct")} + } + {Boolean(errors.addClaimDetails?.type === "invalid_project") && ({ color: theme.palette.error.main, ml: 3, mt: 1 })} variant="overline" display='inline-block' noWrap> + {t("Please ensure the projects are selected")} + } + {Boolean(errors.addClaimDetails?.type === "invalid_amount") && ({ color: theme.palette.error.main, ml: 3, mt: 1 })} variant="overline" display='inline-block' noWrap> + {t("Please ensure the amount are correct")} }
= ({ claims }) => { // onClick: onClaimClick, // buttonIcon: , // }, - { name: "created", label: t("Creation Date") }, - { name: "name", label: t("Related Project Name") }, - { name: "cost", label: t("Amount (HKD)") }, + { name: "created", label: t("Creation Date"), type: "date" }, + { name: "code", label: t("Claim Code") }, + { name: "project", label: t("Related Project Name") }, + { name: "amount", label: t("Amount (HKD)") }, { name: "type", label: t("Expense Type"), needTranslation: true }, { name: "status", label: t("Status"), needTranslation: true }, { name: "remarks", label: t("Remarks") }, @@ -71,13 +73,13 @@ const ClaimSearch: React.FC = ({ claims }) => { onSearch={(query) => { setFilteredClaims( claims.filter( - (claim) => - dateInRange(claim.created, query.created, query.createdTo ?? undefined) && - claim.name.toLowerCase().includes(query.name.toLowerCase()) && - (claim.type.toLowerCase().includes(query.type.toLowerCase()) || query.type.toLowerCase() === "all") && - (claim.status.toLowerCase().includes(query.status.toLowerCase()) || query.status.toLowerCase() === "all") + (claim) => + dateInRange(convertDateArrayToString(claim.created, "YYYY-MM-DD")!!, query.created, query.createdTo ?? undefined) && + // claim.project.name.toLowerCase().includes(query.name.toLowerCase()) && + (claim.type.toLowerCase().includes(query.type.toLowerCase()) || query.type.toLowerCase() === "all") && + (claim.status.toLowerCase().includes(query.status.toLowerCase()) || query.status.toLowerCase() === "all") ), - ); + ); }} /> items={filteredClaims} columns={columns} /> diff --git a/src/components/CreateProject/ProjectClientDetails.tsx b/src/components/CreateProject/ProjectClientDetails.tsx index ef5a89f..09ff66a 100644 --- a/src/components/CreateProject/ProjectClientDetails.tsx +++ b/src/components/CreateProject/ProjectClientDetails.tsx @@ -475,17 +475,17 @@ const ProjectClientDetails: React.FC = ({ > {t("Client Subsidiary")} { - if ( - !customerSubsidiaryIds.find( - (subsidiaryId) => subsidiaryId === value, - ) - ) { - return t("Please choose a valid subsidiary"); - } else return true; - }, - }} + // rules={{ + // validate: (value) => { + // if ( + // !customerSubsidiaryIds.find( + // (subsidiaryId) => subsidiaryId === value, + // ) + // ) { + // return t("Please choose a valid subsidiary"); + // } else return true; + // }, + // }} defaultValue={customerSubsidiaryIds[0]} control={control} name="clientSubsidiaryId" diff --git a/src/components/SearchResults/SearchResults.tsx b/src/components/SearchResults/SearchResults.tsx index ea0744c..9dfb6e4 100644 --- a/src/components/SearchResults/SearchResults.tsx +++ b/src/components/SearchResults/SearchResults.tsx @@ -14,6 +14,7 @@ import TableRow from "@mui/material/TableRow"; import IconButton, { IconButtonOwnProps, IconButtonPropsColorOverrides } from "@mui/material/IconButton"; import { t } from "i18next"; import { useTranslation } from "react-i18next"; +import { convertDateArrayToString } from "@/app/utils/formatUtil"; export interface ResultWithId { id: string | number; @@ -23,7 +24,8 @@ interface BaseColumn { name: keyof T; label: string; color?: IconButtonOwnProps["color"]; - needTranslation?: boolean + needTranslation?: boolean; + type?: string; } interface ColumnWithAction extends BaseColumn { @@ -101,9 +103,13 @@ function SearchResults({ > {column.buttonIcon} - ) : ( - <>{column?.needTranslation ? t(item[columnName] as string) : item[columnName]} - )} + ) : + column?.type === "date" ? ( + <>{convertDateArrayToString(item[columnName] as number[])} + ) : + ( + <>{column?.needTranslation ? t(item[columnName] as string) : item[columnName]} + )} ); })} diff --git a/src/i18n/en/claim.json b/src/i18n/en/claim.json index bfd0f84..16b28f6 100644 --- a/src/i18n/en/claim.json +++ b/src/i18n/en/claim.json @@ -26,6 +26,11 @@ "Approved": "Approved", "Rejected": "Rejected", + "Please ensure at least one row is created, and all the fields are inputted and saved": "Please ensure at least one row is created, and all the fields are inputted and saved", + "Please ensure the date are correct": "Please ensure the date are correct", + "Please ensure the projects are selected": "Please ensure the projects are selected", + "Please ensure the amount are correct": "Please ensure the amount are correct", + "Description": "Description", "Actions": "Actions" } \ No newline at end of file diff --git a/src/i18n/en/common.json b/src/i18n/en/common.json index 51ea204..5f1d289 100644 --- a/src/i18n/en/common.json +++ b/src/i18n/en/common.json @@ -11,6 +11,12 @@ "Approved": "Approved", "Rejected": "Rejected", + "Do you want to submit?": "Do you want to submit?", + "Submit Success": "Submit Success", + "Submit Fail": "Submit Fail", + "Do you want to delete?": "Do you want to delete", + "Delete Success": "Delete Success", + "Search": "Search", "Search Criteria": "Search Criteria", "Cancel": "Cancel", diff --git a/src/i18n/zh/claim.json b/src/i18n/zh/claim.json index a22acca..22c11cb 100644 --- a/src/i18n/zh/claim.json +++ b/src/i18n/zh/claim.json @@ -26,6 +26,11 @@ "Approved": "已批核", "Rejected": "已拒絕", + "Please ensure at least one row is created, and all the fields are inputted and saved": "請確保已建立至少一行, 及已輸入和儲存所有欄位", + "Please ensure the date are correct": "請確保所有日期輸入正確", + "Please ensure the projects are selected": "請確保所有項目欄位已選擇", + "Please ensure the amount are correct": "請確保所有金額輸入正確", + "Description": "描述", "Actions": "行動" } \ No newline at end of file diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json index f857985..e4642ea 100644 --- a/src/i18n/zh/common.json +++ b/src/i18n/zh/common.json @@ -9,6 +9,12 @@ "Approved": "已批核", "Rejected": "已拒絕", + "Do you want to submit?": "你是否確認要提交?", + "Submit Success": "提交成功", + "Submit Fail": "提交失敗", + "Do you want to delete?": "你是否確認要刪除?", + "Delete Success": "刪除成功", + "Search": "搜尋", "Search Criteria": "搜尋條件", "Cancel": "取消",