| @@ -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 ( | |||
| <> | |||
| <Typography variant="h4">{t("Create Claim")}</Typography> | |||
| <CreateClaim /> | |||
| <I18nProvider namespaces={["claim", "common"]}> | |||
| <ClaimDetail /> | |||
| </I18nProvider> | |||
| </> | |||
| ); | |||
| }; | |||
| export default CreateClaims; | |||
| export default ClaimDetails; | |||
| @@ -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")} | |||
| </Button> | |||
| </Stack> | |||
| <Suspense fallback={<ClaimSearch.Loading />}> | |||
| <ClaimSearch /> | |||
| </Suspense> | |||
| <I18nProvider namespaces={["claim", "common"]}> | |||
| <Suspense fallback={<ClaimSearch.Loading />}> | |||
| <ClaimSearch /> | |||
| </Suspense> | |||
| </I18nProvider> | |||
| </> | |||
| ); | |||
| }; | |||
| @@ -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<SaveClaimResponse>( | |||
| `${BASE_API_URL}/claim/save`, | |||
| { | |||
| method: "POST", | |||
| body: JSON.stringify(data), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }, | |||
| ); | |||
| revalidateTag("claims"); | |||
| return saveCustomer; | |||
| }; | |||
| @@ -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<Claim[]>(`${BASE_API_URL}/claim`); | |||
| }); | |||
| export const fetchProjectCombo = cache(async () => { | |||
| return serverFetchJson<ProjectCombo[]>(`${BASE_API_URL}/projects`, { | |||
| next: { tags: ["projects"] }, | |||
| }); | |||
| }); | |||
| const mockClaims: ClaimResult[] = [ | |||
| // export const fetchAllCustomers = cache(async () => { | |||
| // return serverFetchJson<Customer[]>(`${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", | |||
| @@ -0,0 +1,11 @@ | |||
| export const expenseTypeCombo = [ | |||
| "Petty Cash", | |||
| "Expense" | |||
| ] | |||
| export const claimStatusCombo = [ | |||
| "Not Submitted", | |||
| "Waiting for Approval", | |||
| "Approved", | |||
| "Rejected" | |||
| ] | |||
| @@ -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 | |||
| } | |||
| } | |||
| } | |||
| @@ -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", | |||
| @@ -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<Props> = ({ projectCombo }) => { | |||
| const { t } = useTranslation("common"); | |||
| const [serverError, setServerError] = useState(""); | |||
| const router = useRouter(); | |||
| const formProps = useForm<ClaimInputFormByStaff>({ | |||
| defaultValues: { | |||
| id: null, | |||
| expenseType: expenseTypeCombo[0], | |||
| addClaimDetails: [] | |||
| }, | |||
| }); | |||
| const handleCancel = () => { | |||
| router.back(); | |||
| }; | |||
| const onSubmit = useCallback<SubmitHandler<ClaimInputFormByStaff>>( | |||
| 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<SubmitErrorHandler<ClaimInputFormByStaff>>( | |||
| (errors) => { | |||
| // Set the tab so that the focus will go there | |||
| console.log(errors) | |||
| }, | |||
| [], | |||
| ); | |||
| return ( | |||
| <FormProvider {...formProps}> | |||
| <Stack spacing={2} component={"form"} onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)}> | |||
| <ClaimFormInfo projectCombo={projectCombo} /> | |||
| {serverError && ( | |||
| <Typography variant="body2" color="error" alignSelf="flex-end"> | |||
| {serverError} | |||
| </Typography> | |||
| )} | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| <Button variant="text" startIcon={<Close />} onClick={handleCancel}> | |||
| {t("Cancel")} | |||
| </Button> | |||
| <Button variant="outlined" name="save" startIcon={<Check />} type="submit"> | |||
| {t("Save")} | |||
| </Button> | |||
| <Button variant="contained" name="submit" startIcon={<DoneAll />} type="submit"> | |||
| {t("Submit")} | |||
| </Button> | |||
| </Stack> | |||
| </Stack> | |||
| </FormProvider> | |||
| ); | |||
| }; | |||
| export default ClaimDetail; | |||
| @@ -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 ( | |||
| <ClaimDetail projectCombo={projectCombo}/> | |||
| ); | |||
| }; | |||
| export default ClaimDetailWrapper; | |||
| @@ -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<Props> = ({ projectCombo }) => { | |||
| const { t } = useTranslation(); | |||
| const { | |||
| control, | |||
| register, | |||
| } = useFormContext<ClaimInputFormByStaff>(); | |||
| return ( | |||
| <Card> | |||
| <CardContent component={Stack} spacing={4}> | |||
| <Box> | |||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| fullWidth | |||
| label={t("Claim Code")} | |||
| {...register("code")} | |||
| disabled={true} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <FormControl fullWidth> | |||
| <InputLabel>{t("Expense Type")}</InputLabel> | |||
| <Controller | |||
| // defaultValue={expenseTypeCombo[0].value} | |||
| control={control} | |||
| name="expenseType" | |||
| render={({ field }) => ( | |||
| <Select label={t("Expense Type")} {...field}> | |||
| { | |||
| expenseTypeCombo.map((type, index) => ( | |||
| <MenuItem key={`${type}-${index}`} value={type}> | |||
| {t(type)} | |||
| </MenuItem> | |||
| )) | |||
| } | |||
| </Select> | |||
| )} | |||
| /> | |||
| </FormControl> | |||
| </Grid> | |||
| </Grid> | |||
| </Box> | |||
| <Card> | |||
| <ClaimFormInputGrid projectCombo={projectCombo} /> | |||
| </Card> | |||
| </CardContent> | |||
| </Card> | |||
| ); | |||
| }; | |||
| export default ClaimFormInfo; | |||
| @@ -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) => { | |||
| <div> | |||
| <div style={{ display: "flex", justifyContent: "flex", width: "100%" }}> | |||
| <Box flex={1.5} textAlign={"right"} marginRight="4rem"> | |||
| <b>Total:</b> | |||
| <b>{t("Total")}:</b> | |||
| </Box> | |||
| <TotalCell value={getCostTotal()} /> | |||
| </div> | |||
| @@ -133,7 +107,7 @@ const BottomBar = (props: BottomBarProps) => { | |||
| onClick={handleAddClick} | |||
| sx={{ margin: "20px" }} | |||
| > | |||
| Add record | |||
| {t("Add Record")} | |||
| </Button> | |||
| </div> | |||
| ); | |||
| @@ -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<ClaimInputGridProps> = ({ ...props }) => { | |||
| const [rows, setRows] = useState(initialRows); | |||
| const [day, setDay] = useState(dayjs()); | |||
| const ClaimFormInputGrid: React.FC<ClaimFormInputGridProps> = ({ | |||
| // onClose, | |||
| projectCombo, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| const { control, setValue, getValues, formState: { errors } } = useFormContext<ClaimInputFormByStaff>(); | |||
| const { fields } = useFieldArray({ | |||
| control, | |||
| name: "addClaimDetails" | |||
| }) | |||
| const [rows, setRows] = useState<GridRowsProp>([]); | |||
| const [rowModesModel, setRowModesModel] = React.useState<GridRowModesModel>( | |||
| {}, | |||
| ); | |||
| // Row function | |||
| const handleRowEditStop: GridEventListener<"rowEditStop"> = ( | |||
| params, | |||
| event, | |||
| @@ -217,20 +202,77 @@ const ClaimInputGrid: React.FC<ClaimInputGridProps> = ({ ...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<Record<string, HTMLInputElement | null>> = 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<HTMLInputElement>, params: GridRenderEditCellParams<any, any, any, GridTreeNodeWithRender>) => { | |||
| 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<any, any, any, GridTreeNodeWithRender>) => { | |||
| 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<ClaimInputGridProps> = ({ ...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<ClaimInputGridProps> = ({ ...props }) => { | |||
| }, | |||
| }, | |||
| { | |||
| field: "date", | |||
| headerName: "Invoice Date", | |||
| field: "invoiceDate", | |||
| headerName: t("Invoice Date"), | |||
| // width: 220, | |||
| flex: 1, | |||
| editable: true, | |||
| type: "date", | |||
| renderCell: (params: GridRenderCellParams<any, Date>) => { | |||
| 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<ClaimInputGridProps> = ({ ...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 ? ( | |||
| <span> | |||
| <a href="" target="_blank" rel="noopener noreferrer"> | |||
| <Link onClick={() => handleLinkClick(params)} href="#">{params.value}</Link> | |||
| {/* <a href="" target="_blank" rel="noopener noreferrer"> | |||
| {params.value} | |||
| </a> | |||
| </a> */} | |||
| </span> | |||
| ) : ( | |||
| <span style={{ color: palette.text.disabled }}>No Documents</span> | |||
| ); | |||
| }, | |||
| renderEditCell: (params) => { | |||
| return params.value ? ( | |||
| const currentRow = rows.find(row => row.id === params.row.id); | |||
| return params.formattedValue ? ( | |||
| <span> | |||
| <a href="" target="_blank" rel="noopener noreferrer"> | |||
| {params.value} | |||
| </a> | |||
| <Link onClick={() => handleLinkClick(params)} href="#">{params.formattedValue}</Link> | |||
| {/* <a href="" target="_blank" rel="noopener noreferrer"> | |||
| {params.formattedValue} | |||
| </a> */} | |||
| <Button | |||
| title="Remove Document" | |||
| onClick={(event) => console.log(event)} | |||
| onClick={() => handleFileDelete(params.row.id)} | |||
| > | |||
| <ImageNotSupportedOutlinedIcon | |||
| sx={{ fontSize: "25px", color: "red" }} | |||
| @@ -369,15 +440,24 @@ const ClaimInputGrid: React.FC<ClaimInputGridProps> = ({ ...props }) => { | |||
| </Button> | |||
| </span> | |||
| ) : ( | |||
| <Button title="Add Document"> | |||
| <AddPhotoAlternateOutlinedIcon | |||
| sx={{ fontSize: "25px", color: "green" }} | |||
| <div> | |||
| <input | |||
| type="file" | |||
| ref={ele => setFileInputRefs(ele, params.row.id)} | |||
| accept="image/jpg, image/jpeg, image/png, .doc, .docx, .pdf" | |||
| style={{ display: 'none' }} | |||
| onChange={(event) => handleFileChange(event, params)} | |||
| /> | |||
| </Button> | |||
| <Button title="Add Document" onClick={() => handleFileSelect(params.row.id)}> | |||
| <AddPhotoAlternateOutlinedIcon | |||
| sx={{ fontSize: "25px", color: "green" }} | |||
| /> | |||
| </Button> | |||
| </div> | |||
| ); | |||
| }, | |||
| }, | |||
| ]; | |||
| ], [rows, rowModesModel, t],); | |||
| return ( | |||
| <Box | |||
| @@ -402,41 +482,48 @@ const ClaimInputGrid: React.FC<ClaimInputGridProps> = ({ ...props }) => { | |||
| }, | |||
| }} | |||
| > | |||
| <DataGrid | |||
| sx={{ flex: 1 }} | |||
| rows={rows} | |||
| columns={columns} | |||
| editMode="row" | |||
| rowModesModel={rowModesModel} | |||
| onRowModesModelChange={handleRowModesModelChange} | |||
| onRowEditStop={handleRowEditStop} | |||
| processRowUpdate={processRowUpdate} | |||
| disableRowSelectionOnClick={true} | |||
| disableColumnMenu={true} | |||
| hideFooterPagination={true} | |||
| slots={ | |||
| { | |||
| // footer: EditFooter, | |||
| {Boolean(errors.addClaimDetails?.type === "required") && <Typography sx={(theme) => ({ 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")} | |||
| </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")} | |||
| </Typography>} | |||
| <div style={{ height: 400, width: "100%" }}> | |||
| <DataGrid | |||
| sx={{ flex: 1 }} | |||
| rows={rows} | |||
| columns={columns} | |||
| editMode="row" | |||
| rowModesModel={rowModesModel} | |||
| onRowModesModelChange={handleRowModesModelChange} | |||
| onRowEditStop={handleRowEditStop} | |||
| processRowUpdate={processRowUpdate} | |||
| disableRowSelectionOnClick={true} | |||
| disableColumnMenu={true} | |||
| // hideFooterPagination={true} | |||
| slots={ | |||
| { | |||
| // footer: EditFooter, | |||
| } | |||
| } | |||
| } | |||
| slotProps={ | |||
| { | |||
| // footer: { setDay, setRows, setRowModesModel }, | |||
| slotProps={ | |||
| { | |||
| // footer: { setDay, setRows, setRowModesModel }, | |||
| } | |||
| } | |||
| } | |||
| initialState={{ | |||
| pagination: { paginationModel: { pageSize: 100 } }, | |||
| }} | |||
| /> | |||
| initialState={{ | |||
| pagination: { paginationModel: { pageSize: 5 } }, | |||
| }} | |||
| /> | |||
| </div> | |||
| <BottomBar | |||
| getCostTotal={getCostTotal} | |||
| setRows={setRows} | |||
| setRowModesModel={setRowModesModel} | |||
| // sx={{flex:2}} | |||
| // sx={{flex:2}} | |||
| /> | |||
| </Box> | |||
| ); | |||
| }; | |||
| export default ClaimInputGrid; | |||
| export default ClaimFormInputGrid; | |||
| @@ -0,0 +1 @@ | |||
| export { default } from "./ClaimDetailWrapper"; | |||
| @@ -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<Omit<ClaimResult, "id">>; | |||
| type SearchQuery = Partial<Omit<ClaimSearchForm, "id">>; | |||
| type SearchParamNames = keyof SearchQuery; | |||
| const ClaimSearch: React.FC<Props> = ({ 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<SearchParamNames>[] = 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<Column<ClaimResult>[]>( | |||
| const columns = useMemo<Column<Claim>[]>( | |||
| () => [ | |||
| // { | |||
| // name: "action", | |||
| @@ -69,9 +56,9 @@ const ClaimSearch: React.FC<Props> = ({ 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<Props> = ({ claims }) => { | |||
| <SearchBox | |||
| criteria={searchCriteria} | |||
| onSearch={(query) => { | |||
| 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") | |||
| ), | |||
| ); | |||
| }} | |||
| /> | |||
| <SearchResults<ClaimResult> items={filteredClaims} columns={columns} /> | |||
| <SearchResults<Claim> items={filteredClaims} columns={columns} /> | |||
| </> | |||
| ); | |||
| }; | |||
| @@ -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 ( | |||
| <Card> | |||
| <CardContent component={Stack} spacing={4}> | |||
| <Box> | |||
| {/* <Typography variant="overline" display="block" marginBlockEnd={1}> | |||
| {t("Related Project")} | |||
| </Typography> */} | |||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
| <Grid item xs={6}> | |||
| <FormControl fullWidth> | |||
| <InputLabel>{t("Related Project")}</InputLabel> | |||
| <Select label={t("Project Category")}> | |||
| <MenuItem value={"M1001"}>{t("M1001")}</MenuItem> | |||
| <MenuItem value={"M1301"}>{t("M1301")}</MenuItem> | |||
| <MenuItem value={"M1354"}>{t("M1354")}</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <FormControl fullWidth> | |||
| <InputLabel>{t("Expense Type")}</InputLabel> | |||
| <Select label={t("Team Lead")}> | |||
| <MenuItem value={"Petty Cash"}>{"Petty Cash"}</MenuItem> | |||
| <MenuItem value={"Expense"}>{"Expense"}</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| </Grid> | |||
| </Grid> | |||
| </Box> | |||
| <Card> | |||
| <ClaimInputGrid /> | |||
| </Card> | |||
| {/* <CardActions sx={{ justifyContent: "flex-end" }}> | |||
| <Button variant="text" startIcon={<RestartAlt />}> | |||
| {t("Reset")} | |||
| </Button> | |||
| </CardActions> */} | |||
| </CardContent> | |||
| </Card> | |||
| ); | |||
| }; | |||
| export default ClaimDetails; | |||
| @@ -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<NonNullable<TabsProps["onChange"]>>( | |||
| (_e, newValue) => { | |||
| setTabIndex(newValue); | |||
| }, | |||
| [], | |||
| ); | |||
| return ( | |||
| <> | |||
| <ClaimProjectDetails /> | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| <Button variant="outlined" startIcon={<Close />} onClick={handleCancel}> | |||
| {t("Cancel")} | |||
| </Button> | |||
| <Button variant="contained" startIcon={<Check />}> | |||
| {t("Confirm")} | |||
| </Button> | |||
| </Stack> | |||
| </> | |||
| ); | |||
| }; | |||
| export default CreateProject; | |||
| @@ -1 +0,0 @@ | |||
| export { default } from "./CreateClaim"; | |||
| @@ -54,11 +54,12 @@ const StaffAllocation: React.FC<Props> = ({ allStaffs: staff }) => { | |||
| }, []); | |||
| const setTeamLead = useCallback((staff: StaffResult) => { | |||
| setSeletedTeamLead(staff.id) | |||
| const rearrangedList = getValues("addStaffIds").reduce<number[]>((acc, num, index) => { | |||
| if (num === staff.id && index !== 0) { | |||
| acc.splice(index, 1); | |||
| acc.unshift(num); | |||
| acc.unshift(num) | |||
| } | |||
| return acc; | |||
| }, getValues("addStaffIds")); | |||
| @@ -69,10 +70,10 @@ const StaffAllocation: React.FC<Props> = ({ allStaffs: staff }) => { | |||
| return selectedStaff.find((staff) => staff.id === id); | |||
| }); | |||
| console.log(rearrangedStaff) | |||
| // setSelectedStaff(rearrangedStaff as StaffResult[]); | |||
| setSelectedStaff(rearrangedStaff as StaffResult[]); | |||
| setValue("addStaffIds", rearrangedList) | |||
| }, []); | |||
| }, [addStaff, selectedStaff]); | |||
| const clearSubsidiary = useCallback(() => { | |||
| if (defaultValues !== undefined) { | |||
| @@ -92,6 +93,10 @@ const StaffAllocation: React.FC<Props> = ({ allStaffs: staff }) => { | |||
| ); | |||
| }, [selectedStaff, setValue]); | |||
| useEffect(() => { | |||
| console.log(selectedStaff) | |||
| }, [selectedStaff]); | |||
| const StaffPoolColumns = useMemo<Column<StaffResult>[]>( | |||
| () => [ | |||
| { | |||
| @@ -125,7 +130,7 @@ const StaffAllocation: React.FC<Props> = ({ allStaffs: staff }) => { | |||
| buttonIcon: <StarsIcon />, | |||
| }, | |||
| ], | |||
| [removeStaff, t] | |||
| [removeStaff, selectedStaff, t] | |||
| ); | |||
| const [query, setQuery] = React.useState(""); | |||
| @@ -12,7 +12,7 @@ interface CustomDatagridProps { | |||
| columnWidth?: number; | |||
| Style?: boolean; | |||
| sx?: SxProps<Theme>; | |||
| dataGridHeight?: number; | |||
| dataGridHeight?: number | string; | |||
| [key: string]: any; | |||
| checkboxSelection?: boolean; | |||
| onRowSelectionModelChange?: ( | |||
| @@ -262,7 +262,7 @@ const ContactInfo: React.FC<Props> = ({ | |||
| {t("Contact Info")} | |||
| </Typography> | |||
| {Boolean(errors.addContacts?.type === "required") && <Typography sx={(theme) => ({ 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")} | |||
| </Typography>} | |||
| {Boolean(errors.addContacts?.type === "email_format") && <Typography sx={(theme) => ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> | |||
| {t("Please ensure all the email formats are correct")} | |||
| @@ -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 = { | |||
| @@ -68,7 +68,7 @@ const CustomerInfo: React.FC<Props> = ({ | |||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Customer Code")} | |||
| label={`${t("Customer Code")}*`} | |||
| fullWidth | |||
| {...register("code", { | |||
| required: true, | |||
| @@ -79,7 +79,7 @@ const CustomerInfo: React.FC<Props> = ({ | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Customer Name")} | |||
| label={`${t("Customer Name")}*`} | |||
| fullWidth | |||
| {...register("name", { | |||
| required: true, | |||
| @@ -67,6 +67,7 @@ const CustomerSearch: React.FC<Props> = ({ customers }) => { | |||
| label: t("Delete"), | |||
| onClick: onDeleteClick, | |||
| buttonIcon: <DeleteIcon />, | |||
| color: "error" | |||
| }, | |||
| ], | |||
| [onTaskClick, t], | |||
| @@ -137,7 +137,7 @@ function SearchBox<T extends string>({ | |||
| <MenuItem value={"All"}>{t("All")}</MenuItem> | |||
| {c.options.map((option, index) => ( | |||
| <MenuItem key={`${option}-${index}`} value={option}> | |||
| {option} | |||
| {t(option)} | |||
| </MenuItem> | |||
| ))} | |||
| </Select> | |||
| @@ -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<T extends ResultWithId> { | |||
| name: keyof T; | |||
| label: string; | |||
| color?: IconButtonOwnProps["color"]; | |||
| needTranslation?: boolean | |||
| } | |||
| interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> { | |||
| @@ -51,6 +54,7 @@ function SearchResults<T extends ResultWithId>({ | |||
| }: Props<T>) { | |||
| 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<T extends ResultWithId>({ | |||
| {column.buttonIcon} | |||
| </IconButton> | |||
| ) : ( | |||
| <>{item[columnName]}</> | |||
| <>{column?.needTranslation ? t(item[columnName] as string) : item[columnName]}</> | |||
| )} | |||
| </TableCell> | |||
| ); | |||
| @@ -263,7 +263,7 @@ const ContactInfo: React.FC<Props> = ({ | |||
| {t("Contact Info")} | |||
| </Typography> | |||
| {Boolean(errors.addContacts?.type === "required") && <Typography sx={(theme) => ({ 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")} | |||
| </Typography>} | |||
| {Boolean(errors.addContacts?.type === "email_format") && <Typography sx={(theme) => ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> | |||
| {t("Please ensure all the email formats are correct")} | |||
| @@ -57,7 +57,7 @@ const SubsidiaryInfo: React.FC<Props> = ({ | |||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Subsidiary Code")} | |||
| label={`${t("Subsidiary Code")}*`} | |||
| fullWidth | |||
| {...register("code", { | |||
| required: true, | |||
| @@ -68,7 +68,7 @@ const SubsidiaryInfo: React.FC<Props> = ({ | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Subsidiary Name")} | |||
| label={`${t("Subsidiary Name")}*`} | |||
| fullWidth | |||
| {...register("name", { | |||
| required: true, | |||
| @@ -67,6 +67,7 @@ const SubsidiarySearch: React.FC<Props> = ({ subsidiaries }) => { | |||
| label: t("Delete"), | |||
| onClick: onDeleteClick, | |||
| buttonIcon: <DeleteIcon />, | |||
| color: "error" | |||
| }, | |||
| ], | |||
| [onTaskClick, t], | |||
| @@ -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" | |||
| } | |||
| @@ -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" | |||
| } | |||
| @@ -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?", | |||
| @@ -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?", | |||
| @@ -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": "行動" | |||
| } | |||
| @@ -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": "重置" | |||
| } | |||
| @@ -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?": "你是否確認要提交?", | |||
| @@ -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?": "你是否確認要提交?", | |||