Browse Source

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

tags/Baseline_30082024_FRONTEND_UAT
MSI\2Fi 1 year ago
parent
commit
1c17e925ab
83 changed files with 2532 additions and 342 deletions
  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 View File


BIN
public/temp/AR06_Project Completion Report with Outstanding Un-billed Hours.xlsx View File


+ 5
- 5
src/app/(main)/analytics/ProjectCompletionReport/page.tsx View File

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


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


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

+ 5
- 5
src/app/(main)/analytics/ProjectCompletionReportWO/page.tsx View File

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


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


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

+ 5
- 0
src/app/(main)/projects/create/page.tsx View File

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




+ 45
- 0
src/app/(main)/settings/team/create/page.tsx View File

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

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

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


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


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


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


export default CreateClaims;
export default ClaimDetails;

+ 7
- 5
src/app/(main)/staffReimbursement/page.tsx View File

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


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


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


+ 48
- 0
src/app/api/claims/actions.ts View File

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

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


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


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

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

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

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


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

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


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

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


+ 10
- 0
src/app/api/grades/index.ts View File

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

export interface Grade { export interface Grade {
name: string; name: string;
id: number; id: number;
code: string; 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 View File

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


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


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


export const fetchPositions = cache(async () => { 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 View File

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

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

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

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

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

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

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

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


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


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

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

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


+ 33
- 0
src/auth/utils.js View File

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

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

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

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

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

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


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


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

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


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


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


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

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


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


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


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


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

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


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


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


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


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

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

useEffect(() => {

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

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

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

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

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

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

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

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

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


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

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


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

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


export default ClaimInputGrid;
export default ClaimFormInputGrid;

+ 1
- 0
src/components/ClaimDetail/index.ts View File

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

+ 24
- 29
src/components/ClaimSearch/ClaimSearch.tsx View File

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


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


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


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


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


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


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


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


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


+ 0
- 67
src/components/CreateClaim/ClaimDetails.tsx View File

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

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

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

+ 4
- 3
src/components/CreateProject/CreateProject.tsx View File

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


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

// Mocked
grades: Grade[]; grades: Grade[];
} }


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


+ 8
- 9
src/components/CreateProject/CreateProjectWrapper.tsx View File

@@ -10,7 +10,8 @@ import {
fetchProjectWorkNatures, fetchProjectWorkNatures,
} from "@/app/api/projects"; } from "@/app/api/projects";
import { fetchStaff, fetchTeamLeads } from "@/app/api/staff"; 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 CreateProjectWrapper: React.FC = async () => {
const [ const [
@@ -19,6 +20,7 @@ const CreateProjectWrapper: React.FC = async () => {
projectCategories, projectCategories,
teamLeads, teamLeads,
allCustomers, allCustomers,
allSubsidiaries,
contractTypes, contractTypes,
fundingTypes, fundingTypes,
locationTypes, locationTypes,
@@ -26,12 +28,14 @@ const CreateProjectWrapper: React.FC = async () => {
buildingTypes, buildingTypes,
workNatures, workNatures,
allStaffs, allStaffs,
grades,
] = await Promise.all([ ] = await Promise.all([
fetchAllTasks(), fetchAllTasks(),
fetchTaskTemplates(), fetchTaskTemplates(),
fetchProjectCategories(), fetchProjectCategories(),
fetchTeamLeads(), fetchTeamLeads(),
fetchAllCustomers(), fetchAllCustomers(),
fetchAllSubsidiaries(),
fetchProjectContractTypes(), fetchProjectContractTypes(),
fetchProjectFundingTypes(), fetchProjectFundingTypes(),
fetchProjectLocationTypes(), fetchProjectLocationTypes(),
@@ -39,6 +43,7 @@ const CreateProjectWrapper: React.FC = async () => {
fetchProjectBuildingTypes(), fetchProjectBuildingTypes(),
fetchProjectWorkNatures(), fetchProjectWorkNatures(),
fetchStaff(), fetchStaff(),
fetchGrades(),
]); ]);


return ( return (
@@ -47,6 +52,7 @@ const CreateProjectWrapper: React.FC = async () => {
projectCategories={projectCategories} projectCategories={projectCategories}
taskTemplates={taskTemplates} taskTemplates={taskTemplates}
teamLeads={teamLeads} teamLeads={teamLeads}
allSubsidiaries={allSubsidiaries}
allCustomers={allCustomers} allCustomers={allCustomers}
contractTypes={contractTypes} contractTypes={contractTypes}
fundingTypes={fundingTypes} fundingTypes={fundingTypes}
@@ -55,14 +61,7 @@ const CreateProjectWrapper: React.FC = async () => {
buildingTypes={buildingTypes} buildingTypes={buildingTypes}
workNatures={workNatures} workNatures={workNatures}
allStaffs={allStaffs} 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 View File

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


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

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


+ 24
- 9
src/components/CreateProject/ProjectClientDetails.tsx View File

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


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

const selectedCustomerId = watch("clientId"); const selectedCustomerId = watch("clientId");
const selectedCustomer = useMemo( const selectedCustomer = useMemo(
() => allCustomers.find((c) => c.id === selectedCustomerId), () => allCustomers.find((c) => c.id === selectedCustomerId),
@@ -482,14 +491,20 @@ const ProjectClientDetails: React.FC<Props> = ({
name="clientSubsidiaryId" name="clientSubsidiaryId"
render={({ field }) => ( render={({ field }) => (
<Select label={t("Client Lead")} {...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> </Select>
)} )}
/> />


+ 1
- 10
src/components/CreateProject/StaffAllocation.tsx View File

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


const StaffAllocation: React.FC<Props> = ({ const StaffAllocation: React.FC<Props> = ({
allStaffs: dataStaffs,
allStaffs,
allTasks, allTasks,
isActive, isActive,
defaultManhourBreakdownByGrade, defaultManhourBreakdownByGrade,
@@ -63,15 +63,6 @@ const StaffAllocation: React.FC<Props> = ({
const { t } = useTranslation(); const { t } = useTranslation();
const { setValue, getValues, watch } = useFormContext<CreateProjectInputs>(); 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( const [filteredStaff, setFilteredStaff] = React.useState(
allStaffs.sort(staffComparator), allStaffs.sort(staffComparator),
); );


+ 126
- 0
src/components/CreateTeam/CreateTeam.tsx View File

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

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

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

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

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

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

+ 1
- 1
src/components/CustomDatagrid/CustomDatagrid.tsx View File

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


+ 1
- 1
src/components/CustomerDetail/ContactInfo.tsx View File

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


+ 1
- 1
src/components/CustomerDetail/CustomerDetailWrapper.tsx View File

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


// type Props = { // type Props = {


+ 2
- 2
src/components/CustomerDetail/CustomerInfo.tsx View File

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


+ 1
- 0
src/components/CustomerSearch/CustomerSearch.tsx View File

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


+ 17
- 0
src/components/Report/ProjectCompletionReport/ProjectCompletionReport.tsx View File

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

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

+ 44
- 0
src/components/Report/ProjectCompletionReportGen/ProjectCompletionReportGen.tsx View File

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

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

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

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

+ 17
- 0
src/components/Report/ProjectCompletionReportWO/ProjectCompletionReportWO.tsx View File

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

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

+ 44
- 0
src/components/Report/ProjectCompletionReportWOGen/ProjectCompletionReportWOGen.tsx View File

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

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

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

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

+ 2
- 2
src/components/ReportSearchBox/SearchBox.tsx View File

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


+ 2
- 2
src/components/ReportSearchBox2/SearchBox2.tsx View File

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


+ 2
- 2
src/components/ReportSearchBox3/SearchBox3.tsx View File

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


+ 2
- 2
src/components/ReportSearchBox4/SearchBox4.tsx View File

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


+ 302
- 0
src/components/ReportSearchBox5/SearchBox5.tsx View File

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

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

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

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

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


+ 5
- 1
src/components/SearchResults/SearchResults.tsx View File

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


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


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


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


+ 1
- 1
src/components/SubsidiaryDetail/ContactInfo.tsx View File

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


+ 2
- 2
src/components/SubsidiaryDetail/SubsidiaryInfo.tsx View File

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


+ 1
- 0
src/components/SubsidiarySearch/SubsidiarySearch.tsx View File

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


+ 90
- 0
src/components/TeamSearch/TeamSearch.tsx View File

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

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

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

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

+ 31
- 0
src/i18n/en/claim.json View File

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

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

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

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

+ 1
- 1
src/i18n/en/customer.json View File

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


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


+ 1
- 1
src/i18n/en/subsidiary.json View File

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


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


+ 31
- 0
src/i18n/zh/claim.json View File

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

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

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

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

+ 1
- 1
src/i18n/zh/customer.json View File

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


+ 1
- 1
src/i18n/zh/subsidiary.json View File

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


Loading…
Cancel
Save