Переглянути джерело

Merge branch 'main' of https://git.2fi-solutions.com/wayne.lee/tsms

tags/Baseline_180220205_Frontend
MSI\2Fi 10 місяці тому
джерело
коміт
687635c083
12 змінених файлів з 1063 додано та 1 видалено
  1. +19
    -0
      src/app/(main)/dashboard/ProjectFinancialSummaryV2/page.tsx
  2. +6
    -1
      src/app/api/financialsummary/actions.ts
  3. +77
    -0
      src/app/api/financialsummary/index.ts
  4. +0
    -0
      src/components/InvoiceSearch/InvoiceAutocomplete.tsx
  5. +9
    -0
      src/components/NavigationContent/NavigationContent.tsx
  6. +198
    -0
      src/components/ProjectFinancialSummaryV2/FinancialSummary.tsx
  7. +40
    -0
      src/components/ProjectFinancialSummaryV2/FinancialSummaryLoading.tsx
  8. +67
    -0
      src/components/ProjectFinancialSummaryV2/FinancialSummaryWrapper.tsx
  9. +592
    -0
      src/components/ProjectFinancialSummaryV2/FinnancialStatusByProject.tsx
  10. +14
    -0
      src/components/ProjectFinancialSummaryV2/TeamCard.tsx
  11. +40
    -0
      src/components/ProjectFinancialSummaryV2/gptFn.tsx
  12. +1
    -0
      src/components/ProjectFinancialSummaryV2/index.ts

+ 19
- 0
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<searchParamsProps> = ({ searchParams }) => {
preloadClientProjects();
return (
<I18nProvider namespaces={["dashboard", "common"]}>
<ProjectFinancialSummaryV2 searchParams={searchParams}/>
</I18nProvider>
);
};
export default ProjectFinancialSummary;

+ 6
- 1
src/app/api/financialsummary/actions.ts Переглянути файл

@@ -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)
}

+ 77
- 0
src/app/api/financialsummary/index.ts Переглянути файл

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

+ 0
- 0
src/components/InvoiceSearch/InvoiceAutocomplete.tsx Переглянути файл


+ 9
- 0
src/components/NavigationContent/NavigationContent.tsx Переглянути файл

@@ -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 />,


+ 198
- 0
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<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

+ 40
- 0
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 (
<>
<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;

+ 67
- 0
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<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;

+ 592
- 0
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<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;

+ 14
- 0
src/components/ProjectFinancialSummaryV2/TeamCard.tsx Переглянути файл

@@ -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

+ 40
- 0
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<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);
}

+ 1
- 0
src/components/ProjectFinancialSummaryV2/index.ts Переглянути файл

@@ -0,0 +1 @@
export { default } from "./FinancialSummaryWrapper";

Завантаження…
Відмінити
Зберегти