Переглянути джерело

update claim, customer, subsidiary

tags/Baseline_30082024_FRONTEND_UAT
cyril.tsui 1 рік тому
джерело
коміт
b45d61c305
34 змінених файлів з 671 додано та 281 видалено
  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 Переглянути файл

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

+ 7
- 5
src/app/(main)/staffReimbursement/page.tsx Переглянути файл

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

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

return (
@@ -37,9 +37,11 @@ const StaffReimbursement: React.FC = async () => {
{t("Create Claim")}
</Button>
</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 Переглянути файл

@@ -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 Переглянути файл

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


+ 11
- 0
src/app/utils/comboUtil.ts Переглянути файл

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

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

+ 23
- 0
src/app/utils/commonUtil.ts Переглянути файл

@@ -0,0 +1,23 @@
export const dateInRange = (currentDate: string, startDate: string, endDate: string) => {

if (currentDate === undefined) {
return false // can be changed to true if necessary
}

const currentDateTime = new Date(currentDate).getTime()
const startDateTime = startDate === undefined || startDate.length === 0 ? undefined : new Date(startDate).getTime()
const endDateTime = endDate === undefined || startDate.length === 0 ? undefined : new Date(endDate).getTime()

// console.log(currentDateTime, startDateTime, endDateTime)
if (startDateTime === undefined && endDateTime !== undefined) {
return currentDateTime <= endDateTime
} else if (startDateTime !== undefined && endDateTime === undefined) {
return currentDateTime >= startDateTime
} else {
if (startDateTime !== undefined && endDateTime !== undefined) {
return currentDateTime >= startDateTime && currentDateTime <= endDateTime
} else {
return true
}
}
}

+ 8
- 0
src/app/utils/formatUtil.ts Переглянути файл

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

export const manhourFormatter = new Intl.NumberFormat("en-HK", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
@@ -15,6 +17,12 @@ export const percentFormatter = new Intl.NumberFormat("en-HK", {

export const INPUT_DATE_FORMAT = "YYYY-MM-DD";

export const OUTPUT_DATE_FORMAT = "YYYY/MM/DD";

export const convertDateToString = (date: Date, format: string = OUTPUT_DATE_FORMAT) => {
return dayjs(date).format(format)
}

const shortDateFormatter_en = new Intl.DateTimeFormat("en-HK", {
weekday: "short",
year: "numeric",


+ 105
- 0
src/components/ClaimDetail/ClaimDetail.tsx Переглянути файл

@@ -0,0 +1,105 @@
"use client";

import Check from "@mui/icons-material/Check";
import Close from "@mui/icons-material/Close";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import { useRouter } from "next/navigation";
import React, { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import ClaimFormInfo from "./ClaimFormInfo";
import { ProjectCombo } from "@/app/api/claims";
import { Typography } from "@mui/material";
import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form";
import { ClaimInputFormByStaff, saveClaim } from "@/app/api/claims/actions";
import { DoneAll } from "@mui/icons-material";
import { expenseTypeCombo } from "@/app/utils/comboUtil";

export interface Props {
projectCombo: ProjectCombo[]
}

const ClaimDetail: React.FC<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 Переглянути файл

@@ -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 Переглянути файл

@@ -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 Переглянути файл

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

+ 1
- 0
src/components/ClaimDetail/index.ts Переглянути файл

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

+ 24
- 29
src/components/ClaimSearch/ClaimSearch.tsx Переглянути файл

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

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

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

type SearchQuery = Partial<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} />
</>
);
};


+ 0
- 67
src/components/CreateClaim/ClaimDetails.tsx Переглянути файл

@@ -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 Переглянути файл

@@ -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 Переглянути файл

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

+ 1
- 1
src/components/CustomDatagrid/CustomDatagrid.tsx Переглянути файл

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


+ 1
- 1
src/components/CustomerDetail/ContactInfo.tsx Переглянути файл

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


+ 1
- 1
src/components/CustomerDetail/CustomerDetailWrapper.tsx Переглянути файл

@@ -2,7 +2,7 @@
// import CreateProject from "./CreateProject";
// import { fetchProjectCategories } from "@/app/api/projects";
// import { fetchTeamLeads } from "@/app/api/staff";
import { Subsidiary, fetchCustomerTypes, fetchAllSubsidiaries } from "@/app/api/customer";
import { fetchCustomerTypes, fetchAllSubsidiaries } from "@/app/api/customer";
import CustomerDetail from "./CustomerDetail";

// type Props = {


+ 2
- 2
src/components/CustomerDetail/CustomerInfo.tsx Переглянути файл

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


+ 1
- 0
src/components/CustomerSearch/CustomerSearch.tsx Переглянути файл

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


+ 1
- 1
src/components/SearchBox/SearchBox.tsx Переглянути файл

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


+ 5
- 1
src/components/SearchResults/SearchResults.tsx Переглянути файл

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

export interface ResultWithId {
id: string | number;
@@ -21,6 +23,7 @@ interface BaseColumn<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>
);


+ 1
- 1
src/components/SubsidiaryDetail/ContactInfo.tsx Переглянути файл

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


+ 2
- 2
src/components/SubsidiaryDetail/SubsidiaryInfo.tsx Переглянути файл

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


+ 1
- 0
src/components/SubsidiarySearch/SubsidiarySearch.tsx Переглянути файл

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


+ 31
- 0
src/i18n/en/claim.json Переглянути файл

@@ -0,0 +1,31 @@
{
"Staff Reimbursement": "Staff Reimbursement",
"Create Claim": "Create Claim",
"Creation Date": "Creation Date",
"Creation Date From": "Creation Date From",
"Creation Date To": "Creation Date To",
"Related Project": "Related Project",
"Related Project Name": "Related Project Name",
"Expense Type": "Expense Type",
"Status": "Status",
"Amount (HKD)": "Amount (HKD)",
"Remarks": "Remarks",
"Invoice Date": "Invoice Date",
"Supporting Document": "Supporting Document",
"Total": "Total",
"Add Record": "Add Record",
"Project Name": "Project Name",
"Project": "Project",
"Claim Code": "Claim Code",
"Petty Cash": "Petty Cash",
"Expense": "Expense",
"Not Submitted": "Not Submitted",
"Waiting for Approval": "Waiting for Approval",
"Approved": "Approved",
"Rejected": "Rejected",
"Description": "Description",
"Actions": "Actions"
}

+ 12
- 0
src/i18n/en/common.json Переглянути файл

@@ -1,10 +1,22 @@
{
"Grade {{grade}}": "Grade {{grade}}",

"All": "All",
"Petty Cash": "Petty Cash",
"Expense": "Expense",
"Not Submitted": "Not Submitted",
"Waiting for Approval": "Waiting for Approval",
"Approved": "Approved",
"Rejected": "Rejected",

"Search": "Search",
"Search Criteria": "Search Criteria",
"Cancel": "Cancel",
"Confirm": "Confirm",
"Submit": "Submit",
"Save": "Save",
"Save And Submit": "Save And Submit",
"Reset": "Reset"
}

+ 1
- 1
src/i18n/en/customer.json Переглянути файл

@@ -43,7 +43,7 @@
"Contact Name": "Contact Name",
"Contact Email": "Contact Email",
"Contact Phone": "Contact Phone",
"Please ensure all the fields are inputted and saved": "Please ensure all the fields are inputted and saved",
"Please ensure at least one row is created, and all the fields are inputted and saved": "Please ensure at least one row is created, and all the fields are inputted and saved",
"Please ensure all the email formats are correct": "Please ensure all the email formats are correct",

"Do you want to submit?": "Do you want to submit?",


+ 1
- 1
src/i18n/en/subsidiary.json Переглянути файл

@@ -43,7 +43,7 @@
"Contact Name": "Contact Name",
"Contact Email": "Contact Email",
"Contact Phone": "Contact Phone",
"Please ensure all the fields are inputted and saved": "Please ensure all the fields are inputted and saved",
"Please ensure at least one row is created, and all the fields are inputted and saved": "Please ensure at least one row is created, and all the fields are inputted and saved",
"Please ensure all the email formats are correct": "Please ensure all the email formats are correct",

"Do you want to submit?": "Do you want to submit?",


+ 31
- 0
src/i18n/zh/claim.json Переглянути файл

@@ -0,0 +1,31 @@
{
"Staff Reimbursement": "員工報銷",
"Create Claim": "建立報銷",
"Creation Date": "建立日期",
"Creation Date From": "建立日期 (從)",
"Creation Date To": "建立日期 (至)",
"Related Project": "相關項目名稱",
"Related Project Name": "相關項目名稱",
"Expense Type": "費用類別",
"Status": "狀態",
"Amount (HKD)": "金額 (HKD)",
"Remarks": "備註",
"Invoice Date": "收據日期",
"Supporting Document": "支援文件",
"Total": "總金額",
"Add Record": "新增記錄",
"Project Name": "項目名稱",
"Project": "項目",
"Claim Code": "報銷編號",

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

"Not Submitted": "尚未提交",
"Waiting for Approval": "等待批核",
"Approved": "已批核",
"Rejected": "已拒絕",

"Description": "描述",
"Actions": "行動"
}

+ 12
- 0
src/i18n/zh/common.json Переглянути файл

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

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

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

+ 1
- 1
src/i18n/zh/customer.json Переглянути файл

@@ -43,7 +43,7 @@
"Contact Name": "聯絡姓名",
"Contact Email": "聯絡電郵",
"Contact Phone": "聯絡電話",
"Please ensure all the fields are inputted and saved": "請確保所有欄位已輸入及儲存",
"Please ensure at least one row is created, and all the fields are inputted and saved": "請確保已建立至少一行, 及已輸入和儲存所有欄位",
"Please ensure all the email formats are correct": "請確保所有電郵格式輸入正確",
"Do you want to submit?": "你是否確認要提交?",


+ 1
- 1
src/i18n/zh/subsidiary.json Переглянути файл

@@ -43,7 +43,7 @@
"Contact Name": "聯絡姓名",
"Contact Email": "聯絡電郵",
"Contact Phone": "聯絡電話",
"Please ensure all the fields are inputted and saved": "請確保所有欄位已輸入及儲存",
"Please ensure at least one row is created, and all the fields are inputted and saved": "請確保已建立至少一行, 及已輸入和儲存所有欄位",
"Please ensure all the email formats are correct": "請確保所有電郵格式輸入正確",
"Do you want to submit?": "你是否確認要提交?",


Завантаження…
Відмінити
Зберегти