@@ -0,0 +1,29 @@ | |||||
import { Metadata } from "next"; | |||||
import { Suspense } from "react"; | |||||
import { I18nProvider, getServerI18n } from "@/i18n"; | |||||
import { fetchProjects } from "@/app/api/projects"; | |||||
import { Typography } from "@mui/material"; | |||||
import GenerateProjectManhourSummaryReport from "@/components/GenerateProjectManhourSummaryReport"; | |||||
export const metadata: Metadata = { | |||||
title: "Project Manhour Summary Report", | |||||
}; | |||||
const ProjectManhourSummaryReport: React.FC = async () => { | |||||
const { t } = await getServerI18n("reports"); | |||||
return ( | |||||
<> | |||||
<Typography variant="h4" marginInlineEnd={2}> | |||||
{t("Project Manhour Summary Report")} | |||||
</Typography> | |||||
<I18nProvider namespaces={["report", "common"]}> | |||||
<Suspense fallback={<GenerateProjectManhourSummaryReport.Loading />}> | |||||
<GenerateProjectManhourSummaryReport /> | |||||
</Suspense> | |||||
</I18nProvider> | |||||
</> | |||||
); | |||||
}; | |||||
export default ProjectManhourSummaryReport; |
@@ -1,8 +1,8 @@ | |||||
import { Metadata } from "next"; | import { Metadata } from "next"; | ||||
import { I18nProvider } from "@/i18n"; | import { I18nProvider } from "@/i18n"; | ||||
import ProjectFinancialSummaryV2 from "@/components/ProjectFinancialSummaryV2"; | |||||
import { preloadClientProjects } from "@/app/api/clientprojects"; | import { preloadClientProjects } from "@/app/api/clientprojects"; | ||||
import { searchParamsProps } from "@/app/utils/fetchUtil"; | import { searchParamsProps } from "@/app/utils/fetchUtil"; | ||||
import ProjectFinancialSummaryV2 from "@/components/ProjectFinancialSummaryV2"; | |||||
export const metadata: Metadata = { | export const metadata: Metadata = { | ||||
title: "Project Status by Client", | title: "Project Status by Client", | ||||
@@ -19,65 +19,6 @@ export interface FinancialSummaryCardResult { | |||||
cpi: number; | cpi: number; | ||||
} | } | ||||
export type FinancialSummaryType = { | |||||
team: IndividualTeam; | |||||
activeProject: number; | |||||
totalFees: number; | |||||
totalBudget: number; | |||||
cumulativeExpenditure: number; | |||||
manpowerExpense: number; | |||||
projectExpense: number; | |||||
invoicedAmount: number; | |||||
nonInvoicedAmount: number; | |||||
receivedAmount: number; | |||||
cashFlowStatus: String; | |||||
costPerformanceIndex: number; | |||||
projectedCashFlowStatus: String; | |||||
projectedCostPerformanceIndex: number; | |||||
} | |||||
export type FinancialSummaryByProject = { | |||||
// project data | |||||
id: number, | |||||
projectCode: string, | |||||
projectName: string, | |||||
custId: number, | |||||
totalFee: number, | |||||
totalBudget: number, | |||||
customerCode: string, | |||||
customerName: string, | |||||
subsidiaryName: string, | |||||
// timesheet data | |||||
cumulativeExpenditure: number, | |||||
manhourExpense: number, | |||||
projectExpense: number, | |||||
// invoice data | |||||
invoicedAmount: number, | |||||
nonInvoicedAmount: number, | |||||
receivedAmount: number, | |||||
// calculation | |||||
cashFlowStatus: string, | |||||
projectedCashFlowStatus: string, | |||||
cpi: number, | |||||
projectedCpi: number, | |||||
} | |||||
export interface FinancialSummaryByClient { | |||||
id: number; | |||||
customerName: string; | |||||
customerCode: string; | |||||
subsidiaryName: string; | |||||
totalFee: number, | |||||
totalBudget: number, | |||||
cumulativeExpenditure: number | |||||
manhourExpense: number; | |||||
projectExpense: number; | |||||
invoicedAmount: number; | |||||
nonInvoicedAmount: number | |||||
receivedAmount: number; | |||||
numberOfRecords: number; | |||||
} | |||||
export type FinancialByProject = { | export type FinancialByProject = { | ||||
id: number, | id: number, | ||||
projectName: string, | projectName: string, | ||||
@@ -104,23 +45,6 @@ export const fetchFinancialSummaryCard = cache(async () => { | |||||
return serverFetchJson<FinancialSummaryCardResult[]>(`${BASE_API_URL}/dashboard/searchFinancialSummaryCard`); | return serverFetchJson<FinancialSummaryCardResult[]>(`${BASE_API_URL}/dashboard/searchFinancialSummaryCard`); | ||||
}); | }); | ||||
export const fetchFinancialSummary = cache(async (endDate: string, teamId: number | null, startDate: string | null) => { | |||||
var endpoint = `${BASE_API_URL}/dashboard/getFinancialSummary?endDate=${endDate}` | |||||
if (teamId) endpoint += `&teamIds=${teamId}` | |||||
if (startDate) endpoint += `&startDate=${startDate}` | |||||
return serverFetchJson<FinancialSummaryType[]>(endpoint, { | |||||
next: { tags: ["financialSummary"] }, | |||||
}); | |||||
}) | |||||
export const fetchFinancialSummaryByProject = cache(async (endDate: string, teamId: string, startDate?: string ) => { | |||||
var endpoint = `${BASE_API_URL}/dashboard/getFinancialSummaryByProject?endDate=${endDate}&teamId=${teamId}` | |||||
if (startDate) endpoint += `&startDate=${startDate}` | |||||
return serverFetchJson<FinancialSummaryByProject[]>(endpoint, { | |||||
next: { tags: ["financialSummaryByProject"] }, | |||||
}); | |||||
}) | |||||
export const fetchFinancialSummaryByProjectV2 = cache(async (teamId: number, endDate: string, startDate: string) => { | export const fetchFinancialSummaryByProjectV2 = cache(async (teamId: number, endDate: string, startDate: string) => { | ||||
var endpoint = `${BASE_API_URL}/dashboard/getFinancialSummary-final?` | var endpoint = `${BASE_API_URL}/dashboard/getFinancialSummary-final?` | ||||
if (startDate.length > 0) endpoint += `&startDate=${startDate}` | if (startDate.length > 0) endpoint += `&startDate=${startDate}` | ||||
@@ -1,7 +1,7 @@ | |||||
"use server"; | "use server"; | ||||
import { serverFetchBlob } from "@/app/utils/fetchUtil"; | import { serverFetchBlob } from "@/app/utils/fetchUtil"; | ||||
import { MonthlyWorkHoursReportRequest, ProjectCashFlowReportRequest, LateStartReportRequest, ProjectResourceOverconsumptionReportRequest, ProjectPandLReportRequest, ProjectCompletionReportRequest, ProjectPotentialDelayReportRequest, CostAndExpenseReportRequest, CrossTeamChargeReportRequest } from "."; | |||||
import { MonthlyWorkHoursReportRequest, ProjectCashFlowReportRequest, LateStartReportRequest, ProjectResourceOverconsumptionReportRequest, ProjectPandLReportRequest, ProjectCompletionReportRequest, ProjectPotentialDelayReportRequest, CostAndExpenseReportRequest, CrossTeamChargeReportRequest, ProjectManhourSummaryReportRequest } from "."; | |||||
import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
export interface FileResponse { | export interface FileResponse { | ||||
@@ -133,5 +133,19 @@ export const fetchCrossTeamChargeReport = async (data: CrossTeamChargeReportRequ | |||||
}, | }, | ||||
); | ); | ||||
return reportBlob | |||||
}; | |||||
export const fetchProjectManhourSummaryReport = async (data: ProjectManhourSummaryReportRequest) => { | |||||
console.log(data) | |||||
const reportBlob = await serverFetchBlob<FileResponse>( | |||||
`${BASE_API_URL}/reports/ProjectManhourSummary`, | |||||
{ | |||||
method: "POST", | |||||
body: JSON.stringify(data), | |||||
headers: { "Content-Type": "application/json" }, | |||||
}, | |||||
); | |||||
return reportBlob | return reportBlob | ||||
}; | }; |
@@ -126,4 +126,14 @@ export interface CrossTeamChargeReportFilter { | |||||
export interface CrossTeamChargeReportRequest { | export interface CrossTeamChargeReportRequest { | ||||
month: string; | month: string; | ||||
teamId: number | "All"; | teamId: number | "All"; | ||||
} | |||||
export interface ProjectManhourSummaryReportFilter { | |||||
teamId: number; | |||||
startDate: string; | |||||
endDate: string; | |||||
} | |||||
export interface ProjectManhourSummaryReportRequest { | |||||
teamId: number; | |||||
startDate: string; | |||||
endDate: string; | |||||
} | } |
@@ -0,0 +1,71 @@ | |||||
"use client"; | |||||
import { ProjectManhourSummaryReportFilter } from "@/app/api/reports"; | |||||
import SearchBox, { Criterion } from "../SearchBox"; | |||||
import { useMemo } from "react"; | |||||
import { useTranslation } from "react-i18next"; | |||||
import { TeamResult } from "@/app/api/team"; | |||||
import { fetchProjectManhourSummaryReport } from "@/app/api/reports/actions"; | |||||
import { downloadFile } from "@/app/utils/commonUtil"; | |||||
interface Props { | |||||
teams: TeamResult[] | |||||
} | |||||
type SearchQuery = Partial<Omit<ProjectManhourSummaryReportFilter, "id">>; | |||||
type SearchParamNames = keyof SearchQuery; | |||||
const ProjectManhourSummaryReport: React.FC<Props> = ({ teams }) => { | |||||
const { t } = useTranslation("report"); | |||||
const teamsCombo = teams.map((team) => ({label: `${team.code} - ${team.name}`, value: team.id})) | |||||
const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||||
() => [ | |||||
{ | |||||
label: t("Start Date"), | |||||
paramName: "startDate", | |||||
type: "monthYear", | |||||
}, | |||||
{ | |||||
label: t("End Date"), | |||||
paramName: "endDate", | |||||
type: "monthYear", | |||||
}, | |||||
{ | |||||
label: t("Team"), | |||||
paramName: "teamId", | |||||
type: "autocomplete", | |||||
options: teamsCombo, | |||||
needAll: false, | |||||
}, | |||||
], | |||||
[t] | |||||
); | |||||
return ( | |||||
<> | |||||
<SearchBox | |||||
formType={"download"} | |||||
criteria={searchCriteria} | |||||
onSearch={async (query: any) => { | |||||
console.log(query); | |||||
var start = query.startDate | |||||
var start = query.startDate | |||||
const args = { | |||||
teamId: query.teamId, | |||||
startDate: query.startDate + "-01", | |||||
endDate: query.endDate + "-01", | |||||
} | |||||
console.log(args) | |||||
const response = await fetchProjectManhourSummaryReport(args);// | |||||
if (response) { | |||||
console.log("asdasdas") | |||||
downloadFile( | |||||
new Uint8Array(response.blobValue), | |||||
response.filename!! | |||||
); | |||||
} | |||||
}} | |||||
/> | |||||
</>) | |||||
} | |||||
export default ProjectManhourSummaryReport |
@@ -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 ProjectManhourSummaryReportLoading: 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 ProjectManhourSummaryReportLoading; |
@@ -0,0 +1,17 @@ | |||||
import React from "react"; | |||||
import ProjectManhourSummaryReportLoading from "./ProjectManhourSummaryReportLoading"; | |||||
import ProjectManhourSummaryReport from "./ProjectManhourSummaryReport"; | |||||
import { fetchTeam } from "@/app/api/team"; | |||||
interface SubComponents { | |||||
Loading: typeof ProjectManhourSummaryReportLoading; | |||||
} | |||||
const ProjectManhourSummaryReportWrapper: React.FC & SubComponents = async () => { | |||||
const teams = await fetchTeam() | |||||
return <ProjectManhourSummaryReport teams={teams}/> | |||||
} | |||||
ProjectManhourSummaryReportWrapper.Loading = ProjectManhourSummaryReportLoading; | |||||
export default ProjectManhourSummaryReportWrapper; |
@@ -0,0 +1 @@ | |||||
export { default } from "./ProjectManhourSummaryReportWrapper"; |
@@ -312,6 +312,12 @@ const NavigationContent: React.FC<Props> = ({ abilities, username }) => { | |||||
abilities!.includes(ability), | abilities!.includes(ability), | ||||
), | ), | ||||
}, | }, | ||||
{ | |||||
icon: <Analytics />, | |||||
label: "Project Manhour Summary Report", | |||||
path: "/analytics/ProjectManhourSummaryReport", | |||||
isHidden: false | |||||
}, | |||||
{ | { | ||||
icon: <Analytics />, | icon: <Analytics />, | ||||
label: "Staff Monthly Work Hours Analysis Report", | label: "Staff Monthly Work Hours Analysis Report", | ||||
@@ -5,6 +5,7 @@ import ProjectSearchLoading from "./ProjectSearchLoading"; | |||||
import { fetchUserAbilities, fetchUserStaff } from "@/app/utils/fetchUtil"; | import { fetchUserAbilities, fetchUserStaff } from "@/app/utils/fetchUtil"; | ||||
import { authOptions } from "@/config/authConfig"; | import { authOptions } from "@/config/authConfig"; | ||||
import { getServerSession } from "next-auth"; | import { getServerSession } from "next-auth"; | ||||
import { VIEW_ALL_PROJECTS } from "@/middleware"; | |||||
interface SubComponents { | interface SubComponents { | ||||
Loading: typeof ProjectSearchLoading; | Loading: typeof ProjectSearchLoading; | ||||
@@ -15,11 +16,15 @@ const ProjectSearchWrapper: React.FC & SubComponents = async () => { | |||||
const userStaff = await fetchUserStaff() | const userStaff = await fetchUserStaff() | ||||
const teamId = userStaff?.teamId | const teamId = userStaff?.teamId | ||||
const projects = await fetchProjects(); | const projects = await fetchProjects(); | ||||
const abilities = await fetchUserAbilities() | |||||
const isViewAllProjectRight = [VIEW_ALL_PROJECTS].some((ability) => abilities.includes(ability)) | |||||
let filteredProjects = projects | let filteredProjects = projects | ||||
if (teamId) { | |||||
if (!isViewAllProjectRight) { | |||||
filteredProjects = projects.filter(project => project.teamId === teamId) | filteredProjects = projects.filter(project => project.teamId === teamId) | ||||
} | } | ||||
const abilities = await fetchUserAbilities() | |||||
return <ProjectSearch projects={filteredProjects} projectCategories={projectCategories} abilities={abilities}/>; | return <ProjectSearch projects={filteredProjects} projectCategories={projectCategories} abilities={abilities}/>; | ||||
}; | }; | ||||
@@ -70,7 +70,8 @@ export const [ | |||||
GENERATE_FINANCIAL_STATUS_REPORT, | GENERATE_FINANCIAL_STATUS_REPORT, | ||||
GENERATE_PROJECT_CASH_FLOW_REPORT, | GENERATE_PROJECT_CASH_FLOW_REPORT, | ||||
GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, | GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, | ||||
GENERATE_CROSS_TEAM_CHARGE_REPORT | |||||
GENERATE_CROSS_TEAM_CHARGE_REPORT, | |||||
VIEW_ALL_PROJECTS | |||||
] = [ | ] = [ | ||||
'MAINTAIN_USER', | 'MAINTAIN_USER', | ||||
'MAINTAIN_TIMESHEET', | 'MAINTAIN_TIMESHEET', | ||||
@@ -122,7 +123,8 @@ export const [ | |||||
'G_FINANCIAL_STATUS_REPORT', | 'G_FINANCIAL_STATUS_REPORT', | ||||
'G_PROJECT_CASH_FLOW_REPORT', | 'G_PROJECT_CASH_FLOW_REPORT', | ||||
'G_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT', | 'G_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT', | ||||
'G_CROSS_TEAM_CHARGE_REPORT' | |||||
'G_CROSS_TEAM_CHARGE_REPORT', | |||||
'VIEW_ALL_PROJECTS' | |||||
] | ] | ||||
const PRIVATE_ROUTES = [ | const PRIVATE_ROUTES = [ | ||||