diff --git a/src/app/(main)/dashboard/ProjectStatusByTeam/page.tsx b/src/app/(main)/dashboard/ProjectStatusByTeam/page.tsx new file mode 100644 index 0000000..db7fd14 --- /dev/null +++ b/src/app/(main)/dashboard/ProjectStatusByTeam/page.tsx @@ -0,0 +1,29 @@ +import { Metadata } from "next"; +import { I18nProvider } from "@/i18n"; +import DashboardPage from "@/components/DashboardPage/DashboardPage"; +import DashboardPageButton from "@/components/DashboardPage/DashboardTabButton"; +import ProgressByTeamSearch from "@/components/ProgressByTeamSearch"; +import { Suspense } from "react"; +import Tabs, { TabsProps } from "@mui/material/Tabs"; +import Tab from "@mui/material/Tab"; +import Typography from "@mui/material/Typography"; +import ProgressByTeam from "@/components/ProgressByTeam"; + +export const metadata: Metadata = { + title: "Project Status by Team", +}; + +const ProjectStatusByTeam: React.FC = () => { + return ( + + + Project Status by Team + + }> + + + + + ); +}; +export default ProjectStatusByTeam; diff --git a/src/app/api/teamprojects/index.ts b/src/app/api/teamprojects/index.ts new file mode 100644 index 0000000..bba7ba9 --- /dev/null +++ b/src/app/api/teamprojects/index.ts @@ -0,0 +1,43 @@ +import { cache } from "react"; + +export interface TeamProjectResult { + id: number; + teamCode: string; + teamName: string; + NoOfProjects: number; +} + +export const preloadProjects = () => { + fetchTeamProjects(); +}; + +export const fetchTeamProjects = cache(async () => { + return mockProjects; +}); + +const mockProjects: TeamProjectResult[] = [ + { + id: 1, + teamCode: "TEAM-001", + teamName: "Team A", + NoOfProjects: 5, + }, + { + id: 2, + teamCode: "TEAM-002", + teamName: "Team B", + NoOfProjects: 5, + }, + { + id: 3, + teamCode: "TEAM-003", + teamName: "Team C", + NoOfProjects: 3, + }, + { + id: 4, + teamCode: "TEAM-004", + teamName: "Team D", + NoOfProjects: 1, + }, +]; diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index fbb1aa5..c029774 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -62,6 +62,11 @@ const navigationItems: NavigationItem[] = [ label: "Project Status by Client", path: "/dashboard/ProjectStatusByClient", }, + { + icon: , + label: "Project Status by Team", + path: "/dashboard/ProjectStatusByTeam", + }, { icon: , label: "Staff Utilization", diff --git a/src/components/ProgressByTeam/ProgressByTeam.tsx b/src/components/ProgressByTeam/ProgressByTeam.tsx new file mode 100644 index 0000000..0d43049 --- /dev/null +++ b/src/components/ProgressByTeam/ProgressByTeam.tsx @@ -0,0 +1,620 @@ +"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 ProgressByTeamSearch from "@/components/ProgressByTeamSearch"; +import { Suspense } from "react"; + +const ProgressByTeam: React.FC = () => { + const [activeTab, setActiveTab] = useState("financialSummary"); + const [SearchCriteria, setSearchCriteria] = React.useState({}); + const { t } = useTranslation("dashboard"); + + const [teamCode, setTeamCode] = useState(""); + const [teamName, setTeamName] = useState(""); + const [projectArray, setProjectArray]: any[] = useState([]); + const [percentageArray, setPercentageArray]: any[] = useState([]); + const [colorArray, setColorArray]: any[] = useState([]); + const [selectionModel, setSelectionModel]: any[] = React.useState([]); + const [pieChartColor, setPieChartColor]: any[] = React.useState([]); + const [totalSpentPercentage, setTotalSpentPercentage]: any = React.useState(); + const [projectBudgetManhour, setProjectBudgetManhour]: any = + React.useState("-"); + const [actualManhourSpent, setActualManhourSpent]: any = React.useState("-"); + const [remainedManhour, setRemainedManhour]: any = React.useState("-"); + const [lastUpdate, setLastUpdate]: any = React.useState("-"); + const [dropdownDemo, setDropdownDemo] = useState(""); + const [dateDemo, setDateDemo] = useState(null); + const [checkboxDemo, setCheckboxDemo] = useState(false); + const [receiptFromDate, setReceiptFromDate] = useState(null); + const [receiptToDate, setReceiptToDate] = useState(null); + const [selectedRows, setSelectedRows] = useState([]); + const rows = [ + { + id: 1, + teamCode: "TEAM-001", + teamName: "Team A", + noOfProjects: "5", + }, + { + id: 2, + teamCode: "TEAM-001", + teamName: "Team B", + noOfProjects: "5", + }, + { + id: 3, + teamCode: "TEAM-001", + teamName: "Team C", + noOfProjects: "3", + }, + { + id: 4, + teamCode: "TEAM-001", + teamName: "Team D", + noOfProjects: "1", + }, + ]; + //['#f57f90', '#94f7d6', '#87c5f5', '#ab95f5', '#fcd68b'] + const rows2 = [ + { + id: 1, + project: "Consultancy Project 123", + team: "XXX", + teamLeader: "XXX", + currentStage: "Contract Documentation", + budgetedManhour: "200.00", + spentManhour: "120.00", + remainedManhour: "80.00", + comingPaymentMilestone: "31/03/2024", + alert: false, + color: "#f57f90", + }, + { + id: 2, + project: "Consultancy Project 456", + team: "XXX", + teamLeader: "XXX", + currentStage: "Report Preparation", + budgetedManhour: "400.00", + spentManhour: "200.00", + remainedManhour: "200.00", + comingPaymentMilestone: "20/02/2024", + alert: false, + color: "#94f7d6", + }, + { + id: 3, + project: "Construction Project A", + team: "YYY", + teamLeader: "YYY", + currentStage: "Construction", + budgetedManhour: "187.50", + spentManhour: "200.00", + remainedManhour: "12.50", + comingPaymentMilestone: "13/12/2023", + alert: true, + color: "#87c5f5", + }, + { + id: 4, + project: "Construction Project B", + team: "XXX", + teamLeader: "XXX", + currentStage: "Post Construction", + budgetedManhour: "100.00", + spentManhour: "40.00", + remainedManhour: "60.00", + comingPaymentMilestone: "05/01/2024", + alert: false, + color: "#ab95f5", + }, + { + id: 5, + project: "Construction Project C", + team: "YYY", + teamLeader: "YYY", + currentStage: "Construction", + budgetedManhour: "300.00", + spentManhour: "150.00", + remainedManhour: "150.00", + comingPaymentMilestone: "31/03/2024", + alert: false, + color: "#fcd68b", + }, + ]; + + const columns = [ + { + id: "clientCode", + field: "clientCode", + headerName: "Client Code", + flex: 1, + }, + { + id: "clientName", + field: "clientName", + headerName: "Client Name", + flex: 1, + }, + { + id: "clientSubsidiaryCode", + field: "clientSubsidiaryCode", + headerName: "Client Subsidiary Code", + flex: 1, + }, + { + id: "noOfProjects", + field: "noOfProjects", + headerName: "No. of Projects", + flex: 1, + }, + ]; + + const columns2 = [ + { + id: "color", + field: "color", + headerName: "", + renderCell: (params: any) => { + return ( + + ); + }, + flex: 0.1, + }, + { + id: "project", + field: "project", + headerName: "Project", + flex: 1, + }, + { + id: "team", + field: "team", + headerName: "Team", + flex: 0.8, + }, + { + id: "teamLeader", + field: "teamLeader", + headerName: "Team Leader", + flex: 0.8, + }, + { + id: "currentStage", + field: "currentStage", + headerName: "Current Stage", + flex: 1, + }, + { + id: "budgetedManhour", + field: "budgetedManhour", + headerName: "Budgeted Manhour", + flex: 0.8, + }, + { + id: "spentManhour", + field: "spentManhour", + headerName: "Spent Manhour", + renderCell: (params: any) => { + if (params.row.budgetedManhour - params.row.spentManhour <= 0) { + return ( + {params.row.spentManhour} + ); + } else { + return {params.row.spentManhour}; + } + }, + flex: 0.8, + }, + { + id: "remainedManhour", + field: "remainedManhour", + headerName: "Remained Manhour", + renderCell: (params: any) => { + if (params.row.budgetedManhour - params.row.spentManhour <= 0) { + return ( + ({params.row.remainedManhour}) + ); + } else { + return {params.row.remainedManhour}; + } + }, + flex: 1, + }, + { + id: "comingPaymentMilestone", + field: "comingPaymentMilestone", + headerName: "Coming Payment Milestone", + flex: 1, + }, + { + id: "alert", + field: "alert", + headerName: "Alert", + renderCell: (params: any) => { + if (params.row.alert === true) { + return ( + + + + ); + } else { + return ; + } + }, + flex: 0.2, + }, + ]; + + const InputFields = [ + { + id: "teamCode", + label: "Team Code", + type: "text", + value: teamCode, + setValue: setTeamCode, + }, + { + id: "teamName", + label: "Team Name", + type: "text", + value: teamName, + setValue: setTeamName, + }, + + // { id: 'dropdownDemo', label: "dropdownDemo", type: 'dropdown', options: [{id:"1", label:"1"}], value: dropdownDemo, setValue: setDropdownDemo }, + // { id: 'dateDemo', label:'dateDemo', type: 'date', value: dateDemo, setValue: setDateDemo }, + // { id: 'checkboxDemo', label:'checkboxDemo', type: 'checkbox', value: checkboxDemo, setValue: setCheckboxDemo }, + // { id: ['receiptFromDate','receiptToDate'], label: ["收貨日期","收貨日期"], value: [receiptFromDate ? receiptFromDate : null, receiptToDate ? receiptToDate : null], + // setValue: [setReceiptFromDate, setReceiptToDate],type: 'dateRange' }, + ]; + + const stageDeadline = [ + "31/03/2024", + "20/02/2024", + "01/12/2023", + "05/01/2024", + "31/03/2023", + ]; + + const series2: ApexAxisChartSeries | ApexNonAxisChartSeries = [ + { + data: [17.1, 28.6, 5.7, 48.6], + }, + ]; + + const series: ApexAxisChartSeries | ApexNonAxisChartSeries = [ + { + name: "Current Stage Completion Percentage", + data: [80, 55, 40, 65, 70], + }, + ]; + + const options2: ApexOptions = { + chart: { + type: "donut", + }, + colors: colorArray, + plotOptions: { + pie: { + donut: { + labels: { + show: true, + name: { + show: true, + }, + value: { + show: true, + fontWeight: 500, + fontSize: "30px", + color: "#3e98c7", + }, + total: { + show: true, + showAlways: true, + label: "Spent", + fontFamily: "sans-serif", + formatter: function (val) { + return totalSpentPercentage + "%"; + }, + }, + }, + }, + }, + }, + labels: projectArray, + legend: { + show: false, + }, + responsive: [ + { + breakpoint: 480, + options: { + chart: { + width: 200, + }, + legend: { + position: "bottom", + show: false, + }, + }, + }, + ], + }; + + const options: ApexOptions = { + chart: { + type: "bar", + height: 350, + }, + colors: ["#f57f90", "#94f7d6", "#87c5f5", "#ab95f5", "#fcd68b"], + plotOptions: { + bar: { + horizontal: true, + distributed: true, + }, + }, + dataLabels: { + enabled: false, + }, + xaxis: { + categories: [ + "Consultancy Project 123", + "Consultancy Project 456", + "Construction Project A", + "Construction Project B", + "Construction Project C", + ], + }, + yaxis: { + title: { + text: "Projects", + }, + labels: { + maxWidth: 200, + style: { + cssClass: "apexcharts-yaxis-label", + }, + }, + }, + title: { + text: "Current Stage Completion Percentage", + align: "center", + }, + grid: { + borderColor: "#f1f1f1", + }, + annotations: {}, + }; + + const handleSelectionChange = (newSelectionModel: GridRowSelectionModel) => { + const selectedRowsData = rows2.filter((row) => + newSelectionModel.includes(row.id), + ); + console.log(selectedRowsData); + const projectArray = []; + const pieChartColorArray = []; + let totalSpent = 0; + let totalBudgetManhour = 0; + const percentageArray = []; + for (let i = 0; i <= selectedRowsData.length; i++) { + if (i === selectedRowsData.length && i > 0) { + projectArray.push("Remained"); + } else if (selectedRowsData.length > 0) { + projectArray.push(selectedRowsData[i].project); + totalBudgetManhour += Number(selectedRowsData[i].budgetedManhour); + totalSpent += Number(selectedRowsData[i].spentManhour); + pieChartColorArray.push(selectedRowsData[i].color); + } + } + for (let i = 0; i <= selectedRowsData.length; i++) { + if (i === selectedRowsData.length && i > 0) { + const remainedManhour = totalBudgetManhour - totalSpent; + percentageArray.push( + Number(((remainedManhour / totalBudgetManhour) * 100).toFixed(1)), + ); + } else if (selectedRowsData.length > 0) { + const percentage = ( + (Number(selectedRowsData[i].spentManhour) / totalBudgetManhour) * + 100 + ).toFixed(1); + percentageArray.push(Number(percentage)); + } + } + setProjectBudgetManhour(totalBudgetManhour.toFixed(2)); + setActualManhourSpent(totalSpent.toFixed(2)); + setRemainedManhour((totalBudgetManhour - totalSpent).toFixed(2)); + setLastUpdate(new Date().toLocaleDateString("en-GB")); + setSelectionModel(newSelectionModel); + console.log(projectArray); + setProjectArray(projectArray); + setPercentageArray(percentageArray); + console.log(percentageArray); + setTotalSpentPercentage( + ((totalSpent / totalBudgetManhour) * 100).toFixed(1), + ); + if (projectArray.length > 0 && projectArray.includes("Remained")) { + const nonLastRecordColors = pieChartColorArray; + setColorArray([ + ...nonLastRecordColors.slice(0, projectArray.length - 1), + "#a3a3a3", + ]); + } else { + setColorArray(pieChartColorArray); + } + }; + + const applySearch = (data: any) => { + console.log(data); + setSearchCriteria(data); + }; + return ( + + {/* */} + {/* */} +
+ + + +
+ +
+ {/*
+

Stage Deadline

+ {stageDeadline.map((date, index) => { + const marginTop = index === 0 ? 25 : 20; + return ( +

{date}

+ ); + })} +
*/} + +
+ +
+
+
+
+
+ + + + {percentageArray.length === 0 && ( +
+ Please select the project you want to check. +
+ )} + {percentageArray.length > 0 && ( + + )} +
+
+ + +
+
+ Project Budget Manhour +
+
+ {projectBudgetManhour} +
+
+
+
+
+ Actual Manhour Spent +
+
+ {actualManhourSpent} +
+
+
+
+
+ Remained Manhour +
+
+ {remainedManhour} +
+
+
+
+
+ Last Update +
+
+ {lastUpdate} +
+
+
+
+
+
+ ); +}; + +export default ProgressByTeam; diff --git a/src/components/ProgressByTeam/index.ts b/src/components/ProgressByTeam/index.ts new file mode 100644 index 0000000..c89f666 --- /dev/null +++ b/src/components/ProgressByTeam/index.ts @@ -0,0 +1 @@ +export { default } from "./ProgressByTeam"; diff --git a/src/components/ProgressByTeamSearch/ProgressByTeamSearch.tsx b/src/components/ProgressByTeamSearch/ProgressByTeamSearch.tsx new file mode 100644 index 0000000..a28f999 --- /dev/null +++ b/src/components/ProgressByTeamSearch/ProgressByTeamSearch.tsx @@ -0,0 +1,55 @@ +"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 { TeamProjectResult } from "@/app/api/teamprojects"; + +interface Props { + projects: TeamProjectResult[]; +} +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const ProgressByClientSearch: React.FC = ({ projects }) => { + const { t } = useTranslation("projects"); + + // If project searching is done on the server-side, then no need for this. + const [filteredProjects, setFilteredProjects] = useState(projects); + + const searchCriteria: Criterion[] = useMemo( + () => [ + { label: "Team Code", paramName: "teamCode", type: "text" }, + { label: "Team Name", paramName: "teamName", type: "text" }, + ], + [t], + ); + + const columns = useMemo[]>( + () => [ + { name: "teamCode", label: t("Team Code") }, + { name: "teamName", label: t("Team Name") }, + { name: "NoOfProjects", label: t("No. of Projects") }, + ], + [t], + ); + + return ( + <> + { + console.log(query); + }} + /> + + items={filteredProjects} + columns={columns} + /> + + ); +}; + +export default ProgressByClientSearch; diff --git a/src/components/ProgressByTeamSearch/ProgressByTeamSearchLoading.tsx b/src/components/ProgressByTeamSearch/ProgressByTeamSearchLoading.tsx new file mode 100644 index 0000000..c0fd688 --- /dev/null +++ b/src/components/ProgressByTeamSearch/ProgressByTeamSearchLoading.tsx @@ -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 ProgressByTeamSearchLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default ProgressByTeamSearchLoading; diff --git a/src/components/ProgressByTeamSearch/ProgressByTeamSearchWrapper.tsx b/src/components/ProgressByTeamSearch/ProgressByTeamSearchWrapper.tsx new file mode 100644 index 0000000..33a1e38 --- /dev/null +++ b/src/components/ProgressByTeamSearch/ProgressByTeamSearchWrapper.tsx @@ -0,0 +1,18 @@ +import { fetchTeamProjects } from "@/app/api/teamprojects"; +import React from "react"; +import ProgressByTeamSearch from "./ProgressByTeamSearch"; +import ProgressByTeamSearchLoading from "./ProgressByTeamSearchLoading"; + +interface SubComponents { + Loading: typeof ProgressByTeamSearchLoading; +} + +const ProgressByTeamSearchWrapper: React.FC & SubComponents = async () => { + const teamprojects = await fetchTeamProjects(); + + return ; +}; + +ProgressByTeamSearchWrapper.Loading = ProgressByTeamSearchLoading; + +export default ProgressByTeamSearchWrapper; diff --git a/src/components/ProgressByTeamSearch/index.ts b/src/components/ProgressByTeamSearch/index.ts new file mode 100644 index 0000000..bbecb07 --- /dev/null +++ b/src/components/ProgressByTeamSearch/index.ts @@ -0,0 +1 @@ +export { default } from "./ProgressByTeamSearchWrapper";