@@ -12,6 +12,9 @@ export interface ClaimInputFormByStaff { | |||
status: string; | |||
addClaimDetails: ClaimDetailTable[] | |||
// is grid editing | |||
isGridEditing: boolean | null; | |||
} | |||
export interface ClaimDetailTable { | |||
@@ -5,9 +5,10 @@ import "server-only"; | |||
export interface Claim { | |||
id: number; | |||
created: string; | |||
name: string; | |||
cost: number; | |||
code: string; | |||
created: number[]; | |||
project: ProjectCombo; | |||
amount: number; | |||
type: "Expense" | "Petty Cash"; | |||
status: "Not Submitted" | "Waiting for Approval" | "Approved" | "Rejected"; | |||
remarks: string; | |||
@@ -41,8 +42,8 @@ export const preloadClaims = () => { | |||
}; | |||
export const fetchClaims = cache(async () => { | |||
return mockClaims; | |||
// return serverFetchJson<Claim[]>(`${BASE_API_URL}/claim`); | |||
// return mockClaims; | |||
return serverFetchJson<Claim[]>(`${BASE_API_URL}/claim`); | |||
}); | |||
export const fetchProjectCombo = cache(async () => { | |||
@@ -54,7 +55,7 @@ export const fetchProjectCombo = cache(async () => { | |||
// export const fetchAllCustomers = cache(async () => { | |||
// return serverFetchJson<Customer[]>(`${BASE_API_URL}/customer`); | |||
// }); | |||
/* | |||
const mockClaims: Claim[] = [ | |||
{ | |||
id: 1, | |||
@@ -83,4 +84,4 @@ const mockClaims: Claim[] = [ | |||
status: "Rejected", | |||
remarks: "Duplicate Claim Form", | |||
}, | |||
]; | |||
];*/ |
@@ -7,7 +7,7 @@ export interface InvoiceResult { | |||
id: number; | |||
projectCode: string; | |||
projectName: string; | |||
stage: String; | |||
stage: string; | |||
comingPaymentMileStone: string; | |||
paymentMilestoneDate: string; | |||
resourceUsage: number; | |||
@@ -67,6 +67,12 @@ export const fetchStaff = cache(async () => { | |||
}); | |||
}); | |||
export const fetchStaffWithoutTeam = cache(async () => { | |||
return serverFetchJson<StaffResult[]>(`${BASE_API_URL}/staffs/noteam`, { | |||
next: { tags: ["staffs"] }, | |||
}); | |||
}); | |||
// export const fetchStaffCombo = cache(async () => { | |||
// return serverFetchJson<Staff4TransferList>(`${BASE_API_URL}/staffs/combo`, { | |||
// next: { tags: ["staffs"] }, | |||
@@ -51,3 +51,12 @@ export const saveTeam = async (data: CreateTeamInputs) => { | |||
headers: { "Content-Type": "application/json" }, | |||
}); | |||
}; | |||
export const deleteTeam = async (data: TeamResult) => { | |||
return serverFetchJson(`${BASE_API_URL}/team/delete/${data.id}`, { | |||
method: "DELETE", | |||
// body: JSON.stringify(data), | |||
headers: { "Content-Type": "application/json" }, | |||
}); | |||
}; |
@@ -15,6 +15,7 @@ export const serverFetch: typeof fetch = async (input, init) => { | |||
...(accessToken | |||
? { | |||
Authorization: `Bearer ${accessToken}`, | |||
Accept: "application/json" | |||
} | |||
: {}), | |||
}, | |||
@@ -23,6 +23,15 @@ export const convertDateToString = (date: Date, format: string = OUTPUT_DATE_FOR | |||
return dayjs(date).format(format) | |||
} | |||
export const convertDateArrayToString = (dateArray: number[], format: string = OUTPUT_DATE_FORMAT, needTime: boolean = false) => { | |||
if (dateArray.length === 6) { | |||
if (!needTime) { | |||
const dateString = `${dateArray[0]}-${dateArray[1]}-${dateArray[2]}` | |||
return dayjs(dateString).format(format) | |||
} | |||
} | |||
} | |||
const shortDateFormatter_en = new Intl.DateTimeFormat("en-HK", { | |||
weekday: "short", | |||
year: "numeric", | |||
@@ -15,6 +15,7 @@ import { ClaimInputFormByStaff, saveClaim } from "@/app/api/claims/actions"; | |||
import { DoneAll } from "@mui/icons-material"; | |||
import { expenseTypeCombo } from "@/app/utils/comboUtil"; | |||
import { convertDateToString } from "@/app/utils/formatUtil"; | |||
import { errorDialog, submitDialog, successDialog, warningDialog } from "../Swal/CustomAlerts"; | |||
export interface Props { | |||
projectCombo: ProjectCombo[] | |||
@@ -40,17 +41,49 @@ const ClaimDetail: React.FC<Props> = ({ projectCombo }) => { | |||
const onSubmit = useCallback<SubmitHandler<ClaimInputFormByStaff>>( | |||
async (data, event) => { | |||
try { | |||
console.log(data.addClaimDetails[0].newSupportingDocument); | |||
console.log((event?.nativeEvent as any).submitter.name); | |||
if (data.isGridEditing) { | |||
warningDialog(t("Please save all the rows before submitting"), t) | |||
return false | |||
} | |||
let haveError = false | |||
if (data.addClaimDetails.length === 0 || data.addClaimDetails.filter(row => String(row.description).trim().length === 0 || String(row.amount).trim().length === 0 || row.project === null || row.project === undefined || ((row.oldSupportingDocument === null || row.oldSupportingDocument === undefined) && (row.newSupportingDocument === null || row.newSupportingDocument === undefined))).length > 0) { | |||
haveError = true | |||
formProps.setError("addClaimDetails", { message: "Claim details include empty fields", type: "required" }) | |||
} | |||
if (data.addClaimDetails.length > 0 && data.addClaimDetails.filter(row => row.invoiceDate.getTime() > new Date().getTime()).length > 0) { | |||
haveError = true | |||
formProps.setError("addClaimDetails", { message: "Claim details include invalid invoice date", type: "invalid_date" }) | |||
} | |||
if (data.addClaimDetails.length > 0 && data.addClaimDetails.filter(row => row.project === null || row.project === undefined).length > 0) { | |||
haveError = true | |||
formProps.setError("addClaimDetails", { message: "Claim details include empty project", type: "invalid_project" }) | |||
} | |||
if (data.addClaimDetails.length > 0 && data.addClaimDetails.filter(row => row.amount <= 0).length > 0) { | |||
haveError = true | |||
formProps.setError("addClaimDetails", { message: "Claim details include invalid amount", type: "invalid_amount" }) | |||
} | |||
if (haveError) { | |||
return false | |||
} | |||
const buttonName = (event?.nativeEvent as any).submitter.name | |||
console.log(JSON.stringify(data)) | |||
const formData = new FormData() | |||
// formData.append("expenseType", data.expenseType) | |||
formData.append("expenseType", data.expenseType) | |||
data.addClaimDetails.forEach((claimDetail) => { | |||
formData.append("addClaimDetailIds", JSON.stringify(claimDetail.id)) | |||
formData.append("addClaimDetailInvoiceDates", convertDateToString(claimDetail.invoiceDate, "YYYY-MM-DD")) | |||
formData.append("addClaimDetailProjectIds", JSON.stringify(claimDetail.project.id)) | |||
formData.append("addClaimDetailDescriptions", claimDetail.description) | |||
formData.append("addClaimDetailAmounts", JSON.stringify(claimDetail.amount)) | |||
formData.append("addClaimDetailNewSupportingDocuments", claimDetail.newSupportingDocument) | |||
formData.append("addClaimDetailOldSupportingDocumentIds", JSON.stringify(claimDetail?.oldSupportingDocument?.id ?? -1)) | |||
}) | |||
// for (let i = 0; i < data.addClaimDetails.length; i++) { | |||
// // formData.append("addClaimDetails[]", JSON.stringify(data.addClaimDetails[i])) | |||
// // formData.append("addClaimDetailsFiles[]", data.addClaimDetails[i].newSupportingDocument) | |||
// console.log(data.addClaimDetails[i].invoiceDate) | |||
// // data.addClaimDetails[i].invoiceDate = convertDateToString(data.addClaimDetails[i].invoiceDate) | |||
// const updatedData = { | |||
// id: data.addClaimDetails[i].id, | |||
// // project: data.addClaimDetails[i].project, | |||
@@ -63,22 +96,24 @@ const ClaimDetail: React.FC<Props> = ({ projectCombo }) => { | |||
// formData.append("addClaimDetailsFiles", data.addClaimDetails[i].newSupportingDocument) | |||
// formData.append("testFiles", data.addClaimDetails[i].newSupportingDocument) | |||
// } | |||
// if (buttonName === "submit") { | |||
// formData.append("status", "Not Submitted") | |||
// } else if (buttonName === "save") { | |||
// formData.append("status", "Waiting for Approval") | |||
// } | |||
// formData.append("id", "-1") | |||
if (buttonName === "submit") { | |||
formData.append("status", "Waiting for Approval") | |||
} else if (buttonName === "save") { | |||
formData.append("status", "Not Submitted") | |||
} | |||
formData.append("id", "-1") | |||
// 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}) | |||
// } | |||
await saveClaim(formData) | |||
setServerError(""); | |||
// await saveProject(data); | |||
// router.replace("/projects"); | |||
submitDialog(async () => { | |||
const response = await saveClaim(formData); | |||
if (response.message === "Success") { | |||
successDialog(t("Submit Success"), t).then(() => { | |||
router.replace("/staffReimbursement"); | |||
}) | |||
} | |||
}, t) | |||
} catch (e) { | |||
setServerError(t("An error has occurred. Please try again later.")); | |||
} | |||
@@ -107,10 +142,10 @@ const ClaimDetail: React.FC<Props> = ({ projectCombo }) => { | |||
<Button variant="text" startIcon={<Close />} onClick={handleCancel}> | |||
{t("Cancel")} | |||
</Button> | |||
<Button variant="outlined" name="save" startIcon={<Check />} type="submit"> | |||
<Button variant="outlined" name="save" startIcon={<Check />} type="submit" disabled={Boolean(formProps.watch("isGridEditing"))}> | |||
{t("Save")} | |||
</Button> | |||
<Button variant="contained" name="submit" startIcon={<DoneAll />} type="submit"> | |||
<Button variant="contained" name="submit" startIcon={<DoneAll />} type="submit" disabled={Boolean(formProps.watch("isGridEditing"))}> | |||
{t("Submit")} | |||
</Button> | |||
</Stack> | |||
@@ -41,7 +41,7 @@ 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"; | |||
import { convertDateToString, moneyFormatter } from "@/app/utils/formatUtil"; | |||
interface BottomBarProps { | |||
getCostTotal: () => number; | |||
@@ -157,7 +157,7 @@ const ClaimFormInputGrid: React.FC<ClaimFormInputGridProps> = ({ | |||
projectCombo, | |||
}) => { | |||
const { t } = useTranslation() | |||
const { control, setValue, getValues, formState: { errors } } = useFormContext<ClaimInputFormByStaff>(); | |||
const { control, setValue, getValues, formState: { errors }, clearErrors, setError } = useFormContext<ClaimInputFormByStaff>(); | |||
const { fields } = useFieldArray({ | |||
control, | |||
name: "addClaimDetails" | |||
@@ -397,11 +397,12 @@ const ClaimFormInputGrid: React.FC<ClaimFormInputGridProps> = ({ | |||
}, | |||
{ | |||
field: "amount", | |||
headerName: t("Amount (HKD)"), | |||
headerName: t("Amount"), | |||
editable: true, | |||
type: "number", | |||
align: "right", | |||
valueFormatter: (params) => { | |||
return `$ ${params.value ?? 0}`; | |||
return moneyFormatter.format(params.value ?? 0); | |||
}, | |||
}, | |||
{ | |||
@@ -423,7 +424,7 @@ const ClaimFormInputGrid: React.FC<ClaimFormInputGridProps> = ({ | |||
); | |||
}, | |||
renderEditCell: (params) => { | |||
const currentRow = rows.find(row => row.id === params.row.id); | |||
// const currentRow = rows.find(row => row.id === params.row.id); | |||
return params.formattedValue ? ( | |||
<span> | |||
<Link onClick={() => handleLinkClick(params)} href="#">{params.formattedValue}</Link> | |||
@@ -459,6 +460,56 @@ const ClaimFormInputGrid: React.FC<ClaimFormInputGridProps> = ({ | |||
}, | |||
], [rows, rowModesModel, t],); | |||
// check error | |||
useEffect(() => { | |||
if (getValues("addClaimDetails") === undefined || getValues("addClaimDetails") === null) { | |||
return; | |||
} | |||
if (getValues("addClaimDetails").length === 0) { | |||
clearErrors("addClaimDetails") | |||
} else { | |||
console.log(rows) | |||
if (rows.filter(row => String(row.description).trim().length === 0 || String(row.amount).trim().length === 0 || row.project === null || row.project === undefined || ((row.oldSupportingDocument === null || row.oldSupportingDocument === undefined) && (row.newSupportingDocument === null || row.newSupportingDocument === undefined))).length > 0) { | |||
setError("addClaimDetails", { message: "Claim details include empty fields", type: "required" }) | |||
} else { | |||
let haveError = false | |||
if (rows.filter(row => row.invoiceDate.getTime() > new Date().getTime()).length > 0) { | |||
haveError = true | |||
setError("addClaimDetails", { message: "Claim details include invalid invoice date", type: "invalid_date" }) | |||
} | |||
if (rows.filter(row => row.project === null || row.project === undefined).length > 0) { | |||
haveError = true | |||
setError("addClaimDetails", { message: "Claim details include empty project", type: "invalid_project" }) | |||
} | |||
if (rows.filter(row => row.amount <= 0).length > 0) { | |||
haveError = true | |||
setError("addClaimDetails", { message: "Claim details include invalid amount", type: "invalid_amount" }) | |||
} | |||
if (!haveError) { | |||
clearErrors("addClaimDetails") | |||
} | |||
} | |||
} | |||
}, [rows, rowModesModel]) | |||
// check editing | |||
useEffect(() => { | |||
const filteredByKey = Object.fromEntries( | |||
Object.entries(rowModesModel).filter(([key, value]) => rowModesModel[key].mode === 'edit')) | |||
if (Object.keys(filteredByKey).length > 0) { | |||
setValue("isGridEditing", true) | |||
} else { | |||
setValue("isGridEditing", false) | |||
} | |||
}, [rowModesModel]) | |||
return ( | |||
<Box | |||
sx={{ | |||
@@ -482,11 +533,17 @@ const ClaimFormInputGrid: React.FC<ClaimFormInputGridProps> = ({ | |||
}, | |||
}} | |||
> | |||
{Boolean(errors.addClaimDetails?.type === "required") && <Typography sx={(theme) => ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> | |||
{Boolean(errors.addClaimDetails?.type === "required") && <Typography sx={(theme) => ({ color: theme.palette.error.main, ml: 3, mt: 1 })} 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")} | |||
{Boolean(errors.addClaimDetails?.type === "invalid_date") && <Typography sx={(theme) => ({ color: theme.palette.error.main, ml: 3, mt: 1 })} variant="overline" display='inline-block' noWrap> | |||
{t("Please ensure the date are correct")} | |||
</Typography>} | |||
{Boolean(errors.addClaimDetails?.type === "invalid_project") && <Typography sx={(theme) => ({ color: theme.palette.error.main, ml: 3, mt: 1 })} variant="overline" display='inline-block' noWrap> | |||
{t("Please ensure the projects are selected")} | |||
</Typography>} | |||
{Boolean(errors.addClaimDetails?.type === "invalid_amount") && <Typography sx={(theme) => ({ color: theme.palette.error.main, ml: 3, mt: 1 })} variant="overline" display='inline-block' noWrap> | |||
{t("Please ensure the amount are correct")} | |||
</Typography>} | |||
<div style={{ height: 400, width: "100%" }}> | |||
<DataGrid | |||
@@ -8,6 +8,7 @@ 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"; | |||
import { convertDateArrayToString, convertDateToString } from "@/app/utils/formatUtil"; | |||
interface Props { | |||
claims: Claim[]; | |||
@@ -19,13 +20,14 @@ type SearchParamNames = keyof SearchQuery; | |||
const ClaimSearch: React.FC<Props> = ({ claims }) => { | |||
const { t } = useTranslation(); | |||
console.log(claims) | |||
// 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 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("Expense Type"), | |||
paramName: "type", | |||
@@ -54,9 +56,10 @@ const ClaimSearch: React.FC<Props> = ({ claims }) => { | |||
// onClick: onClaimClick, | |||
// buttonIcon: <EditNote />, | |||
// }, | |||
{ name: "created", label: t("Creation Date") }, | |||
{ name: "name", label: t("Related Project Name") }, | |||
{ name: "cost", label: t("Amount (HKD)") }, | |||
{ name: "created", label: t("Creation Date"), type: "date" }, | |||
{ name: "code", label: t("Claim Code") }, | |||
// { name: "project", label: t("Related Project Name") }, | |||
{ name: "amount", label: t("Amount"), type: "money" }, | |||
{ name: "type", label: t("Expense Type"), needTranslation: true }, | |||
{ name: "status", label: t("Status"), needTranslation: true }, | |||
{ name: "remarks", label: t("Remarks") }, | |||
@@ -71,13 +74,13 @@ const ClaimSearch: React.FC<Props> = ({ claims }) => { | |||
onSearch={(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") | |||
(claim) => | |||
dateInRange(convertDateArrayToString(claim.created, "YYYY-MM-DD")!!, query.created, query.createdTo ?? undefined) && | |||
// claim.project.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<Claim> items={filteredClaims} columns={columns} /> | |||
@@ -6,7 +6,7 @@ import Stack from "@mui/material/Stack"; | |||
import Print from '@mui/icons-material/Print'; | |||
// import { CreateInvoiceInputs, saveInvoice } from "@/app/api/companys/actions"; | |||
import { useRouter } from "next/navigation"; | |||
import React, { useCallback, useState, useLayoutEffect } from "react"; | |||
import React, { useCallback, useState, useLayoutEffect, useEffect } from "react"; | |||
import { useTranslation } from "react-i18next"; | |||
import { | |||
FieldErrors, | |||
@@ -23,6 +23,8 @@ import ProjectDetails from "./ProjectDetails"; | |||
import ProjectTotalFee from "./ProjectTotalFee"; | |||
import { timestampToDateString } from "@/app/utils/formatUtil"; | |||
import dayjs from "dayjs"; | |||
import { getSession } from "next-auth/react" | |||
import { BASE_API_URL } from "@/config/api"; | |||
const CreateInvoice: React.FC = ({ | |||
}) => { | |||
@@ -34,7 +36,7 @@ const CreateInvoice: React.FC = ({ | |||
const [invoiceDetail, setInvoiceDetail] = useState<InvoiceInformation>() | |||
const [serverError, setServerError] = useState(""); | |||
// const { getValues } = useForm(); | |||
const [accessToken, setAccessToken] = useState(''); | |||
const fetchProjectDetails = async () =>{ | |||
const projectId = searchParams.get("id") | |||
@@ -78,13 +80,85 @@ const CreateInvoice: React.FC = ({ | |||
fetchInvoiceDetails() | |||
}, []) | |||
// useEffect(() => { | |||
// const fetchData = async () => { | |||
// try { | |||
// const session = await getSession(); | |||
// if (session?.accessToken) { | |||
// const accessToken = session.accessToken; | |||
// // Use the access token as needed | |||
// setAccessToken(accessToken) | |||
// console.log(accessToken); | |||
// } else { | |||
// throw new Error('Access token not found in the session.'); | |||
// } | |||
// } catch (error) { | |||
// console.error(error); | |||
// } | |||
// }; | |||
// fetchData(); | |||
// }, []); | |||
const handleCancel = () => { | |||
router.back(); | |||
}; | |||
const handlePrintout = () => { | |||
// const formData = getValues(); | |||
console.log("Printing in Progress") | |||
const handlePrintout = async () => { | |||
const formData = formProps.getValues() | |||
const projectId = searchParams.get("id") | |||
// console.log(formData, projectId) | |||
const tempData = { | |||
...formData, | |||
id: projectId | |||
} | |||
try { | |||
// Make an API request to generate the JasperReport | |||
const response = await fetch(`https://tsms-uat.2fi-solutions.com/back-api/invoices/pdf`, { | |||
method: 'POST', | |||
headers: { | |||
'Content-Type': 'application/json', | |||
Authorization: `Bearer ${accessToken}`, | |||
Accept: "application/json" | |||
}, | |||
body: JSON.stringify(tempData), | |||
}); | |||
// Check if the request was successful | |||
if (response.ok) { | |||
// Extract the filename from the response headers | |||
const contentDisposition = response.headers.get("Content-Disposition"); | |||
// response.headers.forEach(e => console.log(e)) | |||
const fileName = contentDisposition | |||
? contentDisposition.split('filename=')[1] | |||
: 'invoice.pdf'; | |||
// Convert the response data to a Blob object | |||
const blob = await response.blob(); | |||
// Create a temporary URL for the Blob object | |||
const url = URL.createObjectURL(blob); | |||
// Create a link element to trigger the file download | |||
const link = document.createElement('a'); | |||
link.href = url; | |||
link.download = fileName.replace(/"/g, ''); | |||
link.click(); | |||
// Clean up the temporary URL | |||
URL.revokeObjectURL(url); | |||
} else { | |||
throw new Error('Failed to generate the JasperReport.'); | |||
} | |||
} catch (error) { | |||
console.error(error); | |||
// Handle any errors that occurred during the process | |||
} | |||
} | |||
const onSubmit = useCallback<SubmitHandler<InvoiceResult>>( | |||
@@ -37,7 +37,7 @@ const ProjectTotalFee: React.FC= ({}) => { | |||
<Divider sx={{ paddingBlockStart: 2 }} /> | |||
<Stack direction="row" justifyContent="space-between"> | |||
<Typography variant="h6">{t("Project Total Fee")}</Typography> | |||
<Typography>{moneyFormatter.format(projectTotal += amount)}</Typography> | |||
<Typography>{moneyFormatter.format(amount ? projectTotal += amount : projectTotal)}</Typography> | |||
</Stack> | |||
</Stack> | |||
); | |||
@@ -475,17 +475,17 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||
> | |||
<InputLabel>{t("Client Subsidiary")}</InputLabel> | |||
<Controller | |||
rules={{ | |||
validate: (value) => { | |||
if ( | |||
!customerSubsidiaryIds.find( | |||
(subsidiaryId) => subsidiaryId === value, | |||
) | |||
) { | |||
return t("Please choose a valid subsidiary"); | |||
} else return true; | |||
}, | |||
}} | |||
// rules={{ | |||
// validate: (value) => { | |||
// if ( | |||
// !customerSubsidiaryIds.find( | |||
// (subsidiaryId) => subsidiaryId === value, | |||
// ) | |||
// ) { | |||
// return t("Please choose a valid subsidiary"); | |||
// } else return true; | |||
// }, | |||
// }} | |||
defaultValue={customerSubsidiaryIds[0]} | |||
control={control} | |||
name="clientSubsidiaryId" | |||
@@ -20,12 +20,6 @@ import { fetchPositionCombo } from "@/app/api/positions/actions"; | |||
import { fetchGradeCombo } from "@/app/api/grades/actions"; | |||
import { fetchSkillCombo } from "@/app/api/skill/actions"; | |||
import { fetchSalaryCombo } from "@/app/api/salarys/actions"; | |||
// import { fetchTeamCombo } from "@/app/api/team/actions"; | |||
// import { fetchDepartmentCombo } from "@/app/api/departments/actions"; | |||
// import { fetchPositionCombo } from "@/app/api/positions/actions"; | |||
// import { fetchGradeCombo } from "@/app/api/grades/actions"; | |||
// import { fetchSkillCombo } from "@/app/api/skill/actions"; | |||
// import { fetchSalaryCombo } from "@/app/api/salarys/actions"; | |||
interface Field { | |||
// subtitle: string; | |||
@@ -157,63 +151,61 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => { | |||
id: "staffId", | |||
label: t("Staff ID"), | |||
type: "text", | |||
value: "", | |||
required: true, | |||
}, | |||
{ | |||
id: "name", | |||
label: t("Staff Name"), | |||
type: "text", | |||
value: "", | |||
required: true, | |||
}, | |||
{ | |||
id: "companyId", | |||
label: t("Company"), | |||
type: "combo-Obj", | |||
options: companyCombo, | |||
options: companyCombo || [], | |||
required: true, | |||
}, | |||
{ | |||
id: "teamId", | |||
label: t("Team"), | |||
type: "combo-Obj", | |||
options: teamCombo, | |||
options: teamCombo || [], | |||
required: false, | |||
}, | |||
{ | |||
id: "departmentId", | |||
label: t("Department"), | |||
type: "combo-Obj", | |||
options: departmentCombo, | |||
options: departmentCombo || [], | |||
required: true, | |||
}, | |||
{ | |||
id: "gradeId", | |||
label: t("Grade"), | |||
type: "combo-Obj", | |||
options: gradeCombo, | |||
options: gradeCombo || [], | |||
required: false, | |||
}, | |||
{ | |||
id: "skillSetId", | |||
label: t("Skillset"), | |||
type: "combo-Obj", | |||
options: skillCombo, | |||
options: skillCombo || [], | |||
required: false, | |||
}, | |||
{ | |||
id: "currentPositionId", | |||
label: t("Current Position"), | |||
type: "combo-Obj", | |||
options: positionCombo, | |||
options: positionCombo || [], | |||
required: true, | |||
}, | |||
{ | |||
id: "salaryId", | |||
label: t("Salary Point"), | |||
type: "combo-Obj", | |||
options: salaryCombo, | |||
options: salaryCombo || [], | |||
required: true, | |||
}, | |||
// { | |||
@@ -223,6 +215,12 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => { | |||
// value: "", | |||
// required: false, | |||
// }, | |||
// { | |||
// id: "hourlyRate", | |||
// label: t("Hourly Rate"), | |||
// type: "numeric-testing", | |||
// required: true, | |||
// }, | |||
{ | |||
id: "employType", | |||
label: t("Employ Type"), | |||
@@ -235,7 +233,6 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => { | |||
id: "email", | |||
label: t("Email"), | |||
type: "text", | |||
value: "", | |||
pattern: "^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$", | |||
message: t("input matching format"), | |||
required: true, | |||
@@ -244,8 +241,7 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => { | |||
id: "phone1", | |||
label: t("Phone1"), | |||
type: "text", | |||
value: "", | |||
// pattern: "^\\d{8}$", | |||
pattern: "^\\d{8}$", | |||
message: t("input correct phone no."), | |||
required: true, | |||
}, | |||
@@ -253,8 +249,7 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => { | |||
id: "phone2", | |||
label: t("Phone2"), | |||
type: "text", | |||
value: "", | |||
// pattern: "^\\d{8}$", | |||
pattern: "^\\d{8}$", | |||
message: t("input correct phone no."), | |||
required: false, | |||
}, | |||
@@ -264,15 +259,13 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => { | |||
id: "emergContactName", | |||
label: t("Emergency Contact Name"), | |||
type: "text", | |||
value: "", | |||
required: true, | |||
}, | |||
{ | |||
id: "emergContactPhone", | |||
label: t("Emergency Contact Phone"), | |||
type: "text", | |||
value: "", | |||
// pattern: "^\\d{8}$", | |||
pattern: "^\\d{8}$", | |||
message: t("input correct phone no."), | |||
required: true, | |||
}, | |||
@@ -280,33 +273,29 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => { | |||
id: "joinDate", | |||
label: t("Join Date"), | |||
type: "multiDate", | |||
value: "", | |||
required: true, | |||
}, | |||
{ | |||
id: "joinPositionId", | |||
label: t("Join Position"), | |||
type: "combo-Obj", | |||
options: positionCombo, | |||
options: positionCombo || [], | |||
required: true, | |||
}, | |||
{ | |||
id: "departDate", | |||
label: t("Depart Date"), | |||
type: "multiDate", | |||
value: "", | |||
}, | |||
{ | |||
id: "departReason", | |||
label: t("Depart Reason"), | |||
type: "text", | |||
value: "", | |||
}, | |||
{ | |||
id: "remark", | |||
label: t("Remark"), | |||
type: "remarks", | |||
value: "", | |||
}, | |||
] | |||
]; | |||
@@ -47,7 +47,7 @@ const CreateStaffForm: React.FC<formProps> = ({ Title, fieldLists }) => { | |||
return haveError; | |||
} | |||
//check if joinDate > departDate | |||
if (data.departDate != null && data.departDate != "Invalid Date") { | |||
if (data.departDate != null && data.departDate != "Invalid Date" && data.departDate.length != 0) { | |||
if (data.joinDate != null) { | |||
const joinDate = new Date(data.joinDate); | |||
const departDate = new Date(data.departDate); | |||
@@ -56,6 +56,10 @@ const CreateStaffForm: React.FC<formProps> = ({ Title, fieldLists }) => { | |||
return haveError; | |||
} | |||
} | |||
if (data.departReason == null || data.departReason.length == 0) { | |||
haveError = true; | |||
return haveError; | |||
} | |||
} | |||
if (haveError) { | |||
@@ -68,6 +72,13 @@ const CreateStaffForm: React.FC<formProps> = ({ Title, fieldLists }) => { | |||
phone2: data.phone2.toString(), | |||
hourlyRate: typeof data.hourlyRate === 'string' ? parseInt(data.hourlyRate.replace("$", "").replace(",", "")) : 0 | |||
}; | |||
if (postData.departDate?.length === 0 && postData.departReason?.length === 0) { | |||
delete postData.departDate; | |||
delete postData.departReason; | |||
} | |||
if (postData.remark?.length === 0) { | |||
delete postData.remark; | |||
} | |||
console.log(postData); | |||
setServerError(""); | |||
await saveStaff(postData); | |||
@@ -3,7 +3,7 @@ import CreateTeam from "./CreateTeam"; | |||
import CreateTeamLoading from "./CreateTeamLoading"; | |||
// import { fetchTeam, fetchTeamLeads } from "@/app/api/team"; | |||
import { useSearchParams } from "next/navigation"; | |||
import { fetchStaff } from "@/app/api/staff"; | |||
import { fetchStaff, fetchStaffWithoutTeam } from "@/app/api/staff"; | |||
interface SubComponents { | |||
Loading: typeof CreateTeamLoading; | |||
@@ -14,7 +14,7 @@ const CreateTeamWrapper: React.FC & SubComponents = async () => { | |||
const [ | |||
staff, | |||
] = await Promise.all([ | |||
fetchStaff(), | |||
fetchStaffWithoutTeam(), | |||
]); | |||
return <CreateTeam allstaff={staff}/>; | |||
@@ -53,7 +53,6 @@ const EditStaff: React.FC = async () => { | |||
const searchParams = useSearchParams(); | |||
const { t } = useTranslation(); | |||
const idString = searchParams.get("id"); | |||
const [id, setId] = useState(0); | |||
const [fieldLists, setFieldLists] = useState<Field[][]>(); | |||
const [companyCombo, setCompanyCombo] = useState<comboProp[]>(); | |||
const [teamCombo, setTeamCombo] = useState<comboProp[]>(); | |||
@@ -125,217 +124,217 @@ const EditStaff: React.FC = async () => { | |||
let id = 0; | |||
if (idString) { | |||
id = parseInt(idString); | |||
setId(id); | |||
} | |||
fetchStaffEdit(id).then((staff) => { | |||
console.log(staff.data); | |||
const data = staff.data; | |||
///////////////////// list 1 ///////////////////// | |||
const list1 = keyOrder1 | |||
.map((key) => { | |||
switch (key) { | |||
case "staffId": | |||
return { | |||
id: `${key}`, | |||
label: t(`Staff ID`), | |||
type: "text", | |||
value: data[key] ?? "", | |||
required: true, | |||
}; | |||
case "name": | |||
return { | |||
id: `${key}`, | |||
label: t(`Staff Name`), | |||
type: "text", | |||
value: data[key] ?? "", | |||
required: true, | |||
}; | |||
case "company": | |||
return { | |||
id: `${key}Id`, | |||
label: t(`Company`), | |||
type: "combo-Obj", | |||
options: companyCombo, | |||
value: data[key].id ?? "", | |||
required: true, | |||
}; | |||
case "team": | |||
return { | |||
id: `${key}Id`, | |||
label: t(`Team`), | |||
type: "combo-Obj", | |||
options: teamCombo, | |||
value: data[key]?.id ?? "", | |||
}; | |||
case "department": | |||
return { | |||
id: `${key}Id`, | |||
label: t(`Department`), | |||
type: "combo-Obj", | |||
options: departmentCombo, | |||
value: data[key]?.id ?? "", | |||
required: true, | |||
// later check | |||
}; | |||
case "grade": | |||
return { | |||
id: `${key}Id`, | |||
label: t(`Grade`), | |||
type: "combo-Obj", | |||
options: gradeCombo, | |||
value: data[key] !== null ? data[key].id ?? "" : "", | |||
}; | |||
case "skill": | |||
return { | |||
id: `${key}SetId`, | |||
label: t(`Skillset`), | |||
type: "combo-Obj", | |||
options: skillCombo, | |||
value: data[key] !== null ? data[key].id ?? "" : "", | |||
}; | |||
case "currentPosition": | |||
return { | |||
id: `${key}Id`, | |||
label: t(`Current Position`), | |||
type: "combo-Obj", | |||
options: positionCombo, | |||
value: data[key].id ?? "", | |||
required: true, | |||
}; | |||
case "salary": | |||
return { | |||
id: `salaryId`, | |||
label: t(`Salary Point`), | |||
type: "combo-Obj", | |||
options: salaryCombo, | |||
value: data[key] !== null ? data[key].id ?? "" : "", | |||
required: true, | |||
}; | |||
// case "hourlyRate": | |||
// return { | |||
// id: `${key}`, | |||
// label: t(`hourlyRate`), | |||
// type: "text", | |||
// value: "", | |||
// // value: data[key], | |||
// readOnly: true, | |||
// }; | |||
case "employType": | |||
return { | |||
id: `${key}`, | |||
label: t(`Employ Type`), | |||
type: "combo-Obj", | |||
options: employTypeCombo, | |||
value: data[key] ?? "", | |||
required: true, | |||
}; | |||
case "email": | |||
return { | |||
id: `${key}`, | |||
label: t(`Email`), | |||
type: "text", | |||
value: data[key] ?? "", | |||
pattern: "^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$", | |||
message: t("input matching format"), | |||
required: true, | |||
}; | |||
case "phone1": | |||
return { | |||
id: `${key}`, | |||
label: t(`Phone1`), | |||
type: "text", | |||
// pattern: "^\\d{8}$", | |||
message: t("input correct phone no."), | |||
value: data[key] ?? "", | |||
required: true, | |||
}; | |||
case "phone2": | |||
return { | |||
id: `${key}`, | |||
label: t(`Phone2`), | |||
type: "text", | |||
// pattern: "^\\d{8}$", | |||
message: t("input correct phone no."), | |||
value: data[key] ?? "", | |||
} as Field; | |||
default: | |||
return null; | |||
} | |||
}).filter((item): item is Field => item !== null); | |||
///////////////////// list 2 ///////////////////// | |||
const list2 = keyOrder2 | |||
.map((key) => { | |||
switch (key) { | |||
case "emergContactName": | |||
return { | |||
id: `${key}`, | |||
label: t(`Emergency Contact Name`), | |||
type: "text", | |||
value: data[key] ?? "", | |||
required: true, | |||
} as Field; | |||
case "emergContactPhone": | |||
console.log(id) | |||
fetchStaffEdit(id).then((staff) => { | |||
console.log(staff.data); | |||
const data = staff.data; | |||
///////////////////// list 1 ///////////////////// | |||
const list1 = keyOrder1 | |||
.map((key) => { | |||
switch (key) { | |||
case "staffId": | |||
return { | |||
id: `${key}`, | |||
label: t(`Emergency Contact Phone`), | |||
label: t(`Staff ID`), | |||
type: "text", | |||
value: data[key] ?? "", | |||
required: true, | |||
}; | |||
case "name": | |||
return { | |||
id: `${key}`, | |||
label: t(`Staff Name`), | |||
type: "text", | |||
value: data[key] ?? "", | |||
required: true, | |||
}; | |||
case "company": | |||
return { | |||
id: `${key}Id`, | |||
label: t(`Company`), | |||
type: "combo-Obj", | |||
options: companyCombo, | |||
value: data[key].id ?? "", | |||
required: true, | |||
}; | |||
case "team": | |||
return { | |||
id: `${key}Id`, | |||
label: t(`Team`), | |||
type: "combo-Obj", | |||
options: teamCombo, | |||
value: data[key]?.id ?? "", | |||
}; | |||
case "department": | |||
return { | |||
id: `${key}Id`, | |||
label: t(`Department`), | |||
type: "combo-Obj", | |||
options: departmentCombo, | |||
value: data[key]?.id ?? "", | |||
required: true, | |||
// later check | |||
}; | |||
case "grade": | |||
return { | |||
id: `${key}Id`, | |||
label: t(`Grade`), | |||
type: "combo-Obj", | |||
options: gradeCombo, | |||
value: data[key] !== null ? data[key].id ?? "" : "", | |||
}; | |||
case "skill": | |||
return { | |||
id: `${key}SetId`, | |||
label: t(`Skillset`), | |||
type: "combo-Obj", | |||
options: skillCombo, | |||
value: data[key] !== null ? data[key].id ?? "" : "", | |||
}; | |||
case "currentPosition": | |||
return { | |||
id: `${key}Id`, | |||
label: t(`Current Position`), | |||
type: "combo-Obj", | |||
options: positionCombo, | |||
value: data[key].id ?? "", | |||
required: true, | |||
}; | |||
case "salary": | |||
return { | |||
id: `salaryId`, | |||
label: t(`Salary Point`), | |||
type: "combo-Obj", | |||
options: salaryCombo, | |||
value: data[key] !== null ? data[key].id ?? "" : "", | |||
required: true, | |||
}; | |||
// case "hourlyRate": | |||
// return { | |||
// id: `${key}`, | |||
// label: t(`hourlyRate`), | |||
// type: "text", | |||
// value: "", | |||
// // value: data[key], | |||
// readOnly: true, | |||
// }; | |||
case "employType": | |||
return { | |||
id: `${key}`, | |||
label: t(`Employ Type`), | |||
type: "combo-Obj", | |||
options: employTypeCombo, | |||
value: data[key] ?? "", | |||
required: true, | |||
}; | |||
case "email": | |||
return { | |||
id: `${key}`, | |||
label: t(`Email`), | |||
type: "text", | |||
value: data[key] ?? "", | |||
pattern: "^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$", | |||
message: t("input matching format"), | |||
required: true, | |||
}; | |||
case "phone1": | |||
return { | |||
id: `${key}`, | |||
label: t(`Phone1`), | |||
type: "text", | |||
// pattern: "^\\d{8}$", | |||
message: t("input correct phone no."), | |||
value: data[key] ?? "", | |||
required: true, | |||
}; | |||
case "phone2": | |||
return { | |||
id: `${key}`, | |||
label: t(`Phone2`), | |||
type: "text", | |||
// pattern: "^\\d{8}$", | |||
message: t("input correct phone no."), | |||
value: data[key] ?? "", | |||
} as Field; | |||
case "joinDate": | |||
return { | |||
id: `${key}`, | |||
label: t(`Join Date`), | |||
type: "multiDate", | |||
value: data[key] ?? "", | |||
required: true, | |||
} as Field; | |||
case "joinPosition": | |||
return { | |||
id: `${key}Id`, | |||
label: t(`Join Position`), | |||
type: "combo-Obj", | |||
options: positionCombo, | |||
value: data[key].id ?? "", | |||
required: true, | |||
} as Field; | |||
case "departDate": | |||
return { | |||
id: `${key}`, | |||
label: t(`Depart Date`), | |||
type: "multiDate", | |||
value: data[key] ?? "", | |||
} as Field; | |||
case "departReason": | |||
return { | |||
id: `${key}`, | |||
label: t(`Depart Reason`), | |||
type: "text", | |||
value: data[key] ?? "", | |||
} as Field; | |||
case "remark": | |||
return { | |||
id: `remark`, | |||
label: t(`Remark`), | |||
type: "remarks", | |||
value: data[key] ?? "", | |||
} as Field; | |||
default: | |||
return null; | |||
} | |||
}).filter((item): item is Field => item !== null); | |||
console.log(list2); | |||
console.log([list1]); | |||
setFieldLists([list1,list2]); | |||
}); | |||
}, [companyCombo]); | |||
default: | |||
return null; | |||
} | |||
}).filter((item): item is Field => item !== null); | |||
///////////////////// list 2 ///////////////////// | |||
const list2 = keyOrder2 | |||
.map((key) => { | |||
switch (key) { | |||
case "emergContactName": | |||
return { | |||
id: `${key}`, | |||
label: t(`Emergency Contact Name`), | |||
type: "text", | |||
value: data[key] ?? "", | |||
required: true, | |||
} as Field; | |||
case "emergContactPhone": | |||
return { | |||
id: `${key}`, | |||
label: t(`Emergency Contact Phone`), | |||
type: "text", | |||
// pattern: "^\\d{8}$", | |||
message: t("input correct phone no."), | |||
value: data[key] ?? "", | |||
required: true, | |||
} as Field; | |||
case "joinDate": | |||
return { | |||
id: `${key}`, | |||
label: t(`Join Date`), | |||
type: "multiDate", | |||
value: data[key] ?? "", | |||
required: true, | |||
} as Field; | |||
case "joinPosition": | |||
return { | |||
id: `${key}Id`, | |||
label: t(`Join Position`), | |||
type: "combo-Obj", | |||
options: positionCombo, | |||
value: data[key].id ?? "", | |||
required: true, | |||
} as Field; | |||
case "departDate": | |||
return { | |||
id: `${key}`, | |||
label: t(`Depart Date`), | |||
type: "multiDate", | |||
value: data[key] ?? "", | |||
} as Field; | |||
case "departReason": | |||
return { | |||
id: `${key}`, | |||
label: t(`Depart Reason`), | |||
type: "text", | |||
value: data[key] ?? "", | |||
} as Field; | |||
case "remark": | |||
return { | |||
id: `remark`, | |||
label: t(`Remark`), | |||
type: "remarks", | |||
value: data[key] ?? "", | |||
} as Field; | |||
default: | |||
return null; | |||
} | |||
}).filter((item): item is Field => item !== null); | |||
console.log(list2); | |||
console.log([list1]); | |||
setFieldLists([list1,list2]); | |||
}); | |||
} | |||
}, [companyCombo, teamCombo, departmentCombo, positionCombo, gradeCombo, skillCombo, salaryCombo, idString]); | |||
return ( | |||
<> | |||
{/* {console.log(fieldLists)} */} | |||
<EditStaffForm Title={title} id={id} fieldLists={fieldLists as Field[][] || [[]]} /> | |||
<EditStaffForm Title={title} fieldLists={fieldLists as Field[][] || [[]]} /> | |||
</> | |||
); | |||
}; | |||
@@ -26,14 +26,15 @@ interface Field { | |||
} | |||
interface formProps { | |||
id: number; | |||
Title?: string[]; | |||
fieldLists: Field[][]; | |||
} | |||
const EditStaffForm: React.FC<formProps> = ({ id, Title, fieldLists }) => { | |||
const EditStaffForm: React.FC<formProps> = ({ Title, fieldLists }) => { | |||
const router = useRouter(); | |||
const { t } = useTranslation(); | |||
const searchParams = useSearchParams(); | |||
const idString = searchParams.get("id") | |||
const [serverError, setServerError] = useState(""); | |||
// make new inputs | |||
const onSubmit = useCallback<SubmitHandler<CreateStaffInputs>>( | |||
@@ -49,20 +50,21 @@ const EditStaffForm: React.FC<formProps> = ({ id, Title, fieldLists }) => { | |||
const formattedDate = dayjs(data.departDate, 'MM/DD/YYYY').format('YYYY-MM-DD'); | |||
formatDepartDate = formattedDate; | |||
} | |||
// console.log(data); | |||
const temp = { | |||
id: id, | |||
...data, | |||
emergContactPhone: data.emergContactPhone.toString(), | |||
phone1: data.phone1.toString(), | |||
phone2: data.phone1.toString(), | |||
joinDate: formatJoinDate, | |||
departDate: formatDepartDate, | |||
if (idString) { | |||
const temp = { | |||
id: parseInt(idString), | |||
...data, | |||
emergContactPhone: data.emergContactPhone.toString(), | |||
phone1: data.phone1.toString(), | |||
phone2: data.phone1.toString(), | |||
joinDate: formatJoinDate, | |||
departDate: formatDepartDate, | |||
} | |||
console.log(temp) | |||
setServerError(""); | |||
await saveStaff(temp); | |||
router.replace("/settings/staff"); | |||
} | |||
console.log(temp) | |||
setServerError(""); | |||
await saveStaff(temp); | |||
router.replace("/settings/staff"); | |||
} catch (e) { | |||
setServerError(t("An error has occurred. Please try again later.")); | |||
} | |||
@@ -67,8 +67,8 @@ const Allocation: React.FC<Props> = ({ allStaffs: staff }) => { | |||
const removeStaff = useCallback((staff: StaffResult) => { | |||
setSelectedStaff((s) => s.filter((s) => s.id !== staff.id)); | |||
setDeletedStaffIds((s) => s) | |||
// setValue("deleteStaffIds", [...staff.id]) | |||
// setDeletedStaffIds((s) => s) | |||
setDeletedStaffIds((prevIds) => [...prevIds, staff.id]); | |||
}, []); | |||
const setTeamLead = useCallback( | |||
@@ -118,7 +118,7 @@ const Allocation: React.FC<Props> = ({ allStaffs: staff }) => { | |||
useEffect(() => { | |||
setValue("deleteStaffIds", deletedStaffIds) | |||
console.log(deletedStaffIds) | |||
}, [deletedStaffIds, setValue]); | |||
}, [deletedStaffIds]); | |||
const StaffPoolColumns = useMemo<Column<StaffResult>[]>( | |||
() => [ | |||
@@ -70,7 +70,7 @@ const EditTeam: React.FC<Props> = async ({ staff, desc }) => { | |||
const tempDesc = desc.filter( | |||
(item) => item.id === parseInt(idString) | |||
) | |||
// console.log(filteredTeam); | |||
if (filteredTeam.length > 0) { | |||
const filteredIds: number[] = filteredTeam.map((i) => ( | |||
i.id | |||
@@ -82,21 +82,7 @@ const EditTeam: React.FC<Props> = async ({ staff, desc }) => { | |||
formProps.reset({description: tempDesc[0].description, addStaffIds: idList}) | |||
setFilteredDesc(tempDesc[0].description) | |||
} | |||
// console.log(staff); | |||
// const desc = staff[0]?.description | |||
// setDesc(desc) | |||
// const staff = staff.map((item) => { | |||
// return { | |||
// id: item.id, | |||
// name: item.name, | |||
// staffId: item.staffId, | |||
// teamId: item.teamId, | |||
// staffName: item.staffName, | |||
// currentPosition: item.currentPosition | |||
// } as StaffResult | |||
// }) | |||
console.log(staff) | |||
setAllStaffs(staff) | |||
}, [searchParams]); | |||
@@ -124,11 +110,12 @@ const EditTeam: React.FC<Props> = async ({ staff, desc }) => { | |||
const tempData = { | |||
description: data.description, | |||
addStaffIds: data.addStaffIds, | |||
deleteStaffIds: data.deleteStaffIds, | |||
id: parseInt(idString!!) | |||
} | |||
console.log(tempData) | |||
// await saveTeam(tempData); | |||
// router.replace("/settings/staff"); | |||
await saveTeam(tempData); | |||
router.replace("/settings/team"); | |||
} catch (e) { | |||
console.log(e); | |||
setServerError(t("An error has occurred. Please try again later.")); | |||
@@ -3,6 +3,8 @@ import React from "react"; | |||
import InvoiceSearch from "./InvoiceSearch"; | |||
import InvoiceSearchLoading from "./InvoiceSearchLoading"; | |||
import { fetchInvoices } from "@/app/api/invoices"; | |||
import { timestampToDateString } from "@/app/utils/formatUtil"; | |||
interface SubComponents { | |||
Loading: typeof InvoiceSearchLoading; | |||
@@ -11,7 +13,12 @@ interface SubComponents { | |||
const InvoiceSearchWrapper: React.FC & SubComponents = async () => { | |||
const Invoices = await fetchInvoices(); | |||
return <InvoiceSearch invoices={Invoices} />; | |||
const temp = Invoices.map((invoice) => ({ | |||
...invoice, | |||
paymentMilestoneDate: timestampToDateString(invoice.paymentMilestoneDate) | |||
})) | |||
return <InvoiceSearch invoices={temp} />; | |||
}; | |||
InvoiceSearchWrapper.Loading = InvoiceSearchLoading; | |||
@@ -22,6 +22,7 @@ import Company from '@mui/icons-material/Store'; | |||
import Department from '@mui/icons-material/Diversity3'; | |||
import Position from '@mui/icons-material/Paragliding'; | |||
import Salary from '@mui/icons-material/AttachMoney'; | |||
import Team from '@mui/icons-material/Paragliding'; | |||
import { useTranslation } from "react-i18next"; | |||
import Typography from "@mui/material/Typography"; | |||
import { usePathname } from "next/navigation"; | |||
@@ -119,6 +120,7 @@ const navigationItems: NavigationItem[] = [ | |||
{ icon: <Department />, label: "Department", path: "/settings/department" }, | |||
{ icon: <Position />, label: "Position", path: "/settings/position" }, | |||
{ icon: <Salary />, label: "Salary", path: "/settings/salary" }, | |||
{ icon: <Team />, label: "Team", path: "/settings/team" }, | |||
], | |||
}, | |||
]; | |||
@@ -14,6 +14,7 @@ import TableRow from "@mui/material/TableRow"; | |||
import IconButton, { IconButtonOwnProps, IconButtonPropsColorOverrides } from "@mui/material/IconButton"; | |||
import { t } from "i18next"; | |||
import { useTranslation } from "react-i18next"; | |||
import { convertDateArrayToString, moneyFormatter } from "@/app/utils/formatUtil"; | |||
export interface ResultWithId { | |||
id: string | number; | |||
@@ -23,7 +24,8 @@ interface BaseColumn<T extends ResultWithId> { | |||
name: keyof T; | |||
label: string; | |||
color?: IconButtonOwnProps["color"]; | |||
needTranslation?: boolean | |||
needTranslation?: boolean; | |||
type?: string; | |||
} | |||
interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> { | |||
@@ -78,7 +80,7 @@ function SearchResults<T extends ResultWithId>({ | |||
<TableRow> | |||
{columns.map((column, idx) => ( | |||
<TableCell key={`${column.name.toString()}${idx}`}> | |||
{column.label} | |||
{column?.type === "money" ? <div style={{display: "flex", justifyContent: "flex-end"}}>{column.label}</div> : column.label} | |||
</TableCell> | |||
))} | |||
</TableRow> | |||
@@ -101,9 +103,16 @@ function SearchResults<T extends ResultWithId>({ | |||
> | |||
{column.buttonIcon} | |||
</IconButton> | |||
) : ( | |||
<>{column?.needTranslation ? t(item[columnName] as string) : item[columnName]}</> | |||
)} | |||
) : | |||
column?.type === "date" ? ( | |||
<>{convertDateArrayToString(item[columnName] as number[])}</> | |||
) : | |||
column?.type === "money" ? ( | |||
<div style={{display: "flex", justifyContent: "flex-end"}}>{moneyFormatter.format(item[columnName] as number)}</div> | |||
) : | |||
( | |||
<>{column?.needTranslation ? t(item[columnName] as string) : item[columnName]}</> | |||
)} | |||
</TableCell> | |||
); | |||
})} | |||
@@ -0,0 +1,105 @@ | |||
"use client"; | |||
import React, { useCallback, useMemo, useState } from "react"; | |||
import Button from "@mui/material/Button"; | |||
import { Card, Modal, Stack, Typography } from "@mui/material"; | |||
import { useTranslation } from "react-i18next"; | |||
import { Add } from "@mui/icons-material"; | |||
import Check from "@mui/icons-material/Check"; | |||
import Close from "@mui/icons-material/Close"; | |||
import { TSMS_BUTTON_THEME } from "@/theme/colorConst"; | |||
import { ThemeProvider } from "@emotion/react"; | |||
interface Props { | |||
isOpen: boolean; | |||
onConfirm: (data: any) => void; | |||
onCancel: (data: any | null) => void; | |||
} | |||
const ConfirmModal: React.FC<Props> = ({ ...props }) => { | |||
const { t } = useTranslation(); | |||
return ( | |||
<> | |||
<Modal open={props.isOpen} onClose={props.onCancel}> | |||
<Card | |||
style={{ | |||
flex: 10, | |||
marginBottom: "20px", | |||
width: "auto", | |||
minWidth: "400px", | |||
minHeight: "200px", | |||
position: "fixed", | |||
top: "50%", | |||
left: "50%", | |||
transform: "translate(-50%, -50%)", | |||
}} | |||
> | |||
<> | |||
<Typography | |||
variant="h5" | |||
id="modal-title" | |||
sx={{ | |||
flex: 1, | |||
ml: 4, | |||
mt: 2, | |||
}} | |||
> | |||
{t("Confirm")} | |||
</Typography> | |||
<> | |||
<Typography | |||
variant="h6" | |||
id="modal-title" | |||
sx={{ | |||
flex: 1, | |||
mt: 4, | |||
justifyContent: "center", | |||
textAlign: "center", | |||
}} | |||
> | |||
{t("Are You Sure")} | |||
</Typography> | |||
</> | |||
{/* <ThemeProvider theme={TSMS_BUTTON_THEME}> */} | |||
<Stack direction="row"> | |||
<Button | |||
variant="contained" | |||
endIcon={<Check />} | |||
sx={{ | |||
flex: 1, | |||
ml: 5, | |||
mr: 2, | |||
mt: 4, | |||
justifyContent: "space-between", | |||
}} | |||
onClick={props.onConfirm} | |||
// LinkComponent={Link} | |||
// href="/settings/department/new" | |||
> | |||
Proceed | |||
</Button> | |||
<Button | |||
variant="contained" | |||
startIcon={<Close />} | |||
sx={{ | |||
flex: 1, | |||
mr: 5, | |||
mt: 4, | |||
justifyContent: "space-between", | |||
}} | |||
color="warning" | |||
onClick={props.onCancel} | |||
// LinkComponent={Link} | |||
// href="/settings/department/new" | |||
> | |||
Cancel | |||
</Button> | |||
</Stack> | |||
{/* </ThemeProvider> */} | |||
</> | |||
</Card> | |||
</Modal> | |||
</> | |||
); | |||
}; | |||
export default ConfirmModal; |
@@ -9,6 +9,9 @@ import EditNote from "@mui/icons-material/EditNote"; | |||
import DeleteIcon from '@mui/icons-material/Delete'; | |||
import { deleteStaff } from "@/app/api/staff/actions"; | |||
import { useRouter } from "next/navigation"; | |||
import ConfirmModal from "./ConfirmDeleteModal"; | |||
import { deleteTeam } from "@/app/api/team/actions"; | |||
interface Props { | |||
team: TeamResult[]; | |||
@@ -50,23 +53,47 @@ const TeamSearch: React.FC<Props> = ({ team }) => { | |||
router.push(`/settings/team/edit?id=${id}`); | |||
}, [router, t]); | |||
// const onDeleteClick = useCallback((team: TeamResult) => { | |||
// console.log(team); | |||
// deleteTeam | |||
// }, [router, t]); | |||
const onDeleteClick = (team: TeamResult) => { | |||
console.log(team); | |||
setData(team) | |||
setIsOpen(!isOpen) | |||
}; | |||
const onConfirm = useCallback(async (team: TeamResult) => { | |||
console.log(team); | |||
if (data) | |||
await deleteTeam(data) | |||
setIsOpen(false) | |||
window.location.reload; | |||
}, [deleteTeam, data]); | |||
const onCancel = useCallback(() => { | |||
setIsOpen(false) | |||
}, []); | |||
const columns = useMemo<Column<TeamResult>[]>( | |||
() => [ | |||
{ | |||
name: "action", | |||
label: t("Actions"), | |||
label: t("Edit"), | |||
onClick: onTeamClick, | |||
buttonIcon: <EditNote />, | |||
}, | |||
{ name: "name", label: t("Name") }, | |||
{ name: "code", label: t("Code") }, | |||
{ name: "description", label: t("description") }, | |||
// { | |||
// name: "action", | |||
// label: t("Actions"), | |||
// onClick: deleteClick, | |||
// buttonIcon: <DeleteIcon />, | |||
// }, | |||
{ | |||
name: "action", | |||
label: t("Delete"), | |||
onClick: onDeleteClick, | |||
buttonIcon: <DeleteIcon />, | |||
}, | |||
], | |||
[t], | |||
); | |||
@@ -89,6 +116,11 @@ const TeamSearch: React.FC<Props> = ({ team }) => { | |||
}} | |||
/> | |||
<SearchResults<TeamResult> items={filteredTeam} columns={columns} /> | |||
<ConfirmModal | |||
isOpen={isOpen} | |||
onConfirm={onConfirm} | |||
onCancel={onCancel} | |||
/> | |||
</> | |||
); | |||
@@ -8,7 +8,7 @@ | |||
"Related Project Name": "Related Project Name", | |||
"Expense Type": "Expense Type", | |||
"Status": "Status", | |||
"Amount (HKD)": "Amount (HKD)", | |||
"Amount": "Amount", | |||
"Remarks": "Remarks", | |||
"Invoice Date": "Invoice Date", | |||
"Supporting Document": "Supporting Document", | |||
@@ -26,6 +26,11 @@ | |||
"Approved": "Approved", | |||
"Rejected": "Rejected", | |||
"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 the date are correct": "Please ensure the date are correct", | |||
"Please ensure the projects are selected": "Please ensure the projects are selected", | |||
"Please ensure the amount are correct": "Please ensure the amount are correct", | |||
"Description": "Description", | |||
"Actions": "Actions" | |||
} |
@@ -11,6 +11,12 @@ | |||
"Approved": "Approved", | |||
"Rejected": "Rejected", | |||
"Do you want to submit?": "Do you want to submit?", | |||
"Submit Success": "Submit Success", | |||
"Submit Fail": "Submit Fail", | |||
"Do you want to delete?": "Do you want to delete", | |||
"Delete Success": "Delete Success", | |||
"Search": "Search", | |||
"Search Criteria": "Search Criteria", | |||
"Cancel": "Cancel", | |||
@@ -8,7 +8,7 @@ | |||
"Related Project Name": "相關項目名稱", | |||
"Expense Type": "費用類別", | |||
"Status": "狀態", | |||
"Amount (HKD)": "金額 (HKD)", | |||
"Amount": "金額", | |||
"Remarks": "備註", | |||
"Invoice Date": "收據日期", | |||
"Supporting Document": "支援文件", | |||
@@ -26,6 +26,11 @@ | |||
"Approved": "已批核", | |||
"Rejected": "已拒絕", | |||
"Please ensure at least one row is created, and all the fields are inputted and saved": "請確保已建立至少一行, 及已輸入和儲存所有欄位", | |||
"Please ensure the date are correct": "請確保所有日期輸入正確", | |||
"Please ensure the projects are selected": "請確保所有項目欄位已選擇", | |||
"Please ensure the amount are correct": "請確保所有金額輸入正確", | |||
"Description": "描述", | |||
"Actions": "行動" | |||
} |
@@ -9,6 +9,12 @@ | |||
"Approved": "已批核", | |||
"Rejected": "已拒絕", | |||
"Do you want to submit?": "你是否確認要提交?", | |||
"Submit Success": "提交成功", | |||
"Submit Fail": "提交失敗", | |||
"Do you want to delete?": "你是否確認要刪除?", | |||
"Delete Success": "刪除成功", | |||
"Search": "搜尋", | |||
"Search Criteria": "搜尋條件", | |||
"Cancel": "取消", | |||