@@ -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 ( | return ( | ||||
<div className="mt-5 mb-5" style={{ height: dataGridHeight ?? 400, width: '100%'}}> | <div className="mt-5 mb-5" style={{ height: dataGridHeight ?? 400, width: '100%'}}> | ||||
{Title ? ( | {Title ? ( | ||||
<Card style={{marginRight:20}}> | |||||
<Card style={{marginRight:10}}> | |||||
{Title && <CardHeader className="text-slate-500" title={Title} />} | {Title && <CardHeader className="text-slate-500" title={Title} />} | ||||
<CardContent style={{ display: "flex", alignItems: "center", justifyContent: "center", marginTop:-20 }}> | <CardContent style={{ display: "flex", alignItems: "center", justifyContent: "center", marginTop:-20 }}> | ||||
{Style ? ( | {Style ? ( | ||||
@@ -215,7 +215,7 @@ const CustomDatagrid: React.FC<CustomDatagridProps> = ({ | |||||
rows={rowsWithDefaultValues} | rows={rowsWithDefaultValues} | ||||
columns={modifiedColumns} | columns={modifiedColumns} | ||||
editMode="row" | editMode="row" | ||||
style={{marginRight:20}} | |||||
style={{marginRight:0}} | |||||
checkboxSelection={checkboxSelection} | checkboxSelection={checkboxSelection} | ||||
onRowSelectionModelChange={onRowSelectionModelChange} | onRowSelectionModelChange={onRowSelectionModelChange} | ||||
initialState={{ | initialState={{ | ||||
@@ -31,8 +31,9 @@ interface NavigationItem { | |||||
const navigationItems: NavigationItem[] = [ | const navigationItems: NavigationItem[] = [ | ||||
{ icon: <WorkHistory />, label: "User Workspace", path: "/home" }, | { icon: <WorkHistory />, label: "User Workspace", path: "/home" }, | ||||
{ icon: <Dashboard />, label: "Dashboard", path: "", children: [ | { 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: <RequestQuote />, label: "Expense Claim", path: "/claim" }, | ||||
{ icon: <Assignment />, label: "Project Management", path: "/projects" }, | { 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 Button from "@mui/material/Button"; | ||||
import RestartAlt from "@mui/icons-material/RestartAlt"; | import RestartAlt from "@mui/icons-material/RestartAlt"; | ||||
import Search from "@mui/icons-material/Search"; | 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> { | interface BaseCriterion<T extends string> { | ||||
label: string; | label: string; | ||||
label2?: string; | |||||
paramName: T; | paramName: T; | ||||
paramName2?: T; | |||||
} | } | ||||
interface TextCriterion<T extends string> extends BaseCriterion<T> { | interface TextCriterion<T extends string> extends BaseCriterion<T> { | ||||
@@ -30,7 +36,11 @@ interface SelectCriterion<T extends string> extends BaseCriterion<T> { | |||||
options: string[]; | 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> { | interface Props<T extends string> { | ||||
criteria: Criterion<T>[]; | criteria: Criterion<T>[]; | ||||
@@ -44,6 +54,7 @@ function SearchBox<T extends string>({ | |||||
onReset, | onReset, | ||||
}: Props<T>) { | }: Props<T>) { | ||||
const { t } = useTranslation("common"); | const { t } = useTranslation("common"); | ||||
const [dayRangeFromDate, setDayRangeFromDate] :any = useState(""); | |||||
const defaultInputs = useMemo( | const defaultInputs = useMemo( | ||||
() => | () => | ||||
criteria.reduce<Record<T, string>>( | 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 = () => { | const handleReset = () => { | ||||
setInputs(defaultInputs); | setInputs(defaultInputs); | ||||
onReset?.(); | onReset?.(); | ||||
@@ -113,6 +142,35 @@ function SearchBox<T extends string>({ | |||||
</Select> | </Select> | ||||
</FormControl> | </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> | </Grid> | ||||
); | ); | ||||
})} | })} | ||||