Bläddra i källkod

Merge branch 'main' of https://git.2fi-solutions.com/wayne.lee/tsms

tags/Baseline_30082024_FRONTEND_UAT
leoho2fi 1 år sedan
förälder
incheckning
15a94988b0
29 ändrade filer med 712 tillägg och 349 borttagningar
  1. +3
    -0
      src/app/api/claims/actions.ts
  2. +8
    -7
      src/app/api/claims/index.ts
  3. +1
    -1
      src/app/api/invoices/index.ts
  4. +6
    -0
      src/app/api/staff/index.ts
  5. +9
    -0
      src/app/api/team/actions.ts
  6. +1
    -0
      src/app/utils/fetchUtil.ts
  7. +9
    -0
      src/app/utils/formatUtil.ts
  8. +59
    -24
      src/components/ClaimDetail/ClaimDetail.tsx
  9. +65
    -8
      src/components/ClaimDetail/ClaimFormInputGrid.tsx
  10. +13
    -10
      src/components/ClaimSearch/ClaimSearch.tsx
  11. +79
    -5
      src/components/CreateInvoice/CreateInvoice.tsx
  12. +1
    -1
      src/components/CreateInvoice/ProjectTotalFee.tsx
  13. +11
    -11
      src/components/CreateProject/ProjectClientDetails.tsx
  14. +17
    -28
      src/components/CreateStaff/CreateStaff.tsx
  15. +12
    -1
      src/components/CreateStaffForm/CreateStaffForm.tsx
  16. +2
    -2
      src/components/CreateTeam/CreateTeamWrapper.tsx
  17. +200
    -201
      src/components/EditStaff/EditStaff.tsx
  18. +17
    -15
      src/components/EditStaffForm/EditStaffForm.tsx
  19. +3
    -3
      src/components/EditTeam/Allocation.tsx
  20. +4
    -17
      src/components/EditTeam/EditTeam.tsx
  21. +8
    -1
      src/components/InvoiceSearch/InvoiceSearchWrapper.tsx
  22. +2
    -0
      src/components/NavigationContent/NavigationContent.tsx
  23. +14
    -5
      src/components/SearchResults/SearchResults.tsx
  24. +105
    -0
      src/components/TeamSearch/ConfirmDeleteModal.tsx
  25. +39
    -7
      src/components/TeamSearch/TeamSearch.tsx
  26. +6
    -1
      src/i18n/en/claim.json
  27. +6
    -0
      src/i18n/en/common.json
  28. +6
    -1
      src/i18n/zh/claim.json
  29. +6
    -0
      src/i18n/zh/common.json

+ 3
- 0
src/app/api/claims/actions.ts Visa fil

@@ -12,6 +12,9 @@ export interface ClaimInputFormByStaff {
status: string;

addClaimDetails: ClaimDetailTable[]

// is grid editing
isGridEditing: boolean | null;
}

export interface ClaimDetailTable {


+ 8
- 7
src/app/api/claims/index.ts Visa fil

@@ -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",
},
];
];*/

+ 1
- 1
src/app/api/invoices/index.ts Visa fil

@@ -7,7 +7,7 @@ export interface InvoiceResult {
id: number;
projectCode: string;
projectName: string;
stage: String;
stage: string;
comingPaymentMileStone: string;
paymentMilestoneDate: string;
resourceUsage: number;


+ 6
- 0
src/app/api/staff/index.ts Visa fil

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


+ 9
- 0
src/app/api/team/actions.ts Visa fil

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

+ 1
- 0
src/app/utils/fetchUtil.ts Visa fil

@@ -15,6 +15,7 @@ export const serverFetch: typeof fetch = async (input, init) => {
...(accessToken
? {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
: {}),
},


+ 9
- 0
src/app/utils/formatUtil.ts Visa fil

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


+ 59
- 24
src/components/ClaimDetail/ClaimDetail.tsx Visa fil

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


+ 65
- 8
src/components/ClaimDetail/ClaimFormInputGrid.tsx Visa fil

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


+ 13
- 10
src/components/ClaimSearch/ClaimSearch.tsx Visa fil

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


+ 79
- 5
src/components/CreateInvoice/CreateInvoice.tsx Visa fil

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


+ 1
- 1
src/components/CreateInvoice/ProjectTotalFee.tsx Visa fil

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


+ 11
- 11
src/components/CreateProject/ProjectClientDetails.tsx Visa fil

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


+ 17
- 28
src/components/CreateStaff/CreateStaff.tsx Visa fil

@@ -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: "",
},
]
];


+ 12
- 1
src/components/CreateStaffForm/CreateStaffForm.tsx Visa fil

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


+ 2
- 2
src/components/CreateTeam/CreateTeamWrapper.tsx Visa fil

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


+ 200
- 201
src/components/EditStaff/EditStaff.tsx Visa fil

@@ -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[][] || [[]]} />
</>
);
};


+ 17
- 15
src/components/EditStaffForm/EditStaffForm.tsx Visa fil

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


+ 3
- 3
src/components/EditTeam/Allocation.tsx Visa fil

@@ -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>[]>(
() => [


+ 4
- 17
src/components/EditTeam/EditTeam.tsx Visa fil

@@ -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."));


+ 8
- 1
src/components/InvoiceSearch/InvoiceSearchWrapper.tsx Visa fil

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


+ 2
- 0
src/components/NavigationContent/NavigationContent.tsx Visa fil

@@ -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
- 5
src/components/SearchResults/SearchResults.tsx Visa fil

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


+ 105
- 0
src/components/TeamSearch/ConfirmDeleteModal.tsx Visa fil

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

+ 39
- 7
src/components/TeamSearch/TeamSearch.tsx Visa fil

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

</>
);


+ 6
- 1
src/i18n/en/claim.json Visa fil

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

+ 6
- 0
src/i18n/en/common.json Visa fil

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


+ 6
- 1
src/i18n/zh/claim.json Visa fil

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

+ 6
- 0
src/i18n/zh/common.json Visa fil

@@ -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": "取消",


Laddar…
Avbryt
Spara