Bläddra i källkod

add cross team charge report & update search box

tags/Baseline_30082024_FRONTEND_UAT
cyril.tsui 1 år sedan
förälder
incheckning
2d9fdfc37e
10 ändrade filer med 186 tillägg och 11 borttagningar
  1. +29
    -0
      src/app/(main)/analytics/CrossTeamChargeReport/page.tsx
  2. +13
    -1
      src/app/api/reports/actions.ts
  3. +9
    -0
      src/app/api/reports/index.ts
  4. +1
    -0
      src/components/Breadcrumb/Breadcrumb.tsx
  5. +51
    -0
      src/components/GenerateCrossTeamChargeReport/GenerateCrossTeamChargeReport.tsx
  6. +38
    -0
      src/components/GenerateCrossTeamChargeReport/GenerateCrossTeamChargeReportLoading.tsx
  7. +15
    -0
      src/components/GenerateCrossTeamChargeReport/GenerateCrossTeamChargeReportWrapper.tsx
  8. +1
    -0
      src/components/GenerateCrossTeamChargeReport/index.ts
  9. +5
    -0
      src/components/NavigationContent/NavigationContent.tsx
  10. +24
    -10
      src/components/SearchBox/SearchBox.tsx

+ 29
- 0
src/app/(main)/analytics/CrossTeamChargeReport/page.tsx Visa fil

@@ -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 (
<>
<Typography variant="h4" marginInlineEnd={2}>
{t("Cross Team Charge Report")}
</Typography>
<I18nProvider namespaces={["report", "common"]}>
<Suspense fallback={<GenerateCrossTeamChargeReport.Loading />}>
<GenerateCrossTeamChargeReport />
</Suspense>
</I18nProvider>
</>
);
};

export default CrossTeamChargeReport;

+ 13
- 1
src/app/api/reports/actions.ts Visa fil

@@ -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<FileResponse>(
`${BASE_API_URL}/reports/CrossTeamChargeReport`,
{
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
},
);

return reportBlob
};

+ 9
- 0
src/app/api/reports/index.ts Visa fil

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

+ 1
- 0
src/components/Breadcrumb/Breadcrumb.tsx Visa fil

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



+ 51
- 0
src/components/GenerateCrossTeamChargeReport/GenerateCrossTeamChargeReport.tsx Visa fil

@@ -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<Omit<CrossTeamChargeReportFilter, "id">>;
type SearchParamNames = keyof SearchQuery;

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

const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
() => [
{
label: t("Month"),
paramName: "month",
type: "monthYear",
},
],
[t],
);

return (
<>
<SearchBox
criteria={searchCriteria}
onSearch={async (query) => {

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;

+ 38
- 0
src/components/GenerateCrossTeamChargeReport/GenerateCrossTeamChargeReportLoading.tsx Visa fil

@@ -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 (
<>
<Card>
<CardContent>
<Stack spacing={2}>
<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 GenerateProjectCashFlowReportLoading;

+ 15
- 0
src/components/GenerateCrossTeamChargeReport/GenerateCrossTeamChargeReportWrapper.tsx Visa fil

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

GenerateCrossTeamChargeReportWrapper.Loading = GenerateCrossTeamChargeReportLoading;

export default GenerateCrossTeamChargeReportWrapper;

+ 1
- 0
src/components/GenerateCrossTeamChargeReport/index.ts Visa fil

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

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

@@ -230,6 +230,11 @@ const NavigationContent: React.FC<Props> = ({ abilities, username }) => {
label: "Staff Monthly Work Hours Analysis Report",
path: "/analytics/StaffMonthlyWorkHoursAnalysisReport",
},
{
icon: <Analytics />,
label: "Cross Team Charge Report",
path: "/analytics/CrossTeamChargeReport",
},
],
},
{


+ 24
- 10
src/components/SearchBox/SearchBox.tsx Visa fil

@@ -99,16 +99,30 @@ function SearchBox<T extends string>({
() =>
criteria.reduce<Record<T, string>>(
(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<T, string>
@@ -297,7 +311,7 @@ function SearchBox<T extends string>({
</MenuItem>
);
}}
renderInput={(params) => <TextField {...params} variant="outlined" label={c.label}/>}
renderInput={(params) => <TextField {...params} variant="outlined" label={c.label} />}
/>
)}
{c.type === "autocomplete" && !c.options.some(option => Boolean(option.group)) && (
@@ -356,7 +370,7 @@ function SearchBox<T extends string>({
</MenuItem>
);
}}
renderInput={(params) => <TextField {...params} variant="outlined" label={c.label}/>}
renderInput={(params) => <TextField {...params} variant="outlined" label={c.label} />}
/>
)}
{c.type === "number" && (


Laddar…
Avbryt
Spara