@@ -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 { 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) | |||
} |
@@ -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<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), | |||
), | |||
}, | |||
{ | |||
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 | |||
// { | |||
// 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"; |