Ver a proveniência

dashboard update

tags/Baseline_30082024_FRONTEND_UAT
MSI\User há 1 ano
ascendente
cometimento
1f113362ad
15 ficheiros alterados com 900 adições e 5 eliminações
  1. +31
    -0
      src/app/(main)/dashboard/ProjectCashFlow/page.tsx
  2. +28
    -0
      src/app/(main)/dashboard/ProjectFinancialSummary/page.tsx
  3. +39
    -0
      src/app/api/cashflow/index.ts
  4. +2
    -2
      src/components/CustomDatagrid/CustomDatagrid.tsx
  5. +3
    -2
      src/components/NavigationContent/NavigationContent.tsx
  6. +120
    -0
      src/components/ProgressCashFlowSearch/ProgressCashFlowSearch.tsx
  7. +40
    -0
      src/components/ProgressCashFlowSearch/ProgressCashFlowSearchLoading.tsx
  8. +18
    -0
      src/components/ProgressCashFlowSearch/ProgressCashFlowSearchWrapper.tsx
  9. +1
    -0
      src/components/ProgressCashFlowSearch/index.ts
  10. +210
    -0
      src/components/ProjectCashFlow/ProjectCashFlow.tsx
  11. +1
    -0
      src/components/ProjectCashFlow/index.ts
  12. +75
    -0
      src/components/ProjectFinancialSummary/ProjectFinancialCard.tsx
  13. +272
    -0
      src/components/ProjectFinancialSummary/ProjectFinancialSummary.tsx
  14. +1
    -0
      src/components/ProjectFinancialSummary/index.ts
  15. +59
    -1
      src/components/SearchBox/SearchBox.tsx

+ 31
- 0
src/app/(main)/dashboard/ProjectCashFlow/page.tsx Ver ficheiro

@@ -0,0 +1,31 @@
import { Metadata } from "next";
import { I18nProvider } from "@/i18n";
import DashboardPage from "@/components/DashboardPage/DashboardPage";
import DashboardPageButton from "@/components/DashboardPage/DashboardTabButton";
import ProgressCashFlowSearch from "@/components/ProgressCashFlowSearch";
import { Suspense} from "react";
import Tabs, { TabsProps } from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
import Typography from "@mui/material/Typography";
import ProjectCashFlowComponent from '@/components/ProjectCashFlow'

export const metadata: Metadata = {
title: "Project Status by Client",
};


const ProjectCashFlow: React.FC = () => {

return (
<I18nProvider namespaces={["dashboard"]}>
<Typography variant="h4" marginInlineEnd={2}>
Project Cash Flow
</Typography>
{/* <Suspense fallback={<ProgressCashFlowSearch.Loading />}>
<ProgressCashFlowSearch/>
</Suspense> */}
<ProjectCashFlowComponent/>
</I18nProvider>
);
};
export default ProjectCashFlow;

+ 28
- 0
src/app/(main)/dashboard/ProjectFinancialSummary/page.tsx Ver ficheiro

@@ -0,0 +1,28 @@
import { Metadata } from "next";
import { I18nProvider } from "@/i18n";
import DashboardPage from "@/components/DashboardPage/DashboardPage";
import DashboardPageButton from "@/components/DashboardPage/DashboardTabButton";
import ProgressByClientSearch from "@/components/ProgressByClientSearch";
import { Suspense} from "react";
import Tabs, { TabsProps } from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
import Typography from "@mui/material/Typography";
import ProjectFinancialSummaryComponents from "@/components/ProjectFinancialSummary";

export const metadata: Metadata = {
title: "Project Status by Client",
};


const ProjectFinancialSummary: React.FC = () => {

return (
<I18nProvider namespaces={["dashboard"]}>
<Typography variant="h4" marginInlineEnd={2}>
Project Financial Summary
</Typography>
<ProjectFinancialSummaryComponents/>
</I18nProvider>
);
};
export default ProjectFinancialSummary;

+ 39
- 0
src/app/api/cashflow/index.ts Ver ficheiro

@@ -0,0 +1,39 @@
import { cache } from "react";

export interface CashFlow {
id: number;
projectCode: string;
projectName: string;
team: string;
teamLeader: string;
startDate: string;
startDateFrom: string;
startDateTo: string;
targetEndDate: string;
client: string;
subsidiary: string;
}

export const preloadProjects = () => {
fetchProjectsCashFlow();
};

export const fetchProjectsCashFlow = cache(async () => {
return mockProjects;
});

const mockProjects: CashFlow[] = [
{
id: 1,
projectCode: "CUST-001",
projectName: "Client A",
team: "N/A",
teamLeader: "N/A",
startDate: "5",
startDateFrom: "5",
startDateTo: "5",
targetEndDate: "s",
client: "ss",
subsidiary:"ss",
}
];

+ 2
- 2
src/components/CustomDatagrid/CustomDatagrid.tsx Ver ficheiro

@@ -119,7 +119,7 @@ const CustomDatagrid: React.FC<CustomDatagridProps> = ({
return (
<div className="mt-5 mb-5" style={{ height: dataGridHeight ?? 400, width: '100%'}}>
{Title ? (
<Card style={{marginRight:20}}>
<Card style={{marginRight:10}}>
{Title && <CardHeader className="text-slate-500" title={Title} />}
<CardContent style={{ display: "flex", alignItems: "center", justifyContent: "center", marginTop:-20 }}>
{Style ? (
@@ -215,7 +215,7 @@ const CustomDatagrid: React.FC<CustomDatagridProps> = ({
rows={rowsWithDefaultValues}
columns={modifiedColumns}
editMode="row"
style={{marginRight:20}}
style={{marginRight:0}}
checkboxSelection={checkboxSelection}
onRowSelectionModelChange={onRowSelectionModelChange}
initialState={{


+ 3
- 2
src/components/NavigationContent/NavigationContent.tsx Ver ficheiro

@@ -31,8 +31,9 @@ interface NavigationItem {
const navigationItems: NavigationItem[] = [
{ icon: <WorkHistory />, label: "User Workspace", path: "/home" },
{ icon: <Dashboard />, label: "Dashboard", path: "", children: [
{ icon: <ArrowCircleLeftOutlinedIcon />, label: "Project Status by Client", path: "/dashboard/ProjectStatusByClient" },
{ icon: <ArrowCircleLeftRoundedIcon />, label: "Subitem 2", path: "/dashboard/subitem2" },
{ icon: <Dashboard />, label: "Project Financial Summary", path: "/dashboard/ProjectFinancialSummary" },
{ icon: <Dashboard />, label: "Project Cash Flow", path: "/dashboard/ProjectCashFlow" },
{ icon: <Dashboard />, label: "Project Status by Client", path: "/dashboard/ProjectStatusByClient" },
]},
{ icon: <RequestQuote />, label: "Expense Claim", path: "/claim" },
{ icon: <Assignment />, label: "Project Management", path: "/projects" },


+ 120
- 0
src/components/ProgressCashFlowSearch/ProgressCashFlowSearch.tsx Ver ficheiro

@@ -0,0 +1,120 @@
"use client";

import { ProjectResult } from "@/app/api/projects";
import React, { useMemo, useState } from "react";
import SearchBox, { Criterion } from "../SearchBox";
import { useTranslation } from "react-i18next";
import SearchResults, { Column } from "../SearchResults";
import { CashFlow } from "@/app/api/cashflow";
import CustomDatagrid from '../CustomDatagrid/CustomDatagrid';
import { GridColDef, GridRowSelectionModel} from '@mui/x-data-grid';
import ProjectCashFlow from '../ProjectCashFlow'

interface Props {
projects: CashFlow[];
}
type SearchQuery = Partial<Omit<CashFlow, "id">>;
type SearchParamNames = keyof SearchQuery;

const ProgressByClientSearch: React.FC<Props> = ({ projects}) => {
const { t } = useTranslation("projects");
const [selectionModel, setSelectionModel] : any[] = React.useState([]);
const columns = [
{
id: 'projectCode',
field: 'projectCode',
headerName: "Project Code",
flex: 1,
},
{
id: 'projectName',
field: 'projectName',
headerName: "Project Name",
flex: 1,
},
{
id: 'team',
field: 'team',
headerName: "Team",
flex: 1,
},
{
id: 'teamLeader',
field: 'teamLeader',
headerName: "Team Leader",
flex: 1,
},
{
id: 'startDate',
field: 'startDate',
headerName: "Start Date",
flex: 1,
},
{
id: 'targetEndDate',
field: 'targetEndDate',
headerName: "Target End Date",
flex: 1,
},
{
id: 'client',
field: 'client',
headerName: "Client",
flex: 1,
},
{
id: 'subsidiary',
field: 'subsidiary',
headerName: "Subsidiary",
flex: 1,
},
];
const rows = [{id: 1,projectCode:"M1001",projectName:"Consultancy Project A", team:"XXX", teamLeader:"XXX", startDate:"01/07/2022", targetEndDate: "01/04/2024", client:"Client B", subsidiary:"N/A"},
{id: 2,projectCode:"M1301",projectName:"Consultancy Project AAAA", team:"XXX", teamLeader:"XXX", startDate:"01/09/2022", targetEndDate: "20/02/2024", client:"Client C", subsidiary:"Subsidiary A"},
{id: 3,projectCode:"M1354",projectName:"Consultancy Project BBB", team:"YYY", teamLeader:"YYY", startDate:"01/02/2023", targetEndDate: "31/01/2024", client:"Client D", subsidiary:"Subsidiary C"}
]
const [selectedTeamData, setSelectedTeamData] : any[] = React.useState(rows);
const handleSelectionChange = (newSelectionModel: GridRowSelectionModel) => {
const selectedRowsData = selectedTeamData.filter((row:any) =>
newSelectionModel.includes(row.id)
);
console.log(selectedRowsData)
}

// If project searching is done on the server-side, then no need for this.
const [filteredProjects, setFilteredProjects] = useState(projects);

const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
() => [
{ label: "Project Code", paramName: "projectCode", type: "text" },
{ label: "Project Name", paramName: "projectName", type: "text" },
{ label: "Start Date From",label2: "Start Date To", paramName: "startDateFrom", type: "dateRange" },
],
[t],
);

// const columns = useMemo<Column<CashFlow>[]>(
// () => [
// { name: "clientCode", label: t("Project Code") },
// { name: "clientName", label: t("Project Name") },
// { name: "SubsidiaryClientCode", label: t("Project Category") },
// { name: "SubsidiaryClientName", label: t("Team") },
// { name: "NoOfProjects", label: t("Client") },
// ],
// [t],
// );

return (
<>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
console.log(query);
}}
/>
{/* <ProjectCashFlow/> */}
</>
);
};

export default ProgressByClientSearch;

+ 40
- 0
src/components/ProgressCashFlowSearch/ProgressCashFlowSearchLoading.tsx Ver ficheiro

@@ -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 ProgressCashFlowSearchLoading: 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 ProgressCashFlowSearchLoading;

+ 18
- 0
src/components/ProgressCashFlowSearch/ProgressCashFlowSearchWrapper.tsx Ver ficheiro

@@ -0,0 +1,18 @@
import { fetchProjectsCashFlow } from "@/app/api/cashflow";
import React from "react";
import ProgressCashFlowSearch from "./ProgressCashFlowSearch";
import ProgressCashFlowSearchSearchLoading from "./ProgressCashFlowSearchLoading";

interface SubComponents {
Loading: typeof ProgressCashFlowSearchSearchLoading;
}

const ProgressCashFlowSearchWrapper: React.FC & SubComponents = async () => {
const clentprojects = await fetchProjectsCashFlow();

return <ProgressCashFlowSearch projects={clentprojects} />;
};

ProgressCashFlowSearchWrapper.Loading = ProgressCashFlowSearchSearchLoading;

export default ProgressCashFlowSearchWrapper;

+ 1
- 0
src/components/ProgressCashFlowSearch/index.ts Ver ficheiro

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

+ 210
- 0
src/components/ProjectCashFlow/ProjectCashFlow.tsx Ver ficheiro

@@ -0,0 +1,210 @@
"use client";
import * as React from "react";
import Grid from "@mui/material/Grid";
import { useState,useEffect, useMemo } from 'react'
import Paper from "@mui/material/Paper";
import { TFunction } from "i18next";
import { useTranslation } from "react-i18next";
import {Card,CardHeader} from '@mui/material';
import CustomSearchForm from "../CustomSearchForm/CustomSearchForm";
import CustomDatagrid from '../CustomDatagrid/CustomDatagrid';
import ReactApexChart from 'react-apexcharts';
import { ApexOptions } from 'apexcharts';
import { GridColDef, GridRowSelectionModel} from '@mui/x-data-grid';
import ReportProblemIcon from '@mui/icons-material/ReportProblem';
import dynamic from 'next/dynamic';
import '../../app/global.css';
import { AnyARecord, AnyCnameRecord } from "dns";
import SearchBox, { Criterion } from "../SearchBox";
import ProgressByClientSearch from "@/components/ProgressByClientSearch";
import { Suspense } from "react";
import ProgressCashFlowSearch from "@/components/ProgressCashFlowSearch";

const ProjectCashFlow: React.FC = () => {
const [selectionModel, setSelectionModel] : any[] = React.useState([]);
// const series: ApexAxisChartSeries | ApexNonAxisChartSeries = [{
// name: 'Monthly Income',
// type: 'line',
// data: [80, 55, 40, 65, 70],
// },
// {
// name: 'Monthly Incomess',
// type: 'column',
// data: [80, 55, 40, 65, 70],
// }
// ];
const columns = [
{
id: 'projectCode',
field: 'projectCode',
headerName: "Project Code",
flex: 1,
},
{
id: 'projectName',
field: 'projectName',
headerName: "Project Name",
flex: 1,
},
{
id: 'team',
field: 'team',
headerName: "Team",
flex: 1,
},
{
id: 'teamLeader',
field: 'teamLeader',
headerName: "Team Leader",
flex: 1,
},
{
id: 'startDate',
field: 'startDate',
headerName: "Start Date",
flex: 1,
},
{
id: 'targetEndDate',
field: 'targetEndDate',
headerName: "Target End Date",
flex: 1,
},
{
id: 'client',
field: 'client',
headerName: "Client",
flex: 1,
},
{
id: 'subsidiary',
field: 'subsidiary',
headerName: "Subsidiary",
flex: 1,
},
];

const options: ApexOptions = {
chart: {
height: 350,
type: 'line',
},
plotOptions: {
bar: {
horizontal: false,
distributed: false,
},
},
dataLabels: {
enabled: false
},
xaxis: {
categories: [
'Q1',
'Q2',
'Q3',
'Q4',
'Q5',
'Q6',
'Q7',
'Q8',
'Q9',
'Q10',
'Q11',
'Q12',
],
},
yaxis: [{
title: {
text: 'Monthly Income and Expenditure (HKD)'
},
labels: {
maxWidth: 300,
style: {
cssClass: 'apexcharts-yaxis-label',
},
},
},
{
opposite: true,
title: {
text: 'Cumulative Income and Expenditure (HKD)'
}}
],
title: {
text: 'Current Stage Completion Percentage',
align: 'center'
},
grid: {
borderColor: '#f1f1f1',
},
annotations: {
},
series:[
{
name:"Monthly Income",
type:"column",
color: "#ffde91",
data:[0,110000,0,0,185000,0,0,189000,0,0,300000,0]
},
{
name:"Monthly Expenditure",
type:"column",
color: "#82b59d",
data:[0,160000,120000,120000,55000,55000,55000,55000,55000,70000,55000,55000]
},
{
name:"Cumulative Income",
type:"line",
color: "#EE6D7A",
data:[1,2,3,5,6,9,8,5,6,1,16,15]
},
{
name:"Cumulative Expenditure",
type:"line",
color: "#EE6D7A",
data:[1,2,3,5,6,9,8,5,6,1,16,15]
}
]
};

const rows = [{id: 1,projectCode:"M1001",projectName:"Consultancy Project A", team:"XXX", teamLeader:"XXX", startDate:"01/07/2022", targetEndDate: "01/04/2024", client:"Client B", subsidiary:"N/A"},
{id: 2,projectCode:"M1301",projectName:"Consultancy Project AAAA", team:"XXX", teamLeader:"XXX", startDate:"01/09/2022", targetEndDate: "20/02/2024", client:"Client C", subsidiary:"Subsidiary A"},
{id: 3,projectCode:"M1354",projectName:"Consultancy Project BBB", team:"YYY", teamLeader:"YYY", startDate:"01/02/2023", targetEndDate: "31/01/2024", client:"Client D", subsidiary:"Subsidiary C"}
]
const [selectedTeamData, setSelectedTeamData] : any[] = React.useState(rows);
const handleSelectionChange = (newSelectionModel: GridRowSelectionModel) => {
const selectedRowsData = selectedTeamData.filter((row:any) =>
newSelectionModel.includes(row.id)
);
console.log(selectedRowsData)
}

return (
<>
<Suspense fallback={<ProgressCashFlowSearch.Loading />}>
<ProgressCashFlowSearch/>
</Suspense>
<CustomDatagrid rows={selectedTeamData} columns={columns} columnWidth={200} dataGridHeight={300} checkboxSelection={true} onRowSelectionModelChange={handleSelectionChange} selectionModel={selectionModel}/>
<Grid item sm>
<div style={{display:"inline-block",width:"50%"}}>
<Grid item xs={12} md={12} lg={12}>
<Card>
<CardHeader className="text-slate-500" title="Project Cash Flow by Month"/>
<div style={{display:"inline-block",width:"99%"}}>
<ReactApexChart
options={options}
series={options.series}
height={350}
/>
</div>
</Card>
</Grid>
</div>
</Grid>
</>
);
};

export default ProjectCashFlow;

+ 1
- 0
src/components/ProjectCashFlow/index.ts Ver ficheiro

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

+ 75
- 0
src/components/ProjectFinancialSummary/ProjectFinancialCard.tsx Ver ficheiro

@@ -0,0 +1,75 @@
import * as React from "react";
import Grid from "@mui/material/Grid";
import { useState,useEffect, useMemo } from 'react'
import Paper from "@mui/material/Paper";
import { TFunction } from "i18next";
import { useTranslation } from "react-i18next";
import {Card,CardHeader} from '@mui/material';
import CustomSearchForm from "../CustomSearchForm/CustomSearchForm";
import CustomDatagrid from '../CustomDatagrid/CustomDatagrid';
import ReactApexChart from 'react-apexcharts';
import { ApexOptions } from 'apexcharts';
import { GridColDef, GridRowSelectionModel} from '@mui/x-data-grid';
import ReportProblemIcon from '@mui/icons-material/ReportProblem';
import dynamic from 'next/dynamic';
import '../../app/global.css';
import { AnyARecord, AnyCnameRecord } from "dns";
import SearchBox, { Criterion } from "../SearchBox";
import ProgressByClientSearch from "@/components/ProgressByClientSearch";
import { Suspense } from "react";

interface Props {
Title: string;
TotalActiveProjectNumber: string;
TotalFees: string;
TotalBudget: string;
TotalCumulative: string;
TotalInvoicedAmount: string;
TotalReceivedAmount: string;
CashFlowStatus: string;
CostPerformanceIndex: string;
ClickedIndex: number;
Index: number;
}

const ProjectFinancialCard: React.FC<Props> = ({Title,TotalActiveProjectNumber,TotalFees,TotalBudget,TotalCumulative,TotalInvoicedAmount,TotalReceivedAmount,CashFlowStatus,CostPerformanceIndex,ClickedIndex,Index}) => {
const [SearchCriteria, setSearchCriteria] = React.useState({})
const { t } = useTranslation("dashboard");
const borderColor = CashFlowStatus === "Negative" ? "border-red-300 border-solid" : "border-green-200 border-solid"
const selectedBackgroundColor = ClickedIndex === Index ? "rgb(235 235 235)" : "rgb(255 255 255)"
console.log(ClickedIndex)
console.log(Index)
return (
<Card style={{maxWidth:"25%",minWidth:"280px",boxShadow:"0 0px 10px 0 rgba(0, 0, 0, 0.08), 0 0px 10px 0 rgba(0, 0, 0, 0.08)", backgroundColor:selectedBackgroundColor}} className={`${borderColor}`}>
<div className="text-xl mt-2 font-medium" style={{width:"100%",textAlign:"center",color:"#898d8d"}}>{Title}</div><hr/>
<div className="text-sm font-medium ml-5" style={{color:"#898d8d"}}>Total Active Project</div>
<div className="text-lg font-medium ml-5" style={{color:"#6b87cf"}}>{TotalActiveProjectNumber}</div><hr/>
<div className="text-sm font-medium ml-5" style={{color:"#898d8d"}}>Total Fees</div>
<div className="text-lg font-medium ml-5" style={{color:"#6b87cf"}}>{TotalFees}</div><hr/>
<div className="text-sm font-medium ml-5" style={{color:"#898d8d"}}>Total Budget</div>
<div className="text-lg font-medium ml-5" style={{color:"#6b87cf"}}>{TotalBudget}</div><hr/>
<div className="text-sm font-medium ml-5" style={{color:"#898d8d"}}>Total Cumulative Expenditure</div>
<div className="text-lg font-medium ml-5" style={{color:"#6b87cf"}}>{TotalCumulative}</div><hr/>
<div className="text-sm font-medium ml-5" style={{color:"#898d8d"}}>Total Invoiced Amount</div>
<div className="text-lg font-medium ml-5" style={{color:"#6b87cf"}}>{TotalInvoicedAmount}</div><hr/>
<div className="text-sm font-medium ml-5" style={{color:"#898d8d"}}>Total Received Amount</div>
<div className="text-lg font-medium ml-5" style={{color:"#6b87cf"}}>{TotalReceivedAmount}</div><hr/>
<div className="text-sm font-medium ml-5" style={{color:"#898d8d"}}>Cash Flow Status</div>
{CashFlowStatus === "Negative" && (
<><div className="text-lg font-medium ml-5" style={{color:"#f896aa"}}>{CashFlowStatus}</div><hr/></>
)}
{CashFlowStatus === "Positive" && (
<><div className="text-lg font-medium ml-5" style={{color:"#71d19e"}}>{CashFlowStatus}</div><hr/></>
)}
<div className="text-sm mt-2 font-medium ml-5" style={{color:"#898d8d"}}>Cost Performance Index (CPI)</div>
{Number(CostPerformanceIndex) < 1 && (
<><div className="text-lg font-medium ml-5 mb-2" style={{color:"#f896aa"}}>{CostPerformanceIndex}</div></>
)}
{Number(CostPerformanceIndex) >= 1 && (
<><div className="text-lg font-medium ml-5 mb-2" style={{color:"#71d19e"}}>{CostPerformanceIndex}</div></>
)}
</Card>
);
};
export default ProjectFinancialCard;

+ 272
- 0
src/components/ProjectFinancialSummary/ProjectFinancialSummary.tsx Ver ficheiro

@@ -0,0 +1,272 @@
"use client";
import * as React from "react";
import Grid from "@mui/material/Grid";
import { useState,useEffect, useMemo } from 'react'
import Paper from "@mui/material/Paper";
import { TFunction } from "i18next";
import { useTranslation } from "react-i18next";
import {Card,CardHeader} from '@mui/material';
import CustomSearchForm from "../CustomSearchForm/CustomSearchForm";
import CustomDatagrid from '../CustomDatagrid/CustomDatagrid';
import ReactApexChart from 'react-apexcharts';
import { ApexOptions } from 'apexcharts';
import { GridColDef, GridRowSelectionModel} from '@mui/x-data-grid';
import ReportProblemIcon from '@mui/icons-material/ReportProblem';
import dynamic from 'next/dynamic';
import '../../app/global.css';
import { AnyARecord, AnyCnameRecord } from "dns";
import SearchBox, { Criterion } from "../SearchBox";
import ProgressByClientSearch from "@/components/ProgressByClientSearch";
import { Suspense } from "react";
import ProjectFinancialCard from "./ProjectFinancialCard";

const ProjectFinancialSummary: React.FC = () => {
const [SearchCriteria, setSearchCriteria] = React.useState({})
const { t } = useTranslation("dashboard");
const [selectionModel, setSelectionModel] : any[] = React.useState([]);
const projectFinancialData = [
{id:1,title:"All Teams",activeProject:"147",fees:"22,800,000.00",budget:"18,240,000.00",cumulativeExpenditure:"17,950,000.00",invoicedAmount:"18,240,000.00",receivedAmount:"10,900,000.00",cashFlowStatus:"Negative",CPI:"0.69"},
{id:2,title:"XXX Team",activeProject:"25",fees:"1,500,000.00",budget:"1,200,000.00",cumulativeExpenditure:"1,250,000.00",invoicedAmount:"900,000.00",receivedAmount:"650,000.00",cashFlowStatus:"Negative",CPI:"0.72"},
{id:3,title:"YYY Team",activeProject:"35",fees:"5,000,000.00",budget:"4,000,000.00",cumulativeExpenditure:"3,200,000.00",invoicedAmount:"3,500,000.00",receivedAmount:"3,500,000.00",cashFlowStatus:"Positive",CPI:"1.09"},
{id:4,title:"ZZZ Team",activeProject:"50",fees:"3,500,000.00",budget:"2,800,000.00",cumulativeExpenditure:"5,600,000.00",invoicedAmount:"2,500,000.00",receivedAmount:"2,200,000.00",cashFlowStatus:"Negative",CPI:"0.45"},
{id:5,title:"AAA Team",activeProject:"15",fees:"4,800,000.00",budget:"3,840,000.00",cumulativeExpenditure:"2,500,000.00",invoicedAmount:"1,500,000.00",receivedAmount:"750,000.00",cashFlowStatus:"Negative",CPI:"0.60"},
{id:6,title:"BBB Team",activeProject:"22",fees:"8,000,000.00",budget:"6,400,000.00",cumulativeExpenditure:"5,400,000.00",invoicedAmount:"4,000,000.00",receivedAmount:"3,800,000.00",cashFlowStatus:"Negative",CPI:"0.74"}
]

const rows0 = [{id: 1,projectCode:"M1201",projectName:"Consultancy Project C", team:"XXX", teamLeader:"XXX", startDate:"01/08/2022", targetEndDate: "01/05/2024", client:"Client A", subsidiary:"N/A"},
{id: 2,projectCode:"M1321",projectName:"Consultancy Project CCC", team:"XXX", teamLeader:"XXX", startDate:"01/08/2022", targetEndDate: "20/01/2024", client:"Client E", subsidiary:"Subsidiary B"},
{id: 3,projectCode:"M1001",projectName:"Consultancy Project A", team:"YYY", teamLeader:"YYY", startDate:"01/07/2022", targetEndDate: "01/04/2024", client:"Client B", subsidiary:"N/A"},
{id: 4,projectCode:"M1301",projectName:"Consultancy Project AAAA", team:"YYY", teamLeader:"YYY", startDate:"01/09/2022", targetEndDate: "20/02/2024", client:"Client C", subsidiary:"Subsidiary A"},
{id: 5,projectCode:"M1354",projectName:"Consultancy Project BBB", team:"YYY", teamLeader:"YYY", startDate:"01/02/2023", targetEndDate: "31/01/2024", client:"Client D", subsidiary:"Subsidiary C"}
]

const rows1 = [{id: 1,projectCode:"M1201",projectName:"Consultancy Project C", team:"XXX", teamLeader:"XXX", startDate:"01/08/2022", targetEndDate: "01/05/2024", client:"Client A", subsidiary:"N/A"},
{id: 2,projectCode:"M1321",projectName:"Consultancy Project CCC", team:"XXX", teamLeader:"XXX", startDate:"01/08/2022", targetEndDate: "20/01/2024", client:"Client E", subsidiary:"Subsidiary B"},
]

const rows2 = [{id: 3,projectCode:"M1001",projectName:"Consultancy Project A", team:"YYY", teamLeader:"YYY", startDate:"01/07/2022", targetEndDate: "01/04/2024", client:"Client B", subsidiary:"N/A"},
{id: 4,projectCode:"M1301",projectName:"Consultancy Project AAAA", team:"YYY", teamLeader:"YYY", startDate:"01/09/2022", targetEndDate: "20/02/2024", client:"Client C", subsidiary:"Subsidiary A"},
{id: 5,projectCode:"M1354",projectName:"Consultancy Project BBB", team:"YYY", teamLeader:"YYY", startDate:"01/02/2023", targetEndDate: "31/01/2024", client:"Client D", subsidiary:"Subsidiary C"}
]

const projectFinancialRows = [{id: 1,cashFlowStatus:"Positive",cpi:"1.25", totalFees:"500,000.00", totalBudget:"400,000.00", totalCumulativeExpenditure:"280,000.00", totalInvoicedAmount: "350,000.00", totalUnInvoicedAmount:"150,000.00", totalReceivedAmount:"350,000.00", totalUnReceivedAmount:"0.00"}
]

const [isCardClickedIndex, setIsCardClickedIndex] = React.useState(0);

const [selectedTeamData, setSelectedTeamData] : any[] = React.useState(rows0);

const handleCardClick = (r:any) => {
setIsCardClickedIndex(r)
if (r === 0) {
setSelectedTeamData(rows0)
} else if (r === 1) {
setSelectedTeamData(rows1)
} else if (r === 2) {
setSelectedTeamData(rows2)
}
};


const columns = [
{
id: 'projectCode',
field: 'projectCode',
headerName: "Project Code",
flex: 1,
},
{
id: 'projectName',
field: 'projectName',
headerName: "Project Name",
flex: 1,
},
{
id: 'team',
field: 'team',
headerName: "Team",
flex: 1,
},
{
id: 'teamLeader',
field: 'teamLeader',
headerName: "Team Leader",
flex: 1,
},
{
id: 'startDate',
field: 'startDate',
headerName: "Start Date",
flex: 1,
},
{
id: 'targetEndDate',
field: 'targetEndDate',
headerName: "Target End Date",
flex: 1,
},
{
id: 'client',
field: 'client',
headerName: "Client",
flex: 1,
},
{
id: 'subsidiary',
field: 'subsidiary',
headerName: "Subsidiary",
flex: 1,
},
];

const columns2 = [
{
id: 'cashFlowStatus',
field: 'cashFlowStatus',
headerName: "Cash Flow Status",
flex: 1,
renderCell: (params:any) => {
if (params.row.cashFlowStatus === "Positive") {
return (
<span className="text-lime-500">{params.row.cashFlowStatus}</span>
)
} else if (params.row.cashFlowStatus === "Negative") {
return (
<span className="text-red-500">{params.row.cashFlowStatus}</span>
)
}
},
},
{
id: 'cpi',
field: 'cpi',
headerName: "CPI",
flex: 0.7,
renderCell: (params:any) => {
if (params.row.cpi >= 1) {
return (
<span className="text-lime-500">{params.row.cpi}</span>
)
} else if (params.row.cpi < 1) {
return (
<span className="text-red-500">{params.row.cpi}</span>
)
}
},
},
{
id: 'totalFees',
field: 'totalFees',
headerName: "Total Fees (HKD)",
flex: 1,
renderCell: (params:any) => {
return (
<span>${params.row.totalFees}</span>
)
},
},
{
id: 'totalBudget',
field: 'totalBudget',
headerName: "Total Budget (HKD)",
flex: 1,
renderCell: (params:any) => {
return (
<span>${params.row.totalBudget}</span>
)
},
},
{
id: 'totalCumulativeExpenditure',
field: 'totalCumulativeExpenditure',
headerName: "Total Cumulative Expenditure (HKD)",
flex: 1,
renderCell: (params:any) => {
return (
<span>${params.row.totalCumulativeExpenditure}</span>
)
},
},
{
id: 'totalInvoicedAmount',
field: 'totalInvoicedAmount',
headerName: "Total Invoiced Amount (HKD)",
flex: 1,
renderCell: (params:any) => {
return (
<span>${params.row.totalInvoicedAmount}</span>
)
},
},
{
id: 'totalUnInvoicedAmount',
field: 'totalUnInvoicedAmount',
headerName: "Total Un-invoiced Amount (HKD)",
flex: 1,
renderCell: (params:any) => {
return (
<span>${params.row.totalUnInvoicedAmount}</span>
)
},
},
{
id: 'totalReceivedAmount',
field: 'totalReceivedAmount',
headerName: "Total Received Amount (HKD)",
flex: 1,
renderCell: (params:any) => {
return (
<span>${params.row.totalReceivedAmount}</span>
)
},
},
{
id: 'totalUnReceivedAmount',
field: 'totalUnReceivedAmount',
headerName: "Total Un-received Amount (HKD)",
flex: 1,
renderCell: (params:any) => {
return (
<span>${params.row.totalUnReceivedAmount}</span>
)
},
},
];

const handleSelectionChange = (newSelectionModel: GridRowSelectionModel) => {
const selectedRowsData = selectedTeamData.filter((row:any) =>
newSelectionModel.includes(row.id)
);
console.log(selectedRowsData)
}

return (
<Grid item sm>
<Card>
<CardHeader className="text-slate-500" title="Active Project Financial Status"/>
<div className="ml-10 mr-10" style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'start'}}>
{projectFinancialData.map((record, index) => (
<div className="hover:cursor-pointer ml-4 mt-5 mb-4 inline-block" key={index} onClick={(r) => handleCardClick(index)}>
<ProjectFinancialCard Title={record.title} TotalActiveProjectNumber={record.activeProject} TotalFees={record.fees} TotalBudget={record.budget} TotalCumulative={record.cumulativeExpenditure} TotalInvoicedAmount={record.invoicedAmount} TotalReceivedAmount={record.receivedAmount} CashFlowStatus={record.cashFlowStatus} CostPerformanceIndex={record.CPI} ClickedIndex={isCardClickedIndex} Index={index}/>
</div>
))}
</div>
</Card>
<Card className="mt-5">
<CardHeader className="text-slate-500" title="Selected Team's Project"/>
<div style={{display:"inline-block",width:"99%",marginLeft:10}}>
<CustomDatagrid rows={selectedTeamData} columns={columns} columnWidth={200} dataGridHeight={300} checkboxSelection={true} onRowSelectionModelChange={handleSelectionChange} selectionModel={selectionModel}/>
</div>
</Card>
<Card className="mt-5">
<CardHeader className="text-slate-500" title="Individual Project Financial Status"/>
<div style={{display:"inline-block",width:"99%",marginLeft:10}}>
<CustomDatagrid rows={projectFinancialRows} columns={columns2} columnWidth={200} dataGridHeight={300}/>
</div>
</Card>
</Grid>
);
};

export default ProjectFinancialSummary;

+ 1
- 0
src/components/ProjectFinancialSummary/index.ts Ver ficheiro

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

+ 59
- 1
src/components/SearchBox/SearchBox.tsx Ver ficheiro

@@ -15,10 +15,16 @@ import CardActions from "@mui/material/CardActions";
import Button from "@mui/material/Button";
import RestartAlt from "@mui/icons-material/RestartAlt";
import Search from "@mui/icons-material/Search";
import dayjs from 'dayjs';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';

interface BaseCriterion<T extends string> {
label: string;
label2?: string;
paramName: T;
paramName2?: T;
}

interface TextCriterion<T extends string> extends BaseCriterion<T> {
@@ -30,7 +36,11 @@ interface SelectCriterion<T extends string> extends BaseCriterion<T> {
options: string[];
}

export type Criterion<T extends string> = TextCriterion<T> | SelectCriterion<T>;
interface DateRangeCriterion<T extends string> extends BaseCriterion<T> {
type: "dateRange";
}

export type Criterion<T extends string> = TextCriterion<T> | SelectCriterion<T> | DateRangeCriterion<T>;

interface Props<T extends string> {
criteria: Criterion<T>[];
@@ -44,6 +54,7 @@ function SearchBox<T extends string>({
onReset,
}: Props<T>) {
const { t } = useTranslation("common");
const [dayRangeFromDate, setDayRangeFromDate] :any = useState("");
const defaultInputs = useMemo(
() =>
criteria.reduce<Record<T, string>>(
@@ -71,6 +82,24 @@ function SearchBox<T extends string>({
};
}, []);

const makeDateChangeHandler = useCallback(
(paramName: T) => {
return (e:any) => {
setInputs((i) => ({ ...i, [paramName]: dayjs(e).format('YYYY-MM-DD') }));
};
},
[],
);

const makeDateToChangeHandler = useCallback(
(paramName: T) => {
return (e:any) => {
setInputs((i) => ({ ...i, [paramName + "To"]: dayjs(e).format('YYYY-MM-DD') }));
};
},
[],
);

const handleReset = () => {
setInputs(defaultInputs);
onReset?.();
@@ -113,6 +142,35 @@ function SearchBox<T extends string>({
</Select>
</FormControl>
)}
{c.type === "dateRange" && (
<Grid container>
<Grid item xs={5.5} sm={5.5}>
<FormControl fullWidth>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
label={c.label}
onChange={makeDateChangeHandler(c.paramName)}
/>
</LocalizationProvider>
</FormControl>
</Grid>
<Grid item xs={1} sm={1} md={1} lg={1} sx={{ display: 'flex', justifyContent: "center", alignItems: 'center' }}>
-
</Grid>
<Grid item xs={5.5} sm={5.5}>
<FormControl fullWidth>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
label={c.label2}
onChange={makeDateToChangeHandler(c.paramName)}
/>
</LocalizationProvider>
</FormControl>
</Grid>
</Grid>
)}
</Grid>
);
})}


Carregando…
Cancelar
Guardar