@@ -12,6 +12,9 @@ export interface ClaimInputFormByStaff { | |||||
status: string; | status: string; | ||||
addClaimDetails: ClaimDetailTable[] | addClaimDetails: ClaimDetailTable[] | ||||
// is grid editing | |||||
isGridEditing: boolean | null; | |||||
} | } | ||||
export interface ClaimDetailTable { | export interface ClaimDetailTable { | ||||
@@ -5,9 +5,10 @@ import "server-only"; | |||||
export interface Claim { | export interface Claim { | ||||
id: number; | id: number; | ||||
created: string; | |||||
name: string; | |||||
cost: number; | |||||
code: string; | |||||
created: number[]; | |||||
project: ProjectCombo; | |||||
amount: number; | |||||
type: "Expense" | "Petty Cash"; | type: "Expense" | "Petty Cash"; | ||||
status: "Not Submitted" | "Waiting for Approval" | "Approved" | "Rejected"; | status: "Not Submitted" | "Waiting for Approval" | "Approved" | "Rejected"; | ||||
remarks: string; | remarks: string; | ||||
@@ -41,8 +42,8 @@ export const preloadClaims = () => { | |||||
}; | }; | ||||
export const fetchClaims = cache(async () => { | 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 () => { | export const fetchProjectCombo = cache(async () => { | ||||
@@ -54,7 +55,7 @@ export const fetchProjectCombo = cache(async () => { | |||||
// export const fetchAllCustomers = cache(async () => { | // export const fetchAllCustomers = cache(async () => { | ||||
// return serverFetchJson<Customer[]>(`${BASE_API_URL}/customer`); | // return serverFetchJson<Customer[]>(`${BASE_API_URL}/customer`); | ||||
// }); | // }); | ||||
/* | |||||
const mockClaims: Claim[] = [ | const mockClaims: Claim[] = [ | ||||
{ | { | ||||
id: 1, | id: 1, | ||||
@@ -83,4 +84,4 @@ const mockClaims: Claim[] = [ | |||||
status: "Rejected", | status: "Rejected", | ||||
remarks: "Duplicate Claim Form", | remarks: "Duplicate Claim Form", | ||||
}, | }, | ||||
]; | |||||
];*/ |
@@ -23,6 +23,15 @@ export const convertDateToString = (date: Date, format: string = OUTPUT_DATE_FOR | |||||
return dayjs(date).format(format) | 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", { | const shortDateFormatter_en = new Intl.DateTimeFormat("en-HK", { | ||||
weekday: "short", | weekday: "short", | ||||
year: "numeric", | year: "numeric", | ||||
@@ -15,6 +15,7 @@ import { ClaimInputFormByStaff, saveClaim } from "@/app/api/claims/actions"; | |||||
import { DoneAll } from "@mui/icons-material"; | import { DoneAll } from "@mui/icons-material"; | ||||
import { expenseTypeCombo } from "@/app/utils/comboUtil"; | import { expenseTypeCombo } from "@/app/utils/comboUtil"; | ||||
import { convertDateToString } from "@/app/utils/formatUtil"; | import { convertDateToString } from "@/app/utils/formatUtil"; | ||||
import { errorDialog, submitDialog, successDialog, warningDialog } from "../Swal/CustomAlerts"; | |||||
export interface Props { | export interface Props { | ||||
projectCombo: ProjectCombo[] | projectCombo: ProjectCombo[] | ||||
@@ -40,17 +41,49 @@ const ClaimDetail: React.FC<Props> = ({ projectCombo }) => { | |||||
const onSubmit = useCallback<SubmitHandler<ClaimInputFormByStaff>>( | const onSubmit = useCallback<SubmitHandler<ClaimInputFormByStaff>>( | ||||
async (data, event) => { | async (data, event) => { | ||||
try { | 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 | const buttonName = (event?.nativeEvent as any).submitter.name | ||||
console.log(JSON.stringify(data)) | |||||
const formData = new FormData() | 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++) { | // 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 = { | // const updatedData = { | ||||
// id: data.addClaimDetails[i].id, | // id: data.addClaimDetails[i].id, | ||||
// // project: data.addClaimDetails[i].project, | // // project: data.addClaimDetails[i].project, | ||||
@@ -63,22 +96,24 @@ const ClaimDetail: React.FC<Props> = ({ projectCombo }) => { | |||||
// formData.append("addClaimDetailsFiles", data.addClaimDetails[i].newSupportingDocument) | // formData.append("addClaimDetailsFiles", data.addClaimDetails[i].newSupportingDocument) | ||||
// formData.append("testFiles", 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(""); | 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) { | } catch (e) { | ||||
setServerError(t("An error has occurred. Please try again later.")); | 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}> | <Button variant="text" startIcon={<Close />} onClick={handleCancel}> | ||||
{t("Cancel")} | {t("Cancel")} | ||||
</Button> | </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")} | {t("Save")} | ||||
</Button> | </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")} | {t("Submit")} | ||||
</Button> | </Button> | ||||
</Stack> | </Stack> | ||||
@@ -157,7 +157,7 @@ const ClaimFormInputGrid: React.FC<ClaimFormInputGridProps> = ({ | |||||
projectCombo, | projectCombo, | ||||
}) => { | }) => { | ||||
const { t } = useTranslation() | const { t } = useTranslation() | ||||
const { control, setValue, getValues, formState: { errors } } = useFormContext<ClaimInputFormByStaff>(); | |||||
const { control, setValue, getValues, formState: { errors }, clearErrors, setError } = useFormContext<ClaimInputFormByStaff>(); | |||||
const { fields } = useFieldArray({ | const { fields } = useFieldArray({ | ||||
control, | control, | ||||
name: "addClaimDetails" | name: "addClaimDetails" | ||||
@@ -459,6 +459,56 @@ const ClaimFormInputGrid: React.FC<ClaimFormInputGridProps> = ({ | |||||
}, | }, | ||||
], [rows, rowModesModel, t],); | ], [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 ( | return ( | ||||
<Box | <Box | ||||
sx={{ | 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")} | {t("Please ensure at least one row is created, and all the fields are inputted and saved")} | ||||
</Typography>} | </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>} | </Typography>} | ||||
<div style={{ height: 400, width: "100%" }}> | <div style={{ height: 400, width: "100%" }}> | ||||
<DataGrid | <DataGrid | ||||
@@ -8,6 +8,7 @@ import SearchResults, { Column } from "../SearchResults/index"; | |||||
import EditNote from "@mui/icons-material/EditNote"; | import EditNote from "@mui/icons-material/EditNote"; | ||||
import { dateInRange } from "@/app/utils/commonUtil"; | import { dateInRange } from "@/app/utils/commonUtil"; | ||||
import { claimStatusCombo, expenseTypeCombo } from "@/app/utils/comboUtil"; | import { claimStatusCombo, expenseTypeCombo } from "@/app/utils/comboUtil"; | ||||
import { convertDateArrayToString, convertDateToString } from "@/app/utils/formatUtil"; | |||||
interface Props { | interface Props { | ||||
claims: Claim[]; | claims: Claim[]; | ||||
@@ -54,9 +55,10 @@ const ClaimSearch: React.FC<Props> = ({ claims }) => { | |||||
// onClick: onClaimClick, | // onClick: onClaimClick, | ||||
// buttonIcon: <EditNote />, | // 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: "type", label: t("Expense Type"), needTranslation: true }, | ||||
{ name: "status", label: t("Status"), needTranslation: true }, | { name: "status", label: t("Status"), needTranslation: true }, | ||||
{ name: "remarks", label: t("Remarks") }, | { name: "remarks", label: t("Remarks") }, | ||||
@@ -71,13 +73,13 @@ const ClaimSearch: React.FC<Props> = ({ claims }) => { | |||||
onSearch={(query) => { | onSearch={(query) => { | ||||
setFilteredClaims( | setFilteredClaims( | ||||
claims.filter( | 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} /> | <SearchResults<Claim> items={filteredClaims} columns={columns} /> | ||||
@@ -475,17 +475,17 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||||
> | > | ||||
<InputLabel>{t("Client Subsidiary")}</InputLabel> | <InputLabel>{t("Client Subsidiary")}</InputLabel> | ||||
<Controller | <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]} | defaultValue={customerSubsidiaryIds[0]} | ||||
control={control} | control={control} | ||||
name="clientSubsidiaryId" | name="clientSubsidiaryId" | ||||
@@ -14,6 +14,7 @@ import TableRow from "@mui/material/TableRow"; | |||||
import IconButton, { IconButtonOwnProps, IconButtonPropsColorOverrides } from "@mui/material/IconButton"; | import IconButton, { IconButtonOwnProps, IconButtonPropsColorOverrides } from "@mui/material/IconButton"; | ||||
import { t } from "i18next"; | import { t } from "i18next"; | ||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import { convertDateArrayToString } from "@/app/utils/formatUtil"; | |||||
export interface ResultWithId { | export interface ResultWithId { | ||||
id: string | number; | id: string | number; | ||||
@@ -23,7 +24,8 @@ interface BaseColumn<T extends ResultWithId> { | |||||
name: keyof T; | name: keyof T; | ||||
label: string; | label: string; | ||||
color?: IconButtonOwnProps["color"]; | color?: IconButtonOwnProps["color"]; | ||||
needTranslation?: boolean | |||||
needTranslation?: boolean; | |||||
type?: string; | |||||
} | } | ||||
interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> { | interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> { | ||||
@@ -101,9 +103,13 @@ function SearchResults<T extends ResultWithId>({ | |||||
> | > | ||||
{column.buttonIcon} | {column.buttonIcon} | ||||
</IconButton> | </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> | </TableCell> | ||||
); | ); | ||||
})} | })} | ||||
@@ -26,6 +26,11 @@ | |||||
"Approved": "Approved", | "Approved": "Approved", | ||||
"Rejected": "Rejected", | "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", | "Description": "Description", | ||||
"Actions": "Actions" | "Actions": "Actions" | ||||
} | } |
@@ -11,6 +11,12 @@ | |||||
"Approved": "Approved", | "Approved": "Approved", | ||||
"Rejected": "Rejected", | "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": "Search", | ||||
"Search Criteria": "Search Criteria", | "Search Criteria": "Search Criteria", | ||||
"Cancel": "Cancel", | "Cancel": "Cancel", | ||||
@@ -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 the date are correct": "請確保所有日期輸入正確", | |||||
"Please ensure the projects are selected": "請確保所有項目欄位已選擇", | |||||
"Please ensure the amount are correct": "請確保所有金額輸入正確", | |||||
"Description": "描述", | "Description": "描述", | ||||
"Actions": "行動" | "Actions": "行動" | ||||
} | } |
@@ -9,6 +9,12 @@ | |||||
"Approved": "已批核", | "Approved": "已批核", | ||||
"Rejected": "已拒絕", | "Rejected": "已拒絕", | ||||
"Do you want to submit?": "你是否確認要提交?", | |||||
"Submit Success": "提交成功", | |||||
"Submit Fail": "提交失敗", | |||||
"Do you want to delete?": "你是否確認要刪除?", | |||||
"Delete Success": "刪除成功", | |||||
"Search": "搜尋", | "Search": "搜尋", | ||||
"Search Criteria": "搜尋條件", | "Search Criteria": "搜尋條件", | ||||
"Cancel": "取消", | "Cancel": "取消", | ||||