From 2d9fdfc37e673eed8fe8ddfe405a74d9b8c4b716 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Wed, 3 Jul 2024 18:38:58 +0800 Subject: [PATCH] add cross team charge report & update search box --- .../analytics/CrossTeamChargeReport/page.tsx | 29 +++++++++++ src/app/api/reports/actions.ts | 14 ++++- src/app/api/reports/index.ts | 9 ++++ src/components/Breadcrumb/Breadcrumb.tsx | 1 + .../GenerateCrossTeamChargeReport.tsx | 51 +++++++++++++++++++ .../GenerateCrossTeamChargeReportLoading.tsx | 38 ++++++++++++++ .../GenerateCrossTeamChargeReportWrapper.tsx | 15 ++++++ .../GenerateCrossTeamChargeReport/index.ts | 1 + .../NavigationContent/NavigationContent.tsx | 5 ++ src/components/SearchBox/SearchBox.tsx | 34 +++++++++---- 10 files changed, 186 insertions(+), 11 deletions(-) create mode 100644 src/app/(main)/analytics/CrossTeamChargeReport/page.tsx create mode 100644 src/components/GenerateCrossTeamChargeReport/GenerateCrossTeamChargeReport.tsx create mode 100644 src/components/GenerateCrossTeamChargeReport/GenerateCrossTeamChargeReportLoading.tsx create mode 100644 src/components/GenerateCrossTeamChargeReport/GenerateCrossTeamChargeReportWrapper.tsx create mode 100644 src/components/GenerateCrossTeamChargeReport/index.ts diff --git a/src/app/(main)/analytics/CrossTeamChargeReport/page.tsx b/src/app/(main)/analytics/CrossTeamChargeReport/page.tsx new file mode 100644 index 0000000..dac493c --- /dev/null +++ b/src/app/(main)/analytics/CrossTeamChargeReport/page.tsx @@ -0,0 +1,29 @@ +import { Metadata } from "next"; +import { Suspense } from "react"; +import { I18nProvider, getServerI18n } from "@/i18n"; +import { fetchProjects } from "@/app/api/projects"; +import GenerateCrossTeamChargeReport from "@/components/GenerateCrossTeamChargeReport"; +import { Typography } from "@mui/material"; + +export const metadata: Metadata = { + title: "Cross Team Charge Report", +}; + +const CrossTeamChargeReport: React.FC = async () => { + const { t } = await getServerI18n("reports"); + + return ( + <> + + {t("Cross Team Charge Report")} + + + }> + + + + + ); +}; + +export default CrossTeamChargeReport; \ No newline at end of file diff --git a/src/app/api/reports/actions.ts b/src/app/api/reports/actions.ts index 3da08e3..77fc7b1 100644 --- a/src/app/api/reports/actions.ts +++ b/src/app/api/reports/actions.ts @@ -1,7 +1,7 @@ "use server"; import { serverFetchBlob } from "@/app/utils/fetchUtil"; -import { MonthlyWorkHoursReportRequest, ProjectCashFlowReportRequest, LateStartReportRequest, ProjectResourceOverconsumptionReportRequest, ProjectPandLReportRequest, ProjectCompletionReportRequest, ProjectPotentialDelayReportRequest, CostAndExpenseReportRequest } from "."; +import { MonthlyWorkHoursReportRequest, ProjectCashFlowReportRequest, LateStartReportRequest, ProjectResourceOverconsumptionReportRequest, ProjectPandLReportRequest, ProjectCompletionReportRequest, ProjectPotentialDelayReportRequest, CostAndExpenseReportRequest, CrossTeamChargeReportRequest } from "."; import { BASE_API_URL } from "@/config/api"; export interface FileResponse { @@ -123,3 +123,15 @@ export const fetchCostAndExpenseReport = async (data: CostAndExpenseReportReques return reportBlob }; +export const fetchCrossTeamChargeReport = async (data: CrossTeamChargeReportRequest) => { + const reportBlob = await serverFetchBlob( + `${BASE_API_URL}/reports/CrossTeamChargeReport`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + + return reportBlob +}; \ No newline at end of file diff --git a/src/app/api/reports/index.ts b/src/app/api/reports/index.ts index 2356a2c..a93db29 100644 --- a/src/app/api/reports/index.ts +++ b/src/app/api/reports/index.ts @@ -115,3 +115,12 @@ export interface CostAndExpenseReportRequest { budgetPercentage: number | null; type: string; } + +// - Cross Team Charge Report +export interface CrossTeamChargeReportFilter { + month: string; +} + +export interface CrossTeamChargeReportRequest { + month: string; +} \ No newline at end of file diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx index 30160af..be64dbb 100644 --- a/src/components/Breadcrumb/Breadcrumb.tsx +++ b/src/components/Breadcrumb/Breadcrumb.tsx @@ -72,6 +72,7 @@ const pathToLabelMap: { [path: string]: string } = { "/analytics/FinancialStatusReport": "Financial Status Report", "/analytics/ProjectCashFlowReport": "Project Cash Flow Report", "/analytics/StaffMonthlyWorkHoursAnalysisReport": "Staff Monthly Work Hours Analysis Report", + "/analytics/CrossTeamChargeReport": "Cross Team Charge Report", "/invoice": "Invoice", }; diff --git a/src/components/GenerateCrossTeamChargeReport/GenerateCrossTeamChargeReport.tsx b/src/components/GenerateCrossTeamChargeReport/GenerateCrossTeamChargeReport.tsx new file mode 100644 index 0000000..2d7aaa4 --- /dev/null +++ b/src/components/GenerateCrossTeamChargeReport/GenerateCrossTeamChargeReport.tsx @@ -0,0 +1,51 @@ +"use client"; + +import React, { useMemo } from "react"; +import SearchBox, { Criterion } from "../SearchBox"; +import { useTranslation } from "react-i18next"; +import { CrossTeamChargeReportFilter } from "@/app/api/reports"; +import { fetchCrossTeamChargeReport } from "@/app/api/reports/actions"; +import { downloadFile } from "@/app/utils/commonUtil"; + +interface Props { +} + +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const GenerateCrossTeamChargeReport: React.FC = () => { + const { t } = useTranslation("report"); + + const searchCriteria: Criterion[] = useMemo( + () => [ + { + label: t("Month"), + paramName: "month", + type: "monthYear", + }, + ], + [t], + ); + + return ( + <> + { + + console.log(query.month) + if (Boolean(query.month)) { + // const projectIndex = projectCombo.findIndex(({value}) => value === parseInt(query.project)) + const response = await fetchCrossTeamChargeReport({ month: query.month }) + if (response) { + downloadFile(new Uint8Array(response.blobValue), response.filename!!) + } + } + }} + formType={"download"} + /> + + ); +}; + +export default GenerateCrossTeamChargeReport; \ No newline at end of file diff --git a/src/components/GenerateCrossTeamChargeReport/GenerateCrossTeamChargeReportLoading.tsx b/src/components/GenerateCrossTeamChargeReport/GenerateCrossTeamChargeReportLoading.tsx new file mode 100644 index 0000000..98514e0 --- /dev/null +++ b/src/components/GenerateCrossTeamChargeReport/GenerateCrossTeamChargeReportLoading.tsx @@ -0,0 +1,38 @@ +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 GenerateProjectCashFlowReportLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + ); +}; + +export default GenerateProjectCashFlowReportLoading; \ No newline at end of file diff --git a/src/components/GenerateCrossTeamChargeReport/GenerateCrossTeamChargeReportWrapper.tsx b/src/components/GenerateCrossTeamChargeReport/GenerateCrossTeamChargeReportWrapper.tsx new file mode 100644 index 0000000..5e7d2e5 --- /dev/null +++ b/src/components/GenerateCrossTeamChargeReport/GenerateCrossTeamChargeReportWrapper.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import GenerateCrossTeamChargeReportLoading from "./GenerateCrossTeamChargeReportLoading"; +import GenerateCrossTeamChargeReport from "./GenerateCrossTeamChargeReport"; + +interface SubComponents { + Loading: typeof GenerateCrossTeamChargeReportLoading; +} + +const GenerateCrossTeamChargeReportWrapper: React.FC & SubComponents = async () => { + return ; +}; + +GenerateCrossTeamChargeReportWrapper.Loading = GenerateCrossTeamChargeReportLoading; + +export default GenerateCrossTeamChargeReportWrapper; \ No newline at end of file diff --git a/src/components/GenerateCrossTeamChargeReport/index.ts b/src/components/GenerateCrossTeamChargeReport/index.ts new file mode 100644 index 0000000..3626cfb --- /dev/null +++ b/src/components/GenerateCrossTeamChargeReport/index.ts @@ -0,0 +1 @@ +export { default } from "./GenerateCrossTeamChargeReportWrapper"; \ No newline at end of file diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index 110870d..7e683a4 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -230,6 +230,11 @@ const NavigationContent: React.FC = ({ abilities, username }) => { label: "Staff Monthly Work Hours Analysis Report", path: "/analytics/StaffMonthlyWorkHoursAnalysisReport", }, + { + icon: , + label: "Cross Team Charge Report", + path: "/analytics/CrossTeamChargeReport", + }, ], }, { diff --git a/src/components/SearchBox/SearchBox.tsx b/src/components/SearchBox/SearchBox.tsx index b0257d3..872e540 100644 --- a/src/components/SearchBox/SearchBox.tsx +++ b/src/components/SearchBox/SearchBox.tsx @@ -99,16 +99,30 @@ function SearchBox({ () => criteria.reduce>( (acc, c) => { + let defaultValue: string | number = "" + switch (c.type) { + case "select": + if (!(c.needAll === false)) { + defaultValue = "All" + } else if (c.options.length > 0) { + defaultValue = c.options[0] + } + break; + case "autocomplete": + if (!(c.needAll === false)) { + defaultValue = "All" + } else if (c.options.length > 0) { + defaultValue = c.options[0].value + } + break; + case "monthYear": + defaultValue = dayjs().format("YYYY-MM") + break; + } + return { ...acc, - [c.paramName]: - c.type === "select" || c.type === "autocomplete" - ? !(c.needAll === false) - ? "All" - : c.options.length > 0 - ? c.type === "autocomplete" ? c.options[0].value : c.options[0] - : "" - : "" + [c.paramName]: defaultValue }; }, {} as Record @@ -297,7 +311,7 @@ function SearchBox({ ); }} - renderInput={(params) => } + renderInput={(params) => } /> )} {c.type === "autocomplete" && !c.options.some(option => Boolean(option.group)) && ( @@ -356,7 +370,7 @@ function SearchBox({ ); }} - renderInput={(params) => } + renderInput={(params) => } /> )} {c.type === "number" && (