@@ -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", | |||
@@ -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", | |||
@@ -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; |
@@ -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,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"; | |||
@@ -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, | |||
}, | |||
]; |
@@ -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" }} | |||
> | |||
@@ -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" | |||
@@ -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> | |||
); | |||
}; | |||
@@ -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/> 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/> 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> | |||
); | |||
@@ -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> | |||
); | |||
}; | |||
@@ -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> | |||
); | |||
}; | |||
@@ -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> | |||
); | |||
}; | |||
@@ -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> | |||
); | |||
@@ -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; |
@@ -0,0 +1 @@ | |||
export { default } from "./ProgressByClient"; |
@@ -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; |
@@ -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; |
@@ -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; |
@@ -0,0 +1 @@ | |||
export { default } from "./ProgressByClientSearchWrapper"; |
@@ -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 ( | |||
@@ -0,0 +1,12 @@ | |||
import { createTheme } from '@mui/material/styles'; | |||
const theme = createTheme({ | |||
palette: { | |||
background: { | |||
default: '#fcfcfc' | |||
} | |||
} | |||
}); | |||
export default theme; |
@@ -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`, | |||
}, | |||
}, | |||