| @@ -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", | ||||
| }, | }, | ||||
| ]; | |||||
| ];*/ | |||||
| @@ -7,7 +7,7 @@ export interface InvoiceResult { | |||||
| id: number; | id: number; | ||||
| projectCode: string; | projectCode: string; | ||||
| projectName: string; | projectName: string; | ||||
| stage: String; | |||||
| stage: string; | |||||
| comingPaymentMileStone: string; | comingPaymentMileStone: string; | ||||
| paymentMilestoneDate: string; | paymentMilestoneDate: string; | ||||
| resourceUsage: number; | resourceUsage: number; | ||||
| @@ -67,6 +67,12 @@ export const fetchStaff = cache(async () => { | |||||
| }); | }); | ||||
| }); | }); | ||||
| export const fetchStaffWithoutTeam = cache(async () => { | |||||
| return serverFetchJson<StaffResult[]>(`${BASE_API_URL}/staffs/noteam`, { | |||||
| next: { tags: ["staffs"] }, | |||||
| }); | |||||
| }); | |||||
| // export const fetchStaffCombo = cache(async () => { | // export const fetchStaffCombo = cache(async () => { | ||||
| // return serverFetchJson<Staff4TransferList>(`${BASE_API_URL}/staffs/combo`, { | // return serverFetchJson<Staff4TransferList>(`${BASE_API_URL}/staffs/combo`, { | ||||
| // next: { tags: ["staffs"] }, | // next: { tags: ["staffs"] }, | ||||
| @@ -51,3 +51,12 @@ export const saveTeam = async (data: CreateTeamInputs) => { | |||||
| headers: { "Content-Type": "application/json" }, | headers: { "Content-Type": "application/json" }, | ||||
| }); | }); | ||||
| }; | }; | ||||
| export const deleteTeam = async (data: TeamResult) => { | |||||
| return serverFetchJson(`${BASE_API_URL}/team/delete/${data.id}`, { | |||||
| method: "DELETE", | |||||
| // body: JSON.stringify(data), | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }); | |||||
| }; | |||||
| @@ -15,6 +15,7 @@ export const serverFetch: typeof fetch = async (input, init) => { | |||||
| ...(accessToken | ...(accessToken | ||||
| ? { | ? { | ||||
| Authorization: `Bearer ${accessToken}`, | Authorization: `Bearer ${accessToken}`, | ||||
| Accept: "application/json" | |||||
| } | } | ||||
| : {}), | : {}), | ||||
| }, | }, | ||||
| @@ -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> | ||||
| @@ -41,7 +41,7 @@ import { ProjectCombo } from "@/app/api/claims"; | |||||
| import { ClaimDetailTable, ClaimInputFormByStaff } from "@/app/api/claims/actions"; | import { ClaimDetailTable, ClaimInputFormByStaff } from "@/app/api/claims/actions"; | ||||
| import { useFieldArray, useFormContext } from "react-hook-form"; | import { useFieldArray, useFormContext } from "react-hook-form"; | ||||
| import { GridRenderEditCellParams } from "@mui/x-data-grid"; | import { GridRenderEditCellParams } from "@mui/x-data-grid"; | ||||
| import { convertDateToString } from "@/app/utils/formatUtil"; | |||||
| import { convertDateToString, moneyFormatter } from "@/app/utils/formatUtil"; | |||||
| interface BottomBarProps { | interface BottomBarProps { | ||||
| getCostTotal: () => number; | getCostTotal: () => number; | ||||
| @@ -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" | ||||
| @@ -397,11 +397,12 @@ const ClaimFormInputGrid: React.FC<ClaimFormInputGridProps> = ({ | |||||
| }, | }, | ||||
| { | { | ||||
| field: "amount", | field: "amount", | ||||
| headerName: t("Amount (HKD)"), | |||||
| headerName: t("Amount"), | |||||
| editable: true, | editable: true, | ||||
| type: "number", | type: "number", | ||||
| align: "right", | |||||
| valueFormatter: (params) => { | valueFormatter: (params) => { | ||||
| return `$ ${params.value ?? 0}`; | |||||
| return moneyFormatter.format(params.value ?? 0); | |||||
| }, | }, | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -423,7 +424,7 @@ const ClaimFormInputGrid: React.FC<ClaimFormInputGridProps> = ({ | |||||
| ); | ); | ||||
| }, | }, | ||||
| renderEditCell: (params) => { | renderEditCell: (params) => { | ||||
| const currentRow = rows.find(row => row.id === params.row.id); | |||||
| // const currentRow = rows.find(row => row.id === params.row.id); | |||||
| return params.formattedValue ? ( | return params.formattedValue ? ( | ||||
| <span> | <span> | ||||
| <Link onClick={() => handleLinkClick(params)} href="#">{params.formattedValue}</Link> | <Link onClick={() => handleLinkClick(params)} href="#">{params.formattedValue}</Link> | ||||
| @@ -459,6 +460,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 +533,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[]; | ||||
| @@ -19,13 +20,14 @@ type SearchParamNames = keyof SearchQuery; | |||||
| const ClaimSearch: React.FC<Props> = ({ claims }) => { | const ClaimSearch: React.FC<Props> = ({ claims }) => { | ||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| console.log(claims) | |||||
| // If claim searching is done on the server-side, then no need for this. | // If claim searching is done on the server-side, then no need for this. | ||||
| const [filteredClaims, setFilteredClaims] = useState(claims); | const [filteredClaims, setFilteredClaims] = useState(claims); | ||||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | ||||
| () => [ | () => [ | ||||
| { label: t("Creation Date From"), label2: t("Creation Date To"), paramName: "created", type: "dateRange" }, | { label: t("Creation Date From"), label2: t("Creation Date To"), paramName: "created", type: "dateRange" }, | ||||
| { label: t("Related Project Name"), paramName: "name", type: "text" }, | |||||
| // { label: t("Related Project Name"), paramName: "name", type: "text" }, | |||||
| { | { | ||||
| label: t("Expense Type"), | label: t("Expense Type"), | ||||
| paramName: "type", | paramName: "type", | ||||
| @@ -54,9 +56,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"), type: "money" }, | |||||
| { 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 +74,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} /> | ||||
| @@ -6,7 +6,7 @@ import Stack from "@mui/material/Stack"; | |||||
| import Print from '@mui/icons-material/Print'; | import Print from '@mui/icons-material/Print'; | ||||
| // import { CreateInvoiceInputs, saveInvoice } from "@/app/api/companys/actions"; | // import { CreateInvoiceInputs, saveInvoice } from "@/app/api/companys/actions"; | ||||
| import { useRouter } from "next/navigation"; | import { useRouter } from "next/navigation"; | ||||
| import React, { useCallback, useState, useLayoutEffect } from "react"; | |||||
| import React, { useCallback, useState, useLayoutEffect, useEffect } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { | import { | ||||
| FieldErrors, | FieldErrors, | ||||
| @@ -23,6 +23,8 @@ import ProjectDetails from "./ProjectDetails"; | |||||
| import ProjectTotalFee from "./ProjectTotalFee"; | import ProjectTotalFee from "./ProjectTotalFee"; | ||||
| import { timestampToDateString } from "@/app/utils/formatUtil"; | import { timestampToDateString } from "@/app/utils/formatUtil"; | ||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| import { getSession } from "next-auth/react" | |||||
| import { BASE_API_URL } from "@/config/api"; | |||||
| const CreateInvoice: React.FC = ({ | const CreateInvoice: React.FC = ({ | ||||
| }) => { | }) => { | ||||
| @@ -34,7 +36,7 @@ const CreateInvoice: React.FC = ({ | |||||
| const [invoiceDetail, setInvoiceDetail] = useState<InvoiceInformation>() | const [invoiceDetail, setInvoiceDetail] = useState<InvoiceInformation>() | ||||
| const [serverError, setServerError] = useState(""); | const [serverError, setServerError] = useState(""); | ||||
| // const { getValues } = useForm(); | |||||
| const [accessToken, setAccessToken] = useState(''); | |||||
| const fetchProjectDetails = async () =>{ | const fetchProjectDetails = async () =>{ | ||||
| const projectId = searchParams.get("id") | const projectId = searchParams.get("id") | ||||
| @@ -78,13 +80,85 @@ const CreateInvoice: React.FC = ({ | |||||
| fetchInvoiceDetails() | fetchInvoiceDetails() | ||||
| }, []) | }, []) | ||||
| // useEffect(() => { | |||||
| // const fetchData = async () => { | |||||
| // try { | |||||
| // const session = await getSession(); | |||||
| // if (session?.accessToken) { | |||||
| // const accessToken = session.accessToken; | |||||
| // // Use the access token as needed | |||||
| // setAccessToken(accessToken) | |||||
| // console.log(accessToken); | |||||
| // } else { | |||||
| // throw new Error('Access token not found in the session.'); | |||||
| // } | |||||
| // } catch (error) { | |||||
| // console.error(error); | |||||
| // } | |||||
| // }; | |||||
| // fetchData(); | |||||
| // }, []); | |||||
| const handleCancel = () => { | const handleCancel = () => { | ||||
| router.back(); | router.back(); | ||||
| }; | }; | ||||
| const handlePrintout = () => { | |||||
| // const formData = getValues(); | |||||
| console.log("Printing in Progress") | |||||
| const handlePrintout = async () => { | |||||
| const formData = formProps.getValues() | |||||
| const projectId = searchParams.get("id") | |||||
| // console.log(formData, projectId) | |||||
| const tempData = { | |||||
| ...formData, | |||||
| id: projectId | |||||
| } | |||||
| try { | |||||
| // Make an API request to generate the JasperReport | |||||
| const response = await fetch(`https://tsms-uat.2fi-solutions.com/back-api/invoices/pdf`, { | |||||
| method: 'POST', | |||||
| headers: { | |||||
| 'Content-Type': 'application/json', | |||||
| Authorization: `Bearer ${accessToken}`, | |||||
| Accept: "application/json" | |||||
| }, | |||||
| body: JSON.stringify(tempData), | |||||
| }); | |||||
| // Check if the request was successful | |||||
| if (response.ok) { | |||||
| // Extract the filename from the response headers | |||||
| const contentDisposition = response.headers.get("Content-Disposition"); | |||||
| // response.headers.forEach(e => console.log(e)) | |||||
| const fileName = contentDisposition | |||||
| ? contentDisposition.split('filename=')[1] | |||||
| : 'invoice.pdf'; | |||||
| // Convert the response data to a Blob object | |||||
| const blob = await response.blob(); | |||||
| // Create a temporary URL for the Blob object | |||||
| const url = URL.createObjectURL(blob); | |||||
| // Create a link element to trigger the file download | |||||
| const link = document.createElement('a'); | |||||
| link.href = url; | |||||
| link.download = fileName.replace(/"/g, ''); | |||||
| link.click(); | |||||
| // Clean up the temporary URL | |||||
| URL.revokeObjectURL(url); | |||||
| } else { | |||||
| throw new Error('Failed to generate the JasperReport.'); | |||||
| } | |||||
| } catch (error) { | |||||
| console.error(error); | |||||
| // Handle any errors that occurred during the process | |||||
| } | |||||
| } | } | ||||
| const onSubmit = useCallback<SubmitHandler<InvoiceResult>>( | const onSubmit = useCallback<SubmitHandler<InvoiceResult>>( | ||||
| @@ -37,7 +37,7 @@ const ProjectTotalFee: React.FC= ({}) => { | |||||
| <Divider sx={{ paddingBlockStart: 2 }} /> | <Divider sx={{ paddingBlockStart: 2 }} /> | ||||
| <Stack direction="row" justifyContent="space-between"> | <Stack direction="row" justifyContent="space-between"> | ||||
| <Typography variant="h6">{t("Project Total Fee")}</Typography> | <Typography variant="h6">{t("Project Total Fee")}</Typography> | ||||
| <Typography>{moneyFormatter.format(projectTotal += amount)}</Typography> | |||||
| <Typography>{moneyFormatter.format(amount ? projectTotal += amount : projectTotal)}</Typography> | |||||
| </Stack> | </Stack> | ||||
| </Stack> | </Stack> | ||||
| ); | ); | ||||
| @@ -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" | ||||
| @@ -20,12 +20,6 @@ import { fetchPositionCombo } from "@/app/api/positions/actions"; | |||||
| import { fetchGradeCombo } from "@/app/api/grades/actions"; | import { fetchGradeCombo } from "@/app/api/grades/actions"; | ||||
| import { fetchSkillCombo } from "@/app/api/skill/actions"; | import { fetchSkillCombo } from "@/app/api/skill/actions"; | ||||
| import { fetchSalaryCombo } from "@/app/api/salarys/actions"; | import { fetchSalaryCombo } from "@/app/api/salarys/actions"; | ||||
| // import { fetchTeamCombo } from "@/app/api/team/actions"; | |||||
| // import { fetchDepartmentCombo } from "@/app/api/departments/actions"; | |||||
| // import { fetchPositionCombo } from "@/app/api/positions/actions"; | |||||
| // import { fetchGradeCombo } from "@/app/api/grades/actions"; | |||||
| // import { fetchSkillCombo } from "@/app/api/skill/actions"; | |||||
| // import { fetchSalaryCombo } from "@/app/api/salarys/actions"; | |||||
| interface Field { | interface Field { | ||||
| // subtitle: string; | // subtitle: string; | ||||
| @@ -157,63 +151,61 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => { | |||||
| id: "staffId", | id: "staffId", | ||||
| label: t("Staff ID"), | label: t("Staff ID"), | ||||
| type: "text", | type: "text", | ||||
| value: "", | |||||
| required: true, | required: true, | ||||
| }, | }, | ||||
| { | { | ||||
| id: "name", | id: "name", | ||||
| label: t("Staff Name"), | label: t("Staff Name"), | ||||
| type: "text", | type: "text", | ||||
| value: "", | |||||
| required: true, | required: true, | ||||
| }, | }, | ||||
| { | { | ||||
| id: "companyId", | id: "companyId", | ||||
| label: t("Company"), | label: t("Company"), | ||||
| type: "combo-Obj", | type: "combo-Obj", | ||||
| options: companyCombo, | |||||
| options: companyCombo || [], | |||||
| required: true, | required: true, | ||||
| }, | }, | ||||
| { | { | ||||
| id: "teamId", | id: "teamId", | ||||
| label: t("Team"), | label: t("Team"), | ||||
| type: "combo-Obj", | type: "combo-Obj", | ||||
| options: teamCombo, | |||||
| options: teamCombo || [], | |||||
| required: false, | required: false, | ||||
| }, | }, | ||||
| { | { | ||||
| id: "departmentId", | id: "departmentId", | ||||
| label: t("Department"), | label: t("Department"), | ||||
| type: "combo-Obj", | type: "combo-Obj", | ||||
| options: departmentCombo, | |||||
| options: departmentCombo || [], | |||||
| required: true, | required: true, | ||||
| }, | }, | ||||
| { | { | ||||
| id: "gradeId", | id: "gradeId", | ||||
| label: t("Grade"), | label: t("Grade"), | ||||
| type: "combo-Obj", | type: "combo-Obj", | ||||
| options: gradeCombo, | |||||
| options: gradeCombo || [], | |||||
| required: false, | required: false, | ||||
| }, | }, | ||||
| { | { | ||||
| id: "skillSetId", | id: "skillSetId", | ||||
| label: t("Skillset"), | label: t("Skillset"), | ||||
| type: "combo-Obj", | type: "combo-Obj", | ||||
| options: skillCombo, | |||||
| options: skillCombo || [], | |||||
| required: false, | required: false, | ||||
| }, | }, | ||||
| { | { | ||||
| id: "currentPositionId", | id: "currentPositionId", | ||||
| label: t("Current Position"), | label: t("Current Position"), | ||||
| type: "combo-Obj", | type: "combo-Obj", | ||||
| options: positionCombo, | |||||
| options: positionCombo || [], | |||||
| required: true, | required: true, | ||||
| }, | }, | ||||
| { | { | ||||
| id: "salaryId", | id: "salaryId", | ||||
| label: t("Salary Point"), | label: t("Salary Point"), | ||||
| type: "combo-Obj", | type: "combo-Obj", | ||||
| options: salaryCombo, | |||||
| options: salaryCombo || [], | |||||
| required: true, | required: true, | ||||
| }, | }, | ||||
| // { | // { | ||||
| @@ -223,6 +215,12 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => { | |||||
| // value: "", | // value: "", | ||||
| // required: false, | // required: false, | ||||
| // }, | // }, | ||||
| // { | |||||
| // id: "hourlyRate", | |||||
| // label: t("Hourly Rate"), | |||||
| // type: "numeric-testing", | |||||
| // required: true, | |||||
| // }, | |||||
| { | { | ||||
| id: "employType", | id: "employType", | ||||
| label: t("Employ Type"), | label: t("Employ Type"), | ||||
| @@ -235,7 +233,6 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => { | |||||
| id: "email", | id: "email", | ||||
| label: t("Email"), | label: t("Email"), | ||||
| type: "text", | type: "text", | ||||
| value: "", | |||||
| pattern: "^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$", | pattern: "^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$", | ||||
| message: t("input matching format"), | message: t("input matching format"), | ||||
| required: true, | required: true, | ||||
| @@ -244,8 +241,7 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => { | |||||
| id: "phone1", | id: "phone1", | ||||
| label: t("Phone1"), | label: t("Phone1"), | ||||
| type: "text", | type: "text", | ||||
| value: "", | |||||
| // pattern: "^\\d{8}$", | |||||
| pattern: "^\\d{8}$", | |||||
| message: t("input correct phone no."), | message: t("input correct phone no."), | ||||
| required: true, | required: true, | ||||
| }, | }, | ||||
| @@ -253,8 +249,7 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => { | |||||
| id: "phone2", | id: "phone2", | ||||
| label: t("Phone2"), | label: t("Phone2"), | ||||
| type: "text", | type: "text", | ||||
| value: "", | |||||
| // pattern: "^\\d{8}$", | |||||
| pattern: "^\\d{8}$", | |||||
| message: t("input correct phone no."), | message: t("input correct phone no."), | ||||
| required: false, | required: false, | ||||
| }, | }, | ||||
| @@ -264,15 +259,13 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => { | |||||
| id: "emergContactName", | id: "emergContactName", | ||||
| label: t("Emergency Contact Name"), | label: t("Emergency Contact Name"), | ||||
| type: "text", | type: "text", | ||||
| value: "", | |||||
| required: true, | required: true, | ||||
| }, | }, | ||||
| { | { | ||||
| id: "emergContactPhone", | id: "emergContactPhone", | ||||
| label: t("Emergency Contact Phone"), | label: t("Emergency Contact Phone"), | ||||
| type: "text", | type: "text", | ||||
| value: "", | |||||
| // pattern: "^\\d{8}$", | |||||
| pattern: "^\\d{8}$", | |||||
| message: t("input correct phone no."), | message: t("input correct phone no."), | ||||
| required: true, | required: true, | ||||
| }, | }, | ||||
| @@ -280,33 +273,29 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => { | |||||
| id: "joinDate", | id: "joinDate", | ||||
| label: t("Join Date"), | label: t("Join Date"), | ||||
| type: "multiDate", | type: "multiDate", | ||||
| value: "", | |||||
| required: true, | required: true, | ||||
| }, | }, | ||||
| { | { | ||||
| id: "joinPositionId", | id: "joinPositionId", | ||||
| label: t("Join Position"), | label: t("Join Position"), | ||||
| type: "combo-Obj", | type: "combo-Obj", | ||||
| options: positionCombo, | |||||
| options: positionCombo || [], | |||||
| required: true, | required: true, | ||||
| }, | }, | ||||
| { | { | ||||
| id: "departDate", | id: "departDate", | ||||
| label: t("Depart Date"), | label: t("Depart Date"), | ||||
| type: "multiDate", | type: "multiDate", | ||||
| value: "", | |||||
| }, | }, | ||||
| { | { | ||||
| id: "departReason", | id: "departReason", | ||||
| label: t("Depart Reason"), | label: t("Depart Reason"), | ||||
| type: "text", | type: "text", | ||||
| value: "", | |||||
| }, | }, | ||||
| { | { | ||||
| id: "remark", | id: "remark", | ||||
| label: t("Remark"), | label: t("Remark"), | ||||
| type: "remarks", | type: "remarks", | ||||
| value: "", | |||||
| }, | }, | ||||
| ] | ] | ||||
| ]; | ]; | ||||
| @@ -47,7 +47,7 @@ const CreateStaffForm: React.FC<formProps> = ({ Title, fieldLists }) => { | |||||
| return haveError; | return haveError; | ||||
| } | } | ||||
| //check if joinDate > departDate | //check if joinDate > departDate | ||||
| if (data.departDate != null && data.departDate != "Invalid Date") { | |||||
| if (data.departDate != null && data.departDate != "Invalid Date" && data.departDate.length != 0) { | |||||
| if (data.joinDate != null) { | if (data.joinDate != null) { | ||||
| const joinDate = new Date(data.joinDate); | const joinDate = new Date(data.joinDate); | ||||
| const departDate = new Date(data.departDate); | const departDate = new Date(data.departDate); | ||||
| @@ -56,6 +56,10 @@ const CreateStaffForm: React.FC<formProps> = ({ Title, fieldLists }) => { | |||||
| return haveError; | return haveError; | ||||
| } | } | ||||
| } | } | ||||
| if (data.departReason == null || data.departReason.length == 0) { | |||||
| haveError = true; | |||||
| return haveError; | |||||
| } | |||||
| } | } | ||||
| if (haveError) { | if (haveError) { | ||||
| @@ -68,6 +72,13 @@ const CreateStaffForm: React.FC<formProps> = ({ Title, fieldLists }) => { | |||||
| phone2: data.phone2.toString(), | phone2: data.phone2.toString(), | ||||
| hourlyRate: typeof data.hourlyRate === 'string' ? parseInt(data.hourlyRate.replace("$", "").replace(",", "")) : 0 | hourlyRate: typeof data.hourlyRate === 'string' ? parseInt(data.hourlyRate.replace("$", "").replace(",", "")) : 0 | ||||
| }; | }; | ||||
| if (postData.departDate?.length === 0 && postData.departReason?.length === 0) { | |||||
| delete postData.departDate; | |||||
| delete postData.departReason; | |||||
| } | |||||
| if (postData.remark?.length === 0) { | |||||
| delete postData.remark; | |||||
| } | |||||
| console.log(postData); | console.log(postData); | ||||
| setServerError(""); | setServerError(""); | ||||
| await saveStaff(postData); | await saveStaff(postData); | ||||
| @@ -3,7 +3,7 @@ import CreateTeam from "./CreateTeam"; | |||||
| import CreateTeamLoading from "./CreateTeamLoading"; | import CreateTeamLoading from "./CreateTeamLoading"; | ||||
| // import { fetchTeam, fetchTeamLeads } from "@/app/api/team"; | // import { fetchTeam, fetchTeamLeads } from "@/app/api/team"; | ||||
| import { useSearchParams } from "next/navigation"; | import { useSearchParams } from "next/navigation"; | ||||
| import { fetchStaff } from "@/app/api/staff"; | |||||
| import { fetchStaff, fetchStaffWithoutTeam } from "@/app/api/staff"; | |||||
| interface SubComponents { | interface SubComponents { | ||||
| Loading: typeof CreateTeamLoading; | Loading: typeof CreateTeamLoading; | ||||
| @@ -14,7 +14,7 @@ const CreateTeamWrapper: React.FC & SubComponents = async () => { | |||||
| const [ | const [ | ||||
| staff, | staff, | ||||
| ] = await Promise.all([ | ] = await Promise.all([ | ||||
| fetchStaff(), | |||||
| fetchStaffWithoutTeam(), | |||||
| ]); | ]); | ||||
| return <CreateTeam allstaff={staff}/>; | return <CreateTeam allstaff={staff}/>; | ||||
| @@ -53,7 +53,6 @@ const EditStaff: React.FC = async () => { | |||||
| const searchParams = useSearchParams(); | const searchParams = useSearchParams(); | ||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const idString = searchParams.get("id"); | const idString = searchParams.get("id"); | ||||
| const [id, setId] = useState(0); | |||||
| const [fieldLists, setFieldLists] = useState<Field[][]>(); | const [fieldLists, setFieldLists] = useState<Field[][]>(); | ||||
| const [companyCombo, setCompanyCombo] = useState<comboProp[]>(); | const [companyCombo, setCompanyCombo] = useState<comboProp[]>(); | ||||
| const [teamCombo, setTeamCombo] = useState<comboProp[]>(); | const [teamCombo, setTeamCombo] = useState<comboProp[]>(); | ||||
| @@ -125,217 +124,217 @@ const EditStaff: React.FC = async () => { | |||||
| let id = 0; | let id = 0; | ||||
| if (idString) { | if (idString) { | ||||
| id = parseInt(idString); | id = parseInt(idString); | ||||
| setId(id); | |||||
| } | |||||
| fetchStaffEdit(id).then((staff) => { | |||||
| console.log(staff.data); | |||||
| const data = staff.data; | |||||
| ///////////////////// list 1 ///////////////////// | |||||
| const list1 = keyOrder1 | |||||
| .map((key) => { | |||||
| switch (key) { | |||||
| case "staffId": | |||||
| return { | |||||
| id: `${key}`, | |||||
| label: t(`Staff ID`), | |||||
| type: "text", | |||||
| value: data[key] ?? "", | |||||
| required: true, | |||||
| }; | |||||
| case "name": | |||||
| return { | |||||
| id: `${key}`, | |||||
| label: t(`Staff Name`), | |||||
| type: "text", | |||||
| value: data[key] ?? "", | |||||
| required: true, | |||||
| }; | |||||
| case "company": | |||||
| return { | |||||
| id: `${key}Id`, | |||||
| label: t(`Company`), | |||||
| type: "combo-Obj", | |||||
| options: companyCombo, | |||||
| value: data[key].id ?? "", | |||||
| required: true, | |||||
| }; | |||||
| case "team": | |||||
| return { | |||||
| id: `${key}Id`, | |||||
| label: t(`Team`), | |||||
| type: "combo-Obj", | |||||
| options: teamCombo, | |||||
| value: data[key]?.id ?? "", | |||||
| }; | |||||
| case "department": | |||||
| return { | |||||
| id: `${key}Id`, | |||||
| label: t(`Department`), | |||||
| type: "combo-Obj", | |||||
| options: departmentCombo, | |||||
| value: data[key]?.id ?? "", | |||||
| required: true, | |||||
| // later check | |||||
| }; | |||||
| case "grade": | |||||
| return { | |||||
| id: `${key}Id`, | |||||
| label: t(`Grade`), | |||||
| type: "combo-Obj", | |||||
| options: gradeCombo, | |||||
| value: data[key] !== null ? data[key].id ?? "" : "", | |||||
| }; | |||||
| case "skill": | |||||
| return { | |||||
| id: `${key}SetId`, | |||||
| label: t(`Skillset`), | |||||
| type: "combo-Obj", | |||||
| options: skillCombo, | |||||
| value: data[key] !== null ? data[key].id ?? "" : "", | |||||
| }; | |||||
| case "currentPosition": | |||||
| return { | |||||
| id: `${key}Id`, | |||||
| label: t(`Current Position`), | |||||
| type: "combo-Obj", | |||||
| options: positionCombo, | |||||
| value: data[key].id ?? "", | |||||
| required: true, | |||||
| }; | |||||
| case "salary": | |||||
| return { | |||||
| id: `salaryId`, | |||||
| label: t(`Salary Point`), | |||||
| type: "combo-Obj", | |||||
| options: salaryCombo, | |||||
| value: data[key] !== null ? data[key].id ?? "" : "", | |||||
| required: true, | |||||
| }; | |||||
| // case "hourlyRate": | |||||
| // return { | |||||
| // id: `${key}`, | |||||
| // label: t(`hourlyRate`), | |||||
| // type: "text", | |||||
| // value: "", | |||||
| // // value: data[key], | |||||
| // readOnly: true, | |||||
| // }; | |||||
| case "employType": | |||||
| return { | |||||
| id: `${key}`, | |||||
| label: t(`Employ Type`), | |||||
| type: "combo-Obj", | |||||
| options: employTypeCombo, | |||||
| value: data[key] ?? "", | |||||
| required: true, | |||||
| }; | |||||
| case "email": | |||||
| return { | |||||
| id: `${key}`, | |||||
| label: t(`Email`), | |||||
| type: "text", | |||||
| value: data[key] ?? "", | |||||
| pattern: "^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$", | |||||
| message: t("input matching format"), | |||||
| required: true, | |||||
| }; | |||||
| case "phone1": | |||||
| return { | |||||
| id: `${key}`, | |||||
| label: t(`Phone1`), | |||||
| type: "text", | |||||
| // pattern: "^\\d{8}$", | |||||
| message: t("input correct phone no."), | |||||
| value: data[key] ?? "", | |||||
| required: true, | |||||
| }; | |||||
| case "phone2": | |||||
| return { | |||||
| id: `${key}`, | |||||
| label: t(`Phone2`), | |||||
| type: "text", | |||||
| // pattern: "^\\d{8}$", | |||||
| message: t("input correct phone no."), | |||||
| value: data[key] ?? "", | |||||
| } as Field; | |||||
| default: | |||||
| return null; | |||||
| } | |||||
| }).filter((item): item is Field => item !== null); | |||||
| ///////////////////// list 2 ///////////////////// | |||||
| const list2 = keyOrder2 | |||||
| .map((key) => { | |||||
| switch (key) { | |||||
| case "emergContactName": | |||||
| return { | |||||
| id: `${key}`, | |||||
| label: t(`Emergency Contact Name`), | |||||
| type: "text", | |||||
| value: data[key] ?? "", | |||||
| required: true, | |||||
| } as Field; | |||||
| case "emergContactPhone": | |||||
| console.log(id) | |||||
| fetchStaffEdit(id).then((staff) => { | |||||
| console.log(staff.data); | |||||
| const data = staff.data; | |||||
| ///////////////////// list 1 ///////////////////// | |||||
| const list1 = keyOrder1 | |||||
| .map((key) => { | |||||
| switch (key) { | |||||
| case "staffId": | |||||
| return { | return { | ||||
| id: `${key}`, | id: `${key}`, | ||||
| label: t(`Emergency Contact Phone`), | |||||
| label: t(`Staff ID`), | |||||
| type: "text", | |||||
| value: data[key] ?? "", | |||||
| required: true, | |||||
| }; | |||||
| case "name": | |||||
| return { | |||||
| id: `${key}`, | |||||
| label: t(`Staff Name`), | |||||
| type: "text", | |||||
| value: data[key] ?? "", | |||||
| required: true, | |||||
| }; | |||||
| case "company": | |||||
| return { | |||||
| id: `${key}Id`, | |||||
| label: t(`Company`), | |||||
| type: "combo-Obj", | |||||
| options: companyCombo, | |||||
| value: data[key].id ?? "", | |||||
| required: true, | |||||
| }; | |||||
| case "team": | |||||
| return { | |||||
| id: `${key}Id`, | |||||
| label: t(`Team`), | |||||
| type: "combo-Obj", | |||||
| options: teamCombo, | |||||
| value: data[key]?.id ?? "", | |||||
| }; | |||||
| case "department": | |||||
| return { | |||||
| id: `${key}Id`, | |||||
| label: t(`Department`), | |||||
| type: "combo-Obj", | |||||
| options: departmentCombo, | |||||
| value: data[key]?.id ?? "", | |||||
| required: true, | |||||
| // later check | |||||
| }; | |||||
| case "grade": | |||||
| return { | |||||
| id: `${key}Id`, | |||||
| label: t(`Grade`), | |||||
| type: "combo-Obj", | |||||
| options: gradeCombo, | |||||
| value: data[key] !== null ? data[key].id ?? "" : "", | |||||
| }; | |||||
| case "skill": | |||||
| return { | |||||
| id: `${key}SetId`, | |||||
| label: t(`Skillset`), | |||||
| type: "combo-Obj", | |||||
| options: skillCombo, | |||||
| value: data[key] !== null ? data[key].id ?? "" : "", | |||||
| }; | |||||
| case "currentPosition": | |||||
| return { | |||||
| id: `${key}Id`, | |||||
| label: t(`Current Position`), | |||||
| type: "combo-Obj", | |||||
| options: positionCombo, | |||||
| value: data[key].id ?? "", | |||||
| required: true, | |||||
| }; | |||||
| case "salary": | |||||
| return { | |||||
| id: `salaryId`, | |||||
| label: t(`Salary Point`), | |||||
| type: "combo-Obj", | |||||
| options: salaryCombo, | |||||
| value: data[key] !== null ? data[key].id ?? "" : "", | |||||
| required: true, | |||||
| }; | |||||
| // case "hourlyRate": | |||||
| // return { | |||||
| // id: `${key}`, | |||||
| // label: t(`hourlyRate`), | |||||
| // type: "text", | |||||
| // value: "", | |||||
| // // value: data[key], | |||||
| // readOnly: true, | |||||
| // }; | |||||
| case "employType": | |||||
| return { | |||||
| id: `${key}`, | |||||
| label: t(`Employ Type`), | |||||
| type: "combo-Obj", | |||||
| options: employTypeCombo, | |||||
| value: data[key] ?? "", | |||||
| required: true, | |||||
| }; | |||||
| case "email": | |||||
| return { | |||||
| id: `${key}`, | |||||
| label: t(`Email`), | |||||
| type: "text", | |||||
| value: data[key] ?? "", | |||||
| pattern: "^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$", | |||||
| message: t("input matching format"), | |||||
| required: true, | |||||
| }; | |||||
| case "phone1": | |||||
| return { | |||||
| id: `${key}`, | |||||
| label: t(`Phone1`), | |||||
| type: "text", | type: "text", | ||||
| // pattern: "^\\d{8}$", | // pattern: "^\\d{8}$", | ||||
| message: t("input correct phone no."), | message: t("input correct phone no."), | ||||
| value: data[key] ?? "", | value: data[key] ?? "", | ||||
| required: true, | required: true, | ||||
| }; | |||||
| case "phone2": | |||||
| return { | |||||
| id: `${key}`, | |||||
| label: t(`Phone2`), | |||||
| type: "text", | |||||
| // pattern: "^\\d{8}$", | |||||
| message: t("input correct phone no."), | |||||
| value: data[key] ?? "", | |||||
| } as Field; | } as Field; | ||||
| case "joinDate": | |||||
| return { | |||||
| id: `${key}`, | |||||
| label: t(`Join Date`), | |||||
| type: "multiDate", | |||||
| value: data[key] ?? "", | |||||
| required: true, | |||||
| } as Field; | |||||
| case "joinPosition": | |||||
| return { | |||||
| id: `${key}Id`, | |||||
| label: t(`Join Position`), | |||||
| type: "combo-Obj", | |||||
| options: positionCombo, | |||||
| value: data[key].id ?? "", | |||||
| required: true, | |||||
| } as Field; | |||||
| case "departDate": | |||||
| return { | |||||
| id: `${key}`, | |||||
| label: t(`Depart Date`), | |||||
| type: "multiDate", | |||||
| value: data[key] ?? "", | |||||
| } as Field; | |||||
| case "departReason": | |||||
| return { | |||||
| id: `${key}`, | |||||
| label: t(`Depart Reason`), | |||||
| type: "text", | |||||
| value: data[key] ?? "", | |||||
| } as Field; | |||||
| case "remark": | |||||
| return { | |||||
| id: `remark`, | |||||
| label: t(`Remark`), | |||||
| type: "remarks", | |||||
| value: data[key] ?? "", | |||||
| } as Field; | |||||
| default: | |||||
| return null; | |||||
| } | |||||
| }).filter((item): item is Field => item !== null); | |||||
| console.log(list2); | |||||
| console.log([list1]); | |||||
| setFieldLists([list1,list2]); | |||||
| }); | |||||
| }, [companyCombo]); | |||||
| default: | |||||
| return null; | |||||
| } | |||||
| }).filter((item): item is Field => item !== null); | |||||
| ///////////////////// list 2 ///////////////////// | |||||
| const list2 = keyOrder2 | |||||
| .map((key) => { | |||||
| switch (key) { | |||||
| case "emergContactName": | |||||
| return { | |||||
| id: `${key}`, | |||||
| label: t(`Emergency Contact Name`), | |||||
| type: "text", | |||||
| value: data[key] ?? "", | |||||
| required: true, | |||||
| } as Field; | |||||
| case "emergContactPhone": | |||||
| return { | |||||
| id: `${key}`, | |||||
| label: t(`Emergency Contact Phone`), | |||||
| type: "text", | |||||
| // pattern: "^\\d{8}$", | |||||
| message: t("input correct phone no."), | |||||
| value: data[key] ?? "", | |||||
| required: true, | |||||
| } as Field; | |||||
| case "joinDate": | |||||
| return { | |||||
| id: `${key}`, | |||||
| label: t(`Join Date`), | |||||
| type: "multiDate", | |||||
| value: data[key] ?? "", | |||||
| required: true, | |||||
| } as Field; | |||||
| case "joinPosition": | |||||
| return { | |||||
| id: `${key}Id`, | |||||
| label: t(`Join Position`), | |||||
| type: "combo-Obj", | |||||
| options: positionCombo, | |||||
| value: data[key].id ?? "", | |||||
| required: true, | |||||
| } as Field; | |||||
| case "departDate": | |||||
| return { | |||||
| id: `${key}`, | |||||
| label: t(`Depart Date`), | |||||
| type: "multiDate", | |||||
| value: data[key] ?? "", | |||||
| } as Field; | |||||
| case "departReason": | |||||
| return { | |||||
| id: `${key}`, | |||||
| label: t(`Depart Reason`), | |||||
| type: "text", | |||||
| value: data[key] ?? "", | |||||
| } as Field; | |||||
| case "remark": | |||||
| return { | |||||
| id: `remark`, | |||||
| label: t(`Remark`), | |||||
| type: "remarks", | |||||
| value: data[key] ?? "", | |||||
| } as Field; | |||||
| default: | |||||
| return null; | |||||
| } | |||||
| }).filter((item): item is Field => item !== null); | |||||
| console.log(list2); | |||||
| console.log([list1]); | |||||
| setFieldLists([list1,list2]); | |||||
| }); | |||||
| } | |||||
| }, [companyCombo, teamCombo, departmentCombo, positionCombo, gradeCombo, skillCombo, salaryCombo, idString]); | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| {/* {console.log(fieldLists)} */} | |||||
| <EditStaffForm Title={title} id={id} fieldLists={fieldLists as Field[][] || [[]]} /> | |||||
| <EditStaffForm Title={title} fieldLists={fieldLists as Field[][] || [[]]} /> | |||||
| </> | </> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -26,14 +26,15 @@ interface Field { | |||||
| } | } | ||||
| interface formProps { | interface formProps { | ||||
| id: number; | |||||
| Title?: string[]; | Title?: string[]; | ||||
| fieldLists: Field[][]; | fieldLists: Field[][]; | ||||
| } | } | ||||
| const EditStaffForm: React.FC<formProps> = ({ id, Title, fieldLists }) => { | |||||
| const EditStaffForm: React.FC<formProps> = ({ Title, fieldLists }) => { | |||||
| const router = useRouter(); | const router = useRouter(); | ||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const searchParams = useSearchParams(); | |||||
| const idString = searchParams.get("id") | |||||
| const [serverError, setServerError] = useState(""); | const [serverError, setServerError] = useState(""); | ||||
| // make new inputs | // make new inputs | ||||
| const onSubmit = useCallback<SubmitHandler<CreateStaffInputs>>( | const onSubmit = useCallback<SubmitHandler<CreateStaffInputs>>( | ||||
| @@ -49,20 +50,21 @@ const EditStaffForm: React.FC<formProps> = ({ id, Title, fieldLists }) => { | |||||
| const formattedDate = dayjs(data.departDate, 'MM/DD/YYYY').format('YYYY-MM-DD'); | const formattedDate = dayjs(data.departDate, 'MM/DD/YYYY').format('YYYY-MM-DD'); | ||||
| formatDepartDate = formattedDate; | formatDepartDate = formattedDate; | ||||
| } | } | ||||
| // console.log(data); | |||||
| const temp = { | |||||
| id: id, | |||||
| ...data, | |||||
| emergContactPhone: data.emergContactPhone.toString(), | |||||
| phone1: data.phone1.toString(), | |||||
| phone2: data.phone1.toString(), | |||||
| joinDate: formatJoinDate, | |||||
| departDate: formatDepartDate, | |||||
| if (idString) { | |||||
| const temp = { | |||||
| id: parseInt(idString), | |||||
| ...data, | |||||
| emergContactPhone: data.emergContactPhone.toString(), | |||||
| phone1: data.phone1.toString(), | |||||
| phone2: data.phone1.toString(), | |||||
| joinDate: formatJoinDate, | |||||
| departDate: formatDepartDate, | |||||
| } | |||||
| console.log(temp) | |||||
| setServerError(""); | |||||
| await saveStaff(temp); | |||||
| router.replace("/settings/staff"); | |||||
| } | } | ||||
| console.log(temp) | |||||
| setServerError(""); | |||||
| await saveStaff(temp); | |||||
| router.replace("/settings/staff"); | |||||
| } catch (e) { | } catch (e) { | ||||
| setServerError(t("An error has occurred. Please try again later.")); | setServerError(t("An error has occurred. Please try again later.")); | ||||
| } | } | ||||
| @@ -67,8 +67,8 @@ const Allocation: React.FC<Props> = ({ allStaffs: staff }) => { | |||||
| const removeStaff = useCallback((staff: StaffResult) => { | const removeStaff = useCallback((staff: StaffResult) => { | ||||
| setSelectedStaff((s) => s.filter((s) => s.id !== staff.id)); | setSelectedStaff((s) => s.filter((s) => s.id !== staff.id)); | ||||
| setDeletedStaffIds((s) => s) | |||||
| // setValue("deleteStaffIds", [...staff.id]) | |||||
| // setDeletedStaffIds((s) => s) | |||||
| setDeletedStaffIds((prevIds) => [...prevIds, staff.id]); | |||||
| }, []); | }, []); | ||||
| const setTeamLead = useCallback( | const setTeamLead = useCallback( | ||||
| @@ -118,7 +118,7 @@ const Allocation: React.FC<Props> = ({ allStaffs: staff }) => { | |||||
| useEffect(() => { | useEffect(() => { | ||||
| setValue("deleteStaffIds", deletedStaffIds) | setValue("deleteStaffIds", deletedStaffIds) | ||||
| console.log(deletedStaffIds) | console.log(deletedStaffIds) | ||||
| }, [deletedStaffIds, setValue]); | |||||
| }, [deletedStaffIds]); | |||||
| const StaffPoolColumns = useMemo<Column<StaffResult>[]>( | const StaffPoolColumns = useMemo<Column<StaffResult>[]>( | ||||
| () => [ | () => [ | ||||
| @@ -70,7 +70,7 @@ const EditTeam: React.FC<Props> = async ({ staff, desc }) => { | |||||
| const tempDesc = desc.filter( | const tempDesc = desc.filter( | ||||
| (item) => item.id === parseInt(idString) | (item) => item.id === parseInt(idString) | ||||
| ) | ) | ||||
| // console.log(filteredTeam); | |||||
| if (filteredTeam.length > 0) { | if (filteredTeam.length > 0) { | ||||
| const filteredIds: number[] = filteredTeam.map((i) => ( | const filteredIds: number[] = filteredTeam.map((i) => ( | ||||
| i.id | i.id | ||||
| @@ -82,21 +82,7 @@ const EditTeam: React.FC<Props> = async ({ staff, desc }) => { | |||||
| formProps.reset({description: tempDesc[0].description, addStaffIds: idList}) | formProps.reset({description: tempDesc[0].description, addStaffIds: idList}) | ||||
| setFilteredDesc(tempDesc[0].description) | setFilteredDesc(tempDesc[0].description) | ||||
| } | } | ||||
| // console.log(staff); | |||||
| // const desc = staff[0]?.description | |||||
| // setDesc(desc) | |||||
| // const staff = staff.map((item) => { | |||||
| // return { | |||||
| // id: item.id, | |||||
| // name: item.name, | |||||
| // staffId: item.staffId, | |||||
| // teamId: item.teamId, | |||||
| // staffName: item.staffName, | |||||
| // currentPosition: item.currentPosition | |||||
| // } as StaffResult | |||||
| // }) | |||||
| console.log(staff) | |||||
| setAllStaffs(staff) | setAllStaffs(staff) | ||||
| }, [searchParams]); | }, [searchParams]); | ||||
| @@ -124,11 +110,12 @@ const EditTeam: React.FC<Props> = async ({ staff, desc }) => { | |||||
| const tempData = { | const tempData = { | ||||
| description: data.description, | description: data.description, | ||||
| addStaffIds: data.addStaffIds, | addStaffIds: data.addStaffIds, | ||||
| deleteStaffIds: data.deleteStaffIds, | |||||
| id: parseInt(idString!!) | id: parseInt(idString!!) | ||||
| } | } | ||||
| console.log(tempData) | console.log(tempData) | ||||
| // await saveTeam(tempData); | |||||
| // router.replace("/settings/staff"); | |||||
| await saveTeam(tempData); | |||||
| router.replace("/settings/team"); | |||||
| } catch (e) { | } catch (e) { | ||||
| console.log(e); | console.log(e); | ||||
| setServerError(t("An error has occurred. Please try again later.")); | setServerError(t("An error has occurred. Please try again later.")); | ||||
| @@ -3,6 +3,8 @@ import React from "react"; | |||||
| import InvoiceSearch from "./InvoiceSearch"; | import InvoiceSearch from "./InvoiceSearch"; | ||||
| import InvoiceSearchLoading from "./InvoiceSearchLoading"; | import InvoiceSearchLoading from "./InvoiceSearchLoading"; | ||||
| import { fetchInvoices } from "@/app/api/invoices"; | import { fetchInvoices } from "@/app/api/invoices"; | ||||
| import { timestampToDateString } from "@/app/utils/formatUtil"; | |||||
| interface SubComponents { | interface SubComponents { | ||||
| Loading: typeof InvoiceSearchLoading; | Loading: typeof InvoiceSearchLoading; | ||||
| @@ -11,7 +13,12 @@ interface SubComponents { | |||||
| const InvoiceSearchWrapper: React.FC & SubComponents = async () => { | const InvoiceSearchWrapper: React.FC & SubComponents = async () => { | ||||
| const Invoices = await fetchInvoices(); | const Invoices = await fetchInvoices(); | ||||
| return <InvoiceSearch invoices={Invoices} />; | |||||
| const temp = Invoices.map((invoice) => ({ | |||||
| ...invoice, | |||||
| paymentMilestoneDate: timestampToDateString(invoice.paymentMilestoneDate) | |||||
| })) | |||||
| return <InvoiceSearch invoices={temp} />; | |||||
| }; | }; | ||||
| InvoiceSearchWrapper.Loading = InvoiceSearchLoading; | InvoiceSearchWrapper.Loading = InvoiceSearchLoading; | ||||
| @@ -22,6 +22,7 @@ import Company from '@mui/icons-material/Store'; | |||||
| import Department from '@mui/icons-material/Diversity3'; | import Department from '@mui/icons-material/Diversity3'; | ||||
| import Position from '@mui/icons-material/Paragliding'; | import Position from '@mui/icons-material/Paragliding'; | ||||
| import Salary from '@mui/icons-material/AttachMoney'; | import Salary from '@mui/icons-material/AttachMoney'; | ||||
| import Team from '@mui/icons-material/Paragliding'; | |||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import Typography from "@mui/material/Typography"; | import Typography from "@mui/material/Typography"; | ||||
| import { usePathname } from "next/navigation"; | import { usePathname } from "next/navigation"; | ||||
| @@ -119,6 +120,7 @@ const navigationItems: NavigationItem[] = [ | |||||
| { icon: <Department />, label: "Department", path: "/settings/department" }, | { icon: <Department />, label: "Department", path: "/settings/department" }, | ||||
| { icon: <Position />, label: "Position", path: "/settings/position" }, | { icon: <Position />, label: "Position", path: "/settings/position" }, | ||||
| { icon: <Salary />, label: "Salary", path: "/settings/salary" }, | { icon: <Salary />, label: "Salary", path: "/settings/salary" }, | ||||
| { icon: <Team />, label: "Team", path: "/settings/team" }, | |||||
| ], | ], | ||||
| }, | }, | ||||
| ]; | ]; | ||||
| @@ -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, moneyFormatter } 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> { | ||||
| @@ -78,7 +80,7 @@ function SearchResults<T extends ResultWithId>({ | |||||
| <TableRow> | <TableRow> | ||||
| {columns.map((column, idx) => ( | {columns.map((column, idx) => ( | ||||
| <TableCell key={`${column.name.toString()}${idx}`}> | <TableCell key={`${column.name.toString()}${idx}`}> | ||||
| {column.label} | |||||
| {column?.type === "money" ? <div style={{display: "flex", justifyContent: "flex-end"}}>{column.label}</div> : column.label} | |||||
| </TableCell> | </TableCell> | ||||
| ))} | ))} | ||||
| </TableRow> | </TableRow> | ||||
| @@ -101,9 +103,16 @@ 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?.type === "money" ? ( | |||||
| <div style={{display: "flex", justifyContent: "flex-end"}}>{moneyFormatter.format(item[columnName] as number)}</div> | |||||
| ) : | |||||
| ( | |||||
| <>{column?.needTranslation ? t(item[columnName] as string) : item[columnName]}</> | |||||
| )} | |||||
| </TableCell> | </TableCell> | ||||
| ); | ); | ||||
| })} | })} | ||||
| @@ -0,0 +1,105 @@ | |||||
| "use client"; | |||||
| import React, { useCallback, useMemo, useState } from "react"; | |||||
| import Button from "@mui/material/Button"; | |||||
| import { Card, Modal, Stack, Typography } from "@mui/material"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { Add } from "@mui/icons-material"; | |||||
| import Check from "@mui/icons-material/Check"; | |||||
| import Close from "@mui/icons-material/Close"; | |||||
| import { TSMS_BUTTON_THEME } from "@/theme/colorConst"; | |||||
| import { ThemeProvider } from "@emotion/react"; | |||||
| interface Props { | |||||
| isOpen: boolean; | |||||
| onConfirm: (data: any) => void; | |||||
| onCancel: (data: any | null) => void; | |||||
| } | |||||
| const ConfirmModal: React.FC<Props> = ({ ...props }) => { | |||||
| const { t } = useTranslation(); | |||||
| return ( | |||||
| <> | |||||
| <Modal open={props.isOpen} onClose={props.onCancel}> | |||||
| <Card | |||||
| style={{ | |||||
| flex: 10, | |||||
| marginBottom: "20px", | |||||
| width: "auto", | |||||
| minWidth: "400px", | |||||
| minHeight: "200px", | |||||
| position: "fixed", | |||||
| top: "50%", | |||||
| left: "50%", | |||||
| transform: "translate(-50%, -50%)", | |||||
| }} | |||||
| > | |||||
| <> | |||||
| <Typography | |||||
| variant="h5" | |||||
| id="modal-title" | |||||
| sx={{ | |||||
| flex: 1, | |||||
| ml: 4, | |||||
| mt: 2, | |||||
| }} | |||||
| > | |||||
| {t("Confirm")} | |||||
| </Typography> | |||||
| <> | |||||
| <Typography | |||||
| variant="h6" | |||||
| id="modal-title" | |||||
| sx={{ | |||||
| flex: 1, | |||||
| mt: 4, | |||||
| justifyContent: "center", | |||||
| textAlign: "center", | |||||
| }} | |||||
| > | |||||
| {t("Are You Sure")} | |||||
| </Typography> | |||||
| </> | |||||
| {/* <ThemeProvider theme={TSMS_BUTTON_THEME}> */} | |||||
| <Stack direction="row"> | |||||
| <Button | |||||
| variant="contained" | |||||
| endIcon={<Check />} | |||||
| sx={{ | |||||
| flex: 1, | |||||
| ml: 5, | |||||
| mr: 2, | |||||
| mt: 4, | |||||
| justifyContent: "space-between", | |||||
| }} | |||||
| onClick={props.onConfirm} | |||||
| // LinkComponent={Link} | |||||
| // href="/settings/department/new" | |||||
| > | |||||
| Proceed | |||||
| </Button> | |||||
| <Button | |||||
| variant="contained" | |||||
| startIcon={<Close />} | |||||
| sx={{ | |||||
| flex: 1, | |||||
| mr: 5, | |||||
| mt: 4, | |||||
| justifyContent: "space-between", | |||||
| }} | |||||
| color="warning" | |||||
| onClick={props.onCancel} | |||||
| // LinkComponent={Link} | |||||
| // href="/settings/department/new" | |||||
| > | |||||
| Cancel | |||||
| </Button> | |||||
| </Stack> | |||||
| {/* </ThemeProvider> */} | |||||
| </> | |||||
| </Card> | |||||
| </Modal> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default ConfirmModal; | |||||
| @@ -9,6 +9,9 @@ import EditNote from "@mui/icons-material/EditNote"; | |||||
| import DeleteIcon from '@mui/icons-material/Delete'; | import DeleteIcon from '@mui/icons-material/Delete'; | ||||
| import { deleteStaff } from "@/app/api/staff/actions"; | import { deleteStaff } from "@/app/api/staff/actions"; | ||||
| import { useRouter } from "next/navigation"; | import { useRouter } from "next/navigation"; | ||||
| import ConfirmModal from "./ConfirmDeleteModal"; | |||||
| import { deleteTeam } from "@/app/api/team/actions"; | |||||
| interface Props { | interface Props { | ||||
| team: TeamResult[]; | team: TeamResult[]; | ||||
| @@ -50,23 +53,47 @@ const TeamSearch: React.FC<Props> = ({ team }) => { | |||||
| router.push(`/settings/team/edit?id=${id}`); | router.push(`/settings/team/edit?id=${id}`); | ||||
| }, [router, t]); | }, [router, t]); | ||||
| // const onDeleteClick = useCallback((team: TeamResult) => { | |||||
| // console.log(team); | |||||
| // deleteTeam | |||||
| // }, [router, t]); | |||||
| const onDeleteClick = (team: TeamResult) => { | |||||
| console.log(team); | |||||
| setData(team) | |||||
| setIsOpen(!isOpen) | |||||
| }; | |||||
| const onConfirm = useCallback(async (team: TeamResult) => { | |||||
| console.log(team); | |||||
| if (data) | |||||
| await deleteTeam(data) | |||||
| setIsOpen(false) | |||||
| window.location.reload; | |||||
| }, [deleteTeam, data]); | |||||
| const onCancel = useCallback(() => { | |||||
| setIsOpen(false) | |||||
| }, []); | |||||
| const columns = useMemo<Column<TeamResult>[]>( | const columns = useMemo<Column<TeamResult>[]>( | ||||
| () => [ | () => [ | ||||
| { | { | ||||
| name: "action", | name: "action", | ||||
| label: t("Actions"), | |||||
| label: t("Edit"), | |||||
| onClick: onTeamClick, | onClick: onTeamClick, | ||||
| buttonIcon: <EditNote />, | buttonIcon: <EditNote />, | ||||
| }, | }, | ||||
| { name: "name", label: t("Name") }, | { name: "name", label: t("Name") }, | ||||
| { name: "code", label: t("Code") }, | { name: "code", label: t("Code") }, | ||||
| { name: "description", label: t("description") }, | { name: "description", label: t("description") }, | ||||
| // { | |||||
| // name: "action", | |||||
| // label: t("Actions"), | |||||
| // onClick: deleteClick, | |||||
| // buttonIcon: <DeleteIcon />, | |||||
| // }, | |||||
| { | |||||
| name: "action", | |||||
| label: t("Delete"), | |||||
| onClick: onDeleteClick, | |||||
| buttonIcon: <DeleteIcon />, | |||||
| }, | |||||
| ], | ], | ||||
| [t], | [t], | ||||
| ); | ); | ||||
| @@ -89,6 +116,11 @@ const TeamSearch: React.FC<Props> = ({ team }) => { | |||||
| }} | }} | ||||
| /> | /> | ||||
| <SearchResults<TeamResult> items={filteredTeam} columns={columns} /> | <SearchResults<TeamResult> items={filteredTeam} columns={columns} /> | ||||
| <ConfirmModal | |||||
| isOpen={isOpen} | |||||
| onConfirm={onConfirm} | |||||
| onCancel={onCancel} | |||||
| /> | |||||
| </> | </> | ||||
| ); | ); | ||||
| @@ -8,7 +8,7 @@ | |||||
| "Related Project Name": "Related Project Name", | "Related Project Name": "Related Project Name", | ||||
| "Expense Type": "Expense Type", | "Expense Type": "Expense Type", | ||||
| "Status": "Status", | "Status": "Status", | ||||
| "Amount (HKD)": "Amount (HKD)", | |||||
| "Amount": "Amount", | |||||
| "Remarks": "Remarks", | "Remarks": "Remarks", | ||||
| "Invoice Date": "Invoice Date", | "Invoice Date": "Invoice Date", | ||||
| "Supporting Document": "Supporting Document", | "Supporting Document": "Supporting Document", | ||||
| @@ -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", | ||||
| @@ -8,7 +8,7 @@ | |||||
| "Related Project Name": "相關項目名稱", | "Related Project Name": "相關項目名稱", | ||||
| "Expense Type": "費用類別", | "Expense Type": "費用類別", | ||||
| "Status": "狀態", | "Status": "狀態", | ||||
| "Amount (HKD)": "金額 (HKD)", | |||||
| "Amount": "金額", | |||||
| "Remarks": "備註", | "Remarks": "備註", | ||||
| "Invoice Date": "收據日期", | "Invoice Date": "收據日期", | ||||
| "Supporting Document": "支援文件", | "Supporting Document": "支援文件", | ||||
| @@ -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": "取消", | ||||