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