Переглянути джерело

update claim, project (comment the subsidiary validation)

tags/Baseline_30082024_FRONTEND_UAT
cyril.tsui 1 рік тому
джерело
коміт
5e0e5afddc
12 змінених файлів з 193 додано та 59 видалено
  1. +3
    -0
      src/app/api/claims/actions.ts
  2. +8
    -7
      src/app/api/claims/index.ts
  3. +9
    -0
      src/app/utils/formatUtil.ts
  4. +59
    -24
      src/components/ClaimDetail/ClaimDetail.tsx
  5. +60
    -4
      src/components/ClaimDetail/ClaimFormInputGrid.tsx
  6. +11
    -9
      src/components/ClaimSearch/ClaimSearch.tsx
  7. +11
    -11
      src/components/CreateProject/ProjectClientDetails.tsx
  8. +10
    -4
      src/components/SearchResults/SearchResults.tsx
  9. +5
    -0
      src/i18n/en/claim.json
  10. +6
    -0
      src/i18n/en/common.json
  11. +5
    -0
      src/i18n/zh/claim.json
  12. +6
    -0
      src/i18n/zh/common.json

+ 3
- 0
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 {


+ 8
- 7
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<Claim[]>(`${BASE_API_URL}/claim`);
// return mockClaims;
return serverFetchJson<Claim[]>(`${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<Customer[]>(`${BASE_API_URL}/customer`);
// });
/*
const mockClaims: Claim[] = [
{
id: 1,
@@ -83,4 +84,4 @@ const mockClaims: Claim[] = [
status: "Rejected",
remarks: "Duplicate Claim Form",
},
];
];*/

+ 9
- 0
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",


+ 59
- 24
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<Props> = ({ projectCombo }) => {
const onSubmit = useCallback<SubmitHandler<ClaimInputFormByStaff>>(
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<Props> = ({ 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<Props> = ({ projectCombo }) => {
<Button variant="text" startIcon={<Close />} onClick={handleCancel}>
{t("Cancel")}
</Button>
<Button variant="outlined" name="save" startIcon={<Check />} type="submit">
<Button variant="outlined" name="save" startIcon={<Check />} type="submit" disabled={Boolean(formProps.watch("isGridEditing"))}>
{t("Save")}
</Button>
<Button variant="contained" name="submit" startIcon={<DoneAll />} type="submit">
<Button variant="contained" name="submit" startIcon={<DoneAll />} type="submit" disabled={Boolean(formProps.watch("isGridEditing"))}>
{t("Submit")}
</Button>
</Stack>


+ 60
- 4
src/components/ClaimDetail/ClaimFormInputGrid.tsx Переглянути файл

@@ -157,7 +157,7 @@ const ClaimFormInputGrid: React.FC<ClaimFormInputGridProps> = ({
projectCombo,
}) => {
const { t } = useTranslation()
const { control, setValue, getValues, formState: { errors } } = useFormContext<ClaimInputFormByStaff>();
const { control, setValue, getValues, formState: { errors }, clearErrors, setError } = useFormContext<ClaimInputFormByStaff>();
const { fields } = useFieldArray({
control,
name: "addClaimDetails"
@@ -459,6 +459,56 @@ const ClaimFormInputGrid: React.FC<ClaimFormInputGridProps> = ({
},
], [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 (
<Box
sx={{
@@ -482,11 +532,17 @@ const ClaimFormInputGrid: React.FC<ClaimFormInputGridProps> = ({
},
}}
>
{Boolean(errors.addClaimDetails?.type === "required") && <Typography sx={(theme) => ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap>
{Boolean(errors.addClaimDetails?.type === "required") && <Typography sx={(theme) => ({ 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")}
</Typography>}
{Boolean(errors.addClaimDetails?.type === "format") && <Typography sx={(theme) => ({ 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") && <Typography sx={(theme) => ({ color: theme.palette.error.main, ml: 3, mt: 1 })} variant="overline" display='inline-block' noWrap>
{t("Please ensure the date are correct")}
</Typography>}
{Boolean(errors.addClaimDetails?.type === "invalid_project") && <Typography sx={(theme) => ({ color: theme.palette.error.main, ml: 3, mt: 1 })} variant="overline" display='inline-block' noWrap>
{t("Please ensure the projects are selected")}
</Typography>}
{Boolean(errors.addClaimDetails?.type === "invalid_amount") && <Typography sx={(theme) => ({ color: theme.palette.error.main, ml: 3, mt: 1 })} variant="overline" display='inline-block' noWrap>
{t("Please ensure the amount are correct")}
</Typography>}
<div style={{ height: 400, width: "100%" }}>
<DataGrid


+ 11
- 9
src/components/ClaimSearch/ClaimSearch.tsx Переглянути файл

@@ -8,6 +8,7 @@ import SearchResults, { Column } from "../SearchResults/index";
import EditNote from "@mui/icons-material/EditNote";
import { dateInRange } from "@/app/utils/commonUtil";
import { claimStatusCombo, expenseTypeCombo } from "@/app/utils/comboUtil";
import { convertDateArrayToString, convertDateToString } from "@/app/utils/formatUtil";

interface Props {
claims: Claim[];
@@ -54,9 +55,10 @@ const ClaimSearch: React.FC<Props> = ({ claims }) => {
// onClick: onClaimClick,
// buttonIcon: <EditNote />,
// },
{ 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<Props> = ({ 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")
),
);
);
}}
/>
<SearchResults<Claim> items={filteredClaims} columns={columns} />


+ 11
- 11
src/components/CreateProject/ProjectClientDetails.tsx Переглянути файл

@@ -475,17 +475,17 @@ const ProjectClientDetails: React.FC<Props> = ({
>
<InputLabel>{t("Client Subsidiary")}</InputLabel>
<Controller
rules={{
validate: (value) => {
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"


+ 10
- 4
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<T extends ResultWithId> {
name: keyof T;
label: string;
color?: IconButtonOwnProps["color"];
needTranslation?: boolean
needTranslation?: boolean;
type?: string;
}

interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> {
@@ -101,9 +103,13 @@ function SearchResults<T extends ResultWithId>({
>
{column.buttonIcon}
</IconButton>
) : (
<>{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]}</>
)}
</TableCell>
);
})}


+ 5
- 0
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"
}

+ 6
- 0
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",


+ 5
- 0
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": "行動"
}

+ 6
- 0
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": "取消",


Завантаження…
Відмінити
Зберегти