소스 검색

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

tags/Baseline_30082024_FRONTEND_UAT
MSI\2Fi 1 년 전
부모
커밋
1c17e925ab
83개의 변경된 파일2532개의 추가작업 그리고 342개의 파일을 삭제
  1. BIN
      public/temp/AR05_Project Completion Report.xlsx
  2. BIN
      public/temp/AR06_Project Completion Report with Outstanding Un-billed Hours.xlsx
  3. +5
    -5
      src/app/(main)/analytics/ProjectCompletionReport/page.tsx
  4. +5
    -5
      src/app/(main)/analytics/ProjectCompletionReportWO/page.tsx
  5. +5
    -0
      src/app/(main)/projects/create/page.tsx
  6. +45
    -0
      src/app/(main)/settings/team/create/page.tsx
  7. +53
    -0
      src/app/(main)/settings/team/page.tsx
  8. +8
    -6
      src/app/(main)/staffReimbursement/create/page.tsx
  9. +7
    -5
      src/app/(main)/staffReimbursement/page.tsx
  10. +48
    -0
      src/app/api/claims/actions.ts
  11. +41
    -5
      src/app/api/claims/index.ts
  12. +10
    -0
      src/app/api/grades/index.ts
  13. +9
    -9
      src/app/api/positions/index.ts
  14. +42
    -0
      src/app/api/report5/index.ts
  15. +42
    -0
      src/app/api/report6/index.ts
  16. +20
    -0
      src/app/api/team/index.ts
  17. +28
    -0
      src/app/utils/ComboConst.js
  18. +11
    -0
      src/app/utils/comboUtil.ts
  19. +23
    -0
      src/app/utils/commonUtil.ts
  20. +8
    -0
      src/app/utils/formatUtil.ts
  21. +33
    -0
      src/auth/utils.js
  22. +105
    -0
      src/components/ClaimDetail/ClaimDetail.tsx
  23. +20
    -0
      src/components/ClaimDetail/ClaimDetailWrapper.tsx
  24. +76
    -0
      src/components/ClaimDetail/ClaimFormInfo.tsx
  25. +193
    -106
      src/components/ClaimDetail/ClaimFormInputGrid.tsx
  26. +1
    -0
      src/components/ClaimDetail/index.ts
  27. +24
    -29
      src/components/ClaimSearch/ClaimSearch.tsx
  28. +0
    -67
      src/components/CreateClaim/ClaimDetails.tsx
  29. +0
    -48
      src/components/CreateClaim/CreateClaim.tsx
  30. +0
    -1
      src/components/CreateClaim/index.ts
  31. +4
    -3
      src/components/CreateProject/CreateProject.tsx
  32. +8
    -9
      src/components/CreateProject/CreateProjectWrapper.tsx
  33. +7
    -3
      src/components/CreateProject/MilestoneSection.tsx
  34. +24
    -9
      src/components/CreateProject/ProjectClientDetails.tsx
  35. +1
    -10
      src/components/CreateProject/StaffAllocation.tsx
  36. +126
    -0
      src/components/CreateTeam/CreateTeam.tsx
  37. +40
    -0
      src/components/CreateTeam/CreateTeamLoading.tsx
  38. +26
    -0
      src/components/CreateTeam/CreateTeamWrapper.tsx
  39. +238
    -0
      src/components/CreateTeam/StaffAllocation.tsx
  40. +69
    -0
      src/components/CreateTeam/TeamInfo.tsx
  41. +1
    -0
      src/components/CreateTeam/index.ts
  42. +1
    -1
      src/components/CustomDatagrid/CustomDatagrid.tsx
  43. +1
    -1
      src/components/CustomerDetail/ContactInfo.tsx
  44. +1
    -1
      src/components/CustomerDetail/CustomerDetailWrapper.tsx
  45. +2
    -2
      src/components/CustomerDetail/CustomerInfo.tsx
  46. +1
    -0
      src/components/CustomerSearch/CustomerSearch.tsx
  47. +17
    -0
      src/components/Report/ProjectCompletionReport/ProjectCompletionReport.tsx
  48. +2
    -0
      src/components/Report/ProjectCompletionReport/index.ts
  49. +44
    -0
      src/components/Report/ProjectCompletionReportGen/ProjectCompletionReportGen.tsx
  50. +41
    -0
      src/components/Report/ProjectCompletionReportGen/ProjectCompletionReportGenLoading.tsx
  51. +19
    -0
      src/components/Report/ProjectCompletionReportGen/ProjectCompletionReportGenWrapper.tsx
  52. +2
    -0
      src/components/Report/ProjectCompletionReportGen/index.ts
  53. +17
    -0
      src/components/Report/ProjectCompletionReportWO/ProjectCompletionReportWO.tsx
  54. +2
    -0
      src/components/Report/ProjectCompletionReportWO/index.ts
  55. +44
    -0
      src/components/Report/ProjectCompletionReportWOGen/ProjectCompletionReportWOGen.tsx
  56. +41
    -0
      src/components/Report/ProjectCompletionReportWOGen/ProjectCompletionReportWOGenLoading.tsx
  57. +19
    -0
      src/components/Report/ProjectCompletionReportWOGen/ProjectCompletionReportWOGenWrapper.tsx
  58. +2
    -0
      src/components/Report/ProjectCompletionReportWOGen/index.ts
  59. +2
    -2
      src/components/ReportSearchBox/SearchBox.tsx
  60. +2
    -2
      src/components/ReportSearchBox2/SearchBox2.tsx
  61. +2
    -2
      src/components/ReportSearchBox3/SearchBox3.tsx
  62. +2
    -2
      src/components/ReportSearchBox4/SearchBox4.tsx
  63. +302
    -0
      src/components/ReportSearchBox5/SearchBox5.tsx
  64. +3
    -0
      src/components/ReportSearchBox5/index.ts
  65. +302
    -0
      src/components/ReportSearchBox6/SearchBox6.tsx
  66. +3
    -0
      src/components/ReportSearchBox6/index.ts
  67. +1
    -1
      src/components/SearchBox/SearchBox.tsx
  68. +5
    -1
      src/components/SearchResults/SearchResults.tsx
  69. +1
    -1
      src/components/SubsidiaryDetail/ContactInfo.tsx
  70. +2
    -2
      src/components/SubsidiaryDetail/SubsidiaryInfo.tsx
  71. +1
    -0
      src/components/SubsidiarySearch/SubsidiarySearch.tsx
  72. +90
    -0
      src/components/TeamSearch/TeamSearch.tsx
  73. +40
    -0
      src/components/TeamSearch/TeamSearchLoading.tsx
  74. +21
    -0
      src/components/TeamSearch/TeamSearchWrapper.tsx
  75. +1
    -0
      src/components/TeamSearch/index.ts
  76. +31
    -0
      src/i18n/en/claim.json
  77. +12
    -0
      src/i18n/en/common.json
  78. +1
    -1
      src/i18n/en/customer.json
  79. +1
    -1
      src/i18n/en/subsidiary.json
  80. +31
    -0
      src/i18n/zh/claim.json
  81. +12
    -0
      src/i18n/zh/common.json
  82. +1
    -1
      src/i18n/zh/customer.json
  83. +1
    -1
      src/i18n/zh/subsidiary.json

BIN
public/temp/AR05_Project Completion Report.xlsx 파일 보기


BIN
public/temp/AR06_Project Completion Report with Outstanding Un-billed Hours.xlsx 파일 보기


+ 5
- 5
src/app/(main)/analytics/ProjectCompletionReport/page.tsx 파일 보기

@@ -1,14 +1,14 @@
//src\app\(main)\analytics\LateStartReport\page.tsx
//src\app\(main)\analytics\ProjectCompletionReport\page.tsx
import { Metadata } from "next";
import { I18nProvider } from "@/i18n";
import Typography from "@mui/material/Typography";
import LateStartReportComponent from "@/components/LateStartReport";
import ProjectCompletionReportComponent from "@/components/Report/ProjectCompletionReport";

export const metadata: Metadata = {
title: "Project Status by Client",
};

const ProjectLateReport: React.FC = () => {
const ProjectCompletionReport: React.FC = () => {
return (
<I18nProvider namespaces={["analytics"]}>
<Typography variant="h4" marginInlineEnd={2}>
@@ -17,8 +17,8 @@ const ProjectLateReport: React.FC = () => {
{/* <Suspense fallback={<ProgressCashFlowSearch.Loading />}>
<ProgressCashFlowSearch/>
</Suspense> */}
<LateStartReportComponent />
<ProjectCompletionReportComponent />
</I18nProvider>
);
};
export default ProjectLateReport;
export default ProjectCompletionReport;

+ 5
- 5
src/app/(main)/analytics/ProjectCompletionReportWO/page.tsx 파일 보기

@@ -1,14 +1,14 @@
//src\app\(main)\analytics\LateStartReport\page.tsx
//src\app\(main)\analytics\ProjectCompletionReport\page.tsx
import { Metadata } from "next";
import { I18nProvider } from "@/i18n";
import Typography from "@mui/material/Typography";
import LateStartReportComponent from "@/components/LateStartReport";
import ProjectCompletionReportWOComponent from "@/components/Report/ProjectCompletionReportWO";

export const metadata: Metadata = {
title: "Project Status by Client",
};

const ProjectLateReport: React.FC = () => {
const ProjectCompletionReportWO: React.FC = () => {
return (
<I18nProvider namespaces={["analytics"]}>
<Typography variant="h4" marginInlineEnd={2}>
@@ -17,8 +17,8 @@ const ProjectLateReport: React.FC = () => {
{/* <Suspense fallback={<ProgressCashFlowSearch.Loading />}>
<ProgressCashFlowSearch/>
</Suspense> */}
<LateStartReportComponent />
<ProjectCompletionReportWOComponent />
</I18nProvider>
);
};
export default ProjectLateReport;
export default ProjectCompletionReportWO;

+ 5
- 0
src/app/(main)/projects/create/page.tsx 파일 보기

@@ -1,3 +1,5 @@
import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer";
import { fetchGrades } from "@/app/api/grades";
import {
fetchProjectBuildingTypes,
fetchProjectCategories,
@@ -31,6 +33,9 @@ const Projects: React.FC = async () => {
fetchProjectServiceTypes();
fetchProjectBuildingTypes();
fetchProjectWorkNatures();
fetchAllCustomers();
fetchAllSubsidiaries();
fetchGrades();
preloadTeamLeads();
preloadStaff();



+ 45
- 0
src/app/(main)/settings/team/create/page.tsx 파일 보기

@@ -0,0 +1,45 @@
// 'use client';
import { I18nProvider, getServerI18n } from "@/i18n";
import CustomInputForm from "@/components/CustomInputForm";
import Check from "@mui/icons-material/Check";
import Close from "@mui/icons-material/Close";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import Tab from "@mui/material/Tab";
import Tabs, { TabsProps } from "@mui/material/Tabs";
import { useRouter } from "next/navigation";
import React, { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { Task, TaskTemplate } from "@/app/api/tasks";
import {
FieldErrors,
FormProvider,
SubmitErrorHandler,
SubmitHandler,
useForm,
} from "react-hook-form";
import { CreateProjectInputs, saveProject } from "@/app/api/projects/actions";
import { Error } from "@mui/icons-material";
import { ProjectCategory } from "@/app/api/projects";
import { Grid, Typography } from "@mui/material";
import CreateStaffForm from "@/components/CreateStaff/CreateStaff";
import CreateTeam from "@/components/CreateTeam";

const CreateTeamPage: React.FC = async () => {
const { t } = await getServerI18n("team");

const title = ['', t('Additional Info')]
// const regex = new RegExp("^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$")
// console.log(regex)

return (
<>
<Typography variant="h4">{t("Create Team")}</Typography>
<I18nProvider namespaces={["Team"]}>
<CreateTeam/>
</I18nProvider>
</>
);
};

export default CreateTeamPage;

+ 53
- 0
src/app/(main)/settings/team/page.tsx 파일 보기

@@ -0,0 +1,53 @@
import { preloadClaims } from "@/app/api/claims";
import { preloadStaff, preloadTeamLeads } from "@/app/api/staff";
import StaffSearch from "@/components/StaffSearch";
import TeamSearch from "@/components/TeamSearch";
import { I18nProvider, getServerI18n } from "@/i18n";
import Add from "@mui/icons-material/Add";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { Metadata } from "next";
import Link from "next/link";
import { Suspense } from "react";


export const metadata: Metadata = {
title: "Team",
};


const Team: React.FC = async () => {
const { t } = await getServerI18n("Team");
// preloadTeamLeads();
// preloadStaff();
return (
<>
<Stack
direction="row"
justifyContent="space-between"
flexWrap="wrap"
rowGap={2}
>
<Typography variant="h4" marginInlineEnd={2}>
{t("Team")}
</Typography>
<Button
variant="contained"
startIcon={<Add />}
LinkComponent={Link}
href="/settings/team/create"
>
{t("Create Team")}
</Button>
</Stack>
<I18nProvider namespaces={["Team", "common"]}>
<Suspense fallback={<TeamSearch.Loading />}>
<TeamSearch />
</Suspense>
</I18nProvider>
</>
);
};
export default Team;

+ 8
- 6
src/app/(main)/staffReimbursement/create/page.tsx 파일 보기

@@ -1,5 +1,5 @@
import CreateClaim from "@/components/CreateClaim";
import { getServerI18n } from "@/i18n";
import ClaimDetail from "@/components/ClaimDetail";
import { I18nProvider, getServerI18n } from "@/i18n";
import Typography from "@mui/material/Typography";
import { Metadata } from "next";

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

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

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

export default CreateClaims;
export default ClaimDetails;

+ 7
- 5
src/app/(main)/staffReimbursement/page.tsx 파일 보기

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

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

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


+ 48
- 0
src/app/api/claims/actions.ts 파일 보기

@@ -0,0 +1,48 @@
"use server";

import { BASE_API_URL } from "@/config/api";
import { Claim, ProjectCombo, SupportingDocument } from ".";
import { serverFetchJson } from "@/app/utils/fetchUtil";
import { revalidateTag } from "next/cache";

export interface ClaimInputFormByStaff {
id: number | null;
code: string | null;
expenseType: string;
status: string;

addClaimDetails: ClaimDetailTable[]
}

export interface ClaimDetailTable {
id: number;
invoiceDate: Date;
description: string;
project: ProjectCombo;
amount: number;
supportingDocumentName: string;
oldSupportingDocument: FileList[];
newSupportingDocument: SupportingDocument;
isNew: boolean;
}

export interface SaveClaimResponse {
claim: Claim;
message: string;
}

export const saveClaim = async (data: ClaimInputFormByStaff) => {
console.log(data)
const saveCustomer = await serverFetchJson<SaveClaimResponse>(
`${BASE_API_URL}/claim/save`,
{
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
},
);

revalidateTag("claims");

return saveCustomer;
};

+ 41
- 5
src/app/api/claims/index.ts 파일 보기

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

export interface ClaimResult {
export interface Claim {
id: number;
created: string;
name: string;
@@ -11,18 +13,52 @@ export interface ClaimResult {
remarks: string;
}

export interface ClaimSearchForm {
id: number;
created: string;
createdTo: string;
name: string;
cost: number;
type: "Expense" | "Petty Cash";
status: "Not Submitted" | "Waiting for Approval" | "Approved" | "Rejected";
remarks: string;
}

export interface ProjectCombo {
id: number;
name: string;
code: string;
}

export interface SupportingDocument {
id: number;
skey: string;
filename: string;
}

export const preloadClaims = () => {
fetchClaims();
};

export const fetchClaims = cache(async () => {
return mockClaims;
// return serverFetchJson<Claim[]>(`${BASE_API_URL}/claim`);
});

export const fetchProjectCombo = cache(async () => {
return serverFetchJson<ProjectCombo[]>(`${BASE_API_URL}/projects`, {
next: { tags: ["projects"] },
});
});

const mockClaims: ClaimResult[] = [
// export const fetchAllCustomers = cache(async () => {
// return serverFetchJson<Customer[]>(`${BASE_API_URL}/customer`);
// });

const mockClaims: Claim[] = [
{
id: 1,
created: "2023-11-22",
created: "2023/11/22",
name: "Consultancy Project A",
cost: 121.0,
type: "Expense",
@@ -31,7 +67,7 @@ const mockClaims: ClaimResult[] = [
},
{
id: 2,
created: "2023-11-30",
created: "2023/11/30",
name: "Consultancy Project A",
cost: 4300.0,
type: "Expense",
@@ -40,7 +76,7 @@ const mockClaims: ClaimResult[] = [
},
{
id: 3,
created: "2023-12-12",
created: "2023/12/12",
name: "Construction Project C",
cost: 3675.0,
type: "Petty Cash",


+ 10
- 0
src/app/api/grades/index.ts 파일 보기

@@ -1,5 +1,15 @@
import { serverFetchJson } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";
import { cache } from "react";

export interface Grade {
name: string;
id: number;
code: string;
}

export const fetchGrades = cache(async () => {
return serverFetchJson<Grade[]>(`${BASE_API_URL}/grades`, {
next: { tags: ["grades"] },
});
});

+ 9
- 9
src/app/api/positions/index.ts 파일 보기

@@ -4,18 +4,18 @@ import { cache } from "react";
import "server-only";

export interface PositionResult {
id: number;
code: string;
name: string;
description: string;
id: number;
code: string;
name: string;
description: string;
}

export const preloadPositions = () => {
fetchPositions();
fetchPositions();
};

export const fetchPositions = cache(async () => {
return serverFetchJson<PositionResult[]>(`${BASE_API_URL}/positions`, {
next: { tags: ["positions"] },
});
});
return serverFetchJson<PositionResult[]>(`${BASE_API_URL}/positions`, {
next: { tags: ["positions"] },
});
});

+ 42
- 0
src/app/api/report5/index.ts 파일 보기

@@ -0,0 +1,42 @@
//src\app\api\report\index.ts
import { cache } from "react";

export interface ProjectCompletion {
id: number;
projectCode: string;
projectName: string;
team: string;
teamLeader: string;
startDate: string;
startDateFrom: string;
startDateTo: string;
targetEndDate: string;
client: string;
subsidiary: string;
completeDate: string;
}

export const preloadProjects = () => {
fetchProjectsProjectCompletion();
};

export const fetchProjectsProjectCompletion = cache(async () => {
return mockProjects;
});

const mockProjects: ProjectCompletion[] = [
{
id: 1,
projectCode: "CUST-001",
projectName: "Client A",
team: "N/A",
teamLeader: "N/A",
startDate: "1/2/2024",
startDateFrom: "1/2/2024",
startDateTo: "1/2/2024",
targetEndDate: "30/3/2024",
client: "ss",
subsidiary: "sus",
completeDate:"30/2/2024",
},
];

+ 42
- 0
src/app/api/report6/index.ts 파일 보기

@@ -0,0 +1,42 @@
//src\app\api\report\index.ts
import { cache } from "react";

export interface ProjectClaims {
id: number;
projectCode: string;
projectName: string;
team: string;
teamLeader: string;
startDate: string;
startDateFrom: string;
startDateTo: string;
targetEndDate: string;
client: string;
subsidiary: string;
completeDate: string;
}

export const preloadProjects = () => {
fetchProjectsProjectClaims();
};

export const fetchProjectsProjectClaims = cache(async () => {
return mockProjects;
});

const mockProjects: ProjectClaims[] = [
{
id: 1,
projectCode: "CUST-001",
projectName: "Client A",
team: "N/A",
teamLeader: "N/A",
startDate: "1/2/2024",
startDateFrom: "1/2/2024",
startDateTo: "1/2/2024",
targetEndDate: "30/3/2024",
client: "ss",
subsidiary: "sus",
completeDate:"30/2/2024",
},
];

+ 20
- 0
src/app/api/team/index.ts 파일 보기

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


export interface TeamResult {
action: any;
id: number;
name: string;
code: string;
description: string;
}

export const fetchTeam = cache(async () => {
return serverFetchJson<TeamResult[]>(`${BASE_API_URL}/team`, {
next: { tags: ["team"] },
});
});

+ 28
- 0
src/app/utils/ComboConst.js 파일 보기

@@ -0,0 +1,28 @@
import { useIntl } from "react-intl";
const intl = useIntl()

export const TEAM_COMBO = [
{ id: 1, key: 1, label: 'AAA', value: "AAA" },
{ id: 2, key: 2, label: 'BBB', value: "BBB" },
{ id: 3, key: 3, label: 'CCC', value: "CCC" },
];

export const CLIENT_COMBO = [
{ id: 1, key: 1, label: 'Cust A', value: "Cust A" },
{ id: 2, key: 2, label: 'Cust B', value: "Cust B" },
{ id: 3, key: 3, label: 'Cust C', value: "Cust C" },
];

export function LOCALE_COMBO() {
return ([
{id: 1,label: intl.formatMessage({ id: "en" }),value: "en",},
{id: 2,label: intl.formatMessage({ id: "zh-HK" }),value: "zh-HK",},
{id: 3,label: intl.formatMessage({ id: "zh-CN" }),value: "zh-CN",},
])
}
export function OVERCONSUMPTION_COMBO() {
return ([
{id: 1,label: intl.formatMessage({ id: "Overconsumption" }),value: "Overconsumption",},
{id: 2,label: intl.formatMessage({ id: "Potential Overconsumption" }),value: "Potential Overconsumption",},
])
}

+ 11
- 0
src/app/utils/comboUtil.ts 파일 보기

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

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

+ 23
- 0
src/app/utils/commonUtil.ts 파일 보기

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

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

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

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

+ 8
- 0
src/app/utils/formatUtil.ts 파일 보기

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

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

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

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

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

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


+ 33
- 0
src/auth/utils.js 파일 보기

@@ -0,0 +1,33 @@
import useJwt from 'auth/jwt/coreUseJwt'

/**
* Return if user is logged in
* This is completely up to you and how you want to store the token in your frontend application
* e.g. If you are using cookies to store the application please update this function
*/
// eslint-disable-next-line arrow-body-style
export const hostname = process.env.REACT_APP_BACKEND_HOST
const hostPort = process.env.REACT_APP_BACKEND_PORT
export const hostPath = `${process.env.REACT_APP_BACKEND_PROTOCOL}://${hostname}:${hostPort}`
export const apiPath = `${hostPath}/api`

export const isUserLoggedIn = () => {
return localStorage.getItem('userData') && localStorage.getItem(useJwt.jwtConfig.storageTokenKeyName)
}

export const getUserData = () => JSON.parse(localStorage.getItem('userData'))

/**
* This function is used for demo purpose route navigation
* In real app you won't need this function because your app will navigate to same route for each users regardless of ability
* Please note role field is just for showing purpose it's not used by anything in frontend
* We are checking role just for ease
* NOTE: If you have different pages to navigate based on user ability then this function can be useful. However, you need to update it.
* @param {String} userRole Role of user
*/
export const getHomeRouteForLoggedInUser = userRole => {
if (userRole === 'admin') return '/'
if (userRole === 'user') return '/'
if (userRole === 'client') return {name: 'access-control'}
return {name: 'auth-login'}
}

+ 105
- 0
src/components/ClaimDetail/ClaimDetail.tsx 파일 보기

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

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

export interface Props {
projectCombo: ProjectCombo[]
}

const ClaimDetail: React.FC<Props> = ({ projectCombo }) => {
const { t } = useTranslation("common");
const [serverError, setServerError] = useState("");
const router = useRouter();

const formProps = useForm<ClaimInputFormByStaff>({
defaultValues: {
id: null,
expenseType: expenseTypeCombo[0],
addClaimDetails: []
},
});

const handleCancel = () => {
router.back();
};

const onSubmit = useCallback<SubmitHandler<ClaimInputFormByStaff>>(
async (data, event) => {
try {
console.log(data);
console.log((event?.nativeEvent as any).submitter.name);
const buttonName = (event?.nativeEvent as any).submitter.name
console.log(JSON.stringify(data))
// const formData = new FormData()
// formData.append("expenseType", data.expenseType)
// formData.append("claimDetails", data.addClaimDetails)
if (buttonName === "submit") {
data.status = "Not Submitted"
} else if (buttonName === "save") {
data.status = "Waiting for Approval"
}

// for (let i = 0; i < data.addClaimDetails.length; i++) {
// // const formData = new FormData();
// // formData.append("newSupportingDocument", data.addClaimDetails[i].oldSupportingDocument);
// data.addClaimDetails[i].oldSupportingDocument = new Blob([data.addClaimDetails[i].oldSupportingDocument], {type: data.addClaimDetails[i].oldSupportingDocument.type})
// }
console.log(data);
await saveClaim(data)
setServerError("");
// await saveProject(data);
// router.replace("/projects");
} catch (e) {
setServerError(t("An error has occurred. Please try again later."));
}
},
[router, t],
);

const onSubmitError = useCallback<SubmitErrorHandler<ClaimInputFormByStaff>>(
(errors) => {
// Set the tab so that the focus will go there
console.log(errors)
},
[],
);

return (
<FormProvider {...formProps}>
<Stack spacing={2} component={"form"} onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)}>
<ClaimFormInfo projectCombo={projectCombo} />
{serverError && (
<Typography variant="body2" color="error" alignSelf="flex-end">
{serverError}
</Typography>
)}
<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button variant="text" startIcon={<Close />} onClick={handleCancel}>
{t("Cancel")}
</Button>
<Button variant="outlined" name="save" startIcon={<Check />} type="submit">
{t("Save")}
</Button>
<Button variant="contained" name="submit" startIcon={<DoneAll />} type="submit">
{t("Submit")}
</Button>
</Stack>
</Stack>
</FormProvider>
);
};

export default ClaimDetail;

+ 20
- 0
src/components/ClaimDetail/ClaimDetailWrapper.tsx 파일 보기

@@ -0,0 +1,20 @@

import React from "react";
import ClaimDetail from "./ClaimDetail";
import { fetchProjectCombo } from "@/app/api/claims";
// import TaskSetup from "./TaskSetup";
// import StaffAllocation from "./StaffAllocation";
// import ResourceMilestone from "./ResourceMilestone";

const ClaimDetailWrapper: React.FC = async () => {
const [projectCombo] =
await Promise.all([
fetchProjectCombo()
]);

return (
<ClaimDetail projectCombo={projectCombo}/>
);
};

export default ClaimDetailWrapper;

+ 76
- 0
src/components/ClaimDetail/ClaimFormInfo.tsx 파일 보기

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

import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import FormControl from "@mui/material/FormControl";
import Grid from "@mui/material/Grid";
import InputLabel from "@mui/material/InputLabel";
import MenuItem from "@mui/material/MenuItem";
import Select from "@mui/material/Select";
import { useTranslation } from "react-i18next";
import ClaimFormInputGrid from "./ClaimFormInputGrid";
import { expenseTypeCombo } from "@/app/utils/comboUtil";
import { Controller, useFormContext } from "react-hook-form";
import { ClaimInputFormByStaff } from "@/app/api/claims/actions";
import { ProjectCombo } from "@/app/api/claims";
import { TextField } from "@mui/material";

interface Props {
projectCombo: ProjectCombo[]
}

const ClaimFormInfo: React.FC<Props> = ({ projectCombo }) => {
const { t } = useTranslation();

const {
control,
register,
} = useFormContext<ClaimInputFormByStaff>();

return (
<Card>
<CardContent component={Stack} spacing={4}>
<Box>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}>
<TextField
fullWidth
label={t("Claim Code")}
{...register("code")}
disabled={true}
/>
</Grid>
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel>{t("Expense Type")}</InputLabel>
<Controller
// defaultValue={expenseTypeCombo[0].value}
control={control}
name="expenseType"
render={({ field }) => (
<Select label={t("Expense Type")} {...field}>
{
expenseTypeCombo.map((type, index) => (
<MenuItem key={`${type}-${index}`} value={type}>
{t(type)}
</MenuItem>
))
}
</Select>
)}
/>
</FormControl>
</Grid>
</Grid>
</Box>
<Card>
<ClaimFormInputGrid projectCombo={projectCombo} />
</Card>
</CardContent>
</Card>
);
};

export default ClaimFormInfo;

src/components/CreateClaim/ClaimInputGrid.tsx → src/components/ClaimDetail/ClaimFormInputGrid.tsx 파일 보기

@@ -8,16 +8,9 @@ import { Suspense } from "react";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import Link from "next/link";
import { t } from "i18next";
import {
Box,
Container,
Modal,
Select,
SelectChangeEvent,
Typography,
Box, Card, Typography,
} from "@mui/material";
import { Close } from "@mui/icons-material";
import AddIcon from "@mui/icons-material/Add";
import EditIcon from "@mui/icons-material/Edit";
import DeleteIcon from "@mui/icons-material/DeleteOutlined";
@@ -25,35 +18,30 @@ import SaveIcon from "@mui/icons-material/Save";
import CancelIcon from "@mui/icons-material/Close";
import AddPhotoAlternateOutlinedIcon from "@mui/icons-material/AddPhotoAlternateOutlined";
import ImageNotSupportedOutlinedIcon from "@mui/icons-material/ImageNotSupportedOutlined";
import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import Swal from "sweetalert2";
import { msg } from "../Swal/CustomAlerts";
import React from "react";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import {
GridRowsProp,
GridRowModesModel,
GridRowModes,
DataGrid,
GridColDef,
GridToolbarContainer,
GridFooterContainer,
GridActionsCellItem,
GridEventListener,
GridRowId,
GridRowModel,
GridRowEditStopReasons,
GridEditInputCell,
GridValueSetterParams,
GridTreeNodeWithRender,
GridRenderCellParams,
} from "@mui/x-data-grid";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import dayjs from "dayjs";
import { Props } from "react-intl/src/components/relative";
import palette from "@/theme/devias-material-kit/palette";

const weekdays = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
import { ProjectCombo } from "@/app/api/claims";
import { ClaimDetailTable, ClaimInputFormByStaff } from "@/app/api/claims/actions";
import { useFieldArray, useFormContext } from "react-hook-form";
import { GridRenderEditCellParams } from "@mui/x-data-grid";
import { convertDateToString } from "@/app/utils/formatUtil";

interface BottomBarProps {
getCostTotal: () => number;
@@ -63,15 +51,6 @@ interface BottomBarProps {
) => void;
}

interface EditToolbarProps {
// setDay: (newDay : dayjs.Dayjs) => void;
setDay: (newDay: (oldDay: dayjs.Dayjs) => dayjs.Dayjs) => void;
setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void;
setRowModesModel: (
newModel: (oldModel: GridRowModesModel) => GridRowModesModel,
) => void;
}

interface EditFooterProps {
setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void;
setRowModesModel: (
@@ -80,17 +59,17 @@ interface EditFooterProps {
}

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

const handleAddClick = () => {
const id = newId;
setNewId(newId - 1);
setRows((oldRows) => [
...oldRows,
{ id, projectCode: "", task: "", isNew: true },
{ id, invoiceDate: new Date(), project: null, description: null, amount: null, newSupportingDocument: null, supportingDocumentName: null, isNew: true },
]);
setRowModesModel((oldModel) => ({
...oldModel,
@@ -98,11 +77,6 @@ const BottomBar = (props: BottomBarProps) => {
}));
};

const totalColDef = {
flex: 1,
// style: {color:getCostTotal('mon')>24?"red":"black"}
};

const TotalCell = ({ value }: Props) => {
const [invalid, setInvalid] = useState(false);

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

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

const initialRows: GridRowsProp = [
{
id: 1,
date: new Date(),
invoiceDate: new Date(),
description: "Taxi to client office",
cost: 169.5,
document: "taxi_receipt.jpg",
amount: 169.5,
supportingDocumentName: "taxi_receipt.jpg",
},
{
id: 2,
date: dayjs().add(-14, "days").toDate(),
invoiceDate: dayjs().add(-14, "days").toDate(),
description: "MTR fee to Kowloon Bay Office",
cost: 15.5,
document: "octopus_invoice.jpg",
amount: 15.5,
supportingDocumentName: "octopus_invoice.jpg",
},
{
id: 3,
date: dayjs().add(-44, "days").toDate(),
invoiceDate: dayjs().add(-44, "days").toDate(),
description: "Starbucks",
cost: 504,
amount: 504,
},
];

const ClaimInputGrid: React.FC<ClaimInputGridProps> = ({ ...props }) => {
const [rows, setRows] = useState(initialRows);
const [day, setDay] = useState(dayjs());
const ClaimFormInputGrid: React.FC<ClaimFormInputGridProps> = ({
// onClose,
projectCombo,
}) => {
const { t } = useTranslation()
const { control, setValue, getValues, formState: { errors } } = useFormContext<ClaimInputFormByStaff>();
const { fields } = useFieldArray({
control,
name: "addClaimDetails"
})

const [rows, setRows] = useState<GridRowsProp>([]);
const [rowModesModel, setRowModesModel] = React.useState<GridRowModesModel>(
{},
);

// Row function
const handleRowEditStop: GridEventListener<"rowEditStop"> = (
params,
event,
@@ -217,20 +202,77 @@ const ClaimInputGrid: React.FC<ClaimInputGridProps> = ({ ...props }) => {
}
};

const processRowUpdate = (newRow: GridRowModel) => {
const updatedRow = { ...newRow, isNew: false };
setRows(rows.map((row) => (row.id === newRow.id ? updatedRow : row)));
return updatedRow;
};
const processRowUpdate = React.useCallback((newRow: GridRowModel) => {
const updatedRow = { ...newRow };
const updatedRows = rows.map((row) => (row.id === newRow.id ? { ...updatedRow, supportingDocumentName: row.supportingDocumentName } : row))
setRows(updatedRows);
setValue("addClaimDetails", updatedRows as ClaimDetailTable[])
return updatedRows.find((row) => row.id === newRow.id) as GridRowModel;
}, [rows, rowModesModel, t]);

const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => {
setRowModesModel(newRowModesModel);
};

// File Upload function
const fileInputRef: React.RefObject<Record<string, HTMLInputElement | null>> = React.useRef({})

const setFileInputRefs = (ele: HTMLInputElement | null, key: string) => {
if (fileInputRef.current !== null) {
fileInputRef.current[key] = ele
}
}

useEffect(() => {

}, [])
const handleFileSelect = (key: string) => {
if (fileInputRef !== null && fileInputRef.current !== null && fileInputRef.current[key] !== null) {
fileInputRef.current[key]?.click()
}
}

const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>, params: GridRenderEditCellParams<any, any, any, GridTreeNodeWithRender>) => {

const file = event.target.files?.[0] ?? null

if (file !== null) {
console.log(file)
console.log(typeof file)
const updatedRows = rows.map((row) => (row.id === params.row.id ? { ...row, supportingDocumentName: file.name, newSupportingDocument: file } : row))
setRows(updatedRows);
setValue("addClaimDetails", updatedRows as ClaimDetailTable[])
// const url = URL.createObjectURL(new Blob([file]));
// const link = document.createElement("a");
// link.href = url;
// link.setAttribute("download", file.name);
// link.click();
}
}

const handleFileDelete = (id: number) => {
const updatedRows = rows.map((row) => (row.id === id ? { ...row, supportingDocumentName: null, newSupportingDocument: null } : row))
setRows(updatedRows);
setValue("addClaimDetails", updatedRows as ClaimDetailTable[])
}

const handleLinkClick = (params: GridRenderEditCellParams<any, any, any, GridTreeNodeWithRender>) => {

const url = URL.createObjectURL(new Blob([params.row.newSupportingDocument]));
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", params.row.supportingDocumentName);
link.click();

// console.log(params)
// console.log(rows)
}

// columns
const getCostTotal = () => {
let sum = 0;
rows.forEach((row) => {
sum += row["cost"] ?? 0;
sum += row["amount"] ?? 0;
});
return sum;
};
@@ -256,11 +298,11 @@ const ClaimInputGrid: React.FC<ClaimInputGridProps> = ({ ...props }) => {
),
};

const columns: GridColDef[] = [
const columns: GridColDef[] = React.useMemo(() => [
{
field: "actions",
type: "actions",
headerName: "Actions",
headerName: t("Actions"),
width: 100,
cellClassName: "actions",
getActions: ({ id }) => {
@@ -312,24 +354,50 @@ const ClaimInputGrid: React.FC<ClaimInputGridProps> = ({ ...props }) => {
},
},
{
field: "date",
headerName: "Invoice Date",
field: "invoiceDate",
headerName: t("Invoice Date"),
// width: 220,
flex: 1,
editable: true,
type: "date",
renderCell: (params: GridRenderCellParams<any, Date>) => {
return convertDateToString(params.value!!)
},
},
{
field: "project",
headerName: t("Project"),
// width: 220,
flex: 1,
editable: true,
type: "singleSelect",
getOptionLabel: (value: any) => {
return !value?.code || value?.code.length === 0 ? `${value?.name}` : `${value?.code} - ${value?.name}`;
},
getOptionValue: (value: any) => value,
valueOptions: () => {
const options = projectCombo ?? []

if (options.length === 0) {
options.push({ id: -1, code: "", name: "No Projects" })
}
return options;
},
valueGetter: (params) => {
return params.value ?? projectCombo[0].id ?? -1
},
},
{
field: "description",
headerName: "Description",
headerName: t("Description"),
// width: 220,
flex: 2,
editable: true,
type: "string",
},
{
field: "cost",
headerName: "Cost (HKD)",
field: "amount",
headerName: t("Amount (HKD)"),
editable: true,
type: "number",
valueFormatter: (params) => {
@@ -337,31 +405,34 @@ const ClaimInputGrid: React.FC<ClaimInputGridProps> = ({ ...props }) => {
},
},
{
field: "document",
headerName: "Supporting Document",
type: "string",
field: "supportingDocumentName",
headerName: t("Supporting Document"),
// type: "string",
editable: true,
flex: 2,
renderCell: (params) => {
return params.value ? (
<span>
<a href="" target="_blank" rel="noopener noreferrer">
<Link onClick={() => handleLinkClick(params)} href="#">{params.value}</Link>
{/* <a href="" target="_blank" rel="noopener noreferrer">
{params.value}
</a>
</a> */}
</span>
) : (
<span style={{ color: palette.text.disabled }}>No Documents</span>
);
},
renderEditCell: (params) => {
return params.value ? (
const currentRow = rows.find(row => row.id === params.row.id);
return params.formattedValue ? (
<span>
<a href="" target="_blank" rel="noopener noreferrer">
{params.value}
</a>
<Link onClick={() => handleLinkClick(params)} href="#">{params.formattedValue}</Link>
{/* <a href="" target="_blank" rel="noopener noreferrer">
{params.formattedValue}
</a> */}
<Button
title="Remove Document"
onClick={(event) => console.log(event)}
onClick={() => handleFileDelete(params.row.id)}
>
<ImageNotSupportedOutlinedIcon
sx={{ fontSize: "25px", color: "red" }}
@@ -369,15 +440,24 @@ const ClaimInputGrid: React.FC<ClaimInputGridProps> = ({ ...props }) => {
</Button>
</span>
) : (
<Button title="Add Document">
<AddPhotoAlternateOutlinedIcon
sx={{ fontSize: "25px", color: "green" }}
<div>
<input
type="file"
ref={ele => setFileInputRefs(ele, params.row.id)}
accept="image/jpg, image/jpeg, image/png, .doc, .docx, .pdf"
style={{ display: 'none' }}
onChange={(event) => handleFileChange(event, params)}
/>
</Button>
<Button title="Add Document" onClick={() => handleFileSelect(params.row.id)}>
<AddPhotoAlternateOutlinedIcon
sx={{ fontSize: "25px", color: "green" }}
/>
</Button>
</div>
);
},
},
];
], [rows, rowModesModel, t],);

return (
<Box
@@ -402,41 +482,48 @@ const ClaimInputGrid: React.FC<ClaimInputGridProps> = ({ ...props }) => {
},
}}
>
<DataGrid
sx={{ flex: 1 }}
rows={rows}
columns={columns}
editMode="row"
rowModesModel={rowModesModel}
onRowModesModelChange={handleRowModesModelChange}
onRowEditStop={handleRowEditStop}
processRowUpdate={processRowUpdate}
disableRowSelectionOnClick={true}
disableColumnMenu={true}
hideFooterPagination={true}
slots={
{
// footer: EditFooter,
{Boolean(errors.addClaimDetails?.type === "required") && <Typography sx={(theme) => ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap>
{t("Please ensure at least one row is created, and all the fields are inputted and saved")}
</Typography>}
{Boolean(errors.addClaimDetails?.type === "format") && <Typography sx={(theme) => ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap>
{t("Please ensure the date formats are correct")}
</Typography>}
<div style={{ height: 400, width: "100%" }}>
<DataGrid
sx={{ flex: 1 }}
rows={rows}
columns={columns}
editMode="row"
rowModesModel={rowModesModel}
onRowModesModelChange={handleRowModesModelChange}
onRowEditStop={handleRowEditStop}
processRowUpdate={processRowUpdate}
disableRowSelectionOnClick={true}
disableColumnMenu={true}
// hideFooterPagination={true}
slots={
{
// footer: EditFooter,
}
}
}
slotProps={
{
// footer: { setDay, setRows, setRowModesModel },
slotProps={
{
// footer: { setDay, setRows, setRowModesModel },
}
}
}
initialState={{
pagination: { paginationModel: { pageSize: 100 } },
}}
/>

initialState={{
pagination: { paginationModel: { pageSize: 5 } },
}}
/>
</div>
<BottomBar
getCostTotal={getCostTotal}
setRows={setRows}
setRowModesModel={setRowModesModel}
// sx={{flex:2}}
// sx={{flex:2}}
/>
</Box>
);
};

export default ClaimInputGrid;
export default ClaimFormInputGrid;

+ 1
- 0
src/components/ClaimDetail/index.ts 파일 보기

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

+ 24
- 29
src/components/ClaimSearch/ClaimSearch.tsx 파일 보기

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

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

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

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

const ClaimSearch: React.FC<Props> = ({ claims }) => {
const { t } = useTranslation("claims");
const { t } = useTranslation();

// If claim searching is done on the server-side, then no need for this.
const [filteredClaims, setFilteredClaims] = useState(claims);

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

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

const columns = useMemo<Column<ClaimResult>[]>(
const columns = useMemo<Column<Claim>[]>(
() => [
// {
// name: "action",
@@ -69,9 +56,9 @@ const ClaimSearch: React.FC<Props> = ({ claims }) => {
// },
{ name: "created", label: t("Creation Date") },
{ name: "name", label: t("Related Project Name") },
{ name: "cost", label: t("Cost (HKD)") },
{ name: "type", label: t("Expense Type") },
{ name: "status", label: t("Status") },
{ name: "cost", label: t("Amount (HKD)") },
{ name: "type", label: t("Expense Type"), needTranslation: true },
{ name: "status", label: t("Status"), needTranslation: true },
{ name: "remarks", label: t("Remarks") },
],
[t, onClaimClick],
@@ -82,10 +69,18 @@ const ClaimSearch: React.FC<Props> = ({ claims }) => {
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
console.log(query);
setFilteredClaims(
claims.filter(
(claim) =>
dateInRange(claim.created, query.created, query.createdTo ?? undefined) &&
claim.name.toLowerCase().includes(query.name.toLowerCase()) &&
(claim.type.toLowerCase().includes(query.type.toLowerCase()) || query.type.toLowerCase() === "all") &&
(claim.status.toLowerCase().includes(query.status.toLowerCase()) || query.status.toLowerCase() === "all")
),
);
}}
/>
<SearchResults<ClaimResult> items={filteredClaims} columns={columns} />
<SearchResults<Claim> items={filteredClaims} columns={columns} />
</>
);
};


+ 0
- 67
src/components/CreateClaim/ClaimDetails.tsx 파일 보기

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

import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import FormControl from "@mui/material/FormControl";
import Grid from "@mui/material/Grid";
import InputLabel from "@mui/material/InputLabel";
import MenuItem from "@mui/material/MenuItem";
import Select from "@mui/material/Select";
import TextField from "@mui/material/TextField";
import Typography from "@mui/material/Typography";
import { useTranslation } from "react-i18next";
import CardActions from "@mui/material/CardActions";
import RestartAlt from "@mui/icons-material/RestartAlt";
import Button from "@mui/material/Button";
import ClaimInputGrid from "./ClaimInputGrid";

const ClaimDetails: React.FC = () => {
const { t } = useTranslation();

return (
<Card>
<CardContent component={Stack} spacing={4}>
<Box>
{/* <Typography variant="overline" display="block" marginBlockEnd={1}>
{t("Related Project")}
</Typography> */}
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel>{t("Related Project")}</InputLabel>
<Select label={t("Project Category")}>
<MenuItem value={"M1001"}>{t("M1001")}</MenuItem>
<MenuItem value={"M1301"}>{t("M1301")}</MenuItem>
<MenuItem value={"M1354"}>{t("M1354")}</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel>{t("Expense Type")}</InputLabel>
<Select label={t("Team Lead")}>
<MenuItem value={"Petty Cash"}>{"Petty Cash"}</MenuItem>
<MenuItem value={"Expense"}>{"Expense"}</MenuItem>
</Select>
</FormControl>
</Grid>
</Grid>
</Box>

<Card>
<ClaimInputGrid />
</Card>

{/* <CardActions sx={{ justifyContent: "flex-end" }}>
<Button variant="text" startIcon={<RestartAlt />}>
{t("Reset")}
</Button>
</CardActions> */}
</CardContent>
</Card>
);
};

export default ClaimDetails;

+ 0
- 48
src/components/CreateClaim/CreateClaim.tsx 파일 보기

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

import Check from "@mui/icons-material/Check";
import Close from "@mui/icons-material/Close";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import Tab from "@mui/material/Tab";
import Tabs, { TabsProps } from "@mui/material/Tabs";
import { useRouter } from "next/navigation";
import React, { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import ClaimProjectDetails from "./ClaimDetails";
// import TaskSetup from "./TaskSetup";
// import StaffAllocation from "./StaffAllocation";
// import ResourceMilestone from "./ResourceMilestone";

const CreateProject: React.FC = () => {
const [tabIndex, setTabIndex] = useState(0);
const { t } = useTranslation();
const router = useRouter();

const handleCancel = () => {
router.back();
};

const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
(_e, newValue) => {
setTabIndex(newValue);
},
[],
);

return (
<>
<ClaimProjectDetails />
<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button variant="outlined" startIcon={<Close />} onClick={handleCancel}>
{t("Cancel")}
</Button>
<Button variant="contained" startIcon={<Check />}>
{t("Confirm")}
</Button>
</Stack>
</>
);
};

export default CreateProject;

+ 0
- 1
src/components/CreateClaim/index.ts 파일 보기

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

+ 4
- 3
src/components/CreateProject/CreateProject.tsx 파일 보기

@@ -35,7 +35,7 @@ import {
import { StaffResult } from "@/app/api/staff";
import { Typography } from "@mui/material";
import { Grade } from "@/app/api/grades";
import { Customer } from "@/app/api/customer";
import { Customer, Subsidiary } from "@/app/api/customer";

export interface Props {
allTasks: Task[];
@@ -43,6 +43,7 @@ export interface Props {
taskTemplates: TaskTemplate[];
teamLeads: StaffResult[];
allCustomers: Customer[];
allSubsidiaries: Subsidiary[];
fundingTypes: FundingType[];
serviceTypes: ServiceType[];
contractTypes: ContractType[];
@@ -50,8 +51,6 @@ export interface Props {
buildingTypes: BuildingType[];
workNatures: WorkNature[];
allStaffs: StaffResult[];

// Mocked
grades: Grade[];
}

@@ -76,6 +75,7 @@ const CreateProject: React.FC<Props> = ({
teamLeads,
grades,
allCustomers,
allSubsidiaries,
contractTypes,
fundingTypes,
locationTypes,
@@ -171,6 +171,7 @@ const CreateProject: React.FC<Props> = ({
locationTypes={locationTypes}
serviceTypes={serviceTypes}
allCustomers={allCustomers}
allSubsidiaries={allSubsidiaries}
projectCategories={projectCategories}
teamLeads={teamLeads}
isActive={tabIndex === 0}


+ 8
- 9
src/components/CreateProject/CreateProjectWrapper.tsx 파일 보기

@@ -10,7 +10,8 @@ import {
fetchProjectWorkNatures,
} from "@/app/api/projects";
import { fetchStaff, fetchTeamLeads } from "@/app/api/staff";
import { fetchAllCustomers } from "@/app/api/customer";
import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer";
import { fetchGrades } from "@/app/api/grades";

const CreateProjectWrapper: React.FC = async () => {
const [
@@ -19,6 +20,7 @@ const CreateProjectWrapper: React.FC = async () => {
projectCategories,
teamLeads,
allCustomers,
allSubsidiaries,
contractTypes,
fundingTypes,
locationTypes,
@@ -26,12 +28,14 @@ const CreateProjectWrapper: React.FC = async () => {
buildingTypes,
workNatures,
allStaffs,
grades,
] = await Promise.all([
fetchAllTasks(),
fetchTaskTemplates(),
fetchProjectCategories(),
fetchTeamLeads(),
fetchAllCustomers(),
fetchAllSubsidiaries(),
fetchProjectContractTypes(),
fetchProjectFundingTypes(),
fetchProjectLocationTypes(),
@@ -39,6 +43,7 @@ const CreateProjectWrapper: React.FC = async () => {
fetchProjectBuildingTypes(),
fetchProjectWorkNatures(),
fetchStaff(),
fetchGrades(),
]);

return (
@@ -47,6 +52,7 @@ const CreateProjectWrapper: React.FC = async () => {
projectCategories={projectCategories}
taskTemplates={taskTemplates}
teamLeads={teamLeads}
allSubsidiaries={allSubsidiaries}
allCustomers={allCustomers}
contractTypes={contractTypes}
fundingTypes={fundingTypes}
@@ -55,14 +61,7 @@ const CreateProjectWrapper: React.FC = async () => {
buildingTypes={buildingTypes}
workNatures={workNatures}
allStaffs={allStaffs}
// Mocks
grades={[
{ name: "Grade 1", id: 1, code: "1" },
{ name: "Grade 2", id: 2, code: "2" },
{ name: "Grade 3", id: 3, code: "3" },
{ name: "Grade 4", id: 4, code: "4" },
{ name: "Grade 5", id: 5, code: "5" },
]}
grades={grades}
/>
);
};


+ 7
- 3
src/components/CreateProject/MilestoneSection.tsx 파일 보기

@@ -43,7 +43,8 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => {
t,
i18n: { language },
} = useTranslation();
const { getValues, setValue } = useFormContext<CreateProjectInputs>();
const { getValues, setValue, formState } =
useFormContext<CreateProjectInputs>();
const [payments, setPayments] = useState<PaymentRow[]>(
getValues("milestones")[taskGroupId]?.payments || [],
);
@@ -223,6 +224,9 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => {
</Button>
);

const startDate = getValues("milestones")[taskGroupId]?.startDate;
const endDate = getValues("milestones")[taskGroupId]?.endDate;

return (
<Stack gap={1}>
<Typography variant="overline" display="block" marginBlockEnd={1}>
@@ -237,7 +241,7 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => {
<FormControl fullWidth>
<DatePicker
label={t("Stage Start Date")}
value={dayjs(getValues("milestones")[taskGroupId]?.startDate)}
value={startDate ? dayjs(startDate) : null}
onChange={(date) => {
if (!date) return;
const milestones = getValues("milestones");
@@ -256,7 +260,7 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => {
<FormControl fullWidth>
<DatePicker
label={t("Stage End Date")}
value={dayjs(getValues("milestones")[taskGroupId]?.endDate)}
value={endDate ? dayjs(endDate) : null}
onChange={(date) => {
if (!date) return;
const milestones = getValues("milestones");


+ 24
- 9
src/components/CreateProject/ProjectClientDetails.tsx 파일 보기

@@ -27,7 +27,7 @@ import {
WorkNature,
} from "@/app/api/projects";
import { StaffResult } from "@/app/api/staff";
import { Contact, Customer } from "@/app/api/customer";
import { Contact, Customer, Subsidiary } from "@/app/api/customer";
import Link from "next/link";
import React, { useEffect, useMemo, useState } from "react";
import { fetchCustomer } from "@/app/api/customer/actions";
@@ -39,6 +39,7 @@ interface Props {
projectCategories: ProjectCategory[];
teamLeads: StaffResult[];
allCustomers: Customer[];
allSubsidiaries: Subsidiary[];
serviceTypes: ServiceType[];
contractTypes: ContractType[];
fundingTypes: FundingType[];
@@ -52,6 +53,7 @@ const ProjectClientDetails: React.FC<Props> = ({
projectCategories,
teamLeads,
allCustomers,
allSubsidiaries,
serviceTypes,
contractTypes,
fundingTypes,
@@ -69,6 +71,13 @@ const ProjectClientDetails: React.FC<Props> = ({
getValues,
} = useFormContext<CreateProjectInputs>();

const subsidiaryMap = useMemo<{
[id: Subsidiary["id"]]: Subsidiary;
}>(
() => allSubsidiaries.reduce((acc, sub) => ({ ...acc, [sub.id]: sub }), {}),
[allSubsidiaries],
);

const selectedCustomerId = watch("clientId");
const selectedCustomer = useMemo(
() => allCustomers.find((c) => c.id === selectedCustomerId),
@@ -482,14 +491,20 @@ const ProjectClientDetails: React.FC<Props> = ({
name="clientSubsidiaryId"
render={({ field }) => (
<Select label={t("Client Lead")} {...field}>
{customerSubsidiaryIds.map((subsidiaryId, index) => (
<MenuItem
key={`${subsidiaryId}-${index}`}
value={subsidiaryId}
>
{subsidiaryId}
</MenuItem>
))}
{customerSubsidiaryIds
.filter((subId) => subsidiaryMap[subId])
.map((subsidiaryId, index) => {
const subsidiary = subsidiaryMap[subsidiaryId];

return (
<MenuItem
key={`${subsidiaryId}-${index}`}
value={subsidiaryId}
>
{`${subsidiary.code} - ${subsidiary.name}`}
</MenuItem>
);
})}
</Select>
)}
/>


+ 1
- 10
src/components/CreateProject/StaffAllocation.tsx 파일 보기

@@ -54,7 +54,7 @@ export interface Props {
}

const StaffAllocation: React.FC<Props> = ({
allStaffs: dataStaffs,
allStaffs,
allTasks,
isActive,
defaultManhourBreakdownByGrade,
@@ -63,15 +63,6 @@ const StaffAllocation: React.FC<Props> = ({
const { t } = useTranslation();
const { setValue, getValues, watch } = useFormContext<CreateProjectInputs>();

// TODO: remove this when grade and positions are done
const allStaffs = useMemo<StaffResult[]>(() => {
return dataStaffs.map((staff, index) => ({
...staff,
grade: grades[index % grades.length].name,
currentPosition: `Mock Postion ${index}`,
}));
}, [dataStaffs, grades]);

const [filteredStaff, setFilteredStaff] = React.useState(
allStaffs.sort(staffComparator),
);


+ 126
- 0
src/components/CreateTeam/CreateTeam.tsx 파일 보기

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

import {
FieldErrors,
FormProvider,
SubmitErrorHandler,
SubmitHandler,
useForm,
} from "react-hook-form";
import StaffAllocation from "./StaffAllocation";
import { StaffResult } from "@/app/api/staff";
import { CreateTeamInputs, saveTeam } from "@/app/api/team/actions";
import { Button, Stack, Tab, Tabs, TabsProps, Typography } from "@mui/material";
import { Check, Close } from "@mui/icons-material";
import { useCallback, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useTranslation } from "react-i18next";
import { Error } from "@mui/icons-material";
import TeamInfo from "./TeamInfo";

export interface Props {
allstaff: StaffResult[];
}

const CreateTeam: React.FC<Props> = ({ allstaff }) => {
const formProps = useForm<CreateTeamInputs>();
const [serverError, setServerError] = useState("");
const router = useRouter();
const [tabIndex, setTabIndex] = useState(0);
const { t } = useTranslation();
const searchParams = useSearchParams()

const errors = formProps.formState.errors;

const onSubmit = useCallback<SubmitHandler<CreateTeamInputs>>(
async (data) => {
try {
console.log(data);
await saveTeam(data);
router.replace("/settings/team");
} catch (e) {
console.log(e);
setServerError(t("An error has occurred. Please try again later."));
}
},
[router]
);

const handleCancel = () => {
router.back();
};

const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
(_e, newValue) => {
setTabIndex(newValue);
},
[],
);
const hasErrorsInTab = (
tabIndex: number,
errors: FieldErrors<CreateTeamInputs>,
) => {
switch (tabIndex) {
case 0:
return Object.keys(errors).length > 0;
default:
false;
}
};
return (
<>
<FormProvider {...formProps}>
<Stack
spacing={2}
component="form"
onSubmit={formProps.handleSubmit(onSubmit)}
>
<Tabs
value={tabIndex}
onChange={handleTabChange}
variant="scrollable"
>
<Tab
label={t("Team Info")}
icon={
hasErrorsInTab(0, errors) ? (
<Error sx={{ marginInlineEnd: 1 }} color="error" />
) : undefined
}
iconPosition="end"
/>
<Tab label={t("Subsidiary Allocation")} iconPosition="end" />
</Tabs>
{serverError && (
<Typography variant="body2" color="error" alignSelf="flex-end">
{serverError}
</Typography>
)}
{tabIndex === 0 && <TeamInfo/>}
{tabIndex === 1 && <StaffAllocation allStaffs={allstaff} />}

{/* <StaffAllocation allStaffs={allstaff} /> */}
<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button
variant="outlined"
startIcon={<Close />}
onClick={handleCancel}
>
{t("Cancel")}
</Button>
<Button
variant="contained"
startIcon={<Check />}
type="submit"
// disabled={Boolean(formProps.watch("isGridEditing"))}
>
{t("Confirm")}
</Button>
</Stack>
</Stack>
</FormProvider>
</>
);
};

export default CreateTeam;

+ 40
- 0
src/components/CreateTeam/CreateTeamLoading.tsx 파일 보기

@@ -0,0 +1,40 @@
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Skeleton from "@mui/material/Skeleton";
import Stack from "@mui/material/Stack";
import React from "react";

// Can make this nicer
export const CreateTeamLoading: React.FC = () => {
return (
<>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton
variant="rounded"
height={50}
width={100}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
</CardContent>
</Card>
<Card>CreateTeam
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
</Stack>
</CardContent>
</Card>
</>
);
};

export default CreateTeamLoading;

+ 26
- 0
src/components/CreateTeam/CreateTeamWrapper.tsx 파일 보기

@@ -0,0 +1,26 @@
import React from "react";
import CreateTeam from "./CreateTeam";
import CreateTeamLoading from "./CreateTeamLoading";
// import { fetchTeam, fetchTeamLeads } from "@/app/api/team";
import { useSearchParams } from "next/navigation";
import { fetchStaffCombo } from "@/app/api/staff/actions";
import { fetchStaff } from "@/app/api/staff";

interface SubComponents {
Loading: typeof CreateTeamLoading;
}

const CreateTeamWrapper: React.FC & SubComponents = async () => {

const [
staff,
] = await Promise.all([
fetchStaff(),
]);

return <CreateTeam allstaff={staff}/>;
};

CreateTeamWrapper.Loading = CreateTeamLoading;

export default CreateTeamWrapper;

+ 238
- 0
src/components/CreateTeam/StaffAllocation.tsx 파일 보기

@@ -0,0 +1,238 @@
"use client";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import CustomInputForm from "../CustomInputForm";
import { useRouter } from "next/navigation";
import { useTranslation } from "react-i18next";
import {
FieldErrors,
FormProvider,
SubmitErrorHandler,
SubmitHandler,
useForm,
useFormContext,
} from "react-hook-form";
import CreateTeamForm from "../CreateTeamForm";
import { CreateTeamInputs } from "@/app/api/team/actions";
import { Staff4TransferList, fetchStaffCombo } from "@/app/api/staff/actions";
import { StaffResult, StaffTeamTable } from "@/app/api/staff";
import SearchResults, { Column } from "../SearchResults";
import { Clear, PersonAdd, PersonRemove, Search } from "@mui/icons-material";
import { Card } from "reactstrap";
import { Box, CardContent, Grid, IconButton, InputAdornment, Stack, Tab, Tabs, TabsProps, TextField, Typography } from "@mui/material";
import { differenceBy } from "lodash";
import StarsIcon from '@mui/icons-material/Stars';

export interface Props {
allStaffs: StaffResult[];
}

const StaffAllocation: React.FC<Props> = ({ allStaffs: staff }) => {
const { t } = useTranslation();
const {
setValue,
getValues,
formState: { defaultValues },
reset,
resetField,
} = useFormContext<CreateTeamInputs>();
const initialStaffs = staff.map((s) => ({ ...s }));
// console.log(initialStaffs)
const [filteredStaff, setFilteredStaff] = useState(initialStaffs);
const [selectedStaff, setSelectedStaff] = useState<typeof filteredStaff>(
initialStaffs.filter((s) => getValues("addStaffIds")?.includes(s.id))
);
const [seletedTeamLead, setSeletedTeamLead] = useState<number>()
// Adding / Removing staff

const addStaff = useCallback((staff: StaffResult) => {
setSelectedStaff((s) => [...s, staff]);
}, []);

const removeStaff = useCallback((staff: StaffResult) => {
setSelectedStaff((s) => s.filter((s) => s.id !== staff.id));
}, []);

const setTeamLead = useCallback((staff: StaffResult) => {
setSeletedTeamLead(staff.id)
const rearrangedList = getValues("addStaffIds").reduce<number[]>((acc, num, index) => {
if (num === staff.id && index !== 0) {
acc.splice(index, 1);
acc.unshift(num)
}
return acc;
}, getValues("addStaffIds"));
console.log(rearrangedList)
console.log(selectedStaff)

const rearrangedStaff = rearrangedList.map((id) => {
return selectedStaff.find((staff) => staff.id === id);
});
console.log(rearrangedStaff)
setSelectedStaff(rearrangedStaff as StaffResult[]);

setValue("addStaffIds", rearrangedList)
}, [addStaff, selectedStaff]);

const clearSubsidiary = useCallback(() => {
if (defaultValues !== undefined) {
resetField("addStaffIds");
setSelectedStaff(
initialStaffs.filter((s) => defaultValues.addStaffIds?.includes(s.id))
);
}
}, [defaultValues]);

// Sync with form
useEffect(() => {
console.log(selectedStaff)
setValue(
"addStaffIds",
selectedStaff.map((s) => s.id)
);
}, [selectedStaff, setValue]);

useEffect(() => {
console.log(selectedStaff)
}, [selectedStaff]);

const StaffPoolColumns = useMemo<Column<StaffResult>[]>(
() => [
{
label: t("Add"),
name: "id",
onClick: addStaff,
buttonIcon: <PersonAdd />,
},
{ label: t("Staff Id"), name: "staffId" },
{ label: t("Staff Name"), name: "name" },
{ label: t("Current Position"), name: "currentPosition" },
],
[addStaff, t]
);

const allocatedStaffColumns = useMemo<Column<StaffResult>[]>(
() => [
{
label: t("Remove"),
name: "action",
onClick: removeStaff,
buttonIcon: <PersonRemove />,
},
{ label: t("Staff Id"), name: "staffId" },
{ label: t("Staff Name"), name: "name" },
{ label: t("Current Position"), name: "currentPosition" },
{
label: t("Team Lead"),
name: "action",
onClick: setTeamLead,
buttonIcon: <StarsIcon />,
},
],
[removeStaff, selectedStaff, t]
);

const [query, setQuery] = React.useState("");
const onQueryInputChange = React.useCallback<
React.ChangeEventHandler<HTMLInputElement>
>((e) => {
setQuery(e.target.value);
}, []);
const clearQueryInput = React.useCallback(() => {
setQuery("");
}, []);

React.useEffect(() => {
// setFilteredStaff(
// initialStaffs.filter((s) => {
// const q = query.toLowerCase();
// // s.staffId.toLowerCase().includes(q)
// // const q = query.toLowerCase();
// // return s.name.toLowerCase().includes(q);
// // s.code.toString().includes(q) ||
// // (s.brNo != null && s.brNo.toLowerCase().includes(q))
// })
// );
}, [staff, query]);

const resetStaff = React.useCallback(() => {
clearQueryInput();
clearSubsidiary();
}, [clearQueryInput, clearSubsidiary]);

const formProps = useForm({
});

// Tab related
const [tabIndex, setTabIndex] = React.useState(0);
const handleTabChange = React.useCallback<NonNullable<TabsProps["onChange"]>>(
(_e, newValue) => {
setTabIndex(newValue);
},
[],
);

return (
<>
<FormProvider {...formProps}>
<Card sx={{ display: "block" }}>
<CardContent
sx={{ display: "flex", flexDirection: "column", gap: 1 }}
>
<Stack gap={2}>
<Typography variant="overline" display="block">
{t("staff")}
</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6} display="flex" alignItems="center">
<Search sx={{ marginInlineEnd: 1 }} />
<TextField
variant="standard"
fullWidth
onChange={onQueryInputChange}
value={query}
placeholder={t("Search by subsidiary code, name or br no.")}
InputProps={{
endAdornment: query && (
<InputAdornment position="end">
<IconButton onClick={clearQueryInput}>
<Clear />
</IconButton>
</InputAdornment>
),
}}
/>
</Grid>
</Grid>
<Tabs value={tabIndex} onChange={handleTabChange}>
<Tab label={t("Staff Pool")} />
<Tab
label={`${t("Allocated Staff")} (${selectedStaff.length})`}
/>
</Tabs>
<Box sx={{ marginInline: -3 }}>
{tabIndex === 0 && (
<SearchResults
noWrapper
items={differenceBy(filteredStaff, selectedStaff, "id")}
columns={StaffPoolColumns}
/>
)}
{tabIndex === 1 && (
<SearchResults
noWrapper
items={selectedStaff}
columns={allocatedStaffColumns}
/>
)}
</Box>
</Stack>
</CardContent>
</Card>
</FormProvider>
</>
);
};

export default StaffAllocation;

+ 69
- 0
src/components/CreateTeam/TeamInfo.tsx 파일 보기

@@ -0,0 +1,69 @@
"use client";
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Grid from "@mui/material/Grid";
import TextField from "@mui/material/TextField";
import Typography from "@mui/material/Typography";
import { useTranslation } from "react-i18next";
import CardActions from "@mui/material/CardActions";
import RestartAlt from "@mui/icons-material/RestartAlt";
import Button from "@mui/material/Button";
import { Controller, useFormContext } from "react-hook-form";
import { FormControl, InputLabel, MenuItem, Select } from "@mui/material";
import { useCallback } from "react";
import { CreateTeamInputs } from "@/app/api/team/actions";

const TeamInfo: React.FC = (
{
// customerTypes,
}
) => {
const { t } = useTranslation();
const {
register,
formState: { errors, defaultValues },
control,
reset,
resetField,
setValue,
} = useFormContext<CreateTeamInputs>();

const resetCustomer = useCallback(() => {
console.log(defaultValues);
if (defaultValues !== undefined) {
resetField("description");
}
}, [defaultValues]);

return (
<>
<Card sx={{ display: "block" }}>
<CardContent component={Stack} spacing={4}>
<Box>
<Typography variant="overline" display="block" marginBlockEnd={1}>
{t("Team Info")}
</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={12}>
<TextField
label={t("Team Description")}
fullWidth
multiline
rows={4}
{...register("description", {
required: true,
})}
error={Boolean(errors.description)}
helperText={Boolean(errors.description) && (errors.description?.message ? t(errors.description.message) : t("Please input correct description"))}
/>
</Grid>
</Grid>
</Box>
</CardContent>
</Card>
</>
);
};
export default TeamInfo;

+ 1
- 0
src/components/CreateTeam/index.ts 파일 보기

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

+ 1
- 1
src/components/CustomDatagrid/CustomDatagrid.tsx 파일 보기

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


+ 1
- 1
src/components/CustomerDetail/ContactInfo.tsx 파일 보기

@@ -262,7 +262,7 @@ const ContactInfo: React.FC<Props> = ({
{t("Contact Info")}
</Typography>
{Boolean(errors.addContacts?.type === "required") && <Typography sx={(theme) => ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap>
{t("Please ensure all the fields are inputted and saved")}
{t("Please ensure at least one row is created, and all the fields are inputted and saved")}
</Typography>}
{Boolean(errors.addContacts?.type === "email_format") && <Typography sx={(theme) => ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap>
{t("Please ensure all the email formats are correct")}


+ 1
- 1
src/components/CustomerDetail/CustomerDetailWrapper.tsx 파일 보기

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

// type Props = {


+ 2
- 2
src/components/CustomerDetail/CustomerInfo.tsx 파일 보기

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


+ 1
- 0
src/components/CustomerSearch/CustomerSearch.tsx 파일 보기

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


+ 17
- 0
src/components/Report/ProjectCompletionReport/ProjectCompletionReport.tsx 파일 보기

@@ -0,0 +1,17 @@
//src\components\LateStartReport\LateStartReport.tsx
"use client";
import * as React from "react";
import "../../../app/global.css";
import { Suspense } from "react";
import ProjectCompletionReportGen from "@/components/Report/ProjectCompletionReportGen";

const ProjectCompletionReport: React.FC = () => {

return (
<Suspense fallback={<ProjectCompletionReportGen.Loading />}>
<ProjectCompletionReportGen />
</Suspense>
);
};

export default ProjectCompletionReport;

+ 2
- 0
src/components/Report/ProjectCompletionReport/index.ts 파일 보기

@@ -0,0 +1,2 @@
//src\components\LateStartReport\index.ts
export { default } from "./ProjectCompletionReport";

+ 44
- 0
src/components/Report/ProjectCompletionReportGen/ProjectCompletionReportGen.tsx 파일 보기

@@ -0,0 +1,44 @@
//src\components\LateStartReportGen\LateStartReportGen.tsx
"use client";
import React, { useMemo, useState } from "react";
import SearchBox, { Criterion } from "../../ReportSearchBox5";
import { useTranslation } from "react-i18next";
import { ProjectCompletion } from "@/app/api/report5";
//import { DownloadReportButton } from './DownloadReportButton';
interface Props {
projects: ProjectCompletion[];
}
type SearchQuery = Partial<Omit<ProjectCompletion, "id">>;
type SearchParamNames = keyof SearchQuery;

const ProgressByClientSearch: React.FC<Props> = ({ projects }) => {
const { t } = useTranslation("projects");

const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
() => [
// { label: "Team", paramName: "team", type: "text" },
// { label: "Client", paramName: "client", type: "text" },
{
label: "Report Period From",
label2: "Report Period To",
paramName: "targetEndDate",
type: "dateRange",
},
],
[t],
);

return (
<>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
console.log(query);
}}
/>
{/* <DownloadReportButton /> */}
</>
);
};

export default ProgressByClientSearch;

+ 41
- 0
src/components/Report/ProjectCompletionReportGen/ProjectCompletionReportGenLoading.tsx 파일 보기

@@ -0,0 +1,41 @@
//src\components\LateStartReportGen\LateStartReportGenLoading.tsx
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Skeleton from "@mui/material/Skeleton";
import Stack from "@mui/material/Stack";
import React from "react";

// Can make this nicer
export const ProjectCompletionReportGenLoading: React.FC = () => {
return (
<>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton
variant="rounded"
height={50}
width={100}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
</CardContent>
</Card>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
</Stack>
</CardContent>
</Card>
</>
);
};

export default ProjectCompletionReportGenLoading;

+ 19
- 0
src/components/Report/ProjectCompletionReportGen/ProjectCompletionReportGenWrapper.tsx 파일 보기

@@ -0,0 +1,19 @@
//src\components\LateStartReportGen\LateStartReportGenWrapper.tsx
import { fetchProjectsProjectCompletion } from "@/app/api/report5";
import React from "react";
import ProjectCompletionReportGen from "./ProjectCompletionReportGen";
import ProjectCompletionReportGenLoading from "./ProjectCompletionReportGenLoading";

interface SubComponents {
Loading: typeof ProjectCompletionReportGenLoading;
}

const ProjectCompletionReportGenWrapper: React.FC & SubComponents = async () => {
const clentprojects = await fetchProjectsProjectCompletion();

return <ProjectCompletionReportGen projects={clentprojects} />;
};

ProjectCompletionReportGenWrapper.Loading = ProjectCompletionReportGenLoading;

export default ProjectCompletionReportGenWrapper;

+ 2
- 0
src/components/Report/ProjectCompletionReportGen/index.ts 파일 보기

@@ -0,0 +1,2 @@
//src\components\LateStartReportGen\index.ts
export { default } from "./ProjectCompletionReportGenWrapper";

+ 17
- 0
src/components/Report/ProjectCompletionReportWO/ProjectCompletionReportWO.tsx 파일 보기

@@ -0,0 +1,17 @@
//src\components\LateStartReport\LateStartReport.tsx
"use client";
import * as React from "react";
import "../../../app/global.css";
import { Suspense } from "react";
import ProjectCompletionReportWOGen from "@/components/Report/ProjectCompletionReportWOGen";

const ProjectCompletionReportWO: React.FC = () => {

return (
<Suspense fallback={<ProjectCompletionReportWOGen.Loading />}>
<ProjectCompletionReportWOGen />
</Suspense>
);
};

export default ProjectCompletionReportWO;

+ 2
- 0
src/components/Report/ProjectCompletionReportWO/index.ts 파일 보기

@@ -0,0 +1,2 @@
//src\components\LateStartReport\index.ts
export { default } from "./ProjectCompletionReportWO";

+ 44
- 0
src/components/Report/ProjectCompletionReportWOGen/ProjectCompletionReportWOGen.tsx 파일 보기

@@ -0,0 +1,44 @@
//src\components\LateStartReportGen\LateStartReportGen.tsx
"use client";
import React, { useMemo, useState } from "react";
import SearchBox, { Criterion } from "../../ReportSearchBox6";
import { useTranslation } from "react-i18next";
import { ProjectCompletionWO } from "@/app/api/report6";
//import { DownloadReportButton } from './DownloadReportButton';
interface Props {
projects: ProjectCompletionWO[];
}
type SearchQuery = Partial<Omit<ProjectCompletionWO, "id">>;
type SearchParamNames = keyof SearchQuery;

const ProgressByClientSearch: React.FC<Props> = ({ projects }) => {
const { t } = useTranslation("projects");

const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
() => [
// { label: "Team", paramName: "team", type: "text" },
// { label: "Client", paramName: "client", type: "text" },
{
label: "Report Period From",
label2: "Report Period To",
paramName: "targetEndDate",
type: "dateRange",
},
],
[t],
);

return (
<>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
console.log(query);
}}
/>
{/* <DownloadReportButton /> */}
</>
);
};

export default ProgressByClientSearch;

+ 41
- 0
src/components/Report/ProjectCompletionReportWOGen/ProjectCompletionReportWOGenLoading.tsx 파일 보기

@@ -0,0 +1,41 @@
//src\components\LateStartReportGen\LateStartReportGenLoading.tsx
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Skeleton from "@mui/material/Skeleton";
import Stack from "@mui/material/Stack";
import React from "react";

// Can make this nicer
export const ProjectCompletionReportGenLoading: React.FC = () => {
return (
<>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton
variant="rounded"
height={50}
width={100}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
</CardContent>
</Card>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
</Stack>
</CardContent>
</Card>
</>
);
};

export default ProjectCompletionReportGenLoading;

+ 19
- 0
src/components/Report/ProjectCompletionReportWOGen/ProjectCompletionReportWOGenWrapper.tsx 파일 보기

@@ -0,0 +1,19 @@
//src\components\LateStartReportGen\LateStartReportGenWrapper.tsx
import { fetchProjectsProjectCompletionWO } from "@/app/api/report6";
import React from "react";
import ProjectCompletionReportWOGen from "./ProjectCompletionReportWOGen";
import ProjectCompletionReportWOGenLoading from "./ProjectCompletionReportWOGenLoading";

interface SubComponents {
Loading: typeof ProjectCompletionReportWOGenLoading;
}

const ProjectCompletionReportWOGenWrapper: React.FC & SubComponents = async () => {
const clentprojects = await fetchProjectsProjectCompletionWO();

return <ProjectCompletionReportWOGen projects={clentprojects} />;
};

ProjectCompletionReportWOGenWrapper.Loading = ProjectCompletionReportWOGenLoading;

export default ProjectCompletionReportWOGenWrapper;

+ 2
- 0
src/components/Report/ProjectCompletionReportWOGen/index.ts 파일 보기

@@ -0,0 +1,2 @@
//src\components\LateStartReportGen\index.ts
export { default } from "./ProjectCompletionReportWOGenWrapper";

+ 2
- 2
src/components/ReportSearchBox/SearchBox.tsx 파일 보기

@@ -289,9 +289,9 @@ function SearchBox<T extends string>({
<Button
variant="outlined"
startIcon={<Search />}
onClick={handleSearch}
onClick={handleDownload}
>
{t("Search")}
{t("Download")}
</Button>
</CardActions>
</CardContent>


+ 2
- 2
src/components/ReportSearchBox2/SearchBox2.tsx 파일 보기

@@ -289,9 +289,9 @@ function SearchBox<T extends string>({
<Button
variant="outlined"
startIcon={<Search />}
onClick={handleSearch}
onClick={handleDownload}
>
{t("Search")}
{t("Download")}
</Button>
</CardActions>
</CardContent>


+ 2
- 2
src/components/ReportSearchBox3/SearchBox3.tsx 파일 보기

@@ -289,9 +289,9 @@ function SearchBox<T extends string>({
<Button
variant="outlined"
startIcon={<Search />}
onClick={handleSearch}
onClick={handleDownload}
>
{t("Search")}
{t("Download")}
</Button>
</CardActions>
</CardContent>


+ 2
- 2
src/components/ReportSearchBox4/SearchBox4.tsx 파일 보기

@@ -289,9 +289,9 @@ function SearchBox<T extends string>({
<Button
variant="outlined"
startIcon={<Search />}
onClick={handleSearch}
onClick={handleDownload}
>
{t("Search")}
{t("Download")}
</Button>
</CardActions>
</CardContent>


+ 302
- 0
src/components/ReportSearchBox5/SearchBox5.tsx 파일 보기

@@ -0,0 +1,302 @@
//src\components\ReportSearchBox\SearchBox.tsx
"use client";

import Grid from "@mui/material/Grid";
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Typography from "@mui/material/Typography";
import React, { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import TextField from "@mui/material/TextField";
import FormControl from "@mui/material/FormControl";
import InputLabel from "@mui/material/InputLabel";
import Select, { SelectChangeEvent } from "@mui/material/Select";
import MenuItem from "@mui/material/MenuItem";
import CardActions from "@mui/material/CardActions";
import Button from "@mui/material/Button";
import RestartAlt from "@mui/icons-material/RestartAlt";
import Search from "@mui/icons-material/Search";
import dayjs from "dayjs";
import "dayjs/locale/zh-hk";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { Box } from "@mui/material";
import * as XLSX from 'xlsx-js-style';
//import { DownloadReportButton } from '../LateStartReportGen/DownloadReportButton';

interface BaseCriterion<T extends string> {
label: string;
label2?: string;
paramName: T;
paramName2?: T;
}

interface TextCriterion<T extends string> extends BaseCriterion<T> {
type: "text";
}

interface SelectCriterion<T extends string> extends BaseCriterion<T> {
type: "select";
options: string[];
}

interface DateRangeCriterion<T extends string> extends BaseCriterion<T> {
type: "dateRange";
}

export type Criterion<T extends string> =
| TextCriterion<T>
| SelectCriterion<T>
| DateRangeCriterion<T>;

interface Props<T extends string> {
criteria: Criterion<T>[];
onSearch: (inputs: Record<T, string>) => void;
onReset?: () => void;
}

function SearchBox<T extends string>({
criteria,
onSearch,
onReset,
}: Props<T>) {
const { t } = useTranslation("common");
const defaultInputs = useMemo(
() =>
criteria.reduce<Record<T, string>>(
(acc, c) => {
return { ...acc, [c.paramName]: c.type === "select" ? "All" : "" };
},
{} as Record<T, string>,
),
[criteria],
);
const [inputs, setInputs] = useState(defaultInputs);
const makeInputChangeHandler = useCallback(
(paramName: T): React.ChangeEventHandler<HTMLInputElement> => {
return (e) => {
setInputs((i) => ({ ...i, [paramName]: e.target.value }));
};
},
[],
);

const makeSelectChangeHandler = useCallback((paramName: T) => {
return (e: SelectChangeEvent) => {
setInputs((i) => ({ ...i, [paramName]: e.target.value }));
};
}, []);

const makeDateChangeHandler = useCallback((paramName: T) => {
return (e: any) => {
setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM-DD") }));
};
}, []);

const makeDateToChangeHandler = useCallback((paramName: T) => {
return (e: any) => {
setInputs((i) => ({
...i,
[paramName + "To"]: dayjs(e).format("YYYY-MM-DD"),
}));
};
}, []);

const handleReset = () => {
setInputs(defaultInputs);
onReset?.();
};

const handleSearch = () => {
onSearch(inputs);
};
const handleDownload = async () => {
//setIsLoading(true);

try {
const response = await fetch('/temp/AR05_Project Completion Report.xlsx', {
headers: {
'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
},
});
if (!response.ok) throw new Error('Network response was not ok.');

const data = await response.blob();
const reader = new FileReader();
reader.onload = (e) => {
if (e.target && e.target.result) {
const ab = e.target.result as ArrayBuffer;
const workbook = XLSX.read(ab, { type: 'array' });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
// Add the current date to cell C2
const cellAddress = 'C2';
const date = new Date().toISOString().split('T')[0]; // Format YYYY-MM-DD
const formattedDate = date.replace(/-/g, '/'); // Change format to YYYY/MM/DD
XLSX.utils.sheet_add_aoa(worksheet, [[formattedDate]], { origin: cellAddress });

// Calculate the maximum length of content in each column and set column width
const colWidths: number[] = [];

const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "", blankrows: true }) as (string | number)[][];
jsonData.forEach((row: (string | number)[]) => {
row.forEach((cell: string | number, index: number) => {
const valueLength = cell.toString().length;
colWidths[index] = Math.max(colWidths[index] || 0, valueLength);
});
});

// Apply calculated widths to each column, skipping column A
worksheet['!cols'] = colWidths.map((width, index) => {
if (index === 0) {
return { wch: 8 }; // Set default or specific width for column A if needed
}
return { wch: width + 2 }; // Add padding to width
});

// Style for cell A1: Font size 16 and bold
if (worksheet['A1']) {
worksheet['A1'].s = {
font: {
bold: true,
sz: 16, // Font size 16
//name: 'Times New Roman' // Specify font
}
};
}

// Apply styles from A2 to A3 (bold)
['A2', 'A3'].forEach(cell => {
if (worksheet[cell]) {
worksheet[cell].s = { font: { bold: true } };
}
});

// Formatting from A5 to F5
// Apply styles from A5 to F5 (bold, bottom border, center alignment)
for (let col = 0; col < 6; col++) { // Columns A to F
const cellRef = XLSX.utils.encode_col(col) + '5';
if (worksheet[cellRef]) {
worksheet[cellRef].s = {
font: { bold: true },
alignment: { horizontal: 'center' },
border: {
bottom: { style: 'thin', color: { auto: 1 } }
}
};
}
}

// Format filename with date
const today = new Date().toISOString().split('T')[0].replace(/-/g, '_'); // Get current date and format as YYYY_MM_DD
const filename = `AR05_Project_Completion_Report_${today}.xlsx`; // Append formatted date to the filename

// Convert workbook back to XLSX file
XLSX.writeFile(workbook, filename);
} else {
throw new Error('Failed to load file');
}
};
reader.readAsArrayBuffer(data);
} catch (error) {
console.error('Error downloading the file: ', error);
}

//setIsLoading(false);
};
return (
<Card>
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
<Typography variant="overline">{t("Search Criteria")}</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
{criteria.map((c) => {
return (
<Grid key={c.paramName} item xs={6}>
{c.type === "text" && (
<TextField
label={c.label}
fullWidth
onChange={makeInputChangeHandler(c.paramName)}
value={inputs[c.paramName]}
/>
)}
{c.type === "select" && (
<FormControl fullWidth>
<InputLabel>{c.label}</InputLabel>
<Select
label={c.label}
onChange={makeSelectChangeHandler(c.paramName)}
value={inputs[c.paramName]}
>
<MenuItem value={"All"}>{t("All")}</MenuItem>
{c.options.map((option, index) => (
<MenuItem key={`${option}-${index}`} value={option}>
{option}
</MenuItem>
))}
</Select>
</FormControl>
)}
{c.type === "dateRange" && (
<LocalizationProvider
dateAdapter={AdapterDayjs}
// TODO: Should maybe use a custom adapterLocale here to support YYYY-MM-DD
adapterLocale="zh-hk"
>
<Box display="flex">
<FormControl fullWidth>
<DatePicker
label={c.label}
onChange={makeDateChangeHandler(c.paramName)}
value={inputs[c.paramName] ? dayjs(inputs[c.paramName]) : null}
/>
</FormControl>
<Box
display="flex"
alignItems="center"
justifyContent="center"
marginInline={2}
>
{"-"}
</Box>
<FormControl fullWidth>
<DatePicker
label={c.label2}
onChange={makeDateToChangeHandler(c.paramName)}
value={inputs[c.paramName.concat("To") as T] ? dayjs(inputs[c.paramName.concat("To") as T]) : null}
/>
</FormControl>
</Box>
</LocalizationProvider>
)}
</Grid>
);
})}
</Grid>
<CardActions sx={{ justifyContent: "flex-end" }}>
<Button
variant="text"
startIcon={<RestartAlt />}
onClick={handleReset}
>
{t("Reset")}
</Button>
<Button
variant="outlined"
startIcon={<Search />}
onClick={handleDownload}
>
{t("Download")}
</Button>
</CardActions>
</CardContent>
</Card>
);
}

export default SearchBox;

+ 3
- 0
src/components/ReportSearchBox5/index.ts 파일 보기

@@ -0,0 +1,3 @@
//src\components\SearchBox\index.ts
export { default } from "./SearchBox5";
export type { Criterion } from "./SearchBox5";

+ 302
- 0
src/components/ReportSearchBox6/SearchBox6.tsx 파일 보기

@@ -0,0 +1,302 @@
//src\components\ReportSearchBox\SearchBox.tsx
"use client";

import Grid from "@mui/material/Grid";
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Typography from "@mui/material/Typography";
import React, { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import TextField from "@mui/material/TextField";
import FormControl from "@mui/material/FormControl";
import InputLabel from "@mui/material/InputLabel";
import Select, { SelectChangeEvent } from "@mui/material/Select";
import MenuItem from "@mui/material/MenuItem";
import CardActions from "@mui/material/CardActions";
import Button from "@mui/material/Button";
import RestartAlt from "@mui/icons-material/RestartAlt";
import Search from "@mui/icons-material/Search";
import dayjs from "dayjs";
import "dayjs/locale/zh-hk";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { Box } from "@mui/material";
import * as XLSX from 'xlsx-js-style';
//import { DownloadReportButton } from '../LateStartReportGen/DownloadReportButton';

interface BaseCriterion<T extends string> {
label: string;
label2?: string;
paramName: T;
paramName2?: T;
}

interface TextCriterion<T extends string> extends BaseCriterion<T> {
type: "text";
}

interface SelectCriterion<T extends string> extends BaseCriterion<T> {
type: "select";
options: string[];
}

interface DateRangeCriterion<T extends string> extends BaseCriterion<T> {
type: "dateRange";
}

export type Criterion<T extends string> =
| TextCriterion<T>
| SelectCriterion<T>
| DateRangeCriterion<T>;

interface Props<T extends string> {
criteria: Criterion<T>[];
onSearch: (inputs: Record<T, string>) => void;
onReset?: () => void;
}

function SearchBox<T extends string>({
criteria,
onSearch,
onReset,
}: Props<T>) {
const { t } = useTranslation("common");
const defaultInputs = useMemo(
() =>
criteria.reduce<Record<T, string>>(
(acc, c) => {
return { ...acc, [c.paramName]: c.type === "select" ? "All" : "" };
},
{} as Record<T, string>,
),
[criteria],
);
const [inputs, setInputs] = useState(defaultInputs);

const makeInputChangeHandler = useCallback(
(paramName: T): React.ChangeEventHandler<HTMLInputElement> => {
return (e) => {
setInputs((i) => ({ ...i, [paramName]: e.target.value }));
};
},
[],
);

const makeSelectChangeHandler = useCallback((paramName: T) => {
return (e: SelectChangeEvent) => {
setInputs((i) => ({ ...i, [paramName]: e.target.value }));
};
}, []);

const makeDateChangeHandler = useCallback((paramName: T) => {
return (e: any) => {
setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM-DD") }));
};
}, []);

const makeDateToChangeHandler = useCallback((paramName: T) => {
return (e: any) => {
setInputs((i) => ({
...i,
[paramName + "To"]: dayjs(e).format("YYYY-MM-DD"),
}));
};
}, []);

const handleReset = () => {
setInputs(defaultInputs);
onReset?.();
};

const handleSearch = () => {
onSearch(inputs);
};
const handleDownload = async () => {
//setIsLoading(true);

try {
const response = await fetch('/temp/AR06_Project Completion Report with Outstanding Un-billed Hours.xlsx', {
headers: {
'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
},
});
if (!response.ok) throw new Error('Network response was not ok.');

const data = await response.blob();
const reader = new FileReader();
reader.onload = (e) => {
if (e.target && e.target.result) {
const ab = e.target.result as ArrayBuffer;
const workbook = XLSX.read(ab, { type: 'array' });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
// Add the current date to cell C2
const cellAddress = 'C2';
const date = new Date().toISOString().split('T')[0]; // Format YYYY-MM-DD
const formattedDate = date.replace(/-/g, '/'); // Change format to YYYY/MM/DD
XLSX.utils.sheet_add_aoa(worksheet, [[formattedDate]], { origin: cellAddress });

// Calculate the maximum length of content in each column and set column width
const colWidths: number[] = [];

const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "", blankrows: true }) as (string | number)[][];
jsonData.forEach((row: (string | number)[]) => {
row.forEach((cell: string | number, index: number) => {
const valueLength = cell.toString().length;
colWidths[index] = Math.max(colWidths[index] || 0, valueLength);
});
});

// Apply calculated widths to each column, skipping column A
worksheet['!cols'] = colWidths.map((width, index) => {
if (index === 0) {
return { wch: 8 }; // Set default or specific width for column A if needed
}
return { wch: width + 2 }; // Add padding to width
});

// Style for cell A1: Font size 16 and bold
if (worksheet['A1']) {
worksheet['A1'].s = {
font: {
bold: true,
sz: 16, // Font size 16
//name: 'Times New Roman' // Specify font
}
};
}

// Apply styles from A2 to A3 (bold)
['A2', 'A3'].forEach(cell => {
if (worksheet[cell]) {
worksheet[cell].s = { font: { bold: true } };
}
});

// Formatting from A5 to G5
// Apply styles from A5 to G5 (bold, bottom border, center alignment)
for (let col = 0; col < 7; col++) { // Columns A to G
const cellRef = XLSX.utils.encode_col(col) + '5';
if (worksheet[cellRef]) {
worksheet[cellRef].s = {
font: { bold: true },
alignment: { horizontal: 'center' },
border: {
bottom: { style: 'thin', color: { auto: 1 } }
}
};
}
}

// Format filename with date
const today = new Date().toISOString().split('T')[0].replace(/-/g, '_'); // Get current date and format as YYYY_MM_DD
const filename = `AR06_Project_Completion_Report_with_Outstanding_Un-billed_Hours_${today}.xlsx`; // Append formatted date to the filename

// Convert workbook back to XLSX file
XLSX.writeFile(workbook, filename);
} else {
throw new Error('Failed to load file');
}
};
reader.readAsArrayBuffer(data);
} catch (error) {
console.error('Error downloading the file: ', error);
}

//setIsLoading(false);
};
return (
<Card>
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
<Typography variant="overline">{t("Search Criteria")}</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
{criteria.map((c) => {
return (
<Grid key={c.paramName} item xs={6}>
{c.type === "text" && (
<TextField
label={c.label}
fullWidth
onChange={makeInputChangeHandler(c.paramName)}
value={inputs[c.paramName]}
/>
)}
{c.type === "select" && (
<FormControl fullWidth>
<InputLabel>{c.label}</InputLabel>
<Select
label={c.label}
onChange={makeSelectChangeHandler(c.paramName)}
value={inputs[c.paramName]}
>
<MenuItem value={"All"}>{t("All")}</MenuItem>
{c.options.map((option, index) => (
<MenuItem key={`${option}-${index}`} value={option}>
{option}
</MenuItem>
))}
</Select>
</FormControl>
)}
{c.type === "dateRange" && (
<LocalizationProvider
dateAdapter={AdapterDayjs}
// TODO: Should maybe use a custom adapterLocale here to support YYYY-MM-DD
adapterLocale="zh-hk"
>
<Box display="flex">
<FormControl fullWidth>
<DatePicker
label={c.label}
onChange={makeDateChangeHandler(c.paramName)}
value={inputs[c.paramName] ? dayjs(inputs[c.paramName]) : null}
/>
</FormControl>
<Box
display="flex"
alignItems="center"
justifyContent="center"
marginInline={2}
>
{"-"}
</Box>
<FormControl fullWidth>
<DatePicker
label={c.label2}
onChange={makeDateToChangeHandler(c.paramName)}
value={inputs[c.paramName.concat("To") as T] ? dayjs(inputs[c.paramName.concat("To") as T]) : null}
/>
</FormControl>
</Box>
</LocalizationProvider>
)}
</Grid>
);
})}
</Grid>
<CardActions sx={{ justifyContent: "flex-end" }}>
<Button
variant="text"
startIcon={<RestartAlt />}
onClick={handleReset}
>
{t("Reset")}
</Button>
<Button
variant="outlined"
startIcon={<Search />}
onClick={handleDownload}
>
{t("Download")}
</Button>
</CardActions>
</CardContent>
</Card>
);
}

export default SearchBox;

+ 3
- 0
src/components/ReportSearchBox6/index.ts 파일 보기

@@ -0,0 +1,3 @@
//src\components\SearchBox\index.ts
export { default } from "./SearchBox6";
export type { Criterion } from "./SearchBox6";

+ 1
- 1
src/components/SearchBox/SearchBox.tsx 파일 보기

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


+ 5
- 1
src/components/SearchResults/SearchResults.tsx 파일 보기

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

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

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

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


+ 1
- 1
src/components/SubsidiaryDetail/ContactInfo.tsx 파일 보기

@@ -263,7 +263,7 @@ const ContactInfo: React.FC<Props> = ({
{t("Contact Info")}
</Typography>
{Boolean(errors.addContacts?.type === "required") && <Typography sx={(theme) => ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap>
{t("Please ensure all the fields are inputted and saved")}
{t("Please ensure at least one row is created, and all the fields are inputted and saved")}
</Typography>}
{Boolean(errors.addContacts?.type === "email_format") && <Typography sx={(theme) => ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap>
{t("Please ensure all the email formats are correct")}


+ 2
- 2
src/components/SubsidiaryDetail/SubsidiaryInfo.tsx 파일 보기

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


+ 1
- 0
src/components/SubsidiarySearch/SubsidiarySearch.tsx 파일 보기

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


+ 90
- 0
src/components/TeamSearch/TeamSearch.tsx 파일 보기

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

import { TeamResult } from "@/app/api/team";
import SearchBox, { Criterion } from "../SearchBox";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import SearchResults, { Column } from "../SearchResults/index";
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";

interface Props {
team: TeamResult[];
}
type SearchQuery = Partial<Omit<TeamResult, "id">>;
type SearchParamNames = keyof SearchQuery;

const TeamSearch: React.FC<Props> = ({ team }) => {
const { t } = useTranslation();
const [filteredTeam, setFilteredTeam] = useState(team);
const [data, setData] = useState<TeamResult>();
const [isOpen, setIsOpen] = useState(false);
const router = useRouter();

const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
() => [
{
label: t("Team Name"),
paramName: "name",
type: "text",
},
{
label: t("Team Code"),
paramName: "code",
type: "text",
},
{
label: t("Team Description"),
paramName: "description",
type: "text",
},
],
[t],
);

const columns = useMemo<Column<TeamResult>[]>(
() => [
// {
// name: "action",
// label: t("Actions"),
// onClick: onStaffClick,
// 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 />,
// },
],
[t],
);

return (
<>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
// setFilteredStaff(
// staff.filter(
// (s) =>
// s.staffId.toLowerCase().includes(query.staffId.toLowerCase()) &&
// s.name.toLowerCase().includes(query.name.toLowerCase())
// // (query.team === "All" || s.team === query.team) &&
// // (query.category === "All" || s.category === query.category) &&
// // (query.team === "All" || s.team === query.team),
// )
// )
}}
/>
<SearchResults<TeamResult> items={filteredTeam} columns={columns} />

</>
);
};
export default TeamSearch;

+ 40
- 0
src/components/TeamSearch/TeamSearchLoading.tsx 파일 보기

@@ -0,0 +1,40 @@
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Skeleton from "@mui/material/Skeleton";
import Stack from "@mui/material/Stack";
import React from "react";

// Can make this nicer
export const TeamSearchLoading: React.FC = () => {
return (
<>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton
variant="rounded"
height={50}
width={100}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
</CardContent>
</Card>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
</Stack>
</CardContent>
</Card>
</>
);
};

export default TeamSearchLoading;

+ 21
- 0
src/components/TeamSearch/TeamSearchWrapper.tsx 파일 보기

@@ -0,0 +1,21 @@
// import { fetchTeam, fetchTeamLeads } from "@/app/api/Team";
import React from "react";
import TeamSearch from "./TeamSearch";
import TeamSearchLoading from "./TeamSearchLoading";
import { fetchTeam } from "@/app/api/team";
// import { preloadTeam } from "@/app/api/Team";

interface SubComponents {
Loading: typeof TeamSearchLoading;
}

const TeamSearchWrapper: React.FC & SubComponents = async () => {
const Team = await fetchTeam();
console.log(Team);

return <TeamSearch team={Team} />;
};

TeamSearchWrapper.Loading = TeamSearchLoading;

export default TeamSearchWrapper;

+ 1
- 0
src/components/TeamSearch/index.ts 파일 보기

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

+ 31
- 0
src/i18n/en/claim.json 파일 보기

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

+ 12
- 0
src/i18n/en/common.json 파일 보기

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

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

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

+ 1
- 1
src/i18n/en/customer.json 파일 보기

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

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


+ 1
- 1
src/i18n/en/subsidiary.json 파일 보기

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

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


+ 31
- 0
src/i18n/zh/claim.json 파일 보기

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

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

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

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

+ 12
- 0
src/i18n/zh/common.json 파일 보기

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

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

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

+ 1
- 1
src/i18n/zh/customer.json 파일 보기

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


+ 1
- 1
src/i18n/zh/subsidiary.json 파일 보기

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


불러오는 중...
취소
저장