Ver código fonte

project status by client dashboard update

tags/Baseline_30082024_FRONTEND_UAT
MSI\User 1 ano atrás
pai
commit
205269e62f
23 arquivos alterados com 1299 adições e 110 exclusões
  1. +116
    -0
      package-lock.json
  2. +2
    -0
      package.json
  3. +31
    -0
      src/app/(main)/dashboard/ProjectStatusByClient/page.tsx
  4. +9
    -2
      src/app/(main)/dashboard/page.tsx
  5. +1
    -0
      src/app/(main)/projects/page.tsx
  6. +53
    -0
      src/app/api/clientprojects/index.ts
  7. +1
    -1
      src/components/AppBar/AppBar.tsx
  8. +5
    -6
      src/components/AppBar/NavigationToggle.tsx
  9. +94
    -10
      src/components/CustomDatagrid/CustomDatagrid.tsx
  10. +8
    -3
      src/components/CustomSearchForm/CustomSearchForm.tsx
  11. +33
    -3
      src/components/DashboardPage/DashboardPage.tsx
  12. +44
    -28
      src/components/DashboardPage/DashboardTabButton.tsx
  13. +287
    -50
      src/components/DashboardPage/ProgressByClient.tsx
  14. +49
    -4
      src/components/NavigationContent/NavigationContent.tsx
  15. +434
    -0
      src/components/ProgressByClient/ProgressByClient.tsx
  16. +1
    -0
      src/components/ProgressByClient/index.ts
  17. +57
    -0
      src/components/ProgressByClientSearch/ProgressByClientSearch.tsx
  18. +40
    -0
      src/components/ProgressByClientSearch/ProgressByClientSearchLoading.tsx
  19. +18
    -0
      src/components/ProgressByClientSearch/ProgressByClientSearchWrapper.tsx
  20. +1
    -0
      src/components/ProgressByClientSearch/index.ts
  21. +1
    -1
      src/components/SearchBox/SearchBox.tsx
  22. +12
    -0
      src/theme.ts
  23. +2
    -2
      src/theme/devias-material-kit/components.ts

+ 116
- 0
package-lock.json Ver arquivo

@@ -19,6 +19,7 @@
"@mui/x-data-grid": "^6.18.7",
"@mui/x-date-pickers": "^6.18.7",
"@unly/universal-language-detector": "^2.0.3",
"apexcharts": "^3.45.1",
"dayjs": "^1.11.10",
"i18next": "^23.7.11",
"i18next-resources-to-backend": "^1.2.0",
@@ -26,6 +27,7 @@
"next": "14.0.4",
"next-auth": "^4.24.5",
"react": "^18",
"react-apexcharts": "^1.4.1",
"react-dom": "^18",
"react-hook-form": "^7.49.2",
"react-i18next": "^13.5.0",
@@ -1525,6 +1527,11 @@
"resolved": "https://registry.npmjs.org/@unly/utils/-/utils-1.0.3.tgz",
"integrity": "sha512-QTRknIDX56FvzGcIpBum5D/oRSlX3dkZ+l1op1jsFlYCTd925OGUb991V7zsFv3ePcqFfvfqfR5cNVv+w4JAOw=="
},
"node_modules/@yr/monotone-cubic-spline": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz",
"integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA=="
},
"node_modules/accept-language-parser": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/accept-language-parser/-/accept-language-parser-1.5.0.tgz",
@@ -1619,6 +1626,20 @@
"node": ">= 8"
}
},
"node_modules/apexcharts": {
"version": "3.45.1",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.45.1.tgz",
"integrity": "sha512-pPjj/SA6dfPvR/IKRZF0STdfBGpBh3WRt7K0DFuW9P8erypYkX17EHu3/molPRfo2zSiQwTVpshHC5ncysqfkA==",
"dependencies": {
"@yr/monotone-cubic-spline": "^1.0.3",
"svg.draggable.js": "^2.2.2",
"svg.easing.js": "^2.0.0",
"svg.filter.js": "^2.0.2",
"svg.pathmorphing.js": "^0.1.3",
"svg.resize.js": "^1.4.3",
"svg.select.js": "^3.0.1"
}
},
"node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
@@ -5410,6 +5431,18 @@
"node": ">=0.10.0"
}
},
"node_modules/react-apexcharts": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/react-apexcharts/-/react-apexcharts-1.4.1.tgz",
"integrity": "sha512-G14nVaD64Bnbgy8tYxkjuXEUp/7h30Q0U33xc3AwtGFijJB9nHqOt1a6eG0WBn055RgRg+NwqbKGtqPxy15d0Q==",
"dependencies": {
"prop-types": "^15.8.1"
},
"peerDependencies": {
"apexcharts": "^3.41.0",
"react": ">=0.13"
}
},
"node_modules/react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
@@ -6118,6 +6151,89 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/svg.draggable.js": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz",
"integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==",
"dependencies": {
"svg.js": "^2.0.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.easing.js": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz",
"integrity": "sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==",
"dependencies": {
"svg.js": ">=2.3.x"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.filter.js": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz",
"integrity": "sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==",
"dependencies": {
"svg.js": "^2.2.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.js": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz",
"integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA=="
},
"node_modules/svg.pathmorphing.js": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz",
"integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==",
"dependencies": {
"svg.js": "^2.4.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.resize.js": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz",
"integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==",
"dependencies": {
"svg.js": "^2.6.5",
"svg.select.js": "^2.1.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.resize.js/node_modules/svg.select.js": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz",
"integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==",
"dependencies": {
"svg.js": "^2.2.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.select.js": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz",
"integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==",
"dependencies": {
"svg.js": "^2.6.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/synckit": {
"version": "0.8.6",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.6.tgz",


+ 2
- 0
package.json Ver arquivo

@@ -20,6 +20,7 @@
"@mui/x-data-grid": "^6.18.7",
"@mui/x-date-pickers": "^6.18.7",
"@unly/universal-language-detector": "^2.0.3",
"apexcharts": "^3.45.1",
"dayjs": "^1.11.10",
"i18next": "^23.7.11",
"i18next-resources-to-backend": "^1.2.0",
@@ -27,6 +28,7 @@
"next": "14.0.4",
"next-auth": "^4.24.5",
"react": "^18",
"react-apexcharts": "^1.4.1",
"react-dom": "^18",
"react-hook-form": "^7.49.2",
"react-i18next": "^13.5.0",


+ 31
- 0
src/app/(main)/dashboard/ProjectStatusByClient/page.tsx Ver arquivo

@@ -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 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 ProgressByClient from "@/components/ProgressByClient";

export const metadata: Metadata = {
title: "Project Status by Client",
};


const ProjectStatusByClient: React.FC = () => {

return (
<I18nProvider namespaces={["dashboard"]}>
<Typography variant="h4" marginInlineEnd={2}>
Project Status by Client
</Typography>
<Suspense fallback={<ProgressByClientSearch.Loading />}>
<ProgressByClientSearch/>
</Suspense>
<ProgressByClient/>
</I18nProvider>
);
};
export default ProjectStatusByClient;

+ 9
- 2
src/app/(main)/dashboard/page.tsx Ver arquivo

@@ -1,15 +1,22 @@
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";

export const metadata: Metadata = {
title: "Dashboard",
};

const Dashboard: React.FC = async () => {

const Dashboard: React.FC = () => {

return (
<I18nProvider namespaces={["dashboard"]}>
<DashboardPage />
{/* <DashboardPage /> */}
</I18nProvider>
);
};


+ 1
- 0
src/app/(main)/projects/page.tsx Ver arquivo

@@ -1,5 +1,6 @@
import { preloadProjects } from "@/app/api/projects";
import ProjectSearch from "@/components/ProjectSearch";
import ProgressByClientSearch from "@/components/ProgressByClientSearch";
import { getServerI18n } from "@/i18n";
import Add from "@mui/icons-material/Add";
import Button from "@mui/material/Button";


+ 53
- 0
src/app/api/clientprojects/index.ts Ver arquivo

@@ -0,0 +1,53 @@
import { cache } from "react";

export interface ClientProjectResult {
id: number;
clientCode: string;
clientName: string;
SubsidiaryClientCode: string;
SubsidiaryClientName: string;
NoOfProjects: number;
}

export const preloadProjects = () => {
fetchClientProjects();
};

export const fetchClientProjects = cache(async () => {
return mockProjects;
});

const mockProjects: ClientProjectResult[] = [
{
id: 1,
clientCode: "CUST-001",
clientName: "Client A",
SubsidiaryClientCode: "N/A",
SubsidiaryClientName: "N/A",
NoOfProjects: 5,
},
{
id: 1,
clientCode: "CUST-001",
clientName: "Client A",
SubsidiaryClientCode: "SUBS-001",
SubsidiaryClientName: "Subsidiary A",
NoOfProjects: 5,
},
{
id: 1,
clientCode: "CUST-001",
clientName: "Client A",
SubsidiaryClientCode: "SUBS-002",
SubsidiaryClientName: "Subsidiary B",
NoOfProjects: 3,
},
{
id: 1,
clientCode: "CUST-001",
clientName: "Client A",
SubsidiaryClientCode: "SUBS-003",
SubsidiaryClientName: "Subsidiary C",
NoOfProjects: 1,
},
];

+ 1
- 1
src/components/AppBar/AppBar.tsx Ver arquivo

@@ -16,7 +16,7 @@ const AppBar: React.FC<AppBarProps> = ({ avatarImageSrc, profileName }) => {
<I18nProvider namespaces={["common"]}>
<MUIAppBar position="sticky" color="default" elevation={4}>
<Toolbar>
<NavigationToggle />
<NavigationToggle/>
<Box
sx={{ flexGrow: 1, display: "flex", justifyContent: "flex-end" }}
>


+ 5
- 6
src/components/AppBar/NavigationToggle.tsx Ver arquivo

@@ -1,5 +1,4 @@
"use client";

import IconButton from "@mui/material/IconButton";
import MenuIcon from "@mui/icons-material/Menu";
import NavigationContent from "../NavigationContent";
@@ -18,21 +17,21 @@ const NavigationToggle: React.FC = () => {

return (
<>
<Drawer variant="permanent" sx={{ display: { xs: "none", lg: "block" } }}>
<NavigationContent />
<Drawer variant="permanent" sx={{ display: { xs: "none", xl: "block" } }}>
<NavigationContent/>
</Drawer>
<Drawer
sx={{ display: { lg: "none" } }}
sx={{ display: { xl: "none" } }}
open={isOpened}
onClose={closeNavigation}
ModalProps={{
keepMounted: true,
}}
>
<NavigationContent />
<NavigationContent/>
</Drawer>
<IconButton
sx={{ display: { lg: "none" } }}
sx={{ display: { xl: "none" } }}
onClick={openNavigation}
edge="start"
aria-label="menu"


+ 94
- 10
src/components/CustomDatagrid/CustomDatagrid.tsx Ver arquivo

@@ -1,7 +1,9 @@
"use client";
import * as React from 'react';
import { Card, CardHeader, CardContent, SxProps, Theme } from '@mui/material';
import { DataGrid, GridColDef } from '@mui/x-data-grid';
import { DataGrid, GridColDef, GridRowSelectionModel} from '@mui/x-data-grid';
import { darken, lighten, styled } from '@mui/material/styles';
import { useState } from 'react'

interface CustomDatagridProps {
Title?: string;
@@ -12,6 +14,9 @@ interface CustomDatagridProps {
sx?: SxProps<Theme>;
dataGridHeight?: number;
[key: string]: any;
checkboxSelection?: boolean;
onRowSelectionModelChange?: (newSelectionModel: GridRowSelectionModel) => void;
selectionModel?: any;
}

const CustomDatagrid: React.FC<CustomDatagridProps> = ({
@@ -19,9 +24,12 @@ const CustomDatagrid: React.FC<CustomDatagridProps> = ({
rows,
columns,
columnWidth,
Style = true,
Style = false,
sx,
dataGridHeight,
checkboxSelection, // Destructure the new prop
onRowSelectionModelChange, // Destructure the new prop
selectionModel,
...props
}) => {
const modifiedColumns = columns.map((column) => {
@@ -35,6 +43,16 @@ const CustomDatagrid: React.FC<CustomDatagridProps> = ({
return { ...row };
});

// Event handler to be called when the selection changes
const handleSelectionModelChange = (newSelectionModel: GridRowSelectionModel) => {
// setSelectionModel(newSelectionModel);
// To log selected row data, filter rows based on the new selection model
const selectedRowsData = rows.filter((row) =>
newSelectionModel.includes(row.id)
);
console.log(selectedRowsData);
};

const getBackgroundColor = (color: string, mode: 'light' | 'dark') =>
mode === 'dark' ? darken(color, 0.7) : lighten(color, 0.7);

@@ -99,15 +117,47 @@ const CustomDatagrid: React.FC<CustomDatagridProps> = ({
}));

return (
<div className="mt-5" style={{ height: dataGridHeight ?? 400, width: '100%' }}>
<Card>
{Title && <CardHeader title={Title} />}
<CardContent style={{ display: "flex", alignItems: "center", justifyContent: "center" }}>
<div className="mt-5 mb-5" style={{ height: dataGridHeight ?? 400, width: '100%'}}>
{Title ? (
<Card style={{marginRight:20}}>
{Title && <CardHeader className="text-slate-500" title={Title} />}
<CardContent style={{ display: "flex", alignItems: "center", justifyContent: "center", marginTop:-20 }}>
{Style ? (
<StyledDataGrid
rows={rowsWithDefaultValues}
columns={modifiedColumns}
editMode="row"
checkboxSelection={checkboxSelection}
onRowSelectionModelChange={onRowSelectionModelChange}
initialState={{
pagination: { paginationModel: { pageSize: 10 } },
}}
className="customDataGrid"
sx={{
boxShadow: 1,
border: 0,
borderColor: 'primary.light',
'& .MuiDataGrid-cell:hover': {
color: 'primary.main'
},
height: dataGridHeight ?? 400,
'& .MuiDataGrid-root': {
overflow: 'auto',
},
'& .MuiDataGrid-columnHeaderTitle': {
fontWeight: 'bold',
},
...sx
}}
{...props}
/>
) : (
<DataGrid
rows={rowsWithDefaultValues}
columns={modifiedColumns}
editMode="row"
checkboxSelection={checkboxSelection}
onRowSelectionModelChange={onRowSelectionModelChange}
initialState={{
pagination: { paginationModel: { pageSize: 10 } },
}}
@@ -119,10 +169,43 @@ const CustomDatagrid: React.FC<CustomDatagridProps> = ({
'& .MuiDataGrid-cell:hover': {
color: 'primary.main'
},
height: 300,
'& .MuiDataGrid-root': {
overflow: 'auto',
},
...sx
}}
{...props}
/>
)}
</CardContent>
</Card>)
: (Style ? (
<StyledDataGrid
rows={rowsWithDefaultValues}
columns={modifiedColumns}
editMode="row"
checkboxSelection={checkboxSelection}
onRowSelectionModelChange={onRowSelectionModelChange}
style={{marginRight:20}}
initialState={{
pagination: { paginationModel: { pageSize: 10 } },
}}
className="customDataGrid"
sx={{
boxShadow: 1,
border: 0,
borderColor: 'primary.light',
'& .MuiDataGrid-cell:hover': {
color: 'primary.main'
},
height: dataGridHeight ?? 400,
'& .MuiDataGrid-root': {
overflow: 'auto',
},
'& .MuiDataGrid-columnHeaderTitle': {
fontWeight: 'bold',
},
...sx
}}
{...props}
@@ -132,6 +215,9 @@ const CustomDatagrid: React.FC<CustomDatagridProps> = ({
rows={rowsWithDefaultValues}
columns={modifiedColumns}
editMode="row"
style={{marginRight:20}}
checkboxSelection={checkboxSelection}
onRowSelectionModelChange={onRowSelectionModelChange}
initialState={{
pagination: { paginationModel: { pageSize: 10 } },
}}
@@ -143,7 +229,7 @@ const CustomDatagrid: React.FC<CustomDatagridProps> = ({
'& .MuiDataGrid-cell:hover': {
color: 'primary.main'
},
height: 400,
height: 300,
'& .MuiDataGrid-root': {
overflow: 'auto',
},
@@ -151,9 +237,7 @@ const CustomDatagrid: React.FC<CustomDatagridProps> = ({
}}
{...props}
/>
)}
</CardContent>
</Card>
))}
</div>
);
};


+ 8
- 3
src/components/CustomSearchForm/CustomSearchForm.tsx Ver arquivo

@@ -214,6 +214,11 @@ const FormComponent: FC<FormComponentProps> = ({ fields, onSubmit, resetForm, sx
defaultValue={field.value !== undefined && field.value !== null ? `${field.value}` : ''}
required={field.required === true ? field.required : false}
sx={{ ...sx }}
InputProps={{
style: {
borderRadius: "10px",
}
}}
/>
</Grid>
);
@@ -222,12 +227,12 @@ const FormComponent: FC<FormComponentProps> = ({ fields, onSubmit, resetForm, sx
<Grid container maxWidth="lg" justifyContent="space-between" style={{marginTop:-20}}>
<Stack direction="row">
<Grid item sx={{ ml: 3, mr: 3, mb: 3, mt: 3 }}>
<Button className="h-12 w-32" style={{backgroundColor:"#92c1e9",color:"white",fontSize:"1.15em",fontWeight:100}} type="submit">
<Button className="h-12 w-32" style={{backgroundColor:"#92c1e9",color:"white",fontSize:"1.15em",fontWeight:100,borderRadius:10}} type="submit">
<SearchIcon/>&nbsp;Search
</Button>
</Grid>
<Grid item sx={{ ml: 3, mr: 3, mb: 3, mt: 3 }}>
<Button className="h-12 w-32" style={{backgroundColor:"#f890a5",color:"white",fontSize:"1.15em",fontWeight:100}} onClick={handleFormReset}>
<Button className="h-12 w-32" style={{backgroundColor:"#f890a5",color:"white",fontSize:"1.15em",fontWeight:100,borderRadius:10}} onClick={handleFormReset}>
<RefreshIcon/>&nbsp;Reset
</Button>
</Grid>
@@ -254,7 +259,7 @@ const CustomSearchForm: FC<SearchFormProps> = ({ applySearch, fields, title, sx

return (
<Card style={{marginRight:20}}>
<CardHeader className="text-slate-500" style={{marginTop:-20}} titleTypographyProps={{ variant: 'h5' }} title={Title}></CardHeader>
<CardHeader className="text-slate-500 " style={{marginTop:-5}} title={Title}></CardHeader>
<FormComponent fields={fields} onSubmit={handleSubmit} resetForm={handleFormReset} sx={sx} />
</Card>
);


+ 33
- 3
src/components/DashboardPage/DashboardPage.tsx Ver arquivo

@@ -1,20 +1,50 @@
"use client";
"use client"
import Grid from "@mui/material/Grid";
import Paper from "@mui/material/Paper";
import { TFunction } from "i18next";
import { useTranslation } from "react-i18next";
import PageTitle from "../PageTitle/PageTitle";
import DashboardTabButton from "./DashboardTabButton";
import { ThemeProvider } from '@mui/material/styles';
import theme from '../../theme';
import Tabs, { TabsProps } from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
import React, { useCallback, useState } from "react";
import { useRouter } from "next/navigation";
import ProgressByClient from "./ProgressByClient";
import ProgressByClientSearch from "@/components/ProgressByClientSearch";
import { Suspense } from "react";

const DashboardPage: React.FC = () => {
const [tabIndex, setTabIndex] = useState(0);
const { t } = useTranslation("dashboard");
const router = useRouter();
const handleCancel = () => {
router.back();
};
const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
(_e, newValue) => {
setTabIndex(newValue);
},
[],
);
return (
<Grid container height="100vh">
<ThemeProvider theme={theme}>
<Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable">
<Tab label="Project Financial Summary" />
<Tab label="Project Cash Flow" />
<Tab label="Project Progress by Client" />
<Tab label="Project Resource Utilization" />
<Tab label="Staff Utilization" />
</Tabs>
{tabIndex === 2 && <ProgressByClient />}
{/* <Grid container height="100vh" style={{ backgroundColor: theme.palette.background.default}}>
<Grid item sm>
<PageTitle BigTitle={"Dashboards"}/>
<DashboardTabButton/>
</Grid>
</Grid>
</Grid> */}
</ThemeProvider>
);
};



+ 44
- 28
src/components/DashboardPage/DashboardTabButton.tsx Ver arquivo

@@ -1,11 +1,13 @@
"use client";
import Grid from "@mui/material/Grid";
import { useState } from 'react'
import { useState,useCallback} from 'react'
import Paper from "@mui/material/Paper";
import { TFunction } from "i18next";
import { useTranslation } from "react-i18next";
import PageTitle from "../PageTitle/PageTitle";
import ProgressByClient from "./ProgressByClient";
import Tabs, { TabsProps } from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
import '../../app/global.css';

const DashboardTabButton: React.FC = () => {
@@ -27,34 +29,48 @@ const DashboardTabButton: React.FC = () => {
return <div>Project Financial Summary</div>;
}
};
const [tabIndex, setTabIndex] = useState(0);
const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
(_e, newValue) => {
setTabIndex(newValue);
},
[],
);
return (
<Grid item sm>
<div style={{marginLeft:20}}>
{activeTab !== 'financialSummary' ?
<button onClick={() => setActiveTab('financialSummary')}className="hover:bg-sky-100 hover:cursor-pointer rounded-lg bg-transparent border-cyan-500 border-solid text-cyan-500 ml-0.5 mt-0.5" style={{height:40,width:250,fontSize:18}}>Project Financial Summary</button> :
<button onClick={() => setActiveTab('financialSummary')}className="rounded-lg bg-sky-100 border-cyan-500 border-solid text-cyan-500 ml-0.5 mt-0.5" style={{height:40,width:250,fontSize:18}}>Project Financial Summary</button>
}
{activeTab !== 'cashFlow' ?
<button onClick={() => setActiveTab('cashFlow')} className="hover:bg-sky-100 hover:cursor-pointer rounded-lg bg-transparent border-cyan-500 border-solid text-cyan-500 ml-0.5 mt-0.5" style={{height:39,width:250,fontSize:18}}>Project Cash Flow</button> :
<button onClick={() => setActiveTab('cashFlow')} className="rounded-lg bg-sky-100 border-cyan-500 border-solid text-cyan-500 ml-0.5 mt-0.5" style={{height:39,width:250,fontSize:18}}>Project Cash Flow</button>
}
{activeTab !== 'progressByClient' ?
<button onClick={() => setActiveTab('progressByClient')} className="hover:bg-sky-100 hover:cursor-pointer rounded-lg bg-transparent border-cyan-500 border-solid text-cyan-500 ml-0.5 mt-0.5" style={{height:39,width:250,fontSize:18}}>Project Progress by Client</button> :
<button onClick={() => setActiveTab('progressByClient')} className="rounded-lg bg-sky-100 border-cyan-500 border-solid text-cyan-500 ml-0.5 mt-0.5" style={{height:39,width:250,fontSize:18}}>Project Progress by Client</button>
}
{activeTab !== 'resourceUtilization' ?
<button onClick={() => setActiveTab('resourceUtilization')} className="hover:bg-sky-100 hover:cursor-pointer rounded-lg bg-transparent border-cyan-500 border-solid text-cyan-500 ml-0.5 mt-0.5" style={{height:39,width:250,fontSize:18}}>Project Resource Utilization</button> :
<button onClick={() => setActiveTab('resourceUtilization')} className="rounded-lg bg-sky-100 border-cyan-500 border-solid text-cyan-500 ml-0.5 mt-0.5" style={{height:39,width:250,fontSize:18}}>Project Resource Utilization</button>
}
{activeTab !== 'staffUtilization' ?
<button onClick={() => setActiveTab('staffUtilization')} className="hover:bg-sky-100 hover:cursor-pointer rounded-lg bg-transparent border-cyan-500 border-solid text-cyan-500 ml-0.5 mt-0.5" style={{height:39,width:250,fontSize:18}}>Staff Utilization</button> :
<button onClick={() => setActiveTab('staffUtilization')} className="rounded-lg bg-sky-100 border-cyan-500 border-solid text-cyan-500 ml-0.5 mt-0.5" style={{height:39,width:250,fontSize:18}}>Staff Utilization</button>
}
</div>
<div style={{marginLeft:20,marginTop:20}}>
{renderContent()}
</div>
</Grid>
// <Grid item sm>
// <div style={{marginLeft:20}}>
// {activeTab !== 'financialSummary' ?
// <button onClick={() => setActiveTab('financialSummary')}className="hover:bg-sky-100 hover:cursor-pointer rounded-lg bg-transparent border-slate-400 border-solid text-slate-400 ml-0.5 mt-0.5" style={{height:40,width:250,fontSize:18}}>Project Financial Summary</button> :
// <button onClick={() => setActiveTab('financialSummary')}className="rounded-lg bg-sky-100 border-cyan-500 border-solid text-cyan-500 ml-0.5 mt-0.5" style={{height:40,width:250,fontSize:18}}>Project Financial Summary</button>
// }
// {activeTab !== 'cashFlow' ?
// <button onClick={() => setActiveTab('cashFlow')} className="hover:bg-sky-100 hover:cursor-pointer rounded-lg bg-transparent border-slate-400 border-solid text-slate-400 ml-0.5 mt-0.5" style={{height:39,width:250,fontSize:18}}>Project Cash Flow</button> :
// <button onClick={() => setActiveTab('cashFlow')} className="rounded-lg bg-sky-100 border-cyan-500 border-solid text-cyan-500 ml-0.5 mt-0.5" style={{height:39,width:250,fontSize:18}}>Project Cash Flow</button>
// }
// {activeTab !== 'progressByClient' ?
// <button onClick={() => setActiveTab('progressByClient')} className="hover:bg-sky-100 hover:cursor-pointer rounded-lg bg-transparent border-slate-400 border-solid text-slate-400 ml-0.5 mt-0.5" style={{height:39,width:250,fontSize:18}}>Project Progress by Client</button> :
// <button onClick={() => setActiveTab('progressByClient')} className="rounded-lg bg-sky-100 border-cyan-500 border-solid text-cyan-500 ml-0.5 mt-0.5" style={{height:39,width:250,fontSize:18}}>Project Progress by Client</button>
// }
// {activeTab !== 'resourceUtilization' ?
// <button onClick={() => setActiveTab('resourceUtilization')} className="hover:bg-sky-100 hover:cursor-pointer rounded-lg bg-transparent border-slate-400 border-solid text-slate-400 ml-0.5 mt-0.5" style={{height:39,width:250,fontSize:18}}>Project Resource Utilization</button> :
// <button onClick={() => setActiveTab('resourceUtilization')} className="rounded-lg bg-sky-100 border-cyan-500 border-solid text-cyan-500 ml-0.5 mt-0.5" style={{height:39,width:250,fontSize:18}}>Project Resource Utilization</button>
// }
// {activeTab !== 'staffUtilization' ?
// <button onClick={() => setActiveTab('staffUtilization')} className="hover:bg-sky-100 hover:cursor-pointer rounded-lg bg-transparent border-slate-400 border-solid text-slate-400 ml-0.5 mt-0.5" style={{height:39,width:250,fontSize:18}}>Staff Utilization</button> :
// <button onClick={() => setActiveTab('staffUtilization')} className="rounded-lg bg-sky-100 border-cyan-500 border-solid text-cyan-500 ml-0.5 mt-0.5" style={{height:39,width:250,fontSize:18}}>Staff Utilization</button>
// }
// </div>
// <div style={{marginLeft:20,marginTop:20}}>
// {renderContent()}
// </div>
// </Grid>
<Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable">
<Tab label="Project Financial Summary" />
<Tab label="Project Cash Flow" />
<Tab label="Project Progress by Client" />
<Tab label="Project Resource Utilization" />
<Tab label="Staff Utilization" />
</Tabs>
);
};



+ 287
- 50
src/components/DashboardPage/ProgressByClient.tsx Ver arquivo

@@ -1,14 +1,23 @@
"use client";
import * as React from "react";
import Grid from "@mui/material/Grid";
import { useState } from 'react'
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 } from "dns";
import SearchBox, { Criterion } from "../SearchBox";
import ProgressByClientSearch from "@/components/ProgressByClientSearch";
import { Suspense } from "react";

const ProgressByClient: React.FC = () => {
const [activeTab, setActiveTab] = useState('financialSummary');
@@ -19,74 +28,143 @@ const ProgressByClient: React.FC = () => {
const [clientName, setClientName] = useState('');
const [subsidiaryClientCode, setSubsidiaryClientCode] = useState('');
const [subsidiaryClientName, setSubsidiaryClientName] = useState('');
const [projectArray, setProjectArray] : any[] = useState([]);
const [percentageArray, setPercentageArray] : any[] = useState([]);
const [selectionModel, setSelectionModel] : 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 rows = [{id: 1,processNo:"1",processDescription:"把豬頸背肉絲解凍及洗淨隔乾水份", mat:"豬頸背肉絲", matLot:"Lot001", actlQty:"11.30",unit: "kg", equipment:"解凍盤",operator:"HKPC01"},
{id: 2,processNo:"2",processDescription:"加入調味料作腌製", mat:"洋葱", matLot:"Lot002", actlQty:"2.20",unit: "kg", equipment:"調味盤",operator:"HKPC01"},
{id: 3,processNo:"3",processDescription:"加入調味料作腌製", mat:"茄膏", matLot:"Lot003", actlQty:"50.00",unit: "g", equipment:"調味盤",operator:"HKPC01"},
{id: 4,processNo:"4",processDescription:"加入調味料作腌製", mat:"家樂牌黃汁粉", matLot:"Lot004", actlQty:"500.00",unit: "g", equipment:"調味盤",operator:"HKPC01"},
{id: 5,processNo:"5",processDescription:"放入0-4度雪櫃暫存備用", mat:"-", matLot:"-", actlQty:"-",unit: "-", equipment:"雪櫃",operator:"HKPC01"},
{id: 6,processNo:"6",processDescription:"洋葱洗淨切碎備用", mat:"洋葱", matLot:"Lot002", actlQty:"131.00",unit: "g", equipment:"切割機",operator:"HKPC01"},
{id: 7,processNo:"7",processDescription:"用蒸焗爐煮至熟透", mat:"-", matLot:"-", actlQty:"-",unit: "-", equipment:"蒸焗爐",operator:"HKPC01"},]
const [selectedRows, setSelectedRows] = useState([]);
const rows = [{id: 1,clientCode:"CUST-001",clientName:"Client A", clientSubsidiaryCode:"N/A", clientSubsidiaryName:"N/A", noOfProjects:"5"},
{id: 2,clientCode:"CUST-001",clientName:"Client A", clientSubsidiaryCode:"SUBS-001", clientSubsidiaryName:"Subsidiary A", noOfProjects:"5"},
{id: 3,clientCode:"CUST-001",clientName:"Client A", clientSubsidiaryCode:"SUBS-002", clientSubsidiaryName:"Subsidiary B", noOfProjects:"3"},
{id: 4,clientCode:"CUST-001",clientName:"Client A", clientSubsidiaryCode:"SUBS-003", clientSubsidiaryName:"Subsidiary C", noOfProjects:"1"}
]
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},
{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},
{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},
{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},
{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},
]

const columns = [
{
id: 'processNo',
field: 'processNo',
headerName: "工序次序",
flex: 0.7,
id: 'clientCode',
field: 'clientCode',
headerName: "Client Code",
flex: 1,
},
{
id: 'processDescription',
field: 'processDescription',
headerName: "工序描述",
id: 'clientName',
field: 'clientName',
headerName: "Client Name",
flex: 1,
},
{
id: 'mat',
field: 'mat',
headerName: "原料",
id: 'clientSubsidiaryCode',
field: 'clientSubsidiaryCode',
headerName: "Client Subsidiary Code",
flex: 1,
},
{
id: 'matLot',
field: 'matLot',
headerName: "原料批次",
id: 'noOfProjects',
field: 'noOfProjects',
headerName: "No. of Projects",
flex: 1,
},
];

const columns2 = [
{
id: 'actlQty',
field: 'actlQty',
headerName: "投入數量",
flex: 0.7,
align: "right",
headerAlign: 'right',
},
{
id: 'unit',
field: 'unit',
headerName: "單位",
flex: 0.7,
align: "left",
headerAlign: 'left',
},
{
id: 'equipment',
field: 'equipment',
headerName: "設備",
id: 'project',
field: 'project',
headerName: "Project",
flex: 1,
},
{
id: 'operator',
field: 'operator',
headerName: "操作人員",
},
{
id: 'team',
field: 'team',
headerName: "Team",
flex: 1,
},
{
id: 'teamLeader',
field: 'teamLeader',
headerName: "Team Leader",
flex: 1,
},
{
id: 'currentStage',
field: 'currentStage',
headerName: "Current Stage",
flex: 1,
},
{
id: 'budgetedManhour',
field: 'budgetedManhour',
headerName: "Budgeted Manhour",
flex: 1,
},
{
id: 'spentManhour',
field: 'spentManhour',
headerName: "Spent Manhour",
renderCell: (params:any) => {
if (params.row.budgetedManhour - params.row.spentManhour <= 0) {
return(
<span className="text-red-300">{params.row.spentManhour}</span>
)
} else {
return (
<span>{params.row.spentManhour}</span>
)
}
},
];
flex: 1,
},
{
id: 'remainedManhour',
field: 'remainedManhour',
headerName: "Remained Manhour",
renderCell: (params:any) => {
if (params.row.budgetedManhour - params.row.spentManhour <= 0) {
return(
<span className="text-red-300">({params.row.remainedManhour})</span>
)
} else {
return (
<span>{params.row.remainedManhour}</span>
)
}
},
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 (
<span className="text-red-300 text-center"><ReportProblemIcon/></span>
)
} else {
return (
<span></span>
)
}
},
flex: 1,
},
];

const InputFields = [
{ id: "clientCode", label: "Client Code", type: 'text', value: clientCode, setValue: setClientCode },
@@ -100,14 +178,173 @@ const ProgressByClient: React.FC = () => {
// 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',
},
plotOptions: {
pie: {
donut:{
labels:{
show:false,
}
}
},
},
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: ['#FF4560', '#00E396', '#008FFB', '#775DD0', '#FEB019'],
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:any[] = []
let otherPercentage = 100
let totalBudgetManhour = 0
const percentageArray = []
for (let i = 0; i <= selectedRowsData.length; i++) {
if (i === selectedRowsData.length) {
projectArray.push("Other")
} else {
projectArray.push(selectedRowsData[i].project)
totalBudgetManhour += Number(selectedRowsData[i].budgetedManhour)
}
}
for (let i = 0; i <= selectedRowsData.length; i++) {
if (i === selectedRowsData.length) {
percentageArray.push(otherPercentage)
} else {
let percentage = ((Number(selectedRowsData[i].spentManhour) / totalBudgetManhour) * 100).toFixed(1)
percentageArray.push(Number(percentage))
otherPercentage -= Number(percentage)
}
}
setSelectionModel(newSelectionModel)
setProjectArray(projectArray)
setPercentageArray(percentageArray)
};

const applySearch = (data: any) => {
console.log(data)
setSearchCriteria(data)
}
return (
<Grid item sm>
<CustomSearchForm applySearch={applySearch} fields={InputFields}/>
<CustomDatagrid Title={"工單工序詳情"} rows={rows} columns={columns} columnWidth={200} />
{/* <CustomSearchForm applySearch={applySearch} fields={InputFields}/> */}
{/* <CustomDatagrid rows={rows} columns={columns} columnWidth={200} dataGridHeight={300}/> */}
<div style={{display:"inline-block",width:"70%"}}>
<Grid item xs={12} md={12} lg={12}>
<Card>
<CardHeader className="text-slate-500" title="Project Progress"/>
<div style={{display:"inline-block",width:"99%"}}>
<ReactApexChart
options={options}
series={series}
type="bar"
height={350}
/>
</div>
{/* <div style={{display:"inline-block",width:"20%",verticalAlign:"top",textAlign:"center"}}>
<p><strong><u>Stage Deadline</u></strong></p>
{stageDeadline.map((date, index) => {
const marginTop = index === 0 ? 25 : 20;
return (
<p style={{marginTop:marginTop}} key={index}>{date}</p>
);
})}
</div> */}
<CardHeader className="text-slate-500" title="Current Stage Due Date"/>
<div style={{display:"inline-block",width:"99%",marginLeft:10}}>
<CustomDatagrid rows={rows2} columns={columns2} columnWidth={200} dataGridHeight={300} checkboxSelection={true} onRowSelectionModelChange={handleSelectionChange} selectionModel={selectionModel}/>
</div>
</Card>
</Grid>
</div>
<div style={{display:"inline-block",width:"30%",verticalAlign:"top",marginLeft:0}}>
<Grid item xs={12} md={12} lg={12}>
<Card style={{marginLeft:15,marginRight:20}}>
<CardHeader className="text-slate-500" title="Overall Progress per Project"/>
<ReactApexChart
options={options2}
series={percentageArray}
type="donut"
/>
</Card>
</Grid>
</div>
</Grid>
);
};


+ 49
- 4
src/components/NavigationContent/NavigationContent.tsx Ver arquivo

@@ -18,16 +18,22 @@ import Typography from "@mui/material/Typography";
import { usePathname } from "next/navigation";
import Link from "next/link";
import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig";
import ArrowCircleLeftOutlinedIcon from '@mui/icons-material/ArrowCircleLeftOutlined';
import ArrowCircleLeftRoundedIcon from '@mui/icons-material/ArrowCircleLeftRounded';

interface NavigationItem {
icon: React.ReactNode;
label: string;
path: string;
children?: NavigationItem[];
}

const navigationItems: NavigationItem[] = [
{ icon: <WorkHistory />, label: "User Workspace", path: "/home" },
{ icon: <Dashboard />, label: "Dashboard", path: "/dashboard" },
{ 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: <RequestQuote />, label: "Expense Claim", path: "/claim" },
{ icon: <Assignment />, label: "Project Management", path: "/projects" },
{ icon: <Task />, label: "Task Template", path: "/tasks" },
@@ -40,15 +46,54 @@ const NavigationContent: React.FC = () => {
const { t } = useTranslation("common");
const pathname = usePathname();

const [openItems, setOpenItems] = React.useState<string[]>([]);
const toggleItem = (path: string) => {
setOpenItems(prevOpenItems =>
prevOpenItems.includes(path)
? prevOpenItems.filter(item => item !== path)
: [...prevOpenItems, path]
);
};

const renderNavigationItem = (item: NavigationItem) => {
const isOpen = openItems.includes(item.path);

return (
<Box
key={`${item.label}-${item.path}`}
component={Link}
href={item.path}
sx={{ textDecoration: "none", color: "inherit" }}
>
<ListItemButton
selected={pathname.includes(item.path)}
onClick={() => item.children && toggleItem(item.path)}
>
<ListItemIcon>{item.icon}</ListItemIcon>
<ListItemText primary={t(item.label)} />
</ListItemButton>
{item.children && isOpen && (
<List sx={{ pl: 2 }}>
{item.children.map(child => renderNavigationItem(child))}
</List>
)}
</Box>
);
};
return (
<Box sx={{ width: NAVIGATION_CONTENT_WIDTH }}>
<Box sx={{ p: "1.5rem" }}>
{/* Replace this with company logo and/or name */}
<Typography variant="h4">TSMS</Typography>
<Typography style={{display:"inline-block"}}variant="h4">TSMS</Typography>
{/* <button className="float-right bg-transparent border-transparent" >
<ArrowCircleLeftRoundedIcon className="text-slate-400 hover:text-blue-400 hover:cursor-pointer " style={{ fontSize: '35px' }} />
</button> */}
</Box>
<Divider />
<List component="nav">
{navigationItems.map(({ icon, label, path }, index) => {
{navigationItems.map(item => renderNavigationItem(item))}
{/* {navigationItems.map(({ icon, label, path }, index) => {
return (
<Box
key={`${label}-${index}`}
@@ -62,7 +107,7 @@ const NavigationContent: React.FC = () => {
</ListItemButton>
</Box>
);
})}
})} */}
</List>
</Box>
);


+ 434
- 0
src/components/ProgressByClient/ProgressByClient.tsx Ver arquivo

@@ -0,0 +1,434 @@
"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";

const ProgressByClient: React.FC = () => {
const [activeTab, setActiveTab] = useState('financialSummary');
const [SearchCriteria, setSearchCriteria] = React.useState({})
const { t } = useTranslation("dashboard");

const [clientCode, setClientCode] = useState('');
const [clientName, setClientName] = useState('');
const [subsidiaryClientCode, setSubsidiaryClientCode] = useState('');
const [subsidiaryClientName, setSubsidiaryClientName] = 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,clientCode:"CUST-001",clientName:"Client A", clientSubsidiaryCode:"N/A", clientSubsidiaryName:"N/A", noOfProjects:"5"},
{id: 2,clientCode:"CUST-001",clientName:"Client A", clientSubsidiaryCode:"SUBS-001", clientSubsidiaryName:"Subsidiary A", noOfProjects:"5"},
{id: 3,clientCode:"CUST-001",clientName:"Client A", clientSubsidiaryCode:"SUBS-002", clientSubsidiaryName:"Subsidiary B", noOfProjects:"3"},
{id: 4,clientCode:"CUST-001",clientName:"Client A", clientSubsidiaryCode:"SUBS-003", clientSubsidiaryName:"Subsidiary C", 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 (
<span className="dot" style={{height:"15px",width:"15px",borderRadius:"50%",backgroundColor:`${params.row.color}`,display:"inline-block"}}></span>
)
},
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(
<span className="text-red-300">{params.row.spentManhour}</span>
)
} else {
return (
<span>{params.row.spentManhour}</span>
)
}
},
flex: 0.8,
},
{
id: 'remainedManhour',
field: 'remainedManhour',
headerName: "Remained Manhour",
renderCell: (params:any) => {
if (params.row.budgetedManhour - params.row.spentManhour <= 0) {
return(
<span className="text-red-300">({params.row.remainedManhour})</span>
)
} else {
return (
<span>{params.row.remainedManhour}</span>
)
}
},
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 (
<span className="text-red-300 text-center"><ReportProblemIcon/></span>
)
} else {
return (
<span></span>
)
}
},
flex: 0.2,
},
];

const InputFields = [
{ id: "clientCode", label: "Client Code", type: 'text', value: clientCode, setValue: setClientCode },
{ id: "clientName", label: "Client Name", type: 'text', value: clientName, setValue: setClientName },
{ id: "subsidiaryClientCode", label: "Subsidiary Client Code", type:'text', value:subsidiaryClientCode, setValue: setSubsidiaryClientCode},
{ id: "subsidiaryClientName", label: "Subsidiary Client Name", type:'text', value:subsidiaryClientName, setValue: setSubsidiaryClientName},
// { 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) {
let 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 (
<Grid item sm>
{/* <CustomSearchForm applySearch={applySearch} fields={InputFields}/> */}
{/* <CustomDatagrid rows={rows} columns={columns} columnWidth={200} dataGridHeight={300}/> */}
<div style={{display:"inline-block",width:"70%"}}>
<Grid item xs={12} md={12} lg={12}>
<Card>
<CardHeader className="text-slate-500" title="Project Progress"/>
<div style={{display:"inline-block",width:"99%"}}>
<ReactApexChart
options={options}
series={series}
type="bar"
height={350}
/>
</div>
{/* <div style={{display:"inline-block",width:"20%",verticalAlign:"top",textAlign:"center"}}>
<p><strong><u>Stage Deadline</u></strong></p>
{stageDeadline.map((date, index) => {
const marginTop = index === 0 ? 25 : 20;
return (
<p style={{marginTop:marginTop}} key={index}>{date}</p>
);
})}
</div> */}
<CardHeader className="text-slate-500" title="Current Stage Due Date"/>
<div style={{display:"inline-block",width:"99%",marginLeft:10}}>
<CustomDatagrid rows={rows2} columns={columns2} columnWidth={200} dataGridHeight={300} checkboxSelection={true} onRowSelectionModelChange={handleSelectionChange} selectionModel={selectionModel}/>
</div>
</Card>
</Grid>
</div>
<div style={{display:"inline-block",width:"30%",verticalAlign:"top",marginLeft:0}}>
<Grid item xs={12} md={12} lg={12}>
<Card style={{marginLeft:15,marginRight:20}}>
<CardHeader className="text-slate-500" title="Overall Progress per Project"/>
{percentageArray.length === 0 &&(
<div className="mt-10 mb-10 ml-5 mr-5 text-lg font-medium text-center" style={{color:"#898d8d"}}>Please select the project you want to check.</div>
)}
{percentageArray.length > 0 &&(
<ReactApexChart
options={options2}
series={percentageArray}
type="donut"
/>
)}
</Card>
</Grid>
<Grid item xs={12} md={12} lg={12}>
<Card style={{marginLeft:15,marginRight:20,marginTop:20}}>
<div>
<div className="mt-5 text-lg font-medium" style={{color:"#898d8d"}}><span style={{marginLeft:"5%"}}>Project Budget Manhour</span></div>
<div className="mt-2 text-2xl font-extrabold" style={{color:"#6b87cf"}}><span style={{marginLeft:"5%"}}>{projectBudgetManhour}</span></div>
</div>
<hr/>
<div>
<div className="mt-2 text-lg font-medium" style={{color:"#898d8d"}}><span style={{marginLeft:"5%"}}>Actual Manhour Spent</span></div>
<div className="mt-2 text-2xl font-extrabold" style={{color:"#6b87cf"}}><span style={{marginLeft:"5%"}}>{actualManhourSpent}</span></div>
</div>
<hr/>
<div>
<div className="mt-2 text-lg font-medium" style={{color:"#898d8d"}}><span style={{marginLeft:"5%"}}>Remained Manhour</span></div>
<div className="mt-2 text-2xl font-extrabold" style={{color:"#6b87cf"}}><span style={{marginLeft:"5%"}}>{remainedManhour}</span></div>
</div>
<hr/>
<div>
<div className="mt-2 text-lg font-medium" style={{color:"#898d8d"}}><span style={{marginLeft:"5%"}}>Last Update</span></div>
<div className="mt-2 mb-5 text-2xl font-extrabold" style={{color:"#6b87cf"}}><span style={{marginLeft:"5%"}}>{lastUpdate}</span></div>
</div>
</Card>
</Grid>
</div>
</Grid>
);
};

export default ProgressByClient;

+ 1
- 0
src/components/ProgressByClient/index.ts Ver arquivo

@@ -0,0 +1 @@
export { default } from "./ProgressByClient";

+ 57
- 0
src/components/ProgressByClientSearch/ProgressByClientSearch.tsx Ver arquivo

@@ -0,0 +1,57 @@
"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 { ClientProjectResult } from "@/app/api/clientprojects";

interface Props {
projects: ClientProjectResult[];
}
type SearchQuery = Partial<Omit<ClientProjectResult, "id">>;
type SearchParamNames = keyof SearchQuery;

const ProgressByClientSearch: React.FC<Props> = ({ 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<SearchParamNames>[] = useMemo(
() => [
{ label: "Client Code", paramName: "clientCode", type: "text" },
{ label: "Client Name", paramName: "clientName", type: "text" },
],
[t],
);

const columns = useMemo<Column<ClientProjectResult>[]>(
() => [
{ 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);
}}
/>
<SearchResults<ClientProjectResult>
items={filteredProjects}
columns={columns}
/>
</>
);
};

export default ProgressByClientSearch;

+ 40
- 0
src/components/ProgressByClientSearch/ProgressByClientSearchLoading.tsx Ver arquivo

@@ -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 ProgressByClientSearchLoading: 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 ProgressByClientSearchLoading;

+ 18
- 0
src/components/ProgressByClientSearch/ProgressByClientSearchWrapper.tsx Ver arquivo

@@ -0,0 +1,18 @@
import { fetchClientProjects } from "@/app/api/clientprojects";
import React from "react";
import ProgressByClientSearch from "./ProgressByClientSearch";
import ProgressByClientSearchLoading from "./ProgressByClientSearchLoading";

interface SubComponents {
Loading: typeof ProgressByClientSearchLoading;
}

const ProgressByClientSearchWrapper: React.FC & SubComponents = async () => {
const clentprojects = await fetchClientProjects();

return <ProgressByClientSearch projects={clentprojects} />;
};

ProgressByClientSearchWrapper.Loading = ProgressByClientSearchLoading;

export default ProgressByClientSearchWrapper;

+ 1
- 0
src/components/ProgressByClientSearch/index.ts Ver arquivo

@@ -0,0 +1 @@
export { default } from "./ProgressByClientSearchWrapper";

+ 1
- 1
src/components/SearchBox/SearchBox.tsx Ver arquivo

@@ -77,7 +77,7 @@ function SearchBox<T extends string>({ criteria, onSearch }: Props<T>) {
return (
<Card>
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
<Typography variant="overline">{t("Search Criteria")}</Typography>
<Typography variant="overline" style={{fontWeight:"600"}}>{t("Search Criteria")}</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
{criteria.map((c) => {
return (


+ 12
- 0
src/theme.ts Ver arquivo

@@ -0,0 +1,12 @@
import { createTheme } from '@mui/material/styles';

const theme = createTheme({
palette: {
background: {
default: '#fcfcfc'
}
}

});

export default theme;

+ 2
- 2
src/theme/devias-material-kit/components.ts Ver arquivo

@@ -189,7 +189,7 @@ const components: ThemeOptions["components"] = {
},
[`&.Mui-focused`]: {
backgroundColor: "transparent",
borderColor: palette.primary.main,
borderColor: "palette.primary.main",
boxShadow: `${palette.primary.main} 0 0 0 2px`,
},
[`&.Mui-error`]: {
@@ -216,7 +216,7 @@ const components: ThemeOptions["components"] = {
[`&.Mui-focused`]: {
backgroundColor: "transparent",
[`& .MuiOutlinedInput-notchedOutline`]: {
borderColor: palette.primary.main,
borderColor: "palette.primary.main",
boxShadow: `${palette.primary.main} 0 0 0 2px`,
},
},


Carregando…
Cancelar
Salvar