Przeglądaj źródła

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

tags/Baseline_30082024_FRONTEND_UAT
MSI\2Fi 1 rok temu
rodzic
commit
29cea3a0aa
35 zmienionych plików z 1022 dodań i 26 usunięć
  1. BIN
      public/temp/AR01_Late Start Report.xlsx
  2. BIN
      public/temp/AR02_Delay Report.xlsx
  3. BIN
      public/temp/AR03_Resource Overconsumption.xlsx
  4. BIN
      public/temp/AR04_Cost and Expense Report.xlsx
  5. +5
    -5
      src/app/(main)/analytics/CostandExpenseReport/page.tsx
  6. +24
    -0
      src/app/(main)/analytics/ResourceOverconsumptionReport/page.tsx
  7. +42
    -0
      src/app/api/report3/index.ts
  8. +42
    -0
      src/app/api/report4/index.ts
  9. +1
    -1
      src/app/utils/formatUtil.ts
  10. +8
    -3
      src/components/CreateProject/CreateProject.tsx
  11. +4
    -4
      src/components/CreateProject/MilestoneSection.tsx
  12. +2
    -2
      src/components/NavigationContent/NavigationContent.tsx
  13. +17
    -0
      src/components/Report/CostandExpenseReport/CostandExpenseReport.tsx
  14. +2
    -0
      src/components/Report/CostandExpenseReport/index.ts
  15. +45
    -0
      src/components/Report/CostandExpenseReportGen/CostandExpenseReportGen.tsx
  16. +41
    -0
      src/components/Report/CostandExpenseReportGen/CostandExpenseReportGenLoading.tsx
  17. +19
    -0
      src/components/Report/CostandExpenseReportGen/CostandExpenseReportGenWrapper.tsx
  18. +2
    -0
      src/components/Report/CostandExpenseReportGen/index.ts
  19. +2
    -2
      src/components/Report/DelayReportGen/DelayReportGen.tsx
  20. +14
    -3
      src/components/Report/LateStartReportGen/LateStartReportGen.tsx
  21. +17
    -0
      src/components/Report/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx
  22. +2
    -0
      src/components/Report/ResourceOverconsumptionReport/index.ts
  23. +45
    -0
      src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGen.tsx
  24. +41
    -0
      src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenLoading.tsx
  25. +19
    -0
      src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenWrapper.tsx
  26. +2
    -0
      src/components/Report/ResourceOverconsumptionReportGen/index.ts
  27. +4
    -2
      src/components/ReportSearchBox/SearchBox.tsx
  28. +4
    -2
      src/components/ReportSearchBox2/SearchBox2.tsx
  29. +302
    -0
      src/components/ReportSearchBox3/SearchBox3.tsx
  30. +3
    -0
      src/components/ReportSearchBox3/index.ts
  31. +302
    -0
      src/components/ReportSearchBox4/SearchBox4.tsx
  32. +3
    -0
      src/components/ReportSearchBox4/index.ts
  33. +2
    -0
      src/components/SearchBox/SearchBox.tsx
  34. +3
    -2
      src/components/SearchResults/SearchResults.tsx
  35. +3
    -0
      src/components/TimesheetTable/EntryInputTable.tsx

BIN
public/temp/AR01_Late Start Report.xlsx Wyświetl plik


BIN
public/temp/AR02_Delay Report.xlsx Wyświetl plik


BIN
public/temp/AR03_Resource Overconsumption.xlsx Wyświetl plik


BIN
public/temp/AR04_Cost and Expense Report.xlsx Wyświetl plik


+ 5
- 5
src/app/(main)/analytics/CostandExpenseReport/page.tsx Wyświetl plik

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

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

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

+ 24
- 0
src/app/(main)/analytics/ResourceOverconsumptionReport/page.tsx Wyświetl plik

@@ -0,0 +1,24 @@
//src\app\(main)\analytics\ResourceOvercomsumptionReport\page.tsx
import { Metadata } from "next";
import { I18nProvider } from "@/i18n";
import Typography from "@mui/material/Typography";
import ResourceOverconsumptionReportComponent from "@/components/Report/ResourceOverconsumptionReport";

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

const ResourceOverconsumptionReport: React.FC = () => {
return (
<I18nProvider namespaces={["analytics"]}>
<Typography variant="h4" marginInlineEnd={2}>
Resource Overconsumption Report
</Typography>
{/* <Suspense fallback={<ProgressCashFlowSearch.Loading />}>
<ProgressCashFlowSearch/>
</Suspense> */}
<ResourceOverconsumptionReportComponent />
</I18nProvider>
);
};
export default ResourceOverconsumptionReport;

+ 42
- 0
src/app/api/report3/index.ts Wyświetl plik

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

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

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

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

const mockProjects: ResourceOverconsumption[] = [
{
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",
status: "1",
},
];

+ 42
- 0
src/app/api/report4/index.ts Wyświetl plik

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

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

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

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

const mockProjects: CostandExpense[] = [
{
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",
remainPercent: "1",
},
];

+ 1
- 1
src/app/utils/formatUtil.ts Wyświetl plik

@@ -39,7 +39,7 @@ export const shortDateFormatter = (locale?: string) => {
}
};

export function convertLocaleStringToNumber(numberString: String): number {
export function convertLocaleStringToNumber(numberString: string): number {
const numberWithoutCommas = numberString.replace(/,/g, "");
return parseFloat(numberWithoutCommas);
}

+ 8
- 3
src/components/CreateProject/CreateProject.tsx Wyświetl plik

@@ -61,7 +61,9 @@ const hasErrorsInTab = (
) => {
switch (tabIndex) {
case 0:
return errors.projectName;
return (
errors.projectName || errors.projectCode || errors.projectDescription
);
default:
false;
}
@@ -101,7 +103,6 @@ const CreateProject: React.FC<Props> = ({
const onSubmit = useCallback<SubmitHandler<CreateProjectInputs>>(
async (data) => {
try {
console.log(data);
setServerError("");
await saveProject(data);
router.replace("/projects");
@@ -115,7 +116,11 @@ const CreateProject: React.FC<Props> = ({
const onSubmitError = useCallback<SubmitErrorHandler<CreateProjectInputs>>(
(errors) => {
// Set the tab so that the focus will go there
if (errors.projectName) {
if (
errors.projectName ||
errors.projectDescription ||
errors.projectCode
) {
setTabIndex(0);
}
},


+ 4
- 4
src/components/CreateProject/MilestoneSection.tsx Wyświetl plik

@@ -29,7 +29,7 @@ import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import StyledDataGrid from "../StyledDataGrid";
import { moneyFormatter } from "@/app/utils/formatUtil";
import { INPUT_DATE_FORMAT, moneyFormatter } from "@/app/utils/formatUtil";
import isDate from "lodash/isDate";

interface Props {
@@ -206,7 +206,7 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => {
description: p.description!,
id: p.id!,
amount: p.amount!,
date: dayjs(p.date!).toISOString(),
date: dayjs(p.date!).format(INPUT_DATE_FORMAT),
})),
},
});
@@ -245,7 +245,7 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => {
...milestones,
[taskGroupId]: {
...milestones[taskGroupId],
startDate: date.toISOString(),
startDate: date.format(INPUT_DATE_FORMAT),
},
});
}}
@@ -264,7 +264,7 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => {
...milestones,
[taskGroupId]: {
...milestones[taskGroupId],
endDate: date.toISOString(),
endDate: date.format(INPUT_DATE_FORMAT),
},
});
}}


+ 2
- 2
src/components/NavigationContent/NavigationContent.tsx Wyświetl plik

@@ -103,8 +103,8 @@ const navigationItems: NavigationItem[] = [
{icon: <Analytics />, label:"Delay Report", path: "/analytics/DelayReport"},
{icon: <Analytics />, label:"Resource Overconsumption Report", path: "/analytics/ResourceOverconsumptionReport"},
{icon: <Analytics />, label:"Cost and Expense Report", path: "/analytics/CostandExpenseReport"},
{icon: <Analytics />, label:"Completion Report", path: "/analytics/CompletionReport"},
{icon: <Analytics />, label:"Completion Report with Outstanding Un-billed Hours Report", path: "/analytics/CompletionReportWO"},
{icon: <Analytics />, label:"Completion Report", path: "/analytics/ProjectCompletionReport"},
{icon: <Analytics />, label:"Completion Report with Outstanding Un-billed Hours Report", path: "/analytics/ProjectCompletionReportWO"},
],
},
{


+ 17
- 0
src/components/Report/CostandExpenseReport/CostandExpenseReport.tsx Wyświetl plik

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

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

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

export default CostandExpenseReport;

+ 2
- 0
src/components/Report/CostandExpenseReport/index.ts Wyświetl plik

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

+ 45
- 0
src/components/Report/CostandExpenseReportGen/CostandExpenseReportGen.tsx Wyświetl plik

@@ -0,0 +1,45 @@
//src\components\LateStartReportGen\LateStartReportGen.tsx
"use client";
import React, { useMemo, useState } from "react";
import SearchBox, { Criterion } from "../../ReportSearchBox4";
import { useTranslation } from "react-i18next";
import { CostandExpense } from "@/app/api/report4";

interface Props {
projects: CostandExpense[];
}
type SearchQuery = Partial<Omit<CostandExpense, "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: "select", options: ["AAA", "BBB", "CCC"] },
{ label: "Client", paramName: "client", type: "select", options: ["Cust A", "Cust B", "Cust C"] },
{ label: "Remaining Percentage", paramName: "remainPercent", type: "select", options: ["<50%", "50%-70%", ">70%"] },
// {
// label: "Status",
// label2: "Remained Date To",
// paramName: "targetEndDate",
// type: "dateRange",
// },
],
[t],
);

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

export default ProgressByClientSearch;

+ 41
- 0
src/components/Report/CostandExpenseReportGen/CostandExpenseReportGenLoading.tsx Wyświetl plik

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

+ 19
- 0
src/components/Report/CostandExpenseReportGen/CostandExpenseReportGenWrapper.tsx Wyświetl plik

@@ -0,0 +1,19 @@
//src\components\LateStartReportGen\LateStartReportGenWrapper.tsx
import { fetchProjectsCostandExpense } from "@/app/api/report4";
import React from "react";
import CostandExpenseReportGen from "./CostandExpenseReportGen";
import CostandExpenseReportGenLoading from "./CostandExpenseReportGenLoading";

interface SubComponents {
Loading: typeof CostandExpenseReportGenLoading;
}

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

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

CostandExpenseReportGenWrapper.Loading = CostandExpenseReportGenLoading;

export default CostandExpenseReportGenWrapper;

+ 2
- 0
src/components/Report/CostandExpenseReportGen/index.ts Wyświetl plik

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

+ 2
- 2
src/components/Report/DelayReportGen/DelayReportGen.tsx Wyświetl plik

@@ -16,8 +16,8 @@ const ProgressByClientSearch: React.FC<Props> = ({ projects }) => {

const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
() => [
{ label: "Team", paramName: "team", type: "text" },
{ label: "Client", paramName: "client", type: "text" },
{ label: "Team", paramName: "team", type: "select", options: ["AAA", "BBB", "CCC"] },
{ label: "Client", paramName: "client", type: "select", options: ["Cust A", "Cust B", "Cust C"] },
{ label: "Status", paramName: "status", type: "select", options: ["Delayed", "Potential Delay"] },
// {
// label: "Status",


+ 14
- 3
src/components/Report/LateStartReportGen/LateStartReportGen.tsx Wyświetl plik

@@ -5,6 +5,9 @@ import SearchBox, { Criterion } from "../../ReportSearchBox";
import { useTranslation } from "react-i18next";
import { LateStart } from "@/app/api/report";
//import { DownloadReportButton } from './DownloadReportButton';
import axios from 'axios';
import { apiPath } from '../../../auth/utils';
//import { GET_QC_CATEGORY_COMBO } from 'utils/ApiPathConst';
interface Props {
projects: LateStart[];
}
@@ -13,11 +16,19 @@ type SearchParamNames = keyof SearchQuery;

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

// const [teamCombo, setteamCombo] = useState([]);
// const getteamCombo = () => {
// axios.get(`${apiPath}${GET_QC_CATEGORY_COMBO}`)
// .then(response => {
// setteamCombo(response.data.records)})
// .catch(error => {
// console.error('Error fetching data: ', error);
// });
// }
const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
() => [
{ label: "Team", paramName: "team", type: "text" },
{ label: "Client", paramName: "client", type: "text" },
{ label: "Team", paramName: "team", type: "select", options: ["AAA", "BBB", "CCC"] },
{ label: "Client", paramName: "client", type: "select", options: ["Cust A", "Cust B", "Cust C"] },
{
label: "Remained Date From",
label2: "Remained Date To",


+ 17
- 0
src/components/Report/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx Wyświetl plik

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

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

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

export default ResourceOverconsumptionReport;

+ 2
- 0
src/components/Report/ResourceOverconsumptionReport/index.ts Wyświetl plik

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

+ 45
- 0
src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGen.tsx Wyświetl plik

@@ -0,0 +1,45 @@
//src\components\LateStartReportGen\LateStartReportGen.tsx
"use client";
import React, { useMemo, useState } from "react";
import SearchBox, { Criterion } from "../../ReportSearchBox3";
import { useTranslation } from "react-i18next";
import { ResourceOverconsumption } from "@/app/api/report3";

interface Props {
projects: ResourceOverconsumption[];
}
type SearchQuery = Partial<Omit<ResourceOverconsumption, "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: "select", options: ["AAA", "BBB", "CCC"] },
{ label: "Client", paramName: "client", type: "select", options: ["Cust A", "Cust B", "Cust C"] },
{ label: "Status", paramName: "status", type: "select", options: ["Overconsumption", "Potential Overconsumption"] },
// {
// label: "Status",
// label2: "Remained Date To",
// paramName: "targetEndDate",
// type: "dateRange",
// },
],
[t],
);

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

export default ProgressByClientSearch;

+ 41
- 0
src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenLoading.tsx Wyświetl plik

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

+ 19
- 0
src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenWrapper.tsx Wyświetl plik

@@ -0,0 +1,19 @@
//src\components\LateStartReportGen\LateStartReportGenWrapper.tsx
import { fetchProjectsResourceOverconsumption } from "@/app/api/report3";
import React from "react";
import ResourceOvercomsumptionReportGen from "./ResourceOverconsumptionReportGen";
import ResourceOvercomsumptionReportGenLoading from "./ResourceOverconsumptionReportGenLoading";

interface SubComponents {
Loading: typeof ResourceOvercomsumptionReportGenLoading;
}

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

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

ResourceOvercomsumptionReportGenWrapper.Loading = ResourceOvercomsumptionReportGenLoading;

export default ResourceOvercomsumptionReportGenWrapper;

+ 2
- 0
src/components/Report/ResourceOverconsumptionReportGen/index.ts Wyświetl plik

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

+ 4
- 2
src/components/ReportSearchBox/SearchBox.tsx Wyświetl plik

@@ -253,6 +253,7 @@ function SearchBox<T extends string>({
<DatePicker
label={c.label}
onChange={makeDateChangeHandler(c.paramName)}
value={inputs[c.paramName] ? dayjs(inputs[c.paramName]) : null}
/>
</FormControl>
<Box
@@ -267,6 +268,7 @@ function SearchBox<T extends string>({
<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>
@@ -287,9 +289,9 @@ function SearchBox<T extends string>({
<Button
variant="outlined"
startIcon={<Search />}
onClick={handleDownload}
onClick={handleSearch}
>
{t("Download Report")}
{t("Search")}
</Button>
</CardActions>
</CardContent>


+ 4
- 2
src/components/ReportSearchBox2/SearchBox2.tsx Wyświetl plik

@@ -253,6 +253,7 @@ function SearchBox<T extends string>({
<DatePicker
label={c.label}
onChange={makeDateChangeHandler(c.paramName)}
value={inputs[c.paramName] ? dayjs(inputs[c.paramName]) : null}
/>
</FormControl>
<Box
@@ -267,6 +268,7 @@ function SearchBox<T extends string>({
<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>
@@ -287,9 +289,9 @@ function SearchBox<T extends string>({
<Button
variant="outlined"
startIcon={<Search />}
onClick={handleDownload}
onClick={handleSearch}
>
{t("Download Report")}
{t("Search")}
</Button>
</CardActions>
</CardContent>


+ 302
- 0
src/components/ReportSearchBox3/SearchBox3.tsx Wyświetl plik

@@ -0,0 +1,302 @@
//src\components\ReportSearchBox3\SearchBox3.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/AR03_Resource Overconsumption.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 A4 (bold)
['A2', 'A3', 'A4'].forEach(cell => {
if (worksheet[cell]) {
worksheet[cell].s = { font: { bold: true } };
}
});

// Formatting from A6 to L6
// Apply styles from A6 to L6 (bold, bottom border, center alignment)
for (let col = 0; col < 12; col++) { // Columns A to K
const cellRef = XLSX.utils.encode_col(col) + '6';
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 = `AR03_Resource_Overconsumption_${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={handleSearch}
>
{t("Search")}
</Button>
</CardActions>
</CardContent>
</Card>
);
}

export default SearchBox;

+ 3
- 0
src/components/ReportSearchBox3/index.ts Wyświetl plik

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

+ 302
- 0
src/components/ReportSearchBox4/SearchBox4.tsx Wyświetl plik

@@ -0,0 +1,302 @@
//src\components\ReportSearchBox3\SearchBox3.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/AR04_Cost and Expense 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 A4 (bold)
['A2', 'A3', 'A4'].forEach(cell => {
if (worksheet[cell]) {
worksheet[cell].s = { font: { bold: true } };
}
});

// Formatting from A6 to J6
// Apply styles from A6 to J6 (bold, bottom border, center alignment)
for (let col = 0; col < 10; col++) { // Columns A to K
const cellRef = XLSX.utils.encode_col(col) + '6';
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 = `AR04_Cost_and_Expense_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={handleSearch}
>
{t("Search")}
</Button>
</CardActions>
</CardContent>
</Card>
);
}

export default SearchBox;

+ 3
- 0
src/components/ReportSearchBox4/index.ts Wyświetl plik

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

+ 2
- 0
src/components/SearchBox/SearchBox.tsx Wyświetl plik

@@ -154,6 +154,7 @@ function SearchBox<T extends string>({
<DatePicker
label={c.label}
onChange={makeDateChangeHandler(c.paramName)}
value={inputs[c.paramName] ? dayjs(inputs[c.paramName]) : null}
/>
</FormControl>
<Box
@@ -168,6 +169,7 @@ function SearchBox<T extends string>({
<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>


+ 3
- 2
src/components/SearchResults/SearchResults.tsx Wyświetl plik

@@ -11,7 +11,7 @@ import TablePagination, {
TablePaginationProps,
} from "@mui/material/TablePagination";
import TableRow from "@mui/material/TableRow";
import IconButton from "@mui/material/IconButton";
import IconButton, { IconButtonOwnProps, IconButtonPropsColorOverrides } from "@mui/material/IconButton";

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

interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> {
@@ -91,7 +92,7 @@ function SearchResults<T extends ResultWithId>({
<TableCell key={`${columnName.toString()}-${idx}`}>
{isActionColumn(column) ? (
<IconButton
color="primary"
color={column.color ?? "primary"}
onClick={() => column.onClick(item)}
>
{column.buttonIcon}


+ 3
- 0
src/components/TimesheetTable/EntryInputTable.tsx Wyświetl plik

@@ -22,6 +22,9 @@ import { AssignedProject } from "@/app/api/projects";
import uniqBy from "lodash/uniqBy";
import { TaskGroup } from "@/app/api/tasks";
import dayjs from "dayjs";
import isBetween from "dayjs/plugin/isBetween";

dayjs.extend(isBetween);

const mockProjects: AssignedProject[] = [
{


Ładowanie…
Anuluj
Zapisz