From 9effbd8619e61ffd21dd9565c231a2f52584e5f4 Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Thu, 17 Oct 2024 16:14:29 +0800 Subject: [PATCH] update financial year with filter --- .../ProjectFinancialSummaryV2/page.tsx | 19 + src/app/api/financialsummary/actions.ts | 7 +- src/app/api/financialsummary/index.ts | 77 +++ .../InvoiceSearch/InvoiceAutocomplete.tsx | 0 .../NavigationContent/NavigationContent.tsx | 9 + .../FinancialSummary.tsx | 198 ++++++ .../FinancialSummaryLoading.tsx | 40 ++ .../FinancialSummaryWrapper.tsx | 67 ++ .../FinnancialStatusByProject.tsx | 592 ++++++++++++++++++ .../ProjectFinancialSummaryV2/TeamCard.tsx | 14 + .../ProjectFinancialSummaryV2/gptFn.tsx | 40 ++ .../ProjectFinancialSummaryV2/index.ts | 1 + 12 files changed, 1063 insertions(+), 1 deletion(-) create mode 100644 src/app/(main)/dashboard/ProjectFinancialSummaryV2/page.tsx create mode 100644 src/components/InvoiceSearch/InvoiceAutocomplete.tsx create mode 100644 src/components/ProjectFinancialSummaryV2/FinancialSummary.tsx create mode 100644 src/components/ProjectFinancialSummaryV2/FinancialSummaryLoading.tsx create mode 100644 src/components/ProjectFinancialSummaryV2/FinancialSummaryWrapper.tsx create mode 100644 src/components/ProjectFinancialSummaryV2/FinnancialStatusByProject.tsx create mode 100644 src/components/ProjectFinancialSummaryV2/TeamCard.tsx create mode 100644 src/components/ProjectFinancialSummaryV2/gptFn.tsx create mode 100644 src/components/ProjectFinancialSummaryV2/index.ts diff --git a/src/app/(main)/dashboard/ProjectFinancialSummaryV2/page.tsx b/src/app/(main)/dashboard/ProjectFinancialSummaryV2/page.tsx new file mode 100644 index 0000000..b538cca --- /dev/null +++ b/src/app/(main)/dashboard/ProjectFinancialSummaryV2/page.tsx @@ -0,0 +1,19 @@ +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"; + +export const metadata: Metadata = { + title: "Project Status by Client", +}; + +const ProjectFinancialSummary: React.FC = ({ searchParams }) => { + preloadClientProjects(); + return ( + + + + ); +}; +export default ProjectFinancialSummary; diff --git a/src/app/api/financialsummary/actions.ts b/src/app/api/financialsummary/actions.ts index a29ca81..f90e0e9 100644 --- a/src/app/api/financialsummary/actions.ts +++ b/src/app/api/financialsummary/actions.ts @@ -5,6 +5,7 @@ import { BASE_API_URL } from "@/config/api"; import { Dayjs } from "dayjs"; import { cache } from "react"; import { FileResponse } from "../reports/actions"; +import { revalidateTag } from "next/cache"; export interface FinancialSummaryByClientResult { @@ -135,4 +136,8 @@ export const exportFinancialSummaryByProjectExcel = cache(async (data: ExportFin ); return reportBlob -}) +}) + +export const revalidate = async(tag: string) => { + revalidateTag(tag) +} diff --git a/src/app/api/financialsummary/index.ts b/src/app/api/financialsummary/index.ts index 7e9d78a..adf1288 100644 --- a/src/app/api/financialsummary/index.ts +++ b/src/app/api/financialsummary/index.ts @@ -2,6 +2,7 @@ import { cache } from "react"; import { serverFetchJson } from "@/app/utils/fetchUtil"; import { BASE_API_URL } from "@/config/api"; +import { IndividualTeam } from "../team"; // import "server-only"; @@ -18,6 +19,65 @@ 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 const preloadFinancialSummaryCard = () => { fetchFinancialSummaryCard(); }; @@ -25,3 +85,20 @@ export const preloadFinancialSummaryCard = () => { export const fetchFinancialSummaryCard = cache(async () => { return serverFetchJson(`${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(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(endpoint, { + next: { tags: ["financialSummaryByProject"] }, + }); +}) diff --git a/src/components/InvoiceSearch/InvoiceAutocomplete.tsx b/src/components/InvoiceSearch/InvoiceAutocomplete.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index 658dd02..3ad9802 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -172,6 +172,15 @@ const NavigationContent: React.FC = ({ abilities, username }) => { ), showOnMobile: true, }, + { + icon: , + label: "Financial Summary2", + path: "/dashboard/ProjectFinancialSummaryV2", + isHidden: ![VIEW_DASHBOARD_ALL, VIEW_DASHBOARD_SELF].some((ability) => + abilities!.includes(ability), + ), + showOnMobile: true, + }, // No Claim function in Breaur, will be implement later // { // icon: , diff --git a/src/components/ProjectFinancialSummaryV2/FinancialSummary.tsx b/src/components/ProjectFinancialSummaryV2/FinancialSummary.tsx new file mode 100644 index 0000000..5232814 --- /dev/null +++ b/src/components/ProjectFinancialSummaryV2/FinancialSummary.tsx @@ -0,0 +1,198 @@ +"use client"; +import { fetchFinancialSummary, fetchFinancialSummaryByProject, FinancialSummaryByProject, FinancialSummaryType, FinancialSummaryByClient } from '@/app/api/financialsummary'; +import { Box, Card, CardContent, CardHeader, FormControl, InputLabel, Link, MenuItem, Select, Stack } from '@mui/material'; +import { usePathname, useSearchParams } from 'next/navigation'; +import { useRouter } from 'next/navigation'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from "react-i18next"; +import ProjectFinancialCard from '../ProjectFinancialSummary/ProjectFinancialCard'; +import dayjs from 'dayjs'; +import { INPUT_DATE_FORMAT } from '@/app/utils/formatUtil'; +import { revalidateTag } from 'next/cache'; +import { revalidate } from '@/app/api/financialsummary/actions'; +import CircularProgress from '@mui/material/CircularProgress'; +import FinancialStatusByProject from './FinnancialStatusByProject'; +import { summarizeFinancialData } from './gptFn'; + +interface Props { + _financialSumm: FinancialSummaryType[], + _teamId: number | null + } + + type InputDate = { + startDate: string | null; + endDate: string; + } + + type DateParams = { + 1: InputDate; + 2: InputDate; + 3: InputDate; + 4: InputDate; + } +const FinancialSummaryPage: React.FC = ({ + _financialSumm, + _teamId +}) => { + const { t } = useTranslation(); + const searchParams = useSearchParams(); + const [financialSumm, setFinancialSumm] = useState(_financialSumm) + const [teamId, setTeamId] = useState(_teamId) + const [isCardClickedIndex, setIsCardClickedIndex] = useState(_financialSumm[0].team.id); + const curr = useMemo(() => dayjs().format(INPUT_DATE_FORMAT), []) + const currYear = useMemo(() => dayjs().get("year"), []) + const startDate = useMemo(() => "10-01", []) + const endDate = useMemo(() => "09-30", []) + const currFinancialYear = useMemo(() => curr > `${currYear}-${startDate}` ? currYear + 1 : currYear, [currYear]) + const [period, setPeriod] = useState(0); + const [isLoading, setIsLoading] = useState(true) + const [table1Data, setTable1Data] = useState([]) + const [table2Data, setTable2Data] = useState([]) + const dateMap: DateParams = useMemo(() => ({ + 1: {startDate: `${currFinancialYear-2}-${startDate}`, endDate: `${currFinancialYear-1}-${endDate}`}, + 2: {startDate: `${currFinancialYear-3}-${startDate}`, endDate: `${currFinancialYear-2}-${endDate}`}, + 3: {startDate: `${currFinancialYear-4}-${startDate}`, endDate: `${currFinancialYear-3}-${endDate}`}, + 4: {startDate: null, endDate: `${currFinancialYear-4}-${endDate}`}, + }), [currYear, startDate, endDate]) + const [filter, setFilter] = useState(() => { + if (curr <= `${currYear}-${endDate}`) { + return ({ + startDate: `${currYear - 1}-${startDate}`, + endDate: `${currYear}-${endDate}` + }) + } else { + return ({ + startDate: `${currYear}-${startDate}`, + endDate: `${currFinancialYear}-${endDate}` + }) + } + }) + + const fetchHtmlTable = useCallback(async (teamId: number | null, endDate: string, startDate: string | null) => { + const tableData = await fetchFinancialSummary(endDate , teamId, startDate) + setFinancialSumm(tableData) + }, [fetchFinancialSummary]) + + const fetchTable1Data = useCallback(async (teamId: number, endDate: string, startDate?: string) => { + const tableData = await fetchFinancialSummaryByProject(endDate , teamId.toString(), startDate) + setTable1Data(tableData) + const table2Data = summarizeFinancialData(table1Data) + console.log(table2Data) + setTable2Data(table2Data) + }, [fetchFinancialSummaryByProject]) + + const handleCardClick = useCallback((teamId: number) => { + setIsCardClickedIndex(teamId) + setTeamId(teamId) + }, []); + + const handleFilter = useCallback((value: number) => { + setPeriod(value) + console.log(value) + var _startDate: string | null = "" + var _endDate = "" + if (value == 0) { + if (curr <= `${currYear}-${endDate}`) { + _startDate = `${currYear - 1}-${startDate}` + _endDate = `${currYear}-${endDate}` + } else { + _startDate = `${currYear}-${startDate}` + _endDate = `${currFinancialYear}-${endDate}` + } + } else { + _startDate = dateMap[value as keyof DateParams].startDate + _endDate = dateMap[value as keyof DateParams].endDate + } + setFilter({startDate: _startDate, endDate: _endDate}) + }, [isCardClickedIndex]) + + useEffect(() => { + if (financialSumm.length > 0) setIsLoading(false) + }, [financialSumm]) + + useEffect(() => { + console.log(teamId) + console.log(filter) + fetchHtmlTable(teamId, filter.endDate, filter.startDate) + if (teamId) { + const testing = fetchTable1Data(isCardClickedIndex, filter.endDate, filter.startDate ? filter.startDate : undefined) + console.log(testing) + } + }, [teamId, filter]) + + useEffect(() => { + console.log(searchParams.toString()) + }, [searchParams]) + + return ( + <> + + {/* */} + + + + Financial Year + + + + + + { !isLoading ? ( + <> + + + +
+ {financialSumm.length > 0 && financialSumm.map((record:any) => ( +
handleCardClick(record.team.id)}> + +
+ ))} +
+
+
+ + ) + : + + + + } + + ) +} +export default FinancialSummaryPage diff --git a/src/components/ProjectFinancialSummaryV2/FinancialSummaryLoading.tsx b/src/components/ProjectFinancialSummaryV2/FinancialSummaryLoading.tsx new file mode 100644 index 0000000..edff632 --- /dev/null +++ b/src/components/ProjectFinancialSummaryV2/FinancialSummaryLoading.tsx @@ -0,0 +1,40 @@ +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 FinancialSummaryLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default FinancialSummaryLoading; diff --git a/src/components/ProjectFinancialSummaryV2/FinancialSummaryWrapper.tsx b/src/components/ProjectFinancialSummaryV2/FinancialSummaryWrapper.tsx new file mode 100644 index 0000000..01a0946 --- /dev/null +++ b/src/components/ProjectFinancialSummaryV2/FinancialSummaryWrapper.tsx @@ -0,0 +1,67 @@ +import React from "react"; +import FinancialSummaryLoading from "./FinancialSummaryLoading"; +import FinancialSummary from "./FinancialSummary"; +import { fetchUserStaff, searchParamsProps } from "@/app/utils/fetchUtil"; +import { fetchFinancialSummary, fetchFinancialSummaryByProject, FinancialSummaryByProject, FinancialSummaryType } from "@/app/api/financialsummary"; +import { Grid } from "@mui/material"; +import FinancialStatusByProject from "./FinnancialStatusByProject"; + +interface SubComponents { + Loading: typeof FinancialSummaryLoading; +} +// interface SessionWithAbilities extends Session { +// abilities?: string[] +// } + +const FinancialSummaryWrapper: React.FC & SubComponents = async ({ + searchParams, + }) => { + const curr = new Date() + const currYear = curr.getFullYear() + const start = "10-01" + const end = "09-30" + var defaultEnd: string + var defaultStart: string + if (curr.toISOString().split('T')[0] <= `${currYear}-${end}`) { + defaultStart = `${currYear-1}-${start}` + defaultEnd = `${currYear}-${end}` + } else { + defaultStart = `${currYear}-${start}` + defaultEnd = `${currYear+1}-${end}` + } + // const startDate = searchParams.startDate ?? defaultStart; + // const endDate = searchParams.endDate ?? defaultEnd; + const userStaff = await fetchUserStaff(); + const teamId = userStaff?.isTeamLead ? userStaff.teamId : null; + // let financialSumm: FinancialSummaryType[] = []; + // let financialSummByProject: FinancialSummaryByProject[] = []; + // if (startDate && endDate) { + // financialSumm = await fetchFinancialSummary(endDate as string, teamId, startDate as string); + // } + const [ + financialSumm + ] = await Promise.all([ + fetchFinancialSummary(defaultEnd, teamId, defaultStart) + ]); + // if (teamId) { + // financialSummByProject = await fetchFinancialSummaryByProject(endDate as string, teamId.toString(), startDate as string) + // } else if (paramTeamId) { + // console.log(paramTeamId) + // console.log(startDate) + // console.log(endDate) + // financialSummByProject = await fetchFinancialSummaryByProject(endDate as string, paramTeamId as string, startDate as string) + // } + // console.log(financialSumm) + // console.log(financialSummByProject) + + return ( + + + {/* */} + + ); +}; + +FinancialSummaryWrapper.Loading = FinancialSummaryLoading; + +export default FinancialSummaryWrapper; diff --git a/src/components/ProjectFinancialSummaryV2/FinnancialStatusByProject.tsx b/src/components/ProjectFinancialSummaryV2/FinnancialStatusByProject.tsx new file mode 100644 index 0000000..ecaffef --- /dev/null +++ b/src/components/ProjectFinancialSummaryV2/FinnancialStatusByProject.tsx @@ -0,0 +1,592 @@ +"use client"; + +import { + FinancialSummaryByProject, + FinancialSummaryByClient, +} from "@/app/api/financialsummary"; +import SearchBox, { Criterion } from "../SearchBox"; +import { useEffect, useMemo, useState } from "react"; +import CustomDatagrid from "../CustomDatagrid"; +import { useTranslation } from "react-i18next"; +import { useRouter } from "next/navigation"; +import { Box, Grid } from "@mui/material"; +import { summarizeFinancialData } from "./gptFn"; + +interface Props { + financialSummByProject: FinancialSummaryByProject[]; +// financialSummByClient: FinancialSummaryByClient[]; +} +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +type SearchQuery2 = Partial>; +type SearchParamNames2 = keyof SearchQuery2; + +const FinancialStatusByProject: React.FC = ({ + financialSummByProject, +// financialSummByClient, +}) => { + console.log(financialSummByProject); +// console.log(financialSummByClient); + const { t } = useTranslation("dashboard"); + const router = useRouter(); + const [filteredByProjectRows, setFilteredByProjectRows] = useState(financialSummByProject); + const [filteredByClientRows, setFilteredByClientRows] = useState(() => { + console.log(summarizeFinancialData(financialSummByProject)) + return summarizeFinancialData(financialSummByProject) ?? [] + }); +console.log(filteredByProjectRows); + console.log(filteredByClientRows); + +// const testing = useMemo(() => summarizeFinancialData(filteredByProjectRows), []) + const greenColor = "text-lime-500"; + const redColor = "text-red-500"; + const searchCriteria: Criterion[] = useMemo( + () => [ + { label: t("Project Code"), paramName: "projectCode", type: "text" }, + { label: t("Project Name"), paramName: "projectName", type: "text" }, + ], + [t], + ); + + const searchCriteria2: Criterion[] = useMemo( + () => [ + { label: t("Client Code"), paramName: "customerCode", type: "text" }, + { label: t("Client Name"), paramName: "customerName", type: "text" }, + ], + [t], + ); + + useEffect(() => { + setFilteredByProjectRows(financialSummByProject); + setFilteredByClientRows(summarizeFinancialData(financialSummByProject)) + }, [financialSummByProject]); + + const columns1 = [ + { + id: "projectCode", + field: "projectCode", + headerName: t("Project Code"), + minWidth: 50, + renderCell: (params: any) => ( +
{ + router.push( + `/dashboard/ProjectCashFlow?projectId=${params.row.id}` + ); + }} + > + {params.value} +
+ ), + }, + { + id: "projectName", + field: "projectName", + headerName: t("Project Name"), + minWidth: 50, + }, + { + id: "customerName", + field: "customerName", + headerName: t("Client Name"), + minWidth: 50, + }, + { + id: "subsidiaryName", + field: "subsidiaryName", + headerName: t("Subsidiary"), + minWidth: 50, + }, + { + id: "cashFlowStatus", + field: "cashFlowStatus", + headerName: t("Cash Flow Status"), + minWidth: 80, + renderCell: (params: any) => { + if (params.row.invoicedAmount >= params.row.cumulativeExpenditure) { + return {t("Positive")}; + } else { + return {t("Negative")}; + } + }, + }, + { + id: "cpi", + field: "cpi", + headerName: "CPI", + minWidth: 50, + renderCell: (params: any) => { + return ( + = 1 ? greenColor : redColor}> + {params.row.cpi.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + + ); + }, + }, + { + id: "projectedCashFlowStatus", + field: "projectedCashFlowStatus", + headerName: t("Projected Cash Flow Status"), + minWidth: 100, + renderCell: (params: any) => { + if (params.row.totalFee >= params.row.cumulativeExpenditure) { + return {t("Positive")}; + } else { + return {t("Negative")}; + } + }, + }, + { + id: "projectedCpi", + field: "projectedCpi", + headerName: t("Projected CPI"), + minWidth: 50, + renderCell: (params: any) => { + return ( + = 1 ? greenColor : redColor} + > + {params.row.projectedCpi.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + + ); + }, + }, + { + id: "totalFee", + field: "totalFee", + headerName: t("Total Fees") + t("HKD"), + type: "number", + minWidth: 50, + renderCell: (params: any) => { + return ( + + $ + {params.row.totalFee.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + + ); + }, + }, + { + id: "totalBudget", + field: "totalBudget", + headerName: t("Total Budget") + t("HKD"), + minWidth: 50, + type: "number", + renderCell: (params: any) => { + return ( + + $ + {params.row.totalBudget.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + + ); + }, + }, + { + id: "cumulativeExpenditure", + field: "cumulativeExpenditure", + headerName: t("Total Cumulative Expenditure") + t("HKD"), + minWidth: 250, + type: "number", + renderCell: (params: any) => { + return ( + + $ + {params.row.cumulativeExpenditure.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + + ); + }, + }, + { + id: "manhourExpense", + field: "manhourExpense", + headerName: t("Manpower Expenses") + t("HKD"), + minWidth: 280, + type: "number", + renderCell: (params: any) => { + return ( + + $ + {params.row.manhourExpense.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + + ); + }, + }, + { + id: "projectExpense", + field: "projectExpense", + headerName: t("Project Expense") + t("HKD"), + minWidth: 280, + type: "number", + renderCell: (params: any) => { + return ( + + $ + {(params.row.projectExpense ?? 0).toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + + ); + }, + }, + { + id: "invoicedAmount", + field: "invoicedAmount", + headerName: t("Total Invoiced Amount") + t("HKD"), + minWidth: 250, + type: "number", + renderCell: (params: any) => { + return ( + + $ + {params.row.invoicedAmount.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + + ); + }, + }, + { + id: "nonInvoicedAmount", + field: "nonInvoicedAmount", + headerName: t("Total Un-Invoiced Amount") + t("HKD"), + minWidth: 250, + type: "number", + renderCell: (params: any) => { + return ( + + $ + {params.row.nonInvoicedAmount.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + + ); + }, + }, + { + id: "receivedAmount", + field: "receivedAmount", + headerName: t("Total Received Amount") + t("HKD"), + minWidth: 250, + type: "number", + renderCell: (params: any) => { + return ( + + $ + {params.row.receivedAmount.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + + ); + }, + }, + ]; + + const columns2 = [ + { + id: "customerCode", + field: "customerCode", + headerName: t("Client Code"), + minWidth: 50, + renderCell: (params: any) => ( +
{ + router.push( + `/dashboard/ProjectStatusByClient?customerId=${params.row.id}` + ); + }} + > + {params.value} +
+ ), + }, + { + id: "customerName", + field: "customerName", + headerName: t("Client Name"), + minWidth: 80, + }, + { + id: "numberOfRecords", + field: "numberOfRecords", + headerName: t("Total Project Involved"), + minWidth: 80, + }, + { + id: "cashFlowStatus", + field: "cashFlowStatus", + headerName: t("Cash Flow Status"), + minWidth: 100, + renderCell: (params: any) => { + return params.row.invoicedAmount >= params.row.cumulativeExpenditure ? + {t("Positive")} + : {t("Negative")} + }, + }, + { + id: "cpi", + field: "cpi", + headerName: t("CPI"), + minWidth: 50, + renderCell: (params: any) => { + var cpi = params.row.cumulativeExpenditure != 0 ? params.row.invoicedAmount/params.row.cumulativeExpenditure : 0 + var cpiString = cpi.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }) + return (cpi >= 1) ? + {cpiString}: + {cpiString} + }, + }, + { + id: "projectedCashFlowStatus", + field: "projectedCashFlowStatus", + headerName: t("Projected Cash Flow Status"), + minWidth: 100, + renderCell: (params: any) => { + var status = params.row.invoiceAmount >= params.row.cumulativeExpenditure + return status ? + {t("Positive")} + : {t("Negative")} + }, + }, + { + id: "projectedCpi", + field: "projectedCpi", + headerName: t("Projected CPI"), + minWidth: 50, + renderCell: (params: any) => { + var projectCpi = params.row.cumulativeExpenditure != 0 ? params.row.totalFee/params.row.cumulativeExpenditure : 0 + var projectCpiString = projectCpi.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }) + if (projectCpi >= 1) { + return {projectCpiString}; + } else { + return {projectCpiString}; + } + }, + }, + { + id: "totalFee", + field: "totalFee", + headerName: t("Total Fees") + t("HKD"), + minWidth: 50, + type: "number", + renderCell: (params: any) => { + return ( + + $ + {params.row.totalFee.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + + ); + }, + }, + { + id: "totalBudget", + field: "totalBudget", + headerName: t("Total Budget") + t("HKD"), + minWidth: 50, + type: "number", + renderCell: (params: any) => { + return ( + + $ + {params.row.totalBudget.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + + ); + }, + }, + { + id: "cumulativeExpenditure", + field: "cumulativeExpenditure", + headerName: t("Total Cumulative Expenditure") + t("HKD"), + minWidth: 280, + type: "number", + renderCell: (params: any) => { + return ( + + $ + {params.row.cumulativeExpenditure.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + + ); + }, + }, + { + id: "manhourExpense", + field: "manhourExpense", + headerName: t("Manpower Expenses") + t("HKD"), + minWidth: 280, + type: "number", + renderCell: (params: any) => { + return ( + + $ + {params.row.manhourExpense.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + + ); + }, + }, + { + id: "projectExpense", + field: "projectExpense", + headerName: t("Project Expense") + t("HKD"), + minWidth: 280, + type: "number", + renderCell: (params: any) => { + return ( + + $ + {(params.row.projectExpense ?? 0).toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + + ); + }, + }, + { + id: "invoicedAmount", + field: "invoicedAmount", + headerName: t("Total Invoiced Amount") + t("HKD"), + minWidth: 250, + type: "number", + renderCell: (params: any) => { + return ( + + $ + {params.row.invoicedAmount.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + + ); + }, + }, + { + id: "nonInvoicedAmount", + field: "nonInvoicedAmount", + headerName: t("Total Un-Invoiced Amount") + t("HKD"), + minWidth: 250, + type: "number", + renderCell: (params: any) => { + return ( + + $ + {params.row.nonInvoicedAmount.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + + ); + }, + }, + { + id: "receivedAmount", + field: "receivedAmount", + headerName: t("Total Received Amount") + t("HKD"), + minWidth: 250, + type: "number", + renderCell: (params: any) => { + return ( + + $ + {params.row.receivedAmount.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + + ); + }, + }, + ]; + + return ( + <> + + { + setFilteredByProjectRows( + filteredByProjectRows.filter( + (cp:any) => + cp.projectCode.toLowerCase().includes(query.projectCode.toLowerCase()) && + cp.projectName.toLowerCase().includes(query.projectName.toLowerCase()) + ), + ); + }} + /> +
+ +
+ {/* items={filteredStaff} columns={columns} /> */} +
+ + { + setFilteredByClientRows( + filteredByClientRows.filter( + (cp:any) => + cp.customerCode.toLowerCase().includes(query.customerCode.toLowerCase()) && + cp.customerName.toLowerCase().includes(query.customerName.toLowerCase()) + ), + ); + }} + /> +
+ +
+
+ + ); +}; +export default FinancialStatusByProject; diff --git a/src/components/ProjectFinancialSummaryV2/TeamCard.tsx b/src/components/ProjectFinancialSummaryV2/TeamCard.tsx new file mode 100644 index 0000000..abcd36a --- /dev/null +++ b/src/components/ProjectFinancialSummaryV2/TeamCard.tsx @@ -0,0 +1,14 @@ +import { useRouter } from "next/navigation"; + +type Props = { + +} + +const TeamCard: React.FC = ({ + +}) => { + const router = useRouter(); + + return
+} +export default TeamCard \ No newline at end of file diff --git a/src/components/ProjectFinancialSummaryV2/gptFn.tsx b/src/components/ProjectFinancialSummaryV2/gptFn.tsx new file mode 100644 index 0000000..63c22ec --- /dev/null +++ b/src/components/ProjectFinancialSummaryV2/gptFn.tsx @@ -0,0 +1,40 @@ +import { FinancialSummaryByProject, FinancialSummaryByClient } from "@/app/api/financialsummary"; + + +export function summarizeFinancialData(data: FinancialSummaryByProject[]): FinancialSummaryByClient[] { + const result = data.reduce>((acc, item) => { + if (!acc[item.custId]) { + acc[item.custId] = { + id: item.custId, // Set id to custId + customerName: item.customerName, // First item's customerName + customerCode: item.customerCode, // First item's customerCode + subsidiaryName: item.subsidiaryName, // First item's subsidiaryName + totalFee: 0, + totalBudget: 0, + cumulativeExpenditure: 0, + manhourExpense: 0, + projectExpense: 0, + invoicedAmount: 0, + nonInvoicedAmount: 0, + receivedAmount: 0, + numberOfRecords: 0, // Initialize record count + }; + } + + // Sum the numeric fields + acc[item.custId].totalFee += item.totalFee; + acc[item.custId].totalBudget += item.totalBudget; + acc[item.custId].cumulativeExpenditure += item.cumulativeExpenditure; + acc[item.custId].manhourExpense += item.manhourExpense; + acc[item.custId].projectExpense += item.projectExpense; + acc[item.custId].invoicedAmount += item.invoicedAmount; + acc[item.custId].nonInvoicedAmount += item.nonInvoicedAmount; + acc[item.custId].receivedAmount += item.receivedAmount; + acc[item.custId].numberOfRecords += 1; // Increment record count + + return acc; + }, {}); + + // Convert the result object to an array + return Object.values(result); +} \ No newline at end of file diff --git a/src/components/ProjectFinancialSummaryV2/index.ts b/src/components/ProjectFinancialSummaryV2/index.ts new file mode 100644 index 0000000..13a41b9 --- /dev/null +++ b/src/components/ProjectFinancialSummaryV2/index.ts @@ -0,0 +1 @@ +export { default } from "./FinancialSummaryWrapper";