@@ -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; |
@@ -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; |
@@ -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", | |||
} | |||
]; |
@@ -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={{ | |||
@@ -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" }, | |||
@@ -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; |
@@ -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; |
@@ -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; |
@@ -0,0 +1 @@ | |||
export { default } from "./ProgressCashFlowSearchWrapper"; |
@@ -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; |
@@ -0,0 +1 @@ | |||
export { default } from "./ProjectCashFlow"; |
@@ -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; |
@@ -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; |
@@ -0,0 +1 @@ | |||
export { default } from "./ProjectFinancialSummary"; |
@@ -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> | |||
); | |||
})} | |||