diff --git a/src/app/(main)/staffReimbursement/create/page.tsx b/src/app/(main)/staffReimbursement/create/page.tsx index eafce4f..f1effc4 100644 --- a/src/app/(main)/staffReimbursement/create/page.tsx +++ b/src/app/(main)/staffReimbursement/create/page.tsx @@ -1,5 +1,5 @@ -import CreateClaim from "@/components/CreateClaim"; -import { getServerI18n } from "@/i18n"; +import ClaimDetail from "@/components/ClaimDetail"; +import { I18nProvider, getServerI18n } from "@/i18n"; import Typography from "@mui/material/Typography"; import { Metadata } from "next"; @@ -7,15 +7,17 @@ export const metadata: Metadata = { title: "Create Claim", }; -const CreateClaims: React.FC = async () => { - const { t } = await getServerI18n("claims"); +const ClaimDetails: React.FC = async () => { + const { t } = await getServerI18n("claim"); return ( <> {t("Create Claim")} - + + + ); }; -export default CreateClaims; +export default ClaimDetails; diff --git a/src/app/(main)/staffReimbursement/page.tsx b/src/app/(main)/staffReimbursement/page.tsx index 1a1afcf..ee8781f 100644 --- a/src/app/(main)/staffReimbursement/page.tsx +++ b/src/app/(main)/staffReimbursement/page.tsx @@ -1,6 +1,6 @@ import { preloadClaims } from "@/app/api/claims"; import ClaimSearch from "@/components/ClaimSearch"; -import { getServerI18n } from "@/i18n"; +import { I18nProvider, getServerI18n } from "@/i18n"; import Add from "@mui/icons-material/Add"; import Button from "@mui/material/Button"; import Stack from "@mui/material/Stack"; @@ -14,7 +14,7 @@ export const metadata: Metadata = { }; const StaffReimbursement: React.FC = async () => { - const { t } = await getServerI18n("claims"); + const { t } = await getServerI18n("claim"); preloadClaims(); return ( @@ -37,9 +37,11 @@ const StaffReimbursement: React.FC = async () => { {t("Create Claim")} - }> - - + + }> + + + ); }; diff --git a/src/app/api/claims/actions.ts b/src/app/api/claims/actions.ts new file mode 100644 index 0000000..e77b17f --- /dev/null +++ b/src/app/api/claims/actions.ts @@ -0,0 +1,48 @@ +"use server"; + +import { BASE_API_URL } from "@/config/api"; +import { Claim, ProjectCombo, SupportingDocument } from "."; +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { revalidateTag } from "next/cache"; + +export interface ClaimInputFormByStaff { + id: number | null; + code: string | null; + expenseType: string; + status: string; + + addClaimDetails: ClaimDetailTable[] +} + +export interface ClaimDetailTable { + id: number; + invoiceDate: Date; + description: string; + project: ProjectCombo; + amount: number; + supportingDocumentName: string; + oldSupportingDocument: FileList[]; + newSupportingDocument: SupportingDocument; + isNew: boolean; +} + +export interface SaveClaimResponse { + claim: Claim; + message: string; +} + +export const saveClaim = async (data: ClaimInputFormByStaff) => { + console.log(data) + const saveCustomer = await serverFetchJson( + `${BASE_API_URL}/claim/save`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + + revalidateTag("claims"); + + return saveCustomer; +}; \ No newline at end of file diff --git a/src/app/api/claims/index.ts b/src/app/api/claims/index.ts index f012bcc..ceb9887 100644 --- a/src/app/api/claims/index.ts +++ b/src/app/api/claims/index.ts @@ -1,7 +1,9 @@ +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; import { cache } from "react"; import "server-only"; -export interface ClaimResult { +export interface Claim { id: number; created: string; name: string; @@ -11,18 +13,52 @@ export interface ClaimResult { remarks: string; } +export interface ClaimSearchForm { + id: number; + created: string; + createdTo: string; + name: string; + cost: number; + type: "Expense" | "Petty Cash"; + status: "Not Submitted" | "Waiting for Approval" | "Approved" | "Rejected"; + remarks: string; +} + +export interface ProjectCombo { + id: number; + name: string; + code: string; +} + +export interface SupportingDocument { + id: number; + skey: string; + filename: string; +} + export const preloadClaims = () => { fetchClaims(); }; export const fetchClaims = cache(async () => { return mockClaims; + // return serverFetchJson(`${BASE_API_URL}/claim`); +}); + +export const fetchProjectCombo = cache(async () => { + return serverFetchJson(`${BASE_API_URL}/projects`, { + next: { tags: ["projects"] }, + }); }); -const mockClaims: ClaimResult[] = [ +// export const fetchAllCustomers = cache(async () => { +// return serverFetchJson(`${BASE_API_URL}/customer`); +// }); + +const mockClaims: Claim[] = [ { id: 1, - created: "2023-11-22", + created: "2023/11/22", name: "Consultancy Project A", cost: 121.0, type: "Expense", @@ -31,7 +67,7 @@ const mockClaims: ClaimResult[] = [ }, { id: 2, - created: "2023-11-30", + created: "2023/11/30", name: "Consultancy Project A", cost: 4300.0, type: "Expense", @@ -40,7 +76,7 @@ const mockClaims: ClaimResult[] = [ }, { id: 3, - created: "2023-12-12", + created: "2023/12/12", name: "Construction Project C", cost: 3675.0, type: "Petty Cash", diff --git a/src/app/utils/comboUtil.ts b/src/app/utils/comboUtil.ts new file mode 100644 index 0000000..0fcb75b --- /dev/null +++ b/src/app/utils/comboUtil.ts @@ -0,0 +1,11 @@ +export const expenseTypeCombo = [ + "Petty Cash", + "Expense" +] + +export const claimStatusCombo = [ + "Not Submitted", + "Waiting for Approval", + "Approved", + "Rejected" +] \ No newline at end of file diff --git a/src/app/utils/commonUtil.ts b/src/app/utils/commonUtil.ts index e69de29..d4c71b6 100644 --- a/src/app/utils/commonUtil.ts +++ b/src/app/utils/commonUtil.ts @@ -0,0 +1,23 @@ +export const dateInRange = (currentDate: string, startDate: string, endDate: string) => { + + if (currentDate === undefined) { + return false // can be changed to true if necessary + } + + const currentDateTime = new Date(currentDate).getTime() + const startDateTime = startDate === undefined || startDate.length === 0 ? undefined : new Date(startDate).getTime() + const endDateTime = endDate === undefined || startDate.length === 0 ? undefined : new Date(endDate).getTime() + + // console.log(currentDateTime, startDateTime, endDateTime) + if (startDateTime === undefined && endDateTime !== undefined) { + return currentDateTime <= endDateTime + } else if (startDateTime !== undefined && endDateTime === undefined) { + return currentDateTime >= startDateTime + } else { + if (startDateTime !== undefined && endDateTime !== undefined) { + return currentDateTime >= startDateTime && currentDateTime <= endDateTime + } else { + return true + } + } +} \ No newline at end of file diff --git a/src/app/utils/formatUtil.ts b/src/app/utils/formatUtil.ts index 4f364e8..cd28eb5 100644 --- a/src/app/utils/formatUtil.ts +++ b/src/app/utils/formatUtil.ts @@ -1,3 +1,5 @@ +import dayjs from "dayjs"; + export const manhourFormatter = new Intl.NumberFormat("en-HK", { minimumFractionDigits: 2, maximumFractionDigits: 2, @@ -15,6 +17,12 @@ export const percentFormatter = new Intl.NumberFormat("en-HK", { export const INPUT_DATE_FORMAT = "YYYY-MM-DD"; +export const OUTPUT_DATE_FORMAT = "YYYY/MM/DD"; + +export const convertDateToString = (date: Date, format: string = OUTPUT_DATE_FORMAT) => { + return dayjs(date).format(format) +} + const shortDateFormatter_en = new Intl.DateTimeFormat("en-HK", { weekday: "short", year: "numeric", diff --git a/src/components/ClaimDetail/ClaimDetail.tsx b/src/components/ClaimDetail/ClaimDetail.tsx new file mode 100644 index 0000000..36ff8ed --- /dev/null +++ b/src/components/ClaimDetail/ClaimDetail.tsx @@ -0,0 +1,105 @@ +"use client"; + +import Check from "@mui/icons-material/Check"; +import Close from "@mui/icons-material/Close"; +import Button from "@mui/material/Button"; +import Stack from "@mui/material/Stack"; +import { useRouter } from "next/navigation"; +import React, { useCallback, useState } from "react"; +import { useTranslation } from "react-i18next"; +import ClaimFormInfo from "./ClaimFormInfo"; +import { ProjectCombo } from "@/app/api/claims"; +import { Typography } from "@mui/material"; +import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form"; +import { ClaimInputFormByStaff, saveClaim } from "@/app/api/claims/actions"; +import { DoneAll } from "@mui/icons-material"; +import { expenseTypeCombo } from "@/app/utils/comboUtil"; + +export interface Props { + projectCombo: ProjectCombo[] +} + +const ClaimDetail: React.FC = ({ projectCombo }) => { + const { t } = useTranslation("common"); + const [serverError, setServerError] = useState(""); + const router = useRouter(); + + const formProps = useForm({ + defaultValues: { + id: null, + expenseType: expenseTypeCombo[0], + addClaimDetails: [] + }, + }); + + const handleCancel = () => { + router.back(); + }; + + const onSubmit = useCallback>( + async (data, event) => { + try { + console.log(data); + console.log((event?.nativeEvent as any).submitter.name); + const buttonName = (event?.nativeEvent as any).submitter.name + console.log(JSON.stringify(data)) + // const formData = new FormData() + // formData.append("expenseType", data.expenseType) + // formData.append("claimDetails", data.addClaimDetails) + if (buttonName === "submit") { + data.status = "Not Submitted" + } else if (buttonName === "save") { + data.status = "Waiting for Approval" + } + + // 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}) + // } + console.log(data); + await saveClaim(data) + setServerError(""); + // await saveProject(data); + // router.replace("/projects"); + } catch (e) { + setServerError(t("An error has occurred. Please try again later.")); + } + }, + [router, t], + ); + + const onSubmitError = useCallback>( + (errors) => { + // Set the tab so that the focus will go there + console.log(errors) + }, + [], + ); + + return ( + + + + {serverError && ( + + {serverError} + + )} + + + + + + + + ); +}; + +export default ClaimDetail; diff --git a/src/components/ClaimDetail/ClaimDetailWrapper.tsx b/src/components/ClaimDetail/ClaimDetailWrapper.tsx new file mode 100644 index 0000000..602f9e3 --- /dev/null +++ b/src/components/ClaimDetail/ClaimDetailWrapper.tsx @@ -0,0 +1,20 @@ + +import React from "react"; +import ClaimDetail from "./ClaimDetail"; +import { fetchProjectCombo } from "@/app/api/claims"; +// import TaskSetup from "./TaskSetup"; +// import StaffAllocation from "./StaffAllocation"; +// import ResourceMilestone from "./ResourceMilestone"; + +const ClaimDetailWrapper: React.FC = async () => { + const [projectCombo] = + await Promise.all([ + fetchProjectCombo() + ]); + + return ( + + ); +}; + +export default ClaimDetailWrapper; diff --git a/src/components/ClaimDetail/ClaimFormInfo.tsx b/src/components/ClaimDetail/ClaimFormInfo.tsx new file mode 100644 index 0000000..6db692d --- /dev/null +++ b/src/components/ClaimDetail/ClaimFormInfo.tsx @@ -0,0 +1,76 @@ +"use client"; + +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import FormControl from "@mui/material/FormControl"; +import Grid from "@mui/material/Grid"; +import InputLabel from "@mui/material/InputLabel"; +import MenuItem from "@mui/material/MenuItem"; +import Select from "@mui/material/Select"; +import { useTranslation } from "react-i18next"; +import ClaimFormInputGrid from "./ClaimFormInputGrid"; +import { expenseTypeCombo } from "@/app/utils/comboUtil"; +import { Controller, useFormContext } from "react-hook-form"; +import { ClaimInputFormByStaff } from "@/app/api/claims/actions"; +import { ProjectCombo } from "@/app/api/claims"; +import { TextField } from "@mui/material"; + +interface Props { + projectCombo: ProjectCombo[] +} + +const ClaimFormInfo: React.FC = ({ projectCombo }) => { + const { t } = useTranslation(); + + const { + control, + register, + } = useFormContext(); + + return ( + + + + + + + + + + {t("Expense Type")} + ( + + )} + /> + + + + + + + + + + ); +}; + +export default ClaimFormInfo; diff --git a/src/components/CreateClaim/ClaimInputGrid.tsx b/src/components/ClaimDetail/ClaimFormInputGrid.tsx similarity index 51% rename from src/components/CreateClaim/ClaimInputGrid.tsx rename to src/components/ClaimDetail/ClaimFormInputGrid.tsx index 1231001..b6c2fde 100644 --- a/src/components/CreateClaim/ClaimInputGrid.tsx +++ b/src/components/ClaimDetail/ClaimFormInputGrid.tsx @@ -8,16 +8,9 @@ import { Suspense } from "react"; import Button from "@mui/material/Button"; import Stack from "@mui/material/Stack"; import Link from "next/link"; -import { t } from "i18next"; import { - Box, - Container, - Modal, - Select, - SelectChangeEvent, - Typography, + Box, Card, Typography, } from "@mui/material"; -import { Close } from "@mui/icons-material"; import AddIcon from "@mui/icons-material/Add"; import EditIcon from "@mui/icons-material/Edit"; import DeleteIcon from "@mui/icons-material/DeleteOutlined"; @@ -25,35 +18,30 @@ import SaveIcon from "@mui/icons-material/Save"; import CancelIcon from "@mui/icons-material/Close"; import AddPhotoAlternateOutlinedIcon from "@mui/icons-material/AddPhotoAlternateOutlined"; import ImageNotSupportedOutlinedIcon from "@mui/icons-material/ImageNotSupportedOutlined"; -import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; -import ArrowBackIcon from "@mui/icons-material/ArrowBack"; -import Swal from "sweetalert2"; -import { msg } from "../Swal/CustomAlerts"; import React from "react"; -import { DatePicker } from "@mui/x-date-pickers/DatePicker"; import { GridRowsProp, GridRowModesModel, GridRowModes, DataGrid, GridColDef, - GridToolbarContainer, - GridFooterContainer, GridActionsCellItem, GridEventListener, GridRowId, GridRowModel, GridRowEditStopReasons, GridEditInputCell, - GridValueSetterParams, + GridTreeNodeWithRender, + GridRenderCellParams, } from "@mui/x-data-grid"; -import { LocalizationProvider } from "@mui/x-date-pickers"; -import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import dayjs from "dayjs"; import { Props } from "react-intl/src/components/relative"; import palette from "@/theme/devias-material-kit/palette"; - -const weekdays = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]; +import { ProjectCombo } from "@/app/api/claims"; +import { ClaimDetailTable, ClaimInputFormByStaff } from "@/app/api/claims/actions"; +import { useFieldArray, useFormContext } from "react-hook-form"; +import { GridRenderEditCellParams } from "@mui/x-data-grid"; +import { convertDateToString } from "@/app/utils/formatUtil"; interface BottomBarProps { getCostTotal: () => number; @@ -63,15 +51,6 @@ interface BottomBarProps { ) => void; } -interface EditToolbarProps { - // setDay: (newDay : dayjs.Dayjs) => void; - setDay: (newDay: (oldDay: dayjs.Dayjs) => dayjs.Dayjs) => void; - setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; - setRowModesModel: ( - newModel: (oldModel: GridRowModesModel) => GridRowModesModel, - ) => void; -} - interface EditFooterProps { setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; setRowModesModel: ( @@ -80,17 +59,17 @@ interface EditFooterProps { } const BottomBar = (props: BottomBarProps) => { + const { t } = useTranslation("claim") const { setRows, setRowModesModel, getCostTotal } = props; // const getCostTotal = props.getCostTotal; const [newId, setNewId] = useState(-1); - const [invalidDays, setInvalidDays] = useState(0); const handleAddClick = () => { const id = newId; setNewId(newId - 1); setRows((oldRows) => [ ...oldRows, - { id, projectCode: "", task: "", isNew: true }, + { id, invoiceDate: new Date(), project: null, description: null, amount: null, newSupportingDocument: null, supportingDocumentName: null, isNew: true }, ]); setRowModesModel((oldModel) => ({ ...oldModel, @@ -98,11 +77,6 @@ const BottomBar = (props: BottomBarProps) => { })); }; - const totalColDef = { - flex: 1, - // style: {color:getCostTotal('mon')>24?"red":"black"} - }; - const TotalCell = ({ value }: Props) => { const [invalid, setInvalid] = useState(false); @@ -122,7 +96,7 @@ const BottomBar = (props: BottomBarProps) => {
- Total: + {t("Total")}:
@@ -133,7 +107,7 @@ const BottomBar = (props: BottomBarProps) => { onClick={handleAddClick} sx={{ margin: "20px" }} > - Add record + {t("Add Record")}
); @@ -150,40 +124,51 @@ const EditFooter = (props: EditFooterProps) => { ); }; -interface ClaimInputGridProps { - onClose?: () => void; +interface ClaimFormInputGridProps { + // onClose?: () => void; + projectCombo: ProjectCombo[] } const initialRows: GridRowsProp = [ { id: 1, - date: new Date(), + invoiceDate: new Date(), description: "Taxi to client office", - cost: 169.5, - document: "taxi_receipt.jpg", + amount: 169.5, + supportingDocumentName: "taxi_receipt.jpg", }, { id: 2, - date: dayjs().add(-14, "days").toDate(), + invoiceDate: dayjs().add(-14, "days").toDate(), description: "MTR fee to Kowloon Bay Office", - cost: 15.5, - document: "octopus_invoice.jpg", + amount: 15.5, + supportingDocumentName: "octopus_invoice.jpg", }, { id: 3, - date: dayjs().add(-44, "days").toDate(), + invoiceDate: dayjs().add(-44, "days").toDate(), description: "Starbucks", - cost: 504, + amount: 504, }, ]; -const ClaimInputGrid: React.FC = ({ ...props }) => { - const [rows, setRows] = useState(initialRows); - const [day, setDay] = useState(dayjs()); +const ClaimFormInputGrid: React.FC = ({ + // onClose, + projectCombo, +}) => { + const { t } = useTranslation() + const { control, setValue, getValues, formState: { errors } } = useFormContext(); + const { fields } = useFieldArray({ + control, + name: "addClaimDetails" + }) + + const [rows, setRows] = useState([]); const [rowModesModel, setRowModesModel] = React.useState( {}, ); + // Row function const handleRowEditStop: GridEventListener<"rowEditStop"> = ( params, event, @@ -217,20 +202,77 @@ const ClaimInputGrid: React.FC = ({ ...props }) => { } }; - const processRowUpdate = (newRow: GridRowModel) => { - const updatedRow = { ...newRow, isNew: false }; - setRows(rows.map((row) => (row.id === newRow.id ? updatedRow : row))); - return updatedRow; - }; + const processRowUpdate = React.useCallback((newRow: GridRowModel) => { + const updatedRow = { ...newRow }; + const updatedRows = rows.map((row) => (row.id === newRow.id ? { ...updatedRow, supportingDocumentName: row.supportingDocumentName } : row)) + setRows(updatedRows); + setValue("addClaimDetails", updatedRows as ClaimDetailTable[]) + return updatedRows.find((row) => row.id === newRow.id) as GridRowModel; + }, [rows, rowModesModel, t]); const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => { setRowModesModel(newRowModesModel); }; + // File Upload function + const fileInputRef: React.RefObject> = React.useRef({}) + + const setFileInputRefs = (ele: HTMLInputElement | null, key: string) => { + if (fileInputRef.current !== null) { + fileInputRef.current[key] = ele + } + } + + useEffect(() => { + + }, []) + const handleFileSelect = (key: string) => { + if (fileInputRef !== null && fileInputRef.current !== null && fileInputRef.current[key] !== null) { + fileInputRef.current[key]?.click() + } + } + + const handleFileChange = (event: React.ChangeEvent, params: GridRenderEditCellParams) => { + + const file = event.target.files?.[0] ?? null + + if (file !== null) { + console.log(file) + console.log(typeof file) + const updatedRows = rows.map((row) => (row.id === params.row.id ? { ...row, supportingDocumentName: file.name, newSupportingDocument: file } : row)) + setRows(updatedRows); + setValue("addClaimDetails", updatedRows as ClaimDetailTable[]) + // const url = URL.createObjectURL(new Blob([file])); + // const link = document.createElement("a"); + // link.href = url; + // link.setAttribute("download", file.name); + // link.click(); + } + } + + const handleFileDelete = (id: number) => { + const updatedRows = rows.map((row) => (row.id === id ? { ...row, supportingDocumentName: null, newSupportingDocument: null } : row)) + setRows(updatedRows); + setValue("addClaimDetails", updatedRows as ClaimDetailTable[]) + } + + const handleLinkClick = (params: GridRenderEditCellParams) => { + + const url = URL.createObjectURL(new Blob([params.row.newSupportingDocument])); + const link = document.createElement("a"); + link.href = url; + link.setAttribute("download", params.row.supportingDocumentName); + link.click(); + + // console.log(params) + // console.log(rows) + } + + // columns const getCostTotal = () => { let sum = 0; rows.forEach((row) => { - sum += row["cost"] ?? 0; + sum += row["amount"] ?? 0; }); return sum; }; @@ -256,11 +298,11 @@ const ClaimInputGrid: React.FC = ({ ...props }) => { ), }; - const columns: GridColDef[] = [ + const columns: GridColDef[] = React.useMemo(() => [ { field: "actions", type: "actions", - headerName: "Actions", + headerName: t("Actions"), width: 100, cellClassName: "actions", getActions: ({ id }) => { @@ -312,24 +354,50 @@ const ClaimInputGrid: React.FC = ({ ...props }) => { }, }, { - field: "date", - headerName: "Invoice Date", + field: "invoiceDate", + headerName: t("Invoice Date"), // width: 220, flex: 1, editable: true, type: "date", + renderCell: (params: GridRenderCellParams) => { + return convertDateToString(params.value!!) + }, + }, + { + field: "project", + headerName: t("Project"), + // width: 220, + flex: 1, + editable: true, + type: "singleSelect", + getOptionLabel: (value: any) => { + return !value?.code || value?.code.length === 0 ? `${value?.name}` : `${value?.code} - ${value?.name}`; + }, + getOptionValue: (value: any) => value, + valueOptions: () => { + const options = projectCombo ?? [] + + if (options.length === 0) { + options.push({ id: -1, code: "", name: "No Projects" }) + } + return options; + }, + valueGetter: (params) => { + return params.value ?? projectCombo[0].id ?? -1 + }, }, { field: "description", - headerName: "Description", + headerName: t("Description"), // width: 220, flex: 2, editable: true, type: "string", }, { - field: "cost", - headerName: "Cost (HKD)", + field: "amount", + headerName: t("Amount (HKD)"), editable: true, type: "number", valueFormatter: (params) => { @@ -337,31 +405,34 @@ const ClaimInputGrid: React.FC = ({ ...props }) => { }, }, { - field: "document", - headerName: "Supporting Document", - type: "string", + field: "supportingDocumentName", + headerName: t("Supporting Document"), + // type: "string", editable: true, flex: 2, renderCell: (params) => { return params.value ? ( - + handleLinkClick(params)} href="#">{params.value} + {/* {params.value} - + */} ) : ( No Documents ); }, renderEditCell: (params) => { - return params.value ? ( + const currentRow = rows.find(row => row.id === params.row.id); + return params.formattedValue ? ( - - {params.value} - + handleLinkClick(params)} href="#">{params.formattedValue} + {/* + {params.formattedValue} + */} ) : ( - + + ); }, }, - ]; + ], [rows, rowModesModel, t],); return ( = ({ ...props }) => { }, }} > - ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> + {t("Please ensure at least one row is created, and all the fields are inputted and saved")} + } + {Boolean(errors.addClaimDetails?.type === "format") && ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> + {t("Please ensure the date formats are correct")} + } +
+ - + initialState={{ + pagination: { paginationModel: { pageSize: 5 } }, + }} + /> +
); }; -export default ClaimInputGrid; +export default ClaimFormInputGrid; diff --git a/src/components/ClaimDetail/index.ts b/src/components/ClaimDetail/index.ts new file mode 100644 index 0000000..0fa3ab2 --- /dev/null +++ b/src/components/ClaimDetail/index.ts @@ -0,0 +1 @@ +export { default } from "./ClaimDetailWrapper"; diff --git a/src/components/ClaimSearch/ClaimSearch.tsx b/src/components/ClaimSearch/ClaimSearch.tsx index 6ab02cf..582b9f8 100644 --- a/src/components/ClaimSearch/ClaimSearch.tsx +++ b/src/components/ClaimSearch/ClaimSearch.tsx @@ -1,65 +1,52 @@ "use client"; -import { ClaimResult } from "@/app/api/claims"; +import { Claim, ClaimSearchForm } from "@/app/api/claims"; import React, { useCallback, useMemo, useState } from "react"; import SearchBox, { Criterion } from "../SearchBox/index"; import { useTranslation } from "react-i18next"; import SearchResults, { Column } from "../SearchResults/index"; import EditNote from "@mui/icons-material/EditNote"; +import { dateInRange } from "@/app/utils/commonUtil"; +import { claimStatusCombo, expenseTypeCombo } from "@/app/utils/comboUtil"; interface Props { - claims: ClaimResult[]; + claims: Claim[]; } -type SearchQuery = Partial>; +type SearchQuery = Partial>; type SearchParamNames = keyof SearchQuery; const ClaimSearch: React.FC = ({ claims }) => { - const { t } = useTranslation("claims"); + const { t } = useTranslation(); // If claim searching is done on the server-side, then no need for this. const [filteredClaims, setFilteredClaims] = useState(claims); const searchCriteria: Criterion[] = useMemo( () => [ - { label: t("Creation Date"), 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("Cost (HKD)"), - paramName: "cost", - type: "text", - }, { label: t("Expense Type"), paramName: "type", type: "select", - options: ["Expense", "Petty Cash"], + options: expenseTypeCombo, }, { label: t("Status"), paramName: "status", type: "select", - options: [ - "Not Submitted", - "Waiting for Approval", - "Approved", - "Rejected", - ], - }, - { - label: t("Remarks"), - paramName: "remarks", - type: "text", + options: claimStatusCombo, }, ], [t], ); - const onClaimClick = useCallback((claim: ClaimResult) => { + const onClaimClick = useCallback((claim: Claim) => { console.log(claim); }, []); - const columns = useMemo[]>( + const columns = useMemo[]>( () => [ // { // name: "action", @@ -69,9 +56,9 @@ const ClaimSearch: React.FC = ({ claims }) => { // }, { name: "created", label: t("Creation Date") }, { name: "name", label: t("Related Project Name") }, - { name: "cost", label: t("Cost (HKD)") }, - { name: "type", label: t("Expense Type") }, - { name: "status", label: t("Status") }, + { name: "cost", label: t("Amount (HKD)") }, + { name: "type", label: t("Expense Type"), needTranslation: true }, + { name: "status", label: t("Status"), needTranslation: true }, { name: "remarks", label: t("Remarks") }, ], [t, onClaimClick], @@ -82,10 +69,18 @@ const ClaimSearch: React.FC = ({ claims }) => { { - console.log(query); + setFilteredClaims( + claims.filter( + (claim) => + dateInRange(claim.created, query.created, query.createdTo ?? undefined) && + claim.name.toLowerCase().includes(query.name.toLowerCase()) && + (claim.type.toLowerCase().includes(query.type.toLowerCase()) || query.type.toLowerCase() === "all") && + (claim.status.toLowerCase().includes(query.status.toLowerCase()) || query.status.toLowerCase() === "all") + ), + ); }} /> - items={filteredClaims} columns={columns} /> + items={filteredClaims} columns={columns} /> ); }; diff --git a/src/components/CreateClaim/ClaimDetails.tsx b/src/components/CreateClaim/ClaimDetails.tsx deleted file mode 100644 index 0a56ef4..0000000 --- a/src/components/CreateClaim/ClaimDetails.tsx +++ /dev/null @@ -1,67 +0,0 @@ -"use client"; - -import Stack from "@mui/material/Stack"; -import Box from "@mui/material/Box"; -import Card from "@mui/material/Card"; -import CardContent from "@mui/material/CardContent"; -import FormControl from "@mui/material/FormControl"; -import Grid from "@mui/material/Grid"; -import InputLabel from "@mui/material/InputLabel"; -import MenuItem from "@mui/material/MenuItem"; -import Select from "@mui/material/Select"; -import TextField from "@mui/material/TextField"; -import Typography from "@mui/material/Typography"; -import { useTranslation } from "react-i18next"; -import CardActions from "@mui/material/CardActions"; -import RestartAlt from "@mui/icons-material/RestartAlt"; -import Button from "@mui/material/Button"; -import ClaimInputGrid from "./ClaimInputGrid"; - -const ClaimDetails: React.FC = () => { - const { t } = useTranslation(); - - return ( - - - - {/* - {t("Related Project")} - */} - - - - {t("Related Project")} - - - - - - {t("Expense Type")} - - - - - - - - - - - {/* - - */} - - - ); -}; - -export default ClaimDetails; diff --git a/src/components/CreateClaim/CreateClaim.tsx b/src/components/CreateClaim/CreateClaim.tsx deleted file mode 100644 index 5e89398..0000000 --- a/src/components/CreateClaim/CreateClaim.tsx +++ /dev/null @@ -1,48 +0,0 @@ -"use client"; - -import Check from "@mui/icons-material/Check"; -import Close from "@mui/icons-material/Close"; -import Button from "@mui/material/Button"; -import Stack from "@mui/material/Stack"; -import Tab from "@mui/material/Tab"; -import Tabs, { TabsProps } from "@mui/material/Tabs"; -import { useRouter } from "next/navigation"; -import React, { useCallback, useState } from "react"; -import { useTranslation } from "react-i18next"; -import ClaimProjectDetails from "./ClaimDetails"; -// import TaskSetup from "./TaskSetup"; -// import StaffAllocation from "./StaffAllocation"; -// import ResourceMilestone from "./ResourceMilestone"; - -const CreateProject: React.FC = () => { - const [tabIndex, setTabIndex] = useState(0); - const { t } = useTranslation(); - const router = useRouter(); - - const handleCancel = () => { - router.back(); - }; - - const handleTabChange = useCallback>( - (_e, newValue) => { - setTabIndex(newValue); - }, - [], - ); - - return ( - <> - - - - - - - ); -}; - -export default CreateProject; diff --git a/src/components/CreateClaim/index.ts b/src/components/CreateClaim/index.ts deleted file mode 100644 index a0bd052..0000000 --- a/src/components/CreateClaim/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./CreateClaim"; diff --git a/src/components/CustomDatagrid/CustomDatagrid.tsx b/src/components/CustomDatagrid/CustomDatagrid.tsx index 151e90d..314ba6c 100644 --- a/src/components/CustomDatagrid/CustomDatagrid.tsx +++ b/src/components/CustomDatagrid/CustomDatagrid.tsx @@ -12,7 +12,7 @@ interface CustomDatagridProps { columnWidth?: number; Style?: boolean; sx?: SxProps; - dataGridHeight?: number; + dataGridHeight?: number | string; [key: string]: any; checkboxSelection?: boolean; onRowSelectionModelChange?: ( diff --git a/src/components/CustomerDetail/ContactInfo.tsx b/src/components/CustomerDetail/ContactInfo.tsx index f608b00..fdc6c76 100644 --- a/src/components/CustomerDetail/ContactInfo.tsx +++ b/src/components/CustomerDetail/ContactInfo.tsx @@ -262,7 +262,7 @@ const ContactInfo: React.FC = ({ {t("Contact Info")} {Boolean(errors.addContacts?.type === "required") && ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> - {t("Please ensure all the fields are inputted and saved")} + {t("Please ensure at least one row is created, and all the fields are inputted and saved")} } {Boolean(errors.addContacts?.type === "email_format") && ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> {t("Please ensure all the email formats are correct")} diff --git a/src/components/CustomerDetail/CustomerDetailWrapper.tsx b/src/components/CustomerDetail/CustomerDetailWrapper.tsx index f8d0d12..0206940 100644 --- a/src/components/CustomerDetail/CustomerDetailWrapper.tsx +++ b/src/components/CustomerDetail/CustomerDetailWrapper.tsx @@ -2,7 +2,7 @@ // import CreateProject from "./CreateProject"; // import { fetchProjectCategories } from "@/app/api/projects"; // import { fetchTeamLeads } from "@/app/api/staff"; -import { Subsidiary, fetchCustomerTypes, fetchAllSubsidiaries } from "@/app/api/customer"; +import { fetchCustomerTypes, fetchAllSubsidiaries } from "@/app/api/customer"; import CustomerDetail from "./CustomerDetail"; // type Props = { diff --git a/src/components/CustomerDetail/CustomerInfo.tsx b/src/components/CustomerDetail/CustomerInfo.tsx index 3eaa327..416902e 100644 --- a/src/components/CustomerDetail/CustomerInfo.tsx +++ b/src/components/CustomerDetail/CustomerInfo.tsx @@ -68,7 +68,7 @@ const CustomerInfo: React.FC = ({ = ({ = ({ customers }) => { label: t("Delete"), onClick: onDeleteClick, buttonIcon: , + color: "error" }, ], [onTaskClick, t], diff --git a/src/components/SearchBox/SearchBox.tsx b/src/components/SearchBox/SearchBox.tsx index 13b03ee..26914fe 100644 --- a/src/components/SearchBox/SearchBox.tsx +++ b/src/components/SearchBox/SearchBox.tsx @@ -137,7 +137,7 @@ function SearchBox({ {t("All")} {c.options.map((option, index) => ( - {option} + {t(option)} ))} diff --git a/src/components/SearchResults/SearchResults.tsx b/src/components/SearchResults/SearchResults.tsx index bfecd36..ea0744c 100644 --- a/src/components/SearchResults/SearchResults.tsx +++ b/src/components/SearchResults/SearchResults.tsx @@ -12,6 +12,8 @@ import TablePagination, { } from "@mui/material/TablePagination"; import TableRow from "@mui/material/TableRow"; import IconButton, { IconButtonOwnProps, IconButtonPropsColorOverrides } from "@mui/material/IconButton"; +import { t } from "i18next"; +import { useTranslation } from "react-i18next"; export interface ResultWithId { id: string | number; @@ -21,6 +23,7 @@ interface BaseColumn { name: keyof T; label: string; color?: IconButtonOwnProps["color"]; + needTranslation?: boolean } interface ColumnWithAction extends BaseColumn { @@ -51,6 +54,7 @@ function SearchResults({ }: Props) { const [page, setPage] = React.useState(0); const [rowsPerPage, setRowsPerPage] = React.useState(10); + const { t } = useTranslation() const handleChangePage: TablePaginationProps["onPageChange"] = ( _event, @@ -98,7 +102,7 @@ function SearchResults({ {column.buttonIcon} ) : ( - <>{item[columnName]} + <>{column?.needTranslation ? t(item[columnName] as string) : item[columnName]} )} ); diff --git a/src/components/SubsidiaryDetail/ContactInfo.tsx b/src/components/SubsidiaryDetail/ContactInfo.tsx index ef84ad7..e88201c 100644 --- a/src/components/SubsidiaryDetail/ContactInfo.tsx +++ b/src/components/SubsidiaryDetail/ContactInfo.tsx @@ -263,7 +263,7 @@ const ContactInfo: React.FC = ({ {t("Contact Info")} {Boolean(errors.addContacts?.type === "required") && ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> - {t("Please ensure all the fields are inputted and saved")} + {t("Please ensure at least one row is created, and all the fields are inputted and saved")} } {Boolean(errors.addContacts?.type === "email_format") && ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> {t("Please ensure all the email formats are correct")} diff --git a/src/components/SubsidiaryDetail/SubsidiaryInfo.tsx b/src/components/SubsidiaryDetail/SubsidiaryInfo.tsx index 84522a4..046becd 100644 --- a/src/components/SubsidiaryDetail/SubsidiaryInfo.tsx +++ b/src/components/SubsidiaryDetail/SubsidiaryInfo.tsx @@ -57,7 +57,7 @@ const SubsidiaryInfo: React.FC = ({ = ({ = ({ subsidiaries }) => { label: t("Delete"), onClick: onDeleteClick, buttonIcon: , + color: "error" }, ], [onTaskClick, t], diff --git a/src/i18n/en/claim.json b/src/i18n/en/claim.json new file mode 100644 index 0000000..bfd0f84 --- /dev/null +++ b/src/i18n/en/claim.json @@ -0,0 +1,31 @@ +{ + "Staff Reimbursement": "Staff Reimbursement", + "Create Claim": "Create Claim", + "Creation Date": "Creation Date", + "Creation Date From": "Creation Date From", + "Creation Date To": "Creation Date To", + "Related Project": "Related Project", + "Related Project Name": "Related Project Name", + "Expense Type": "Expense Type", + "Status": "Status", + "Amount (HKD)": "Amount (HKD)", + "Remarks": "Remarks", + "Invoice Date": "Invoice Date", + "Supporting Document": "Supporting Document", + "Total": "Total", + "Add Record": "Add Record", + "Project Name": "Project Name", + "Project": "Project", + "Claim Code": "Claim Code", + + "Petty Cash": "Petty Cash", + "Expense": "Expense", + + "Not Submitted": "Not Submitted", + "Waiting for Approval": "Waiting for Approval", + "Approved": "Approved", + "Rejected": "Rejected", + + "Description": "Description", + "Actions": "Actions" +} \ No newline at end of file diff --git a/src/i18n/en/common.json b/src/i18n/en/common.json index 536e11b..51ea204 100644 --- a/src/i18n/en/common.json +++ b/src/i18n/en/common.json @@ -1,10 +1,22 @@ { "Grade {{grade}}": "Grade {{grade}}", + + "All": "All", + "Petty Cash": "Petty Cash", + "Expense": "Expense", + + "Not Submitted": "Not Submitted", + "Waiting for Approval": "Waiting for Approval", + "Approved": "Approved", + "Rejected": "Rejected", + "Search": "Search", "Search Criteria": "Search Criteria", "Cancel": "Cancel", "Confirm": "Confirm", "Submit": "Submit", + "Save": "Save", + "Save And Submit": "Save And Submit", "Reset": "Reset" } \ No newline at end of file diff --git a/src/i18n/en/customer.json b/src/i18n/en/customer.json index 38871c6..f7b7dff 100644 --- a/src/i18n/en/customer.json +++ b/src/i18n/en/customer.json @@ -43,7 +43,7 @@ "Contact Name": "Contact Name", "Contact Email": "Contact Email", "Contact Phone": "Contact Phone", - "Please ensure all the fields are inputted and saved": "Please ensure 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 at least one row is created, and all the fields are inputted and saved", "Please ensure all the email formats are correct": "Please ensure all the email formats are correct", "Do you want to submit?": "Do you want to submit?", diff --git a/src/i18n/en/subsidiary.json b/src/i18n/en/subsidiary.json index 3c06c1c..f62a640 100644 --- a/src/i18n/en/subsidiary.json +++ b/src/i18n/en/subsidiary.json @@ -43,7 +43,7 @@ "Contact Name": "Contact Name", "Contact Email": "Contact Email", "Contact Phone": "Contact Phone", - "Please ensure all the fields are inputted and saved": "Please ensure 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 at least one row is created, and all the fields are inputted and saved", "Please ensure all the email formats are correct": "Please ensure all the email formats are correct", "Do you want to submit?": "Do you want to submit?", diff --git a/src/i18n/zh/claim.json b/src/i18n/zh/claim.json new file mode 100644 index 0000000..a22acca --- /dev/null +++ b/src/i18n/zh/claim.json @@ -0,0 +1,31 @@ +{ + "Staff Reimbursement": "員工報銷", + "Create Claim": "建立報銷", + "Creation Date": "建立日期", + "Creation Date From": "建立日期 (從)", + "Creation Date To": "建立日期 (至)", + "Related Project": "相關項目名稱", + "Related Project Name": "相關項目名稱", + "Expense Type": "費用類別", + "Status": "狀態", + "Amount (HKD)": "金額 (HKD)", + "Remarks": "備註", + "Invoice Date": "收據日期", + "Supporting Document": "支援文件", + "Total": "總金額", + "Add Record": "新增記錄", + "Project Name": "項目名稱", + "Project": "項目", + "Claim Code": "報銷編號", + + "Petty Cash": "小額開支", + "Expense": "普通開支", + + "Not Submitted": "尚未提交", + "Waiting for Approval": "等待批核", + "Approved": "已批核", + "Rejected": "已拒絕", + + "Description": "描述", + "Actions": "行動" +} \ No newline at end of file diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json index c1cb0a9..f857985 100644 --- a/src/i18n/zh/common.json +++ b/src/i18n/zh/common.json @@ -1,8 +1,20 @@ { + "All": "全部", + + "Petty Cash": "小額開支", + "Expense": "普通開支", + + "Not Submitted": "尚未提交", + "Waiting for Approval": "等待批核", + "Approved": "已批核", + "Rejected": "已拒絕", + "Search": "搜尋", "Search Criteria": "搜尋條件", "Cancel": "取消", "Confirm": "確認", "Submit": "提交", + "Save": "儲存", + "Save And Submit": "儲存及提交", "Reset": "重置" } \ No newline at end of file diff --git a/src/i18n/zh/customer.json b/src/i18n/zh/customer.json index 71cb293..d682f81 100644 --- a/src/i18n/zh/customer.json +++ b/src/i18n/zh/customer.json @@ -43,7 +43,7 @@ "Contact Name": "聯絡姓名", "Contact Email": "聯絡電郵", "Contact Phone": "聯絡電話", - "Please ensure 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 all the email formats are correct": "請確保所有電郵格式輸入正確", "Do you want to submit?": "你是否確認要提交?", diff --git a/src/i18n/zh/subsidiary.json b/src/i18n/zh/subsidiary.json index e1c861a..c56a884 100644 --- a/src/i18n/zh/subsidiary.json +++ b/src/i18n/zh/subsidiary.json @@ -43,7 +43,7 @@ "Contact Name": "聯絡姓名", "Contact Email": "聯絡電郵", "Contact Phone": "聯絡電話", - "Please ensure 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 all the email formats are correct": "請確保所有電郵格式輸入正確", "Do you want to submit?": "你是否確認要提交?",