| @@ -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<searchParamsProps> = ({ searchParams }) => { | |||||
| preloadClientProjects(); | |||||
| return ( | |||||
| <I18nProvider namespaces={["dashboard", "common"]}> | |||||
| <ProjectFinancialSummaryV2 searchParams={searchParams}/> | |||||
| </I18nProvider> | |||||
| ); | |||||
| }; | |||||
| export default ProjectFinancialSummary; | |||||
| @@ -5,6 +5,7 @@ import { BASE_API_URL } from "@/config/api"; | |||||
| import { Dayjs } from "dayjs"; | import { Dayjs } from "dayjs"; | ||||
| import { cache } from "react"; | import { cache } from "react"; | ||||
| import { FileResponse } from "../reports/actions"; | import { FileResponse } from "../reports/actions"; | ||||
| import { revalidateTag } from "next/cache"; | |||||
| export interface FinancialSummaryByClientResult { | export interface FinancialSummaryByClientResult { | ||||
| @@ -135,4 +136,8 @@ export const exportFinancialSummaryByProjectExcel = cache(async (data: ExportFin | |||||
| ); | ); | ||||
| return reportBlob | return reportBlob | ||||
| }) | |||||
| }) | |||||
| export const revalidate = async(tag: string) => { | |||||
| revalidateTag(tag) | |||||
| } | |||||
| @@ -2,6 +2,7 @@ | |||||
| import { cache } from "react"; | import { cache } from "react"; | ||||
| import { serverFetchJson } from "@/app/utils/fetchUtil"; | import { serverFetchJson } from "@/app/utils/fetchUtil"; | ||||
| import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
| import { IndividualTeam } from "../team"; | |||||
| // import "server-only"; | // import "server-only"; | ||||
| @@ -18,6 +19,65 @@ 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 const preloadFinancialSummaryCard = () => { | export const preloadFinancialSummaryCard = () => { | ||||
| fetchFinancialSummaryCard(); | fetchFinancialSummaryCard(); | ||||
| }; | }; | ||||
| @@ -25,3 +85,20 @@ export const preloadFinancialSummaryCard = () => { | |||||
| export const fetchFinancialSummaryCard = cache(async () => { | 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"] }, | |||||
| }); | |||||
| }) | |||||
| @@ -171,6 +171,15 @@ const NavigationContent: React.FC<Props> = ({ abilities, username }) => { | |||||
| abilities!.includes(ability), | abilities!.includes(ability), | ||||
| ), | ), | ||||
| }, | }, | ||||
| { | |||||
| icon: <SummarizeIcon />, | |||||
| 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 | // No Claim function in Breaur, will be implement later | ||||
| // { | // { | ||||
| // icon: <RequestQuote />, | // icon: <RequestQuote />, | ||||
| @@ -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<Props> = ({ | |||||
| _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<FinancialSummaryByProject[]>([]) | |||||
| const [table2Data, setTable2Data] = useState<FinancialSummaryByClient[]>([]) | |||||
| 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<InputDate>(() => { | |||||
| 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 ( | |||||
| <> | |||||
| <Card sx={{ display: "block" }}> | |||||
| {/* <CardHeader className="text-slate-500" title= {t("Filter")}/> */} | |||||
| <CardContent component={Stack} spacing={1}> | |||||
| <Box sx={{ minWidth: 120 }}> | |||||
| <FormControl fullWidth> | |||||
| <InputLabel id="demo-simple-select-label">Financial Year</InputLabel> | |||||
| <Select | |||||
| labelId="demo-simple-select-label" | |||||
| id="demo-simple-select" | |||||
| value={period} | |||||
| label="Age" | |||||
| onChange={(e) => handleFilter(Number(e.target.value))} | |||||
| > | |||||
| {Array.from({ length: 5 }).map((_, i) => { | |||||
| if (i == 0) { | |||||
| return <MenuItem key={i} value={i}>{`${currFinancialYear - i - 1} - ${currFinancialYear - i} (current)`}</MenuItem> | |||||
| } else if (i == 4) { | |||||
| return <MenuItem value={i}>{`< ${currYear - i}`}</MenuItem> | |||||
| } else { | |||||
| return <MenuItem key={i} value={i}>{`${currFinancialYear - i - 1} - ${currFinancialYear - i}`}</MenuItem> | |||||
| } | |||||
| } | |||||
| )} | |||||
| </Select> | |||||
| </FormControl> | |||||
| </Box> | |||||
| </CardContent> | |||||
| </Card> | |||||
| { !isLoading ? ( | |||||
| <> | |||||
| <Card sx={{ display: "block" }}> | |||||
| <CardHeader className="text-slate-500" title= {t("Active Project Financial Status")}/> | |||||
| <CardContent component={Stack} spacing={4}> | |||||
| <div className="ml-10 mr-10" style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'start'}}> | |||||
| {financialSumm.length > 0 && financialSumm.map((record:any) => ( | |||||
| <div className="hover:cursor-pointer ml-4 mt-5 mb-4 inline-block" key={record.team.id} onClick={() => handleCardClick(record.team.id)}> | |||||
| <ProjectFinancialCard | |||||
| Title={record.teamName == "All Team" ? t("All Team") : record.team.name} | |||||
| TeamId={record.team.id} | |||||
| TotalActiveProjectNumber={record.activeProject} | |||||
| TotalFees={record.totalFees} | |||||
| TotalBudget={record.totalBudget} | |||||
| TotalCumulative={record.cumulativeExpenditure} | |||||
| TotalProjectExpense={record.projectExpense} | |||||
| TotalInvoicedAmount={record.invoicedAmount} | |||||
| TotalUnInvoicedAmount={record.nonInvoicedAmount} | |||||
| TotalReceivedAmount={record.receivedAmount} | |||||
| CashFlowStatus={record.cashFlowStatus} | |||||
| CostPerformanceIndex={record.costPerformanceIndex} | |||||
| ProjectedCashFlowStatus={record.projectedCashFlowStatus} | |||||
| ProjectedCPI={record.projectedCostPerformanceIndex} | |||||
| ClickedIndex={isCardClickedIndex} | |||||
| Index={record.team.id}/> | |||||
| </div> | |||||
| ))} | |||||
| </div> | |||||
| </CardContent> | |||||
| </Card> | |||||
| <FinancialStatusByProject | |||||
| financialSummByProject={table1Data}/> | |||||
| </>) | |||||
| : | |||||
| <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}> | |||||
| <CircularProgress /> | |||||
| </Box> | |||||
| } | |||||
| </> | |||||
| ) | |||||
| } | |||||
| export default FinancialSummaryPage | |||||
| @@ -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 ( | |||||
| <> | |||||
| <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 FinancialSummaryLoading; | |||||
| @@ -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<searchParamsProps> & 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 ( | |||||
| <Grid> | |||||
| <FinancialSummary _financialSumm={financialSumm} _teamId={teamId}/> | |||||
| {/* <FinancialStatusByProject financialSummByProject={financialSummByProject}/> */} | |||||
| </Grid> | |||||
| ); | |||||
| }; | |||||
| FinancialSummaryWrapper.Loading = FinancialSummaryLoading; | |||||
| export default FinancialSummaryWrapper; | |||||
| @@ -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<Omit<FinancialSummaryByProject, "id">>; | |||||
| type SearchParamNames = keyof SearchQuery; | |||||
| type SearchQuery2 = Partial<Omit<FinancialSummaryByClient, "id">>; | |||||
| type SearchParamNames2 = keyof SearchQuery2; | |||||
| const FinancialStatusByProject: React.FC<Props> = ({ | |||||
| 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<SearchParamNames>[] = useMemo( | |||||
| () => [ | |||||
| { label: t("Project Code"), paramName: "projectCode", type: "text" }, | |||||
| { label: t("Project Name"), paramName: "projectName", type: "text" }, | |||||
| ], | |||||
| [t], | |||||
| ); | |||||
| const searchCriteria2: Criterion<SearchParamNames2>[] = 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) => ( | |||||
| <div | |||||
| className="text-blue-600 hover:underline cursor-pointer" | |||||
| onClick={() => { | |||||
| router.push( | |||||
| `/dashboard/ProjectCashFlow?projectId=${params.row.id}` | |||||
| ); | |||||
| }} | |||||
| > | |||||
| {params.value} | |||||
| </div> | |||||
| ), | |||||
| }, | |||||
| { | |||||
| 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 <span className={greenColor}>{t("Positive")}</span>; | |||||
| } else { | |||||
| return <span className={redColor}>{t("Negative")}</span>; | |||||
| } | |||||
| }, | |||||
| }, | |||||
| { | |||||
| id: "cpi", | |||||
| field: "cpi", | |||||
| headerName: "CPI", | |||||
| minWidth: 50, | |||||
| renderCell: (params: any) => { | |||||
| return ( | |||||
| <span className={params.row.cpi >= 1 ? greenColor : redColor}> | |||||
| {params.row.cpi.toLocaleString(undefined, { | |||||
| minimumFractionDigits: 2, | |||||
| maximumFractionDigits: 2, | |||||
| })} | |||||
| </span> | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| id: "projectedCashFlowStatus", | |||||
| field: "projectedCashFlowStatus", | |||||
| headerName: t("Projected Cash Flow Status"), | |||||
| minWidth: 100, | |||||
| renderCell: (params: any) => { | |||||
| if (params.row.totalFee >= params.row.cumulativeExpenditure) { | |||||
| return <span className={greenColor}>{t("Positive")}</span>; | |||||
| } else { | |||||
| return <span className={redColor}>{t("Negative")}</span>; | |||||
| } | |||||
| }, | |||||
| }, | |||||
| { | |||||
| id: "projectedCpi", | |||||
| field: "projectedCpi", | |||||
| headerName: t("Projected CPI"), | |||||
| minWidth: 50, | |||||
| renderCell: (params: any) => { | |||||
| return ( | |||||
| <span | |||||
| className={params.row.projectedCpi >= 1 ? greenColor : redColor} | |||||
| > | |||||
| {params.row.projectedCpi.toLocaleString(undefined, { | |||||
| minimumFractionDigits: 2, | |||||
| maximumFractionDigits: 2, | |||||
| })} | |||||
| </span> | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| id: "totalFee", | |||||
| field: "totalFee", | |||||
| headerName: t("Total Fees") + t("HKD"), | |||||
| type: "number", | |||||
| minWidth: 50, | |||||
| renderCell: (params: any) => { | |||||
| return ( | |||||
| <span> | |||||
| $ | |||||
| {params.row.totalFee.toLocaleString(undefined, { | |||||
| minimumFractionDigits: 2, | |||||
| maximumFractionDigits: 2, | |||||
| })} | |||||
| </span> | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| id: "totalBudget", | |||||
| field: "totalBudget", | |||||
| headerName: t("Total Budget") + t("HKD"), | |||||
| minWidth: 50, | |||||
| type: "number", | |||||
| renderCell: (params: any) => { | |||||
| return ( | |||||
| <span> | |||||
| $ | |||||
| {params.row.totalBudget.toLocaleString(undefined, { | |||||
| minimumFractionDigits: 2, | |||||
| maximumFractionDigits: 2, | |||||
| })} | |||||
| </span> | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| id: "cumulativeExpenditure", | |||||
| field: "cumulativeExpenditure", | |||||
| headerName: t("Total Cumulative Expenditure") + t("HKD"), | |||||
| minWidth: 250, | |||||
| type: "number", | |||||
| renderCell: (params: any) => { | |||||
| return ( | |||||
| <span> | |||||
| $ | |||||
| {params.row.cumulativeExpenditure.toLocaleString(undefined, { | |||||
| minimumFractionDigits: 2, | |||||
| maximumFractionDigits: 2, | |||||
| })} | |||||
| </span> | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| id: "manhourExpense", | |||||
| field: "manhourExpense", | |||||
| headerName: t("Manpower Expenses") + t("HKD"), | |||||
| minWidth: 280, | |||||
| type: "number", | |||||
| renderCell: (params: any) => { | |||||
| return ( | |||||
| <span> | |||||
| $ | |||||
| {params.row.manhourExpense.toLocaleString(undefined, { | |||||
| minimumFractionDigits: 2, | |||||
| maximumFractionDigits: 2, | |||||
| })} | |||||
| </span> | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| id: "projectExpense", | |||||
| field: "projectExpense", | |||||
| headerName: t("Project Expense") + t("HKD"), | |||||
| minWidth: 280, | |||||
| type: "number", | |||||
| renderCell: (params: any) => { | |||||
| return ( | |||||
| <span> | |||||
| $ | |||||
| {(params.row.projectExpense ?? 0).toLocaleString(undefined, { | |||||
| minimumFractionDigits: 2, | |||||
| maximumFractionDigits: 2, | |||||
| })} | |||||
| </span> | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| id: "invoicedAmount", | |||||
| field: "invoicedAmount", | |||||
| headerName: t("Total Invoiced Amount") + t("HKD"), | |||||
| minWidth: 250, | |||||
| type: "number", | |||||
| renderCell: (params: any) => { | |||||
| return ( | |||||
| <span> | |||||
| $ | |||||
| {params.row.invoicedAmount.toLocaleString(undefined, { | |||||
| minimumFractionDigits: 2, | |||||
| maximumFractionDigits: 2, | |||||
| })} | |||||
| </span> | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| id: "nonInvoicedAmount", | |||||
| field: "nonInvoicedAmount", | |||||
| headerName: t("Total Un-Invoiced Amount") + t("HKD"), | |||||
| minWidth: 250, | |||||
| type: "number", | |||||
| renderCell: (params: any) => { | |||||
| return ( | |||||
| <span> | |||||
| $ | |||||
| {params.row.nonInvoicedAmount.toLocaleString(undefined, { | |||||
| minimumFractionDigits: 2, | |||||
| maximumFractionDigits: 2, | |||||
| })} | |||||
| </span> | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| id: "receivedAmount", | |||||
| field: "receivedAmount", | |||||
| headerName: t("Total Received Amount") + t("HKD"), | |||||
| minWidth: 250, | |||||
| type: "number", | |||||
| renderCell: (params: any) => { | |||||
| return ( | |||||
| <span> | |||||
| $ | |||||
| {params.row.receivedAmount.toLocaleString(undefined, { | |||||
| minimumFractionDigits: 2, | |||||
| maximumFractionDigits: 2, | |||||
| })} | |||||
| </span> | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| ]; | |||||
| const columns2 = [ | |||||
| { | |||||
| id: "customerCode", | |||||
| field: "customerCode", | |||||
| headerName: t("Client Code"), | |||||
| minWidth: 50, | |||||
| renderCell: (params: any) => ( | |||||
| <div | |||||
| className="text-blue-600 hover:underline cursor-pointer" | |||||
| onClick={() => { | |||||
| router.push( | |||||
| `/dashboard/ProjectStatusByClient?customerId=${params.row.id}` | |||||
| ); | |||||
| }} | |||||
| > | |||||
| {params.value} | |||||
| </div> | |||||
| ), | |||||
| }, | |||||
| { | |||||
| 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 ? | |||||
| <span className={greenColor}>{t("Positive")}</span> | |||||
| : <span className={redColor}>{t("Negative")}</span> | |||||
| }, | |||||
| }, | |||||
| { | |||||
| 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) ? | |||||
| <span className={greenColor}>{cpiString}</span>: | |||||
| <span className={redColor}>{cpiString}</span> | |||||
| }, | |||||
| }, | |||||
| { | |||||
| 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 ? | |||||
| <span className={greenColor}>{t("Positive")}</span> | |||||
| : <span className={redColor}>{t("Negative")}</span> | |||||
| }, | |||||
| }, | |||||
| { | |||||
| 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 <span className={greenColor}>{projectCpiString}</span>; | |||||
| } else { | |||||
| return <span className={redColor}>{projectCpiString}</span>; | |||||
| } | |||||
| }, | |||||
| }, | |||||
| { | |||||
| id: "totalFee", | |||||
| field: "totalFee", | |||||
| headerName: t("Total Fees") + t("HKD"), | |||||
| minWidth: 50, | |||||
| type: "number", | |||||
| renderCell: (params: any) => { | |||||
| return ( | |||||
| <span> | |||||
| $ | |||||
| {params.row.totalFee.toLocaleString(undefined, { | |||||
| minimumFractionDigits: 2, | |||||
| maximumFractionDigits: 2, | |||||
| })} | |||||
| </span> | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| id: "totalBudget", | |||||
| field: "totalBudget", | |||||
| headerName: t("Total Budget") + t("HKD"), | |||||
| minWidth: 50, | |||||
| type: "number", | |||||
| renderCell: (params: any) => { | |||||
| return ( | |||||
| <span> | |||||
| $ | |||||
| {params.row.totalBudget.toLocaleString(undefined, { | |||||
| minimumFractionDigits: 2, | |||||
| maximumFractionDigits: 2, | |||||
| })} | |||||
| </span> | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| id: "cumulativeExpenditure", | |||||
| field: "cumulativeExpenditure", | |||||
| headerName: t("Total Cumulative Expenditure") + t("HKD"), | |||||
| minWidth: 280, | |||||
| type: "number", | |||||
| renderCell: (params: any) => { | |||||
| return ( | |||||
| <span> | |||||
| $ | |||||
| {params.row.cumulativeExpenditure.toLocaleString(undefined, { | |||||
| minimumFractionDigits: 2, | |||||
| maximumFractionDigits: 2, | |||||
| })} | |||||
| </span> | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| id: "manhourExpense", | |||||
| field: "manhourExpense", | |||||
| headerName: t("Manpower Expenses") + t("HKD"), | |||||
| minWidth: 280, | |||||
| type: "number", | |||||
| renderCell: (params: any) => { | |||||
| return ( | |||||
| <span> | |||||
| $ | |||||
| {params.row.manhourExpense.toLocaleString(undefined, { | |||||
| minimumFractionDigits: 2, | |||||
| maximumFractionDigits: 2, | |||||
| })} | |||||
| </span> | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| id: "projectExpense", | |||||
| field: "projectExpense", | |||||
| headerName: t("Project Expense") + t("HKD"), | |||||
| minWidth: 280, | |||||
| type: "number", | |||||
| renderCell: (params: any) => { | |||||
| return ( | |||||
| <span> | |||||
| $ | |||||
| {(params.row.projectExpense ?? 0).toLocaleString(undefined, { | |||||
| minimumFractionDigits: 2, | |||||
| maximumFractionDigits: 2, | |||||
| })} | |||||
| </span> | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| id: "invoicedAmount", | |||||
| field: "invoicedAmount", | |||||
| headerName: t("Total Invoiced Amount") + t("HKD"), | |||||
| minWidth: 250, | |||||
| type: "number", | |||||
| renderCell: (params: any) => { | |||||
| return ( | |||||
| <span> | |||||
| $ | |||||
| {params.row.invoicedAmount.toLocaleString(undefined, { | |||||
| minimumFractionDigits: 2, | |||||
| maximumFractionDigits: 2, | |||||
| })} | |||||
| </span> | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| id: "nonInvoicedAmount", | |||||
| field: "nonInvoicedAmount", | |||||
| headerName: t("Total Un-Invoiced Amount") + t("HKD"), | |||||
| minWidth: 250, | |||||
| type: "number", | |||||
| renderCell: (params: any) => { | |||||
| return ( | |||||
| <span> | |||||
| $ | |||||
| {params.row.nonInvoicedAmount.toLocaleString(undefined, { | |||||
| minimumFractionDigits: 2, | |||||
| maximumFractionDigits: 2, | |||||
| })} | |||||
| </span> | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| id: "receivedAmount", | |||||
| field: "receivedAmount", | |||||
| headerName: t("Total Received Amount") + t("HKD"), | |||||
| minWidth: 250, | |||||
| type: "number", | |||||
| renderCell: (params: any) => { | |||||
| return ( | |||||
| <span> | |||||
| $ | |||||
| {params.row.receivedAmount.toLocaleString(undefined, { | |||||
| minimumFractionDigits: 2, | |||||
| maximumFractionDigits: 2, | |||||
| })} | |||||
| </span> | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| ]; | |||||
| return ( | |||||
| <> | |||||
| <Box sx={{ mt: 3 }}> | |||||
| <SearchBox | |||||
| criteria={searchCriteria} | |||||
| onSearch={(query) => { | |||||
| setFilteredByProjectRows( | |||||
| filteredByProjectRows.filter( | |||||
| (cp:any) => | |||||
| cp.projectCode.toLowerCase().includes(query.projectCode.toLowerCase()) && | |||||
| cp.projectName.toLowerCase().includes(query.projectName.toLowerCase()) | |||||
| ), | |||||
| ); | |||||
| }} | |||||
| /> | |||||
| <div style={{ display: "inline-block", width: "99%", marginLeft: 10 }}> | |||||
| <CustomDatagrid | |||||
| rows={filteredByProjectRows} | |||||
| columns={columns1} | |||||
| columnWidth={200} | |||||
| dataGridHeight={300} | |||||
| /> | |||||
| </div> | |||||
| {/* <SearchResults<StaffResult> items={filteredStaff} columns={columns} /> */} | |||||
| </Box> | |||||
| <Box sx={{ mt: 3 }}> | |||||
| <SearchBox | |||||
| criteria={searchCriteria2} | |||||
| onSearch={(query) => { | |||||
| setFilteredByClientRows( | |||||
| filteredByClientRows.filter( | |||||
| (cp:any) => | |||||
| cp.customerCode.toLowerCase().includes(query.customerCode.toLowerCase()) && | |||||
| cp.customerName.toLowerCase().includes(query.customerName.toLowerCase()) | |||||
| ), | |||||
| ); | |||||
| }} | |||||
| /> | |||||
| <div style={{ display: "inline-block", width: "99%", marginLeft: 10 }}> | |||||
| <CustomDatagrid | |||||
| rows={filteredByClientRows} | |||||
| columns={columns2} | |||||
| columnWidth={200} | |||||
| dataGridHeight={300} | |||||
| /> | |||||
| </div> | |||||
| </Box> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default FinancialStatusByProject; | |||||
| @@ -0,0 +1,14 @@ | |||||
| import { useRouter } from "next/navigation"; | |||||
| type Props = { | |||||
| } | |||||
| const TeamCard: React.FC<Props> = ({ | |||||
| }) => { | |||||
| const router = useRouter(); | |||||
| return <div></div> | |||||
| } | |||||
| export default TeamCard | |||||
| @@ -0,0 +1,40 @@ | |||||
| import { FinancialSummaryByProject, FinancialSummaryByClient } from "@/app/api/financialsummary"; | |||||
| export function summarizeFinancialData(data: FinancialSummaryByProject[]): FinancialSummaryByClient[] { | |||||
| const result = data.reduce<Record<number, FinancialSummaryByClient>>((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); | |||||
| } | |||||
| @@ -0,0 +1 @@ | |||||
| export { default } from "./FinancialSummaryWrapper"; | |||||