Przeglądaj źródła

update claim, customer, subsidiary

tags/Baseline_30082024_FRONTEND_UAT
cyril.tsui 1 rok temu
rodzic
commit
b45d61c305
34 zmienionych plików z 671 dodań i 281 usunięć
  1. +8
    -6
      src/app/(main)/staffReimbursement/create/page.tsx
  2. +7
    -5
      src/app/(main)/staffReimbursement/page.tsx
  3. +48
    -0
      src/app/api/claims/actions.ts
  4. +41
    -5
      src/app/api/claims/index.ts
  5. +11
    -0
      src/app/utils/comboUtil.ts
  6. +23
    -0
      src/app/utils/commonUtil.ts
  7. +8
    -0
      src/app/utils/formatUtil.ts
  8. +105
    -0
      src/components/ClaimDetail/ClaimDetail.tsx
  9. +20
    -0
      src/components/ClaimDetail/ClaimDetailWrapper.tsx
  10. +76
    -0
      src/components/ClaimDetail/ClaimFormInfo.tsx
  11. +193
    -106
      src/components/ClaimDetail/ClaimFormInputGrid.tsx
  12. +1
    -0
      src/components/ClaimDetail/index.ts
  13. +24
    -29
      src/components/ClaimSearch/ClaimSearch.tsx
  14. +0
    -67
      src/components/CreateClaim/ClaimDetails.tsx
  15. +0
    -48
      src/components/CreateClaim/CreateClaim.tsx
  16. +0
    -1
      src/components/CreateClaim/index.ts
  17. +1
    -1
      src/components/CustomDatagrid/CustomDatagrid.tsx
  18. +1
    -1
      src/components/CustomerDetail/ContactInfo.tsx
  19. +1
    -1
      src/components/CustomerDetail/CustomerDetailWrapper.tsx
  20. +2
    -2
      src/components/CustomerDetail/CustomerInfo.tsx
  21. +1
    -0
      src/components/CustomerSearch/CustomerSearch.tsx
  22. +1
    -1
      src/components/SearchBox/SearchBox.tsx
  23. +5
    -1
      src/components/SearchResults/SearchResults.tsx
  24. +1
    -1
      src/components/SubsidiaryDetail/ContactInfo.tsx
  25. +2
    -2
      src/components/SubsidiaryDetail/SubsidiaryInfo.tsx
  26. +1
    -0
      src/components/SubsidiarySearch/SubsidiarySearch.tsx
  27. +31
    -0
      src/i18n/en/claim.json
  28. +12
    -0
      src/i18n/en/common.json
  29. +1
    -1
      src/i18n/en/customer.json
  30. +1
    -1
      src/i18n/en/subsidiary.json
  31. +31
    -0
      src/i18n/zh/claim.json
  32. +12
    -0
      src/i18n/zh/common.json
  33. +1
    -1
      src/i18n/zh/customer.json
  34. +1
    -1
      src/i18n/zh/subsidiary.json

+ 8
- 6
src/app/(main)/staffReimbursement/create/page.tsx Wyświetl plik

@@ -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 Typography from "@mui/material/Typography";
import { Metadata } from "next"; import { Metadata } from "next";


@@ -7,15 +7,17 @@ export const metadata: Metadata = {
title: "Create Claim", title: "Create Claim",
}; };


const CreateClaims: React.FC = async () => {
const { t } = await getServerI18n("claims");
const ClaimDetails: React.FC = async () => {
const { t } = await getServerI18n("claim");


return ( return (
<> <>
<Typography variant="h4">{t("Create Claim")}</Typography> <Typography variant="h4">{t("Create Claim")}</Typography>
<CreateClaim />
<I18nProvider namespaces={["claim", "common"]}>
<ClaimDetail />
</I18nProvider>
</> </>
); );
}; };


export default CreateClaims;
export default ClaimDetails;

+ 7
- 5
src/app/(main)/staffReimbursement/page.tsx Wyświetl plik

@@ -1,6 +1,6 @@
import { preloadClaims } from "@/app/api/claims"; import { preloadClaims } from "@/app/api/claims";
import ClaimSearch from "@/components/ClaimSearch"; import ClaimSearch from "@/components/ClaimSearch";
import { getServerI18n } from "@/i18n";
import { I18nProvider, getServerI18n } from "@/i18n";
import Add from "@mui/icons-material/Add"; import Add from "@mui/icons-material/Add";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack"; import Stack from "@mui/material/Stack";
@@ -14,7 +14,7 @@ export const metadata: Metadata = {
}; };


const StaffReimbursement: React.FC = async () => { const StaffReimbursement: React.FC = async () => {
const { t } = await getServerI18n("claims");
const { t } = await getServerI18n("claim");
preloadClaims(); preloadClaims();


return ( return (
@@ -37,9 +37,11 @@ const StaffReimbursement: React.FC = async () => {
{t("Create Claim")} {t("Create Claim")}
</Button> </Button>
</Stack> </Stack>
<Suspense fallback={<ClaimSearch.Loading />}>
<ClaimSearch />
</Suspense>
<I18nProvider namespaces={["claim", "common"]}>
<Suspense fallback={<ClaimSearch.Loading />}>
<ClaimSearch />
</Suspense>
</I18nProvider>
</> </>
); );
}; };


+ 48
- 0
src/app/api/claims/actions.ts Wyświetl plik

@@ -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;
};

+ 41
- 5
src/app/api/claims/index.ts Wyświetl plik

@@ -1,7 +1,9 @@
import { serverFetchJson } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";
import { cache } from "react"; import { cache } from "react";
import "server-only"; import "server-only";


export interface ClaimResult {
export interface Claim {
id: number; id: number;
created: string; created: string;
name: string; name: string;
@@ -11,18 +13,52 @@ export interface ClaimResult {
remarks: string; 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 = () => { export const preloadClaims = () => {
fetchClaims(); fetchClaims();
}; };


export const fetchClaims = cache(async () => { export const fetchClaims = cache(async () => {
return mockClaims; 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, id: 1,
created: "2023-11-22",
created: "2023/11/22",
name: "Consultancy Project A", name: "Consultancy Project A",
cost: 121.0, cost: 121.0,
type: "Expense", type: "Expense",
@@ -31,7 +67,7 @@ const mockClaims: ClaimResult[] = [
}, },
{ {
id: 2, id: 2,
created: "2023-11-30",
created: "2023/11/30",
name: "Consultancy Project A", name: "Consultancy Project A",
cost: 4300.0, cost: 4300.0,
type: "Expense", type: "Expense",
@@ -40,7 +76,7 @@ const mockClaims: ClaimResult[] = [
}, },
{ {
id: 3, id: 3,
created: "2023-12-12",
created: "2023/12/12",
name: "Construction Project C", name: "Construction Project C",
cost: 3675.0, cost: 3675.0,
type: "Petty Cash", type: "Petty Cash",


+ 11
- 0
src/app/utils/comboUtil.ts Wyświetl plik

@@ -0,0 +1,11 @@
export const expenseTypeCombo = [
"Petty Cash",
"Expense"
]

export const claimStatusCombo = [
"Not Submitted",
"Waiting for Approval",
"Approved",
"Rejected"
]

+ 23
- 0
src/app/utils/commonUtil.ts Wyświetl plik

@@ -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
}
}
}

+ 8
- 0
src/app/utils/formatUtil.ts Wyświetl plik

@@ -1,3 +1,5 @@
import dayjs from "dayjs";

export const manhourFormatter = new Intl.NumberFormat("en-HK", { export const manhourFormatter = new Intl.NumberFormat("en-HK", {
minimumFractionDigits: 2, minimumFractionDigits: 2,
maximumFractionDigits: 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 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", { const shortDateFormatter_en = new Intl.DateTimeFormat("en-HK", {
weekday: "short", weekday: "short",
year: "numeric", year: "numeric",


+ 105
- 0
src/components/ClaimDetail/ClaimDetail.tsx Wyświetl plik

@@ -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;

+ 20
- 0
src/components/ClaimDetail/ClaimDetailWrapper.tsx Wyświetl plik

@@ -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;

+ 76
- 0
src/components/ClaimDetail/ClaimFormInfo.tsx Wyświetl plik

@@ -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;

src/components/CreateClaim/ClaimInputGrid.tsx → src/components/ClaimDetail/ClaimFormInputGrid.tsx Wyświetl plik

@@ -8,16 +8,9 @@ import { Suspense } from "react";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack"; import Stack from "@mui/material/Stack";
import Link from "next/link"; import Link from "next/link";
import { t } from "i18next";
import { import {
Box,
Container,
Modal,
Select,
SelectChangeEvent,
Typography,
Box, Card, Typography,
} from "@mui/material"; } from "@mui/material";
import { Close } from "@mui/icons-material";
import AddIcon from "@mui/icons-material/Add"; import AddIcon from "@mui/icons-material/Add";
import EditIcon from "@mui/icons-material/Edit"; import EditIcon from "@mui/icons-material/Edit";
import DeleteIcon from "@mui/icons-material/DeleteOutlined"; 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 CancelIcon from "@mui/icons-material/Close";
import AddPhotoAlternateOutlinedIcon from "@mui/icons-material/AddPhotoAlternateOutlined"; import AddPhotoAlternateOutlinedIcon from "@mui/icons-material/AddPhotoAlternateOutlined";
import ImageNotSupportedOutlinedIcon from "@mui/icons-material/ImageNotSupportedOutlined"; 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 React from "react";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { import {
GridRowsProp, GridRowsProp,
GridRowModesModel, GridRowModesModel,
GridRowModes, GridRowModes,
DataGrid, DataGrid,
GridColDef, GridColDef,
GridToolbarContainer,
GridFooterContainer,
GridActionsCellItem, GridActionsCellItem,
GridEventListener, GridEventListener,
GridRowId, GridRowId,
GridRowModel, GridRowModel,
GridRowEditStopReasons, GridRowEditStopReasons,
GridEditInputCell, GridEditInputCell,
GridValueSetterParams,
GridTreeNodeWithRender,
GridRenderCellParams,
} from "@mui/x-data-grid"; } 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 dayjs from "dayjs";
import { Props } from "react-intl/src/components/relative"; import { Props } from "react-intl/src/components/relative";
import palette from "@/theme/devias-material-kit/palette"; 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 { interface BottomBarProps {
getCostTotal: () => number; getCostTotal: () => number;
@@ -63,15 +51,6 @@ interface BottomBarProps {
) => void; ) => 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 { interface EditFooterProps {
setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void;
setRowModesModel: ( setRowModesModel: (
@@ -80,17 +59,17 @@ interface EditFooterProps {
} }


const BottomBar = (props: BottomBarProps) => { const BottomBar = (props: BottomBarProps) => {
const { t } = useTranslation("claim")
const { setRows, setRowModesModel, getCostTotal } = props; const { setRows, setRowModesModel, getCostTotal } = props;
// const getCostTotal = props.getCostTotal; // const getCostTotal = props.getCostTotal;
const [newId, setNewId] = useState(-1); const [newId, setNewId] = useState(-1);
const [invalidDays, setInvalidDays] = useState(0);


const handleAddClick = () => { const handleAddClick = () => {
const id = newId; const id = newId;
setNewId(newId - 1); setNewId(newId - 1);
setRows((oldRows) => [ setRows((oldRows) => [
...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) => ({ setRowModesModel((oldModel) => ({
...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 TotalCell = ({ value }: Props) => {
const [invalid, setInvalid] = useState(false); const [invalid, setInvalid] = useState(false);


@@ -122,7 +96,7 @@ const BottomBar = (props: BottomBarProps) => {
<div> <div>
<div style={{ display: "flex", justifyContent: "flex", width: "100%" }}> <div style={{ display: "flex", justifyContent: "flex", width: "100%" }}>
<Box flex={1.5} textAlign={"right"} marginRight="4rem"> <Box flex={1.5} textAlign={"right"} marginRight="4rem">
<b>Total:</b>
<b>{t("Total")}:</b>
</Box> </Box>
<TotalCell value={getCostTotal()} /> <TotalCell value={getCostTotal()} />
</div> </div>
@@ -133,7 +107,7 @@ const BottomBar = (props: BottomBarProps) => {
onClick={handleAddClick} onClick={handleAddClick}
sx={{ margin: "20px" }} sx={{ margin: "20px" }}
> >
Add record
{t("Add Record")}
</Button> </Button>
</div> </div>
); );
@@ -150,40 +124,51 @@ const EditFooter = (props: EditFooterProps) => {
); );
}; };


interface ClaimInputGridProps {
onClose?: () => void;
interface ClaimFormInputGridProps {
// onClose?: () => void;
projectCombo: ProjectCombo[]
} }


const initialRows: GridRowsProp = [ const initialRows: GridRowsProp = [
{ {
id: 1, id: 1,
date: new Date(),
invoiceDate: new Date(),
description: "Taxi to client office", description: "Taxi to client office",
cost: 169.5,
document: "taxi_receipt.jpg",
amount: 169.5,
supportingDocumentName: "taxi_receipt.jpg",
}, },
{ {
id: 2, id: 2,
date: dayjs().add(-14, "days").toDate(),
invoiceDate: dayjs().add(-14, "days").toDate(),
description: "MTR fee to Kowloon Bay Office", description: "MTR fee to Kowloon Bay Office",
cost: 15.5,
document: "octopus_invoice.jpg",
amount: 15.5,
supportingDocumentName: "octopus_invoice.jpg",
}, },
{ {
id: 3, id: 3,
date: dayjs().add(-44, "days").toDate(),
invoiceDate: dayjs().add(-44, "days").toDate(),
description: "Starbucks", 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>( const [rowModesModel, setRowModesModel] = React.useState<GridRowModesModel>(
{}, {},
); );


// Row function
const handleRowEditStop: GridEventListener<"rowEditStop"> = ( const handleRowEditStop: GridEventListener<"rowEditStop"> = (
params, params,
event, 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) => { const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => {
setRowModesModel(newRowModesModel); 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 = () => { const getCostTotal = () => {
let sum = 0; let sum = 0;
rows.forEach((row) => { rows.forEach((row) => {
sum += row["cost"] ?? 0;
sum += row["amount"] ?? 0;
}); });
return sum; return sum;
}; };
@@ -256,11 +298,11 @@ const ClaimInputGrid: React.FC<ClaimInputGridProps> = ({ ...props }) => {
), ),
}; };


const columns: GridColDef[] = [
const columns: GridColDef[] = React.useMemo(() => [
{ {
field: "actions", field: "actions",
type: "actions", type: "actions",
headerName: "Actions",
headerName: t("Actions"),
width: 100, width: 100,
cellClassName: "actions", cellClassName: "actions",
getActions: ({ id }) => { 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, // width: 220,
flex: 1, flex: 1,
editable: true, editable: true,
type: "date", 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", field: "description",
headerName: "Description",
headerName: t("Description"),
// width: 220, // width: 220,
flex: 2, flex: 2,
editable: true, editable: true,
type: "string", type: "string",
}, },
{ {
field: "cost",
headerName: "Cost (HKD)",
field: "amount",
headerName: t("Amount (HKD)"),
editable: true, editable: true,
type: "number", type: "number",
valueFormatter: (params) => { 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, editable: true,
flex: 2, flex: 2,
renderCell: (params) => { renderCell: (params) => {
return params.value ? ( return params.value ? (
<span> <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} {params.value}
</a>
</a> */}
</span> </span>
) : ( ) : (
<span style={{ color: palette.text.disabled }}>No Documents</span> <span style={{ color: palette.text.disabled }}>No Documents</span>
); );
}, },
renderEditCell: (params) => { renderEditCell: (params) => {
return params.value ? (
const currentRow = rows.find(row => row.id === params.row.id);
return params.formattedValue ? (
<span> <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 <Button
title="Remove Document" title="Remove Document"
onClick={(event) => console.log(event)}
onClick={() => handleFileDelete(params.row.id)}
> >
<ImageNotSupportedOutlinedIcon <ImageNotSupportedOutlinedIcon
sx={{ fontSize: "25px", color: "red" }} sx={{ fontSize: "25px", color: "red" }}
@@ -369,15 +440,24 @@ const ClaimInputGrid: React.FC<ClaimInputGridProps> = ({ ...props }) => {
</Button> </Button>
</span> </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 ( return (
<Box <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 <BottomBar
getCostTotal={getCostTotal} getCostTotal={getCostTotal}
setRows={setRows} setRows={setRows}
setRowModesModel={setRowModesModel} setRowModesModel={setRowModesModel}
// sx={{flex:2}}
// sx={{flex:2}}
/> />
</Box> </Box>
); );
}; };


export default ClaimInputGrid;
export default ClaimFormInputGrid;

+ 1
- 0
src/components/ClaimDetail/index.ts Wyświetl plik

@@ -0,0 +1 @@
export { default } from "./ClaimDetailWrapper";

+ 24
- 29
src/components/ClaimSearch/ClaimSearch.tsx Wyświetl plik

@@ -1,65 +1,52 @@
"use client"; "use client";


import { ClaimResult } from "@/app/api/claims";
import { Claim, ClaimSearchForm } from "@/app/api/claims";
import React, { useCallback, useMemo, useState } from "react"; import React, { useCallback, useMemo, useState } from "react";
import SearchBox, { Criterion } from "../SearchBox/index"; import SearchBox, { Criterion } from "../SearchBox/index";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import SearchResults, { Column } from "../SearchResults/index"; import SearchResults, { Column } from "../SearchResults/index";
import EditNote from "@mui/icons-material/EditNote"; import EditNote from "@mui/icons-material/EditNote";
import { dateInRange } from "@/app/utils/commonUtil";
import { claimStatusCombo, expenseTypeCombo } from "@/app/utils/comboUtil";


interface Props { interface Props {
claims: ClaimResult[];
claims: Claim[];
} }


type SearchQuery = Partial<Omit<ClaimResult, "id">>;
type SearchQuery = Partial<Omit<ClaimSearchForm, "id">>;
type SearchParamNames = keyof SearchQuery; type SearchParamNames = keyof SearchQuery;


const ClaimSearch: React.FC<Props> = ({ claims }) => { 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. // If claim searching is done on the server-side, then no need for this.
const [filteredClaims, setFilteredClaims] = useState(claims); const [filteredClaims, setFilteredClaims] = useState(claims);


const searchCriteria: Criterion<SearchParamNames>[] = useMemo( const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
() => [ () => [
{ label: t("Creation Date"), paramName: "created", type: "dateRange" },
{ label: t("Creation Date From"), label2: t("Creation Date To"), paramName: "created", type: "dateRange" },
{ label: t("Related Project Name"), paramName: "name", type: "text" }, { label: t("Related Project Name"), paramName: "name", type: "text" },
{
label: t("Cost (HKD)"),
paramName: "cost",
type: "text",
},
{ {
label: t("Expense Type"), label: t("Expense Type"),
paramName: "type", paramName: "type",
type: "select", type: "select",
options: ["Expense", "Petty Cash"],
options: expenseTypeCombo,
}, },
{ {
label: t("Status"), label: t("Status"),
paramName: "status", paramName: "status",
type: "select", type: "select",
options: [
"Not Submitted",
"Waiting for Approval",
"Approved",
"Rejected",
],
},
{
label: t("Remarks"),
paramName: "remarks",
type: "text",
options: claimStatusCombo,
}, },
], ],
[t], [t],
); );


const onClaimClick = useCallback((claim: ClaimResult) => {
const onClaimClick = useCallback((claim: Claim) => {
console.log(claim); console.log(claim);
}, []); }, []);


const columns = useMemo<Column<ClaimResult>[]>(
const columns = useMemo<Column<Claim>[]>(
() => [ () => [
// { // {
// name: "action", // name: "action",
@@ -69,9 +56,9 @@ const ClaimSearch: React.FC<Props> = ({ claims }) => {
// }, // },
{ name: "created", label: t("Creation Date") }, { name: "created", label: t("Creation Date") },
{ name: "name", label: t("Related Project Name") }, { 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") }, { name: "remarks", label: t("Remarks") },
], ],
[t, onClaimClick], [t, onClaimClick],
@@ -82,10 +69,18 @@ const ClaimSearch: React.FC<Props> = ({ claims }) => {
<SearchBox <SearchBox
criteria={searchCriteria} criteria={searchCriteria}
onSearch={(query) => { 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} />
</> </>
); );
}; };


+ 0
- 67
src/components/CreateClaim/ClaimDetails.tsx Wyświetl plik

@@ -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;

+ 0
- 48
src/components/CreateClaim/CreateClaim.tsx Wyświetl plik

@@ -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;

+ 0
- 1
src/components/CreateClaim/index.ts Wyświetl plik

@@ -1 +0,0 @@
export { default } from "./CreateClaim";

+ 1
- 1
src/components/CustomDatagrid/CustomDatagrid.tsx Wyświetl plik

@@ -12,7 +12,7 @@ interface CustomDatagridProps {
columnWidth?: number; columnWidth?: number;
Style?: boolean; Style?: boolean;
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
dataGridHeight?: number;
dataGridHeight?: number | string;
[key: string]: any; [key: string]: any;
checkboxSelection?: boolean; checkboxSelection?: boolean;
onRowSelectionModelChange?: ( onRowSelectionModelChange?: (


+ 1
- 1
src/components/CustomerDetail/ContactInfo.tsx Wyświetl plik

@@ -262,7 +262,7 @@ const ContactInfo: React.FC<Props> = ({
{t("Contact Info")} {t("Contact Info")}
</Typography> </Typography>
{Boolean(errors.addContacts?.type === "required") && <Typography sx={(theme) => ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> {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>} </Typography>}
{Boolean(errors.addContacts?.type === "email_format") && <Typography sx={(theme) => ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> {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")} {t("Please ensure all the email formats are correct")}


+ 1
- 1
src/components/CustomerDetail/CustomerDetailWrapper.tsx Wyświetl plik

@@ -2,7 +2,7 @@
// import CreateProject from "./CreateProject"; // import CreateProject from "./CreateProject";
// import { fetchProjectCategories } from "@/app/api/projects"; // import { fetchProjectCategories } from "@/app/api/projects";
// import { fetchTeamLeads } from "@/app/api/staff"; // 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"; import CustomerDetail from "./CustomerDetail";


// type Props = { // type Props = {


+ 2
- 2
src/components/CustomerDetail/CustomerInfo.tsx Wyświetl plik

@@ -68,7 +68,7 @@ const CustomerInfo: React.FC<Props> = ({
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}> <Grid item xs={6}>
<TextField <TextField
label={t("Customer Code")}
label={`${t("Customer Code")}*`}
fullWidth fullWidth
{...register("code", { {...register("code", {
required: true, required: true,
@@ -79,7 +79,7 @@ const CustomerInfo: React.FC<Props> = ({
</Grid> </Grid>
<Grid item xs={6}> <Grid item xs={6}>
<TextField <TextField
label={t("Customer Name")}
label={`${t("Customer Name")}*`}
fullWidth fullWidth
{...register("name", { {...register("name", {
required: true, required: true,


+ 1
- 0
src/components/CustomerSearch/CustomerSearch.tsx Wyświetl plik

@@ -67,6 +67,7 @@ const CustomerSearch: React.FC<Props> = ({ customers }) => {
label: t("Delete"), label: t("Delete"),
onClick: onDeleteClick, onClick: onDeleteClick,
buttonIcon: <DeleteIcon />, buttonIcon: <DeleteIcon />,
color: "error"
}, },
], ],
[onTaskClick, t], [onTaskClick, t],


+ 1
- 1
src/components/SearchBox/SearchBox.tsx Wyświetl plik

@@ -137,7 +137,7 @@ function SearchBox<T extends string>({
<MenuItem value={"All"}>{t("All")}</MenuItem> <MenuItem value={"All"}>{t("All")}</MenuItem>
{c.options.map((option, index) => ( {c.options.map((option, index) => (
<MenuItem key={`${option}-${index}`} value={option}> <MenuItem key={`${option}-${index}`} value={option}>
{option}
{t(option)}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>


+ 5
- 1
src/components/SearchResults/SearchResults.tsx Wyświetl plik

@@ -12,6 +12,8 @@ import TablePagination, {
} from "@mui/material/TablePagination"; } from "@mui/material/TablePagination";
import TableRow from "@mui/material/TableRow"; import TableRow from "@mui/material/TableRow";
import IconButton, { IconButtonOwnProps, IconButtonPropsColorOverrides } from "@mui/material/IconButton"; import IconButton, { IconButtonOwnProps, IconButtonPropsColorOverrides } from "@mui/material/IconButton";
import { t } from "i18next";
import { useTranslation } from "react-i18next";


export interface ResultWithId { export interface ResultWithId {
id: string | number; id: string | number;
@@ -21,6 +23,7 @@ interface BaseColumn<T extends ResultWithId> {
name: keyof T; name: keyof T;
label: string; label: string;
color?: IconButtonOwnProps["color"]; color?: IconButtonOwnProps["color"];
needTranslation?: boolean
} }


interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> { interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> {
@@ -51,6 +54,7 @@ function SearchResults<T extends ResultWithId>({
}: Props<T>) { }: Props<T>) {
const [page, setPage] = React.useState(0); const [page, setPage] = React.useState(0);
const [rowsPerPage, setRowsPerPage] = React.useState(10); const [rowsPerPage, setRowsPerPage] = React.useState(10);
const { t } = useTranslation()


const handleChangePage: TablePaginationProps["onPageChange"] = ( const handleChangePage: TablePaginationProps["onPageChange"] = (
_event, _event,
@@ -98,7 +102,7 @@ function SearchResults<T extends ResultWithId>({
{column.buttonIcon} {column.buttonIcon}
</IconButton> </IconButton>
) : ( ) : (
<>{item[columnName]}</>
<>{column?.needTranslation ? t(item[columnName] as string) : item[columnName]}</>
)} )}
</TableCell> </TableCell>
); );


+ 1
- 1
src/components/SubsidiaryDetail/ContactInfo.tsx Wyświetl plik

@@ -263,7 +263,7 @@ const ContactInfo: React.FC<Props> = ({
{t("Contact Info")} {t("Contact Info")}
</Typography> </Typography>
{Boolean(errors.addContacts?.type === "required") && <Typography sx={(theme) => ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> {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>} </Typography>}
{Boolean(errors.addContacts?.type === "email_format") && <Typography sx={(theme) => ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> {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")} {t("Please ensure all the email formats are correct")}


+ 2
- 2
src/components/SubsidiaryDetail/SubsidiaryInfo.tsx Wyświetl plik

@@ -57,7 +57,7 @@ const SubsidiaryInfo: React.FC<Props> = ({
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}> <Grid item xs={6}>
<TextField <TextField
label={t("Subsidiary Code")}
label={`${t("Subsidiary Code")}*`}
fullWidth fullWidth
{...register("code", { {...register("code", {
required: true, required: true,
@@ -68,7 +68,7 @@ const SubsidiaryInfo: React.FC<Props> = ({
</Grid> </Grid>
<Grid item xs={6}> <Grid item xs={6}>
<TextField <TextField
label={t("Subsidiary Name")}
label={`${t("Subsidiary Name")}*`}
fullWidth fullWidth
{...register("name", { {...register("name", {
required: true, required: true,


+ 1
- 0
src/components/SubsidiarySearch/SubsidiarySearch.tsx Wyświetl plik

@@ -67,6 +67,7 @@ const SubsidiarySearch: React.FC<Props> = ({ subsidiaries }) => {
label: t("Delete"), label: t("Delete"),
onClick: onDeleteClick, onClick: onDeleteClick,
buttonIcon: <DeleteIcon />, buttonIcon: <DeleteIcon />,
color: "error"
}, },
], ],
[onTaskClick, t], [onTaskClick, t],


+ 31
- 0
src/i18n/en/claim.json Wyświetl plik

@@ -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"
}

+ 12
- 0
src/i18n/en/common.json Wyświetl plik

@@ -1,10 +1,22 @@
{ {
"Grade {{grade}}": "Grade {{grade}}", "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": "Search",
"Search Criteria": "Search Criteria", "Search Criteria": "Search Criteria",
"Cancel": "Cancel", "Cancel": "Cancel",
"Confirm": "Confirm", "Confirm": "Confirm",
"Submit": "Submit", "Submit": "Submit",
"Save": "Save",
"Save And Submit": "Save And Submit",
"Reset": "Reset" "Reset": "Reset"
} }

+ 1
- 1
src/i18n/en/customer.json Wyświetl plik

@@ -43,7 +43,7 @@
"Contact Name": "Contact Name", "Contact Name": "Contact Name",
"Contact Email": "Contact Email", "Contact Email": "Contact Email",
"Contact Phone": "Contact Phone", "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", "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?", "Do you want to submit?": "Do you want to submit?",


+ 1
- 1
src/i18n/en/subsidiary.json Wyświetl plik

@@ -43,7 +43,7 @@
"Contact Name": "Contact Name", "Contact Name": "Contact Name",
"Contact Email": "Contact Email", "Contact Email": "Contact Email",
"Contact Phone": "Contact Phone", "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", "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?", "Do you want to submit?": "Do you want to submit?",


+ 31
- 0
src/i18n/zh/claim.json Wyświetl plik

@@ -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": "行動"
}

+ 12
- 0
src/i18n/zh/common.json Wyświetl plik

@@ -1,8 +1,20 @@
{ {
"All": "全部",

"Petty Cash": "小額開支",
"Expense": "普通開支",

"Not Submitted": "尚未提交",
"Waiting for Approval": "等待批核",
"Approved": "已批核",
"Rejected": "已拒絕",
"Search": "搜尋", "Search": "搜尋",
"Search Criteria": "搜尋條件", "Search Criteria": "搜尋條件",
"Cancel": "取消", "Cancel": "取消",
"Confirm": "確認", "Confirm": "確認",
"Submit": "提交", "Submit": "提交",
"Save": "儲存",
"Save And Submit": "儲存及提交",
"Reset": "重置" "Reset": "重置"
} }

+ 1
- 1
src/i18n/zh/customer.json Wyświetl plik

@@ -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 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?": "你是否確認要提交?",


+ 1
- 1
src/i18n/zh/subsidiary.json Wyświetl plik

@@ -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 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?": "你是否確認要提交?",


Ładowanie…
Anuluj
Zapisz