@@ -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; |
@@ -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> | |||||
</> | </> | ||||
); | ); | ||||
}; | }; | ||||
@@ -0,0 +1,48 @@ | |||||
"use server"; | |||||
import { BASE_API_URL } from "@/config/api"; | |||||
import { Claim, ProjectCombo, SupportingDocument } from "."; | |||||
import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
import { revalidateTag } from "next/cache"; | |||||
export interface ClaimInputFormByStaff { | |||||
id: number | null; | |||||
code: string | null; | |||||
expenseType: string; | |||||
status: string; | |||||
addClaimDetails: ClaimDetailTable[] | |||||
} | |||||
export interface ClaimDetailTable { | |||||
id: number; | |||||
invoiceDate: Date; | |||||
description: string; | |||||
project: ProjectCombo; | |||||
amount: number; | |||||
supportingDocumentName: string; | |||||
oldSupportingDocument: FileList[]; | |||||
newSupportingDocument: SupportingDocument; | |||||
isNew: boolean; | |||||
} | |||||
export interface SaveClaimResponse { | |||||
claim: Claim; | |||||
message: string; | |||||
} | |||||
export const saveClaim = async (data: ClaimInputFormByStaff) => { | |||||
console.log(data) | |||||
const saveCustomer = await serverFetchJson<SaveClaimResponse>( | |||||
`${BASE_API_URL}/claim/save`, | |||||
{ | |||||
method: "POST", | |||||
body: JSON.stringify(data), | |||||
headers: { "Content-Type": "application/json" }, | |||||
}, | |||||
); | |||||
revalidateTag("claims"); | |||||
return saveCustomer; | |||||
}; |
@@ -1,7 +1,9 @@ | |||||
import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
import { BASE_API_URL } from "@/config/api"; | |||||
import { cache } from "react"; | import { 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", | ||||
@@ -0,0 +1,11 @@ | |||||
export const expenseTypeCombo = [ | |||||
"Petty Cash", | |||||
"Expense" | |||||
] | |||||
export const claimStatusCombo = [ | |||||
"Not Submitted", | |||||
"Waiting for Approval", | |||||
"Approved", | |||||
"Rejected" | |||||
] |
@@ -0,0 +1,23 @@ | |||||
export const dateInRange = (currentDate: string, startDate: string, endDate: string) => { | |||||
if (currentDate === undefined) { | |||||
return false // can be changed to true if necessary | |||||
} | |||||
const currentDateTime = new Date(currentDate).getTime() | |||||
const startDateTime = startDate === undefined || startDate.length === 0 ? undefined : new Date(startDate).getTime() | |||||
const endDateTime = endDate === undefined || startDate.length === 0 ? undefined : new Date(endDate).getTime() | |||||
// console.log(currentDateTime, startDateTime, endDateTime) | |||||
if (startDateTime === undefined && endDateTime !== undefined) { | |||||
return currentDateTime <= endDateTime | |||||
} else if (startDateTime !== undefined && endDateTime === undefined) { | |||||
return currentDateTime >= startDateTime | |||||
} else { | |||||
if (startDateTime !== undefined && endDateTime !== undefined) { | |||||
return currentDateTime >= startDateTime && currentDateTime <= endDateTime | |||||
} else { | |||||
return true | |||||
} | |||||
} | |||||
} |
@@ -1,3 +1,5 @@ | |||||
import dayjs from "dayjs"; | |||||
export const manhourFormatter = new Intl.NumberFormat("en-HK", { | 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", | ||||
@@ -0,0 +1,105 @@ | |||||
"use client"; | |||||
import Check from "@mui/icons-material/Check"; | |||||
import Close from "@mui/icons-material/Close"; | |||||
import Button from "@mui/material/Button"; | |||||
import Stack from "@mui/material/Stack"; | |||||
import { useRouter } from "next/navigation"; | |||||
import React, { useCallback, useState } from "react"; | |||||
import { useTranslation } from "react-i18next"; | |||||
import ClaimFormInfo from "./ClaimFormInfo"; | |||||
import { ProjectCombo } from "@/app/api/claims"; | |||||
import { Typography } from "@mui/material"; | |||||
import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form"; | |||||
import { ClaimInputFormByStaff, saveClaim } from "@/app/api/claims/actions"; | |||||
import { DoneAll } from "@mui/icons-material"; | |||||
import { expenseTypeCombo } from "@/app/utils/comboUtil"; | |||||
export interface Props { | |||||
projectCombo: ProjectCombo[] | |||||
} | |||||
const ClaimDetail: React.FC<Props> = ({ projectCombo }) => { | |||||
const { t } = useTranslation("common"); | |||||
const [serverError, setServerError] = useState(""); | |||||
const router = useRouter(); | |||||
const formProps = useForm<ClaimInputFormByStaff>({ | |||||
defaultValues: { | |||||
id: null, | |||||
expenseType: expenseTypeCombo[0], | |||||
addClaimDetails: [] | |||||
}, | |||||
}); | |||||
const handleCancel = () => { | |||||
router.back(); | |||||
}; | |||||
const onSubmit = useCallback<SubmitHandler<ClaimInputFormByStaff>>( | |||||
async (data, event) => { | |||||
try { | |||||
console.log(data); | |||||
console.log((event?.nativeEvent as any).submitter.name); | |||||
const buttonName = (event?.nativeEvent as any).submitter.name | |||||
console.log(JSON.stringify(data)) | |||||
// const formData = new FormData() | |||||
// formData.append("expenseType", data.expenseType) | |||||
// formData.append("claimDetails", data.addClaimDetails) | |||||
if (buttonName === "submit") { | |||||
data.status = "Not Submitted" | |||||
} else if (buttonName === "save") { | |||||
data.status = "Waiting for Approval" | |||||
} | |||||
// for (let i = 0; i < data.addClaimDetails.length; i++) { | |||||
// // const formData = new FormData(); | |||||
// // formData.append("newSupportingDocument", data.addClaimDetails[i].oldSupportingDocument); | |||||
// data.addClaimDetails[i].oldSupportingDocument = new Blob([data.addClaimDetails[i].oldSupportingDocument], {type: data.addClaimDetails[i].oldSupportingDocument.type}) | |||||
// } | |||||
console.log(data); | |||||
await saveClaim(data) | |||||
setServerError(""); | |||||
// await saveProject(data); | |||||
// router.replace("/projects"); | |||||
} catch (e) { | |||||
setServerError(t("An error has occurred. Please try again later.")); | |||||
} | |||||
}, | |||||
[router, t], | |||||
); | |||||
const onSubmitError = useCallback<SubmitErrorHandler<ClaimInputFormByStaff>>( | |||||
(errors) => { | |||||
// Set the tab so that the focus will go there | |||||
console.log(errors) | |||||
}, | |||||
[], | |||||
); | |||||
return ( | |||||
<FormProvider {...formProps}> | |||||
<Stack spacing={2} component={"form"} onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)}> | |||||
<ClaimFormInfo projectCombo={projectCombo} /> | |||||
{serverError && ( | |||||
<Typography variant="body2" color="error" alignSelf="flex-end"> | |||||
{serverError} | |||||
</Typography> | |||||
)} | |||||
<Stack direction="row" justifyContent="flex-end" gap={1}> | |||||
<Button variant="text" startIcon={<Close />} onClick={handleCancel}> | |||||
{t("Cancel")} | |||||
</Button> | |||||
<Button variant="outlined" name="save" startIcon={<Check />} type="submit"> | |||||
{t("Save")} | |||||
</Button> | |||||
<Button variant="contained" name="submit" startIcon={<DoneAll />} type="submit"> | |||||
{t("Submit")} | |||||
</Button> | |||||
</Stack> | |||||
</Stack> | |||||
</FormProvider> | |||||
); | |||||
}; | |||||
export default ClaimDetail; |
@@ -0,0 +1,20 @@ | |||||
import React from "react"; | |||||
import ClaimDetail from "./ClaimDetail"; | |||||
import { fetchProjectCombo } from "@/app/api/claims"; | |||||
// import TaskSetup from "./TaskSetup"; | |||||
// import StaffAllocation from "./StaffAllocation"; | |||||
// import ResourceMilestone from "./ResourceMilestone"; | |||||
const ClaimDetailWrapper: React.FC = async () => { | |||||
const [projectCombo] = | |||||
await Promise.all([ | |||||
fetchProjectCombo() | |||||
]); | |||||
return ( | |||||
<ClaimDetail projectCombo={projectCombo}/> | |||||
); | |||||
}; | |||||
export default ClaimDetailWrapper; |
@@ -0,0 +1,76 @@ | |||||
"use client"; | |||||
import Stack from "@mui/material/Stack"; | |||||
import Box from "@mui/material/Box"; | |||||
import Card from "@mui/material/Card"; | |||||
import CardContent from "@mui/material/CardContent"; | |||||
import FormControl from "@mui/material/FormControl"; | |||||
import Grid from "@mui/material/Grid"; | |||||
import InputLabel from "@mui/material/InputLabel"; | |||||
import MenuItem from "@mui/material/MenuItem"; | |||||
import Select from "@mui/material/Select"; | |||||
import { useTranslation } from "react-i18next"; | |||||
import ClaimFormInputGrid from "./ClaimFormInputGrid"; | |||||
import { expenseTypeCombo } from "@/app/utils/comboUtil"; | |||||
import { Controller, useFormContext } from "react-hook-form"; | |||||
import { ClaimInputFormByStaff } from "@/app/api/claims/actions"; | |||||
import { ProjectCombo } from "@/app/api/claims"; | |||||
import { TextField } from "@mui/material"; | |||||
interface Props { | |||||
projectCombo: ProjectCombo[] | |||||
} | |||||
const ClaimFormInfo: React.FC<Props> = ({ projectCombo }) => { | |||||
const { t } = useTranslation(); | |||||
const { | |||||
control, | |||||
register, | |||||
} = useFormContext<ClaimInputFormByStaff>(); | |||||
return ( | |||||
<Card> | |||||
<CardContent component={Stack} spacing={4}> | |||||
<Box> | |||||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||||
<Grid item xs={6}> | |||||
<TextField | |||||
fullWidth | |||||
label={t("Claim Code")} | |||||
{...register("code")} | |||||
disabled={true} | |||||
/> | |||||
</Grid> | |||||
<Grid item xs={6}> | |||||
<FormControl fullWidth> | |||||
<InputLabel>{t("Expense Type")}</InputLabel> | |||||
<Controller | |||||
// defaultValue={expenseTypeCombo[0].value} | |||||
control={control} | |||||
name="expenseType" | |||||
render={({ field }) => ( | |||||
<Select label={t("Expense Type")} {...field}> | |||||
{ | |||||
expenseTypeCombo.map((type, index) => ( | |||||
<MenuItem key={`${type}-${index}`} value={type}> | |||||
{t(type)} | |||||
</MenuItem> | |||||
)) | |||||
} | |||||
</Select> | |||||
)} | |||||
/> | |||||
</FormControl> | |||||
</Grid> | |||||
</Grid> | |||||
</Box> | |||||
<Card> | |||||
<ClaimFormInputGrid projectCombo={projectCombo} /> | |||||
</Card> | |||||
</CardContent> | |||||
</Card> | |||||
); | |||||
}; | |||||
export default ClaimFormInfo; |
@@ -8,16 +8,9 @@ import { Suspense } from "react"; | |||||
import Button from "@mui/material/Button"; | import 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; |
@@ -0,0 +1 @@ | |||||
export { default } from "./ClaimDetailWrapper"; |
@@ -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} /> | |||||
</> | </> | ||||
); | ); | ||||
}; | }; | ||||
@@ -1,67 +0,0 @@ | |||||
"use client"; | |||||
import Stack from "@mui/material/Stack"; | |||||
import Box from "@mui/material/Box"; | |||||
import Card from "@mui/material/Card"; | |||||
import CardContent from "@mui/material/CardContent"; | |||||
import FormControl from "@mui/material/FormControl"; | |||||
import Grid from "@mui/material/Grid"; | |||||
import InputLabel from "@mui/material/InputLabel"; | |||||
import MenuItem from "@mui/material/MenuItem"; | |||||
import Select from "@mui/material/Select"; | |||||
import TextField from "@mui/material/TextField"; | |||||
import Typography from "@mui/material/Typography"; | |||||
import { useTranslation } from "react-i18next"; | |||||
import CardActions from "@mui/material/CardActions"; | |||||
import RestartAlt from "@mui/icons-material/RestartAlt"; | |||||
import Button from "@mui/material/Button"; | |||||
import ClaimInputGrid from "./ClaimInputGrid"; | |||||
const ClaimDetails: React.FC = () => { | |||||
const { t } = useTranslation(); | |||||
return ( | |||||
<Card> | |||||
<CardContent component={Stack} spacing={4}> | |||||
<Box> | |||||
{/* <Typography variant="overline" display="block" marginBlockEnd={1}> | |||||
{t("Related Project")} | |||||
</Typography> */} | |||||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||||
<Grid item xs={6}> | |||||
<FormControl fullWidth> | |||||
<InputLabel>{t("Related Project")}</InputLabel> | |||||
<Select label={t("Project Category")}> | |||||
<MenuItem value={"M1001"}>{t("M1001")}</MenuItem> | |||||
<MenuItem value={"M1301"}>{t("M1301")}</MenuItem> | |||||
<MenuItem value={"M1354"}>{t("M1354")}</MenuItem> | |||||
</Select> | |||||
</FormControl> | |||||
</Grid> | |||||
<Grid item xs={6}> | |||||
<FormControl fullWidth> | |||||
<InputLabel>{t("Expense Type")}</InputLabel> | |||||
<Select label={t("Team Lead")}> | |||||
<MenuItem value={"Petty Cash"}>{"Petty Cash"}</MenuItem> | |||||
<MenuItem value={"Expense"}>{"Expense"}</MenuItem> | |||||
</Select> | |||||
</FormControl> | |||||
</Grid> | |||||
</Grid> | |||||
</Box> | |||||
<Card> | |||||
<ClaimInputGrid /> | |||||
</Card> | |||||
{/* <CardActions sx={{ justifyContent: "flex-end" }}> | |||||
<Button variant="text" startIcon={<RestartAlt />}> | |||||
{t("Reset")} | |||||
</Button> | |||||
</CardActions> */} | |||||
</CardContent> | |||||
</Card> | |||||
); | |||||
}; | |||||
export default ClaimDetails; |
@@ -1,48 +0,0 @@ | |||||
"use client"; | |||||
import Check from "@mui/icons-material/Check"; | |||||
import Close from "@mui/icons-material/Close"; | |||||
import Button from "@mui/material/Button"; | |||||
import Stack from "@mui/material/Stack"; | |||||
import Tab from "@mui/material/Tab"; | |||||
import Tabs, { TabsProps } from "@mui/material/Tabs"; | |||||
import { useRouter } from "next/navigation"; | |||||
import React, { useCallback, useState } from "react"; | |||||
import { useTranslation } from "react-i18next"; | |||||
import ClaimProjectDetails from "./ClaimDetails"; | |||||
// import TaskSetup from "./TaskSetup"; | |||||
// import StaffAllocation from "./StaffAllocation"; | |||||
// import ResourceMilestone from "./ResourceMilestone"; | |||||
const CreateProject: React.FC = () => { | |||||
const [tabIndex, setTabIndex] = useState(0); | |||||
const { t } = useTranslation(); | |||||
const router = useRouter(); | |||||
const handleCancel = () => { | |||||
router.back(); | |||||
}; | |||||
const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | |||||
(_e, newValue) => { | |||||
setTabIndex(newValue); | |||||
}, | |||||
[], | |||||
); | |||||
return ( | |||||
<> | |||||
<ClaimProjectDetails /> | |||||
<Stack direction="row" justifyContent="flex-end" gap={1}> | |||||
<Button variant="outlined" startIcon={<Close />} onClick={handleCancel}> | |||||
{t("Cancel")} | |||||
</Button> | |||||
<Button variant="contained" startIcon={<Check />}> | |||||
{t("Confirm")} | |||||
</Button> | |||||
</Stack> | |||||
</> | |||||
); | |||||
}; | |||||
export default CreateProject; |
@@ -1 +0,0 @@ | |||||
export { default } from "./CreateClaim"; |
@@ -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?: ( | ||||
@@ -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")} | ||||
@@ -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 = { | ||||
@@ -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, | ||||
@@ -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], | ||||
@@ -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> | ||||
@@ -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> | ||||
); | ); | ||||
@@ -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")} | ||||
@@ -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, | ||||
@@ -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], | ||||
@@ -0,0 +1,31 @@ | |||||
{ | |||||
"Staff Reimbursement": "Staff Reimbursement", | |||||
"Create Claim": "Create Claim", | |||||
"Creation Date": "Creation Date", | |||||
"Creation Date From": "Creation Date From", | |||||
"Creation Date To": "Creation Date To", | |||||
"Related Project": "Related Project", | |||||
"Related Project Name": "Related Project Name", | |||||
"Expense Type": "Expense Type", | |||||
"Status": "Status", | |||||
"Amount (HKD)": "Amount (HKD)", | |||||
"Remarks": "Remarks", | |||||
"Invoice Date": "Invoice Date", | |||||
"Supporting Document": "Supporting Document", | |||||
"Total": "Total", | |||||
"Add Record": "Add Record", | |||||
"Project Name": "Project Name", | |||||
"Project": "Project", | |||||
"Claim Code": "Claim Code", | |||||
"Petty Cash": "Petty Cash", | |||||
"Expense": "Expense", | |||||
"Not Submitted": "Not Submitted", | |||||
"Waiting for Approval": "Waiting for Approval", | |||||
"Approved": "Approved", | |||||
"Rejected": "Rejected", | |||||
"Description": "Description", | |||||
"Actions": "Actions" | |||||
} |
@@ -1,10 +1,22 @@ | |||||
{ | { | ||||
"Grade {{grade}}": "Grade {{grade}}", | "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" | ||||
} | } |
@@ -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?", | ||||
@@ -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?", | ||||
@@ -0,0 +1,31 @@ | |||||
{ | |||||
"Staff Reimbursement": "員工報銷", | |||||
"Create Claim": "建立報銷", | |||||
"Creation Date": "建立日期", | |||||
"Creation Date From": "建立日期 (從)", | |||||
"Creation Date To": "建立日期 (至)", | |||||
"Related Project": "相關項目名稱", | |||||
"Related Project Name": "相關項目名稱", | |||||
"Expense Type": "費用類別", | |||||
"Status": "狀態", | |||||
"Amount (HKD)": "金額 (HKD)", | |||||
"Remarks": "備註", | |||||
"Invoice Date": "收據日期", | |||||
"Supporting Document": "支援文件", | |||||
"Total": "總金額", | |||||
"Add Record": "新增記錄", | |||||
"Project Name": "項目名稱", | |||||
"Project": "項目", | |||||
"Claim Code": "報銷編號", | |||||
"Petty Cash": "小額開支", | |||||
"Expense": "普通開支", | |||||
"Not Submitted": "尚未提交", | |||||
"Waiting for Approval": "等待批核", | |||||
"Approved": "已批核", | |||||
"Rejected": "已拒絕", | |||||
"Description": "描述", | |||||
"Actions": "行動" | |||||
} |
@@ -1,8 +1,20 @@ | |||||
{ | { | ||||
"All": "全部", | |||||
"Petty Cash": "小額開支", | |||||
"Expense": "普通開支", | |||||
"Not Submitted": "尚未提交", | |||||
"Waiting for Approval": "等待批核", | |||||
"Approved": "已批核", | |||||
"Rejected": "已拒絕", | |||||
"Search": "搜尋", | "Search": "搜尋", | ||||
"Search Criteria": "搜尋條件", | "Search Criteria": "搜尋條件", | ||||
"Cancel": "取消", | "Cancel": "取消", | ||||
"Confirm": "確認", | "Confirm": "確認", | ||||
"Submit": "提交", | "Submit": "提交", | ||||
"Save": "儲存", | |||||
"Save And Submit": "儲存及提交", | |||||
"Reset": "重置" | "Reset": "重置" | ||||
} | } |
@@ -43,7 +43,7 @@ | |||||
"Contact Name": "聯絡姓名", | "Contact Name": "聯絡姓名", | ||||
"Contact Email": "聯絡電郵", | "Contact Email": "聯絡電郵", | ||||
"Contact Phone": "聯絡電話", | "Contact Phone": "聯絡電話", | ||||
"Please ensure all the fields are inputted and saved": "請確保所有欄位已輸入及儲存", | |||||
"Please ensure at least one row is created, and all the fields are inputted and saved": "請確保已建立至少一行, 及已輸入和儲存所有欄位", | |||||
"Please ensure all the email formats are correct": "請確保所有電郵格式輸入正確", | "Please ensure all the email formats are correct": "請確保所有電郵格式輸入正確", | ||||
"Do you want to submit?": "你是否確認要提交?", | "Do you want to submit?": "你是否確認要提交?", | ||||
@@ -43,7 +43,7 @@ | |||||
"Contact Name": "聯絡姓名", | "Contact Name": "聯絡姓名", | ||||
"Contact Email": "聯絡電郵", | "Contact Email": "聯絡電郵", | ||||
"Contact Phone": "聯絡電話", | "Contact Phone": "聯絡電話", | ||||
"Please ensure all the fields are inputted and saved": "請確保所有欄位已輸入及儲存", | |||||
"Please ensure 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?": "你是否確認要提交?", | ||||