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