| @@ -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 { I18nProvider } from "@/i18n"; | |||
| import ProjectFinancialSummaryV2 from "@/components/ProjectFinancialSummaryV2"; | |||
| import { preloadClientProjects } from "@/app/api/clientprojects"; | |||
| import { searchParamsProps } from "@/app/utils/fetchUtil"; | |||
| import ProjectFinancialSummaryV2 from "@/components/ProjectFinancialSummaryV2"; | |||
| export const metadata: Metadata = { | |||
| title: "Project Status by Client", | |||
| @@ -19,65 +19,6 @@ export interface FinancialSummaryCardResult { | |||
| 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 = { | |||
| id: number, | |||
| projectName: string, | |||
| @@ -104,23 +45,6 @@ export const fetchFinancialSummaryCard = cache(async () => { | |||
| 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) => { | |||
| var endpoint = `${BASE_API_URL}/dashboard/getFinancialSummary-final?` | |||
| if (startDate.length > 0) endpoint += `&startDate=${startDate}` | |||
| @@ -1,7 +1,7 @@ | |||
| "use server"; | |||
| 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"; | |||
| 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 | |||
| }; | |||
| @@ -126,4 +126,14 @@ export interface CrossTeamChargeReportFilter { | |||
| export interface CrossTeamChargeReportRequest { | |||
| month: string; | |||
| 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), | |||
| ), | |||
| }, | |||
| { | |||
| icon: <Analytics />, | |||
| label: "Project Manhour Summary Report", | |||
| path: "/analytics/ProjectManhourSummaryReport", | |||
| isHidden: false | |||
| }, | |||
| { | |||
| icon: <Analytics />, | |||
| label: "Staff Monthly Work Hours Analysis Report", | |||
| @@ -5,6 +5,7 @@ import ProjectSearchLoading from "./ProjectSearchLoading"; | |||
| import { fetchUserAbilities, fetchUserStaff } from "@/app/utils/fetchUtil"; | |||
| import { authOptions } from "@/config/authConfig"; | |||
| import { getServerSession } from "next-auth"; | |||
| import { VIEW_ALL_PROJECTS } from "@/middleware"; | |||
| interface SubComponents { | |||
| Loading: typeof ProjectSearchLoading; | |||
| @@ -15,11 +16,15 @@ const ProjectSearchWrapper: React.FC & SubComponents = async () => { | |||
| const userStaff = await fetchUserStaff() | |||
| const teamId = userStaff?.teamId | |||
| const projects = await fetchProjects(); | |||
| const abilities = await fetchUserAbilities() | |||
| const isViewAllProjectRight = [VIEW_ALL_PROJECTS].some((ability) => abilities.includes(ability)) | |||
| let filteredProjects = projects | |||
| if (teamId) { | |||
| if (!isViewAllProjectRight) { | |||
| filteredProjects = projects.filter(project => project.teamId === teamId) | |||
| } | |||
| const abilities = await fetchUserAbilities() | |||
| return <ProjectSearch projects={filteredProjects} projectCategories={projectCategories} abilities={abilities}/>; | |||
| }; | |||
| @@ -70,7 +70,8 @@ export const [ | |||
| GENERATE_FINANCIAL_STATUS_REPORT, | |||
| GENERATE_PROJECT_CASH_FLOW_REPORT, | |||
| GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, | |||
| GENERATE_CROSS_TEAM_CHARGE_REPORT | |||
| GENERATE_CROSS_TEAM_CHARGE_REPORT, | |||
| VIEW_ALL_PROJECTS | |||
| ] = [ | |||
| 'MAINTAIN_USER', | |||
| 'MAINTAIN_TIMESHEET', | |||
| @@ -122,7 +123,8 @@ export const [ | |||
| 'G_FINANCIAL_STATUS_REPORT', | |||
| 'G_PROJECT_CASH_FLOW_REPORT', | |||
| 'G_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT', | |||
| 'G_CROSS_TEAM_CHARGE_REPORT' | |||
| 'G_CROSS_TEAM_CHARGE_REPORT', | |||
| 'VIEW_ALL_PROJECTS' | |||
| ] | |||
| const PRIVATE_ROUTES = [ | |||