@@ -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; |
@@ -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 | |||
}; |
@@ -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; | |||
} |
@@ -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", | |||
}; | |||
@@ -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; |
@@ -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; |
@@ -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; |
@@ -0,0 +1 @@ | |||
export { default } from "./GenerateCrossTeamChargeReportWrapper"; |
@@ -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", | |||
}, | |||
], | |||
}, | |||
{ | |||
@@ -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" && ( | |||