Initial commit
This commit is contained in:
94
layout/AppBreadCrumb.tsx
Normal file
94
layout/AppBreadCrumb.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
'use client';
|
||||
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { ObjectUtils, classNames } from 'primereact/utils';
|
||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||
import { LayoutContext } from './context/layoutcontext';
|
||||
import { Breadcrumb } from '../types/layout';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { Button } from 'primereact/button';
|
||||
|
||||
const AppBreadcrumb = () => {
|
||||
const [searchActive, setSearchActive] = useState(false);
|
||||
const pathname = usePathname();
|
||||
const [breadcrumb, setBreadcrumb] = useState<Breadcrumb | null>(null);
|
||||
const { breadcrumbs, showSidebar } = useContext(LayoutContext);
|
||||
const searchInput = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const filteredBreadcrumbs = breadcrumbs?.find((crumb) => {
|
||||
const lastPathSegment = crumb.to.split('/').pop();
|
||||
const lastRouterSegment = pathname.split('/').pop();
|
||||
|
||||
if (lastRouterSegment?.startsWith('[') && !isNaN(Number(lastPathSegment))) {
|
||||
return pathname.split('/').slice(0, -1).join('/') === crumb.to?.split('/').slice(0, -1).join('/');
|
||||
}
|
||||
return crumb.to === pathname;
|
||||
});
|
||||
|
||||
setBreadcrumb(filteredBreadcrumbs);
|
||||
}, [pathname, breadcrumbs]);
|
||||
|
||||
const activateSearch = () => {
|
||||
setSearchActive(true);
|
||||
setTimeout(() => {
|
||||
searchInput.current.focus();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const deactivateSearch = () => {
|
||||
setSearchActive(false);
|
||||
};
|
||||
|
||||
const onSidebarButtonClick = () => {
|
||||
showSidebar();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="layout-breadcrumb flex align-items-center relative h-3rem">
|
||||
<nav className="layout-breadcrumb">
|
||||
<ol>
|
||||
{ObjectUtils.isNotEmpty(breadcrumb) && pathname !== '/' ? (
|
||||
breadcrumb.labels.map((label, index) => {
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
{index !== 0 && <li className="layout-breadcrumb-chevron"> / </li>}
|
||||
<li key={index}>{label}</li>
|
||||
</React.Fragment>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<li key={'home'}>E-Commerce Dashboard</li>
|
||||
)}
|
||||
</ol>
|
||||
</nav>
|
||||
<ul className="breadcrumb-menu flex align-items-center justify-content-end lg:hidden absolute right-0 top-0 z-4 h-3rem w-screen">
|
||||
<li className="w-full m-0 ml-3">
|
||||
<div className={classNames('breadcrumb-search flex justify-content-end', { 'breadcrumb-search-active': searchActive })}>
|
||||
<Button icon="pi pi-search" className="breadcrumb-searchbutton p-button-text p-button-secondary text-color-secondary p-button-rounded flex-shrink-0" type="button" onClick={activateSearch}></Button>
|
||||
<div className="search-input-wrapper">
|
||||
<span className="p-input-icon-right">
|
||||
<InputText
|
||||
ref={searchInput}
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
onBlur={deactivateSearch}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'ESCAPE') deactivateSearch();
|
||||
}}
|
||||
/>
|
||||
<i className="pi pi-search"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li className="right-panel-button relative lg:block">
|
||||
<Button type="button" label="Today" style={{ width: '6.7rem' }} icon="pi pi-bookmark" className="layout-rightmenu-button hidden md:block font-normal" onClick={onSidebarButtonClick}></Button>
|
||||
<Button type="button" style={{ width: '3.286rem' }} icon="pi pi-bookmark" className="layout-rightmenu-button block md:hidden font-normal" onClick={onSidebarButtonClick}></Button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppBreadcrumb;
|
||||
209
layout/AppConfig.tsx
Normal file
209
layout/AppConfig.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { classNames } from 'primereact/utils';
|
||||
import { PrimeReactContext } from 'primereact/api';
|
||||
import { RadioButton, RadioButtonChangeEvent } from 'primereact/radiobutton';
|
||||
import { InputSwitch, InputSwitchChangeEvent } from 'primereact/inputswitch';
|
||||
import { Button } from 'primereact/button';
|
||||
import { LayoutContext } from './context/layoutcontext';
|
||||
import { Sidebar } from 'primereact/sidebar';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { AppConfigProps, ColorScheme } from '../types/layout';
|
||||
|
||||
const AppConfig = (props: AppConfigProps) => {
|
||||
const { layoutConfig, setLayoutConfig, layoutState, setLayoutState, isSlim, isHorizontal, isSlimPlus } = useContext(LayoutContext);
|
||||
const { setRipple, changeTheme } = useContext(PrimeReactContext);
|
||||
const scales = [12, 13, 14, 15, 16];
|
||||
|
||||
const componentThemes = [
|
||||
{ name: 'blue', color: '#0F8BFD' },
|
||||
{ name: 'green', color: '#0BD18A' },
|
||||
{ name: 'magenta', color: '#EC4DBC' },
|
||||
{ name: 'orange', color: '#FD9214' },
|
||||
{ name: 'purple', color: '#873EFE' },
|
||||
{ name: 'red', color: '#FC6161' },
|
||||
{ name: 'teal', color: '#00D0DE' },
|
||||
{ name: 'yellow', color: '#EEE500' }
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (isSlim() || isHorizontal() || isSlimPlus()) {
|
||||
setLayoutState((prevState) => ({ ...prevState, resetMenu: true }));
|
||||
}
|
||||
}, [layoutConfig.menuMode]);
|
||||
|
||||
const changeInputStyle = (e: RadioButtonChangeEvent) => {
|
||||
setLayoutConfig((prevState) => ({ ...prevState, inputStyle: e.value }));
|
||||
};
|
||||
|
||||
const changeRipple = (e: InputSwitchChangeEvent) => {
|
||||
setRipple(e.value);
|
||||
setLayoutConfig((prevState) => ({ ...prevState, ripple: e.value }));
|
||||
};
|
||||
|
||||
const changeMenuMode = (e: RadioButtonChangeEvent) => {
|
||||
setLayoutConfig((prevState) => ({ ...prevState, menuMode: e.value }));
|
||||
};
|
||||
|
||||
const changeColorScheme = (colorScheme: ColorScheme) => {
|
||||
changeTheme(layoutConfig.colorScheme, colorScheme, 'theme-link', () => {
|
||||
setLayoutConfig((prevState) => ({ ...prevState, colorScheme }));
|
||||
});
|
||||
};
|
||||
|
||||
const _changeTheme = (theme: string) => {
|
||||
changeTheme(layoutConfig.theme, theme, 'theme-link', () => {
|
||||
setLayoutConfig((prevState) => ({ ...prevState, theme }));
|
||||
});
|
||||
};
|
||||
|
||||
const getComponentThemes = () => {
|
||||
return (
|
||||
<div className="flex flex-wrap row-gap-3">
|
||||
{componentThemes.map((theme, i) => {
|
||||
return (
|
||||
<div key={i} className="w-3">
|
||||
<a
|
||||
className="cursor-pointer p-link w-2rem h-2rem border-circle flex-shrink-0 flex align-items-center justify-content-center"
|
||||
style={{ cursor: 'pointer', backgroundColor: theme.color }}
|
||||
onClick={() => _changeTheme(theme.name)}
|
||||
>
|
||||
{layoutConfig.theme === theme.name && (
|
||||
<span className="check flex align-items-center justify-content-center">
|
||||
<i className="pi pi-check" style={{ color: 'white' }}></i>
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const componentThemesElement = getComponentThemes();
|
||||
|
||||
const decrementScale = () => {
|
||||
setLayoutConfig((prevState) => ({
|
||||
...prevState,
|
||||
scale: prevState.scale - 1
|
||||
}));
|
||||
};
|
||||
|
||||
const incrementScale = () => {
|
||||
setLayoutConfig((prevState) => ({
|
||||
...prevState,
|
||||
scale: prevState.scale + 1
|
||||
}));
|
||||
};
|
||||
|
||||
const applyScale = () => {
|
||||
document.documentElement.style.fontSize = layoutConfig.scale + 'px';
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
applyScale();
|
||||
}, [layoutConfig.scale]);
|
||||
|
||||
return (
|
||||
<div id="layout-config">
|
||||
<a
|
||||
className="layout-config-button"
|
||||
onClick={() =>
|
||||
setLayoutState((prevState) => ({
|
||||
...prevState,
|
||||
configSidebarVisible: true
|
||||
}))
|
||||
}
|
||||
>
|
||||
<i className="pi pi-cog"></i>
|
||||
</a>
|
||||
|
||||
<Sidebar
|
||||
visible={layoutState.configSidebarVisible}
|
||||
position="right"
|
||||
onHide={() =>
|
||||
setLayoutState((prevState) => ({
|
||||
...prevState,
|
||||
configSidebarVisible: false
|
||||
}))
|
||||
}
|
||||
>
|
||||
<h5>Themes</h5>
|
||||
{componentThemesElement}
|
||||
|
||||
<h5>Scale</h5>
|
||||
<div className="flex align-items-center">
|
||||
<Button text rounded icon="pi pi-minus" onClick={decrementScale} className=" w-2rem h-2rem mr-2" disabled={layoutConfig.scale === scales[0]}></Button>
|
||||
<div className="flex gap-2 align-items-center">
|
||||
{scales.map((s, i) => {
|
||||
return (
|
||||
<i
|
||||
key={i}
|
||||
className={classNames('pi pi-circle-fill text-300', {
|
||||
'text-primary-500': s === layoutConfig.scale
|
||||
})}
|
||||
></i>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Button text rounded icon="pi pi-plus" onClick={incrementScale} className=" w-2rem h-2rem ml-2" disabled={layoutConfig.scale === scales[scales.length - 1]}></Button>
|
||||
</div>
|
||||
|
||||
{!props.minimal && (
|
||||
<>
|
||||
<h5>Menu Type</h5>
|
||||
<div className="flex flex-wrap row-gap-3">
|
||||
<div className="flex align-items-center gap-2 w-6">
|
||||
<RadioButton name="menuMode" value="static" checked={layoutConfig.menuMode === 'static'} inputId="mode1" onChange={(e) => changeMenuMode(e)}></RadioButton>
|
||||
<label htmlFor="mode1">Static</label>
|
||||
</div>
|
||||
<div className="flex align-items-center gap-2 w-6">
|
||||
<RadioButton name="menuMode" value="overlay" checked={layoutConfig.menuMode === 'overlay'} inputId="mode2" onChange={(e) => changeMenuMode(e)}></RadioButton>
|
||||
<label htmlFor="mode2">Overlay</label>
|
||||
</div>
|
||||
<div className="flex align-items-center gap-2 w-6">
|
||||
<RadioButton name="menuMode" value="slim" checked={layoutConfig.menuMode === 'slim'} inputId="mode3" onChange={(e) => changeMenuMode(e)}></RadioButton>
|
||||
<label htmlFor="mode3">Slim</label>
|
||||
</div>
|
||||
<div className="flex align-items-center gap-2 w-6">
|
||||
<RadioButton name="menuMode" value="slim-plus" checked={layoutConfig.menuMode === 'slim-plus'} inputId="mode4" onChange={(e) => changeMenuMode(e)}></RadioButton>
|
||||
<label htmlFor="mode4">Slim+</label>
|
||||
</div>
|
||||
<div className="flex align-items-center gap-2 w-6">
|
||||
<RadioButton name="menuMode" value="horizontal" checked={layoutConfig.menuMode === 'horizontal'} inputId="mode4" onChange={(e) => changeMenuMode(e)}></RadioButton>
|
||||
<label htmlFor="mode4">Horizontal</label>
|
||||
</div>
|
||||
<div className="flex align-items-center gap-2 w-6">
|
||||
<RadioButton name="menuMode" value="reveal" checked={layoutConfig.menuMode === 'reveal'} inputId="mode5" onChange={(e) => changeMenuMode(e)}></RadioButton>
|
||||
<label htmlFor="mode5">Reveal</label>
|
||||
</div>
|
||||
<div className="flex align-items-center gap-2 w-6">
|
||||
<RadioButton name="menuMode" value="drawer" checked={layoutConfig.menuMode === 'drawer'} inputId="mode6" onChange={(e) => changeMenuMode(e)}></RadioButton>
|
||||
<label htmlFor="mode6">Drawer</label>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<h5>Color Scheme</h5>
|
||||
|
||||
<div className="field-radiobutton">
|
||||
<RadioButton name="colorScheme" value="light" checked={layoutConfig.colorScheme === 'light'} inputId="theme3" onChange={(e) => changeColorScheme(e.value)}></RadioButton>
|
||||
<label htmlFor="theme3">Light</label>
|
||||
</div>
|
||||
|
||||
<div className="field-radiobutton">
|
||||
<RadioButton name="colorScheme" value="dark" checked={layoutConfig.colorScheme === 'dark'} inputId="theme1" onChange={(e) => changeColorScheme(e.value)}></RadioButton>
|
||||
<label htmlFor="theme1">Dark</label>
|
||||
</div>
|
||||
|
||||
<h5>Ripple Effect</h5>
|
||||
<InputSwitch checked={layoutConfig.ripple} onChange={(e) => changeRipple(e)} />
|
||||
</Sidebar>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppConfig;
|
||||
936
layout/AppMenu.tsx
Normal file
936
layout/AppMenu.tsx
Normal file
@@ -0,0 +1,936 @@
|
||||
import { MenuModal } from '../types/layout';
|
||||
import { UserRole } from '@/types/auth';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import AppSubMenu from './AppSubMenu';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const AppMenu = () => {
|
||||
const { user, hasRole, hasPermission, isAuthenticated } = useAuth();
|
||||
|
||||
// Menu basé sur les API endpoints réellement disponibles dans le backend
|
||||
|
||||
const canAccess = (requiredRoles: UserRole[], requiredPermissions?: string[]) => {
|
||||
if (!isAuthenticated || !user) return false;
|
||||
|
||||
// Vérifier les rôles requis
|
||||
if (requiredRoles && requiredRoles.length > 0) {
|
||||
const hasRequiredRole = requiredRoles.some(role => hasRole(role));
|
||||
if (!hasRequiredRole) return false;
|
||||
}
|
||||
|
||||
// Vérifier les permissions requises
|
||||
if (requiredPermissions && requiredPermissions.length > 0) {
|
||||
const hasRequiredPermission = requiredPermissions.some(permission => hasPermission(permission));
|
||||
if (!hasRequiredPermission) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const filterMenuItems = (items: MenuModal[]): MenuModal[] => {
|
||||
return items.filter(item => {
|
||||
if (item.separator) return true;
|
||||
if (!item.requiredRoles && !item.requiredPermissions) return true;
|
||||
|
||||
const hasAccess = canAccess(item.requiredRoles || [], item.requiredPermissions);
|
||||
|
||||
if (hasAccess && item.items) {
|
||||
item.items = filterMenuItems(item.items);
|
||||
return item.items.length > 0;
|
||||
}
|
||||
|
||||
return hasAccess;
|
||||
});
|
||||
};
|
||||
|
||||
const baseModel: MenuModal[] = [
|
||||
{
|
||||
label: 'Tableau de Bord',
|
||||
icon: 'pi pi-home',
|
||||
items: [
|
||||
{
|
||||
label: 'Vue d\'ensemble',
|
||||
icon: 'pi pi-fw pi-chart-line',
|
||||
to: '/dashboard',
|
||||
requiredRoles: [UserRole.ADMIN, UserRole.MANAGER]
|
||||
},
|
||||
{
|
||||
label: 'Dashboard Gestionnaire',
|
||||
icon: 'pi pi-fw pi-users',
|
||||
to: '/gestionnaire/dashboard',
|
||||
requiredRoles: [UserRole.GESTIONNAIRE_PROJET]
|
||||
},
|
||||
{
|
||||
label: 'Mon Espace Client',
|
||||
icon: 'pi pi-fw pi-user',
|
||||
to: '/client/dashboard',
|
||||
requiredRoles: [UserRole.CLIENT]
|
||||
},
|
||||
{
|
||||
label: 'Dashboard Chantiers',
|
||||
icon: 'pi pi-fw pi-building',
|
||||
to: '/dashboard/chantiers',
|
||||
requiredRoles: [UserRole.ADMIN, UserRole.MANAGER, UserRole.CHEF_CHANTIER]
|
||||
},
|
||||
{
|
||||
label: 'Dashboard Maintenance',
|
||||
icon: 'pi pi-fw pi-wrench',
|
||||
to: '/dashboard/maintenance',
|
||||
requiredRoles: [UserRole.ADMIN, UserRole.MANAGER]
|
||||
},
|
||||
{
|
||||
label: 'Dashboard Ressources',
|
||||
icon: 'pi pi-fw pi-users',
|
||||
to: '/dashboard/ressources'
|
||||
},
|
||||
{
|
||||
label: 'Dashboard Planning',
|
||||
icon: 'pi pi-fw pi-calendar',
|
||||
to: '/dashboard/planning'
|
||||
},
|
||||
{
|
||||
label: 'Alertes & KPI',
|
||||
icon: 'pi pi-fw pi-exclamation-triangle',
|
||||
to: '/dashboard/alertes'
|
||||
},
|
||||
{
|
||||
label: 'Résumé Quotidien',
|
||||
icon: 'pi pi-fw pi-file-o',
|
||||
to: '/dashboard/resume-quotidien'
|
||||
}
|
||||
]
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
label: 'Gestion BTP',
|
||||
icon: 'pi pi-building',
|
||||
items: [
|
||||
{
|
||||
label: 'Chantiers',
|
||||
icon: 'pi pi-fw pi-map',
|
||||
items: [
|
||||
{
|
||||
label: 'Tous les chantiers',
|
||||
icon: 'pi pi-fw pi-list',
|
||||
to: '/chantiers'
|
||||
},
|
||||
{
|
||||
label: 'En cours',
|
||||
icon: 'pi pi-fw pi-play-circle',
|
||||
to: '/chantiers/en-cours'
|
||||
},
|
||||
{
|
||||
label: 'Planifiés',
|
||||
icon: 'pi pi-fw pi-calendar',
|
||||
to: '/chantiers/planifies'
|
||||
},
|
||||
{
|
||||
label: 'Terminés',
|
||||
icon: 'pi pi-fw pi-check-circle',
|
||||
to: '/chantiers/termines'
|
||||
},
|
||||
{
|
||||
label: 'Statistiques',
|
||||
icon: 'pi pi-fw pi-chart-bar',
|
||||
to: '/chantiers/stats'
|
||||
},
|
||||
{
|
||||
label: 'Exécution granulaire',
|
||||
icon: 'pi pi-fw pi-check-square',
|
||||
to: '/chantiers/execution-granulaire',
|
||||
requiredRoles: [UserRole.ADMIN, UserRole.MANAGER, UserRole.CHEF_CHANTIER]
|
||||
},
|
||||
{
|
||||
label: 'Nouveau chantier',
|
||||
icon: 'pi pi-fw pi-plus',
|
||||
to: '/chantiers/nouveau'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Clients',
|
||||
icon: 'pi pi-fw pi-users',
|
||||
items: [
|
||||
{
|
||||
label: 'Liste des clients',
|
||||
icon: 'pi pi-fw pi-list',
|
||||
to: '/clients'
|
||||
},
|
||||
{
|
||||
label: 'Recherche clients',
|
||||
icon: 'pi pi-fw pi-search',
|
||||
to: '/clients/recherche'
|
||||
},
|
||||
{
|
||||
label: 'Nouveau client',
|
||||
icon: 'pi pi-fw pi-user-plus',
|
||||
to: '/clients/nouveau'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Devis',
|
||||
icon: 'pi pi-fw pi-file-edit',
|
||||
items: [
|
||||
{
|
||||
label: 'Tous les devis',
|
||||
icon: 'pi pi-fw pi-list',
|
||||
to: '/devis'
|
||||
},
|
||||
{
|
||||
label: 'En attente',
|
||||
icon: 'pi pi-fw pi-clock',
|
||||
to: '/devis/en-attente'
|
||||
},
|
||||
{
|
||||
label: 'Acceptés',
|
||||
icon: 'pi pi-fw pi-check',
|
||||
to: '/devis/acceptes'
|
||||
},
|
||||
{
|
||||
label: 'Expirant bientôt',
|
||||
icon: 'pi pi-fw pi-exclamation-triangle',
|
||||
to: '/devis/expiring'
|
||||
},
|
||||
{
|
||||
label: 'Recherche par dates',
|
||||
icon: 'pi pi-fw pi-search',
|
||||
to: '/devis/search'
|
||||
},
|
||||
{
|
||||
label: 'Nouveau devis',
|
||||
icon: 'pi pi-fw pi-plus',
|
||||
to: '/devis/nouveau'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Factures',
|
||||
icon: 'pi pi-fw pi-receipt',
|
||||
items: [
|
||||
{
|
||||
label: 'Toutes les factures',
|
||||
icon: 'pi pi-fw pi-list',
|
||||
to: '/factures'
|
||||
},
|
||||
{
|
||||
label: 'Échues',
|
||||
icon: 'pi pi-fw pi-exclamation-circle',
|
||||
to: '/factures/echues'
|
||||
},
|
||||
{
|
||||
label: 'Proches échéance',
|
||||
icon: 'pi pi-fw pi-clock',
|
||||
to: '/factures/proches-echeance'
|
||||
},
|
||||
{
|
||||
label: 'Par période',
|
||||
icon: 'pi pi-fw pi-calendar',
|
||||
to: '/factures/date-range'
|
||||
},
|
||||
{
|
||||
label: 'Chiffre d\'affaires',
|
||||
icon: 'pi pi-fw pi-dollar',
|
||||
to: '/factures/chiffre-affaires'
|
||||
},
|
||||
{
|
||||
label: 'Nouvelle facture',
|
||||
icon: 'pi pi-fw pi-plus',
|
||||
to: '/factures/nouvelle'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
label: 'Configuration & Templates',
|
||||
icon: 'pi pi-cog',
|
||||
items: [
|
||||
{
|
||||
label: 'Templates de Tâches',
|
||||
icon: 'pi pi-fw pi-list-check',
|
||||
to: '/templates/taches',
|
||||
requiredRoles: [UserRole.ADMIN, UserRole.MANAGER]
|
||||
}
|
||||
]
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
label: 'Gestion Budgétaire',
|
||||
icon: 'pi pi-calculator',
|
||||
items: [
|
||||
{
|
||||
label: 'Planification Budgétaire',
|
||||
icon: 'pi pi-fw pi-chart-pie',
|
||||
items: [
|
||||
{
|
||||
label: 'Vue d\'ensemble budgets',
|
||||
icon: 'pi pi-fw pi-eye',
|
||||
to: '/budget/planification'
|
||||
},
|
||||
{
|
||||
label: 'Budgets par chantier',
|
||||
icon: 'pi pi-fw pi-building',
|
||||
to: '/budget/planification/chantiers'
|
||||
},
|
||||
{
|
||||
label: 'Budgets par phase',
|
||||
icon: 'pi pi-fw pi-sitemap',
|
||||
to: '/budget/planification/phases'
|
||||
},
|
||||
{
|
||||
label: 'Modèles de budget',
|
||||
icon: 'pi pi-fw pi-clone',
|
||||
to: '/budget/planification/modeles'
|
||||
},
|
||||
{
|
||||
label: 'Nouvelle planification',
|
||||
icon: 'pi pi-fw pi-plus',
|
||||
to: '/budget/planification/nouveau'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Suivi des Dépenses',
|
||||
icon: 'pi pi-fw pi-chart-line',
|
||||
items: [
|
||||
{
|
||||
label: 'Tableau de bord dépenses',
|
||||
icon: 'pi pi-fw pi-chart-bar',
|
||||
to: '/budget/suivi'
|
||||
},
|
||||
{
|
||||
label: 'Dépenses par chantier',
|
||||
icon: 'pi pi-fw pi-building',
|
||||
to: '/budget/suivi/chantiers'
|
||||
},
|
||||
{
|
||||
label: 'Dépenses par catégorie',
|
||||
icon: 'pi pi-fw pi-tags',
|
||||
to: '/budget/suivi/categories'
|
||||
},
|
||||
{
|
||||
label: 'Analyse des écarts',
|
||||
icon: 'pi pi-fw pi-exclamation-triangle',
|
||||
to: '/budget/suivi/ecarts'
|
||||
},
|
||||
{
|
||||
label: 'Alertes budgétaires',
|
||||
icon: 'pi pi-fw pi-bell',
|
||||
to: '/budget/suivi/alertes'
|
||||
},
|
||||
{
|
||||
label: 'Saisir nouvelle dépense',
|
||||
icon: 'pi pi-fw pi-plus',
|
||||
to: '/budget/suivi/nouvelle-depense'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Analyses Budgétaires',
|
||||
icon: 'pi pi-fw pi-chart-bar',
|
||||
items: [
|
||||
{
|
||||
label: 'Rentabilité projets',
|
||||
icon: 'pi pi-fw pi-percentage',
|
||||
to: '/budget/analyses/rentabilite'
|
||||
},
|
||||
{
|
||||
label: 'Évolution des coûts',
|
||||
icon: 'pi pi-fw pi-chart-line',
|
||||
to: '/budget/analyses/evolution-couts'
|
||||
},
|
||||
{
|
||||
label: 'Prévisions budgétaires',
|
||||
icon: 'pi pi-fw pi-forward',
|
||||
to: '/budget/analyses/previsions'
|
||||
},
|
||||
{
|
||||
label: 'Comparaisons historiques',
|
||||
icon: 'pi pi-fw pi-history',
|
||||
to: '/budget/analyses/historique'
|
||||
},
|
||||
{
|
||||
label: 'Export rapports budget',
|
||||
icon: 'pi pi-fw pi-file-excel',
|
||||
to: '/budget/analyses/export'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
label: 'Ressources Humaines',
|
||||
icon: 'pi pi-users',
|
||||
items: [
|
||||
{
|
||||
label: 'Employés',
|
||||
icon: 'pi pi-fw pi-user',
|
||||
items: [
|
||||
{
|
||||
label: 'Liste des employés',
|
||||
icon: 'pi pi-fw pi-list',
|
||||
to: '/employes'
|
||||
},
|
||||
{
|
||||
label: 'Employés actifs',
|
||||
icon: 'pi pi-fw pi-check-circle',
|
||||
to: '/employes/actifs'
|
||||
},
|
||||
{
|
||||
label: 'Employés disponibles',
|
||||
icon: 'pi pi-fw pi-calendar-check',
|
||||
to: '/employes/disponibles'
|
||||
},
|
||||
{
|
||||
label: 'Statistiques employés',
|
||||
icon: 'pi pi-fw pi-chart-bar',
|
||||
to: '/employes/stats'
|
||||
},
|
||||
{
|
||||
label: 'Nouvel employé',
|
||||
icon: 'pi pi-fw pi-user-plus',
|
||||
to: '/employes/nouveau'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Équipes',
|
||||
icon: 'pi pi-fw pi-users',
|
||||
items: [
|
||||
{
|
||||
label: 'Toutes les équipes',
|
||||
icon: 'pi pi-fw pi-list',
|
||||
to: '/equipes'
|
||||
},
|
||||
{
|
||||
label: 'Équipes disponibles',
|
||||
icon: 'pi pi-fw pi-check-circle',
|
||||
to: '/equipes/disponibles'
|
||||
},
|
||||
{
|
||||
label: 'Spécialités équipes',
|
||||
icon: 'pi pi-fw pi-star',
|
||||
to: '/equipes/specialites'
|
||||
},
|
||||
{
|
||||
label: 'Équipe optimale',
|
||||
icon: 'pi pi-fw pi-search',
|
||||
to: '/equipes/optimal'
|
||||
},
|
||||
{
|
||||
label: 'Statistiques équipes',
|
||||
icon: 'pi pi-fw pi-chart-line',
|
||||
to: '/equipes/stats'
|
||||
},
|
||||
{
|
||||
label: 'Nouvelle équipe',
|
||||
icon: 'pi pi-fw pi-plus',
|
||||
to: '/equipes/nouvelle'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Disponibilités',
|
||||
icon: 'pi pi-fw pi-calendar-times',
|
||||
items: [
|
||||
{
|
||||
label: 'Toutes les disponibilités',
|
||||
icon: 'pi pi-fw pi-list',
|
||||
to: '/disponibilites'
|
||||
},
|
||||
{
|
||||
label: 'Disponibilités actuelles',
|
||||
icon: 'pi pi-fw pi-calendar',
|
||||
to: '/disponibilites/actuelles'
|
||||
},
|
||||
{
|
||||
label: 'Disponibilités futures',
|
||||
icon: 'pi pi-fw pi-forward',
|
||||
to: '/disponibilites/futures'
|
||||
},
|
||||
{
|
||||
label: 'Demandes en attente',
|
||||
icon: 'pi pi-fw pi-clock',
|
||||
to: '/disponibilites/en-attente'
|
||||
},
|
||||
{
|
||||
label: 'Recherche par période',
|
||||
icon: 'pi pi-fw pi-search',
|
||||
to: '/disponibilites/periode'
|
||||
},
|
||||
{
|
||||
label: 'Conflits de planning',
|
||||
icon: 'pi pi-fw pi-exclamation-triangle',
|
||||
to: '/disponibilites/conflits'
|
||||
},
|
||||
{
|
||||
label: 'Statistiques',
|
||||
icon: 'pi pi-fw pi-chart-bar',
|
||||
to: '/disponibilites/statistiques'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
label: 'Matériel & Maintenance',
|
||||
icon: 'pi pi-wrench',
|
||||
items: [
|
||||
{
|
||||
label: 'Parc Matériel',
|
||||
icon: 'pi pi-fw pi-cog',
|
||||
items: [
|
||||
{
|
||||
label: 'Inventaire matériel',
|
||||
icon: 'pi pi-fw pi-list',
|
||||
to: '/materiels'
|
||||
},
|
||||
{
|
||||
label: 'Matériel disponible',
|
||||
icon: 'pi pi-fw pi-check-circle',
|
||||
to: '/materiels/disponibles'
|
||||
},
|
||||
{
|
||||
label: 'Maintenance prévue',
|
||||
icon: 'pi pi-fw pi-clock',
|
||||
to: '/materiels/maintenance-prevue'
|
||||
},
|
||||
{
|
||||
label: 'Par type de matériel',
|
||||
icon: 'pi pi-fw pi-sitemap',
|
||||
to: '/materiels/by-type'
|
||||
},
|
||||
{
|
||||
label: 'Recherche matériel',
|
||||
icon: 'pi pi-fw pi-search',
|
||||
to: '/materiels/search'
|
||||
},
|
||||
{
|
||||
label: 'Statistiques & Valeur',
|
||||
icon: 'pi pi-fw pi-chart-bar',
|
||||
to: '/materiels/stats'
|
||||
},
|
||||
{
|
||||
label: 'Nouveau matériel',
|
||||
icon: 'pi pi-fw pi-plus',
|
||||
to: '/materiels/nouveau'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Maintenance',
|
||||
icon: 'pi pi-fw pi-wrench',
|
||||
items: [
|
||||
{
|
||||
label: 'Toutes les maintenances',
|
||||
icon: 'pi pi-fw pi-list',
|
||||
to: '/maintenances'
|
||||
},
|
||||
{
|
||||
label: 'Planifiées',
|
||||
icon: 'pi pi-fw pi-calendar',
|
||||
to: '/maintenances/planifiees'
|
||||
},
|
||||
{
|
||||
label: 'En cours',
|
||||
icon: 'pi pi-fw pi-play-circle',
|
||||
to: '/maintenances/en-cours'
|
||||
},
|
||||
{
|
||||
label: 'Terminées',
|
||||
icon: 'pi pi-fw pi-check-circle',
|
||||
to: '/maintenances/terminees'
|
||||
},
|
||||
{
|
||||
label: 'En retard',
|
||||
icon: 'pi pi-fw pi-exclamation-triangle',
|
||||
to: '/maintenances/en-retard'
|
||||
},
|
||||
{
|
||||
label: 'Prochaines maintenances',
|
||||
icon: 'pi pi-fw pi-forward',
|
||||
to: '/maintenances/prochaines'
|
||||
},
|
||||
{
|
||||
label: 'Préventives',
|
||||
icon: 'pi pi-fw pi-shield',
|
||||
to: '/maintenances/preventives'
|
||||
},
|
||||
{
|
||||
label: 'Correctives',
|
||||
icon: 'pi pi-fw pi-bolt',
|
||||
to: '/maintenances/correctives'
|
||||
},
|
||||
{
|
||||
label: 'Matériel attention requise',
|
||||
icon: 'pi pi-fw pi-eye',
|
||||
to: '/maintenances/attention-requise'
|
||||
},
|
||||
{
|
||||
label: 'Coûts maintenance',
|
||||
icon: 'pi pi-fw pi-dollar',
|
||||
to: '/maintenances/cout-total-periode'
|
||||
},
|
||||
{
|
||||
label: 'Statistiques détaillées',
|
||||
icon: 'pi pi-fw pi-chart-line',
|
||||
to: '/maintenances/statistiques'
|
||||
},
|
||||
{
|
||||
label: 'Nouvelle maintenance',
|
||||
icon: 'pi pi-fw pi-plus',
|
||||
to: '/maintenances/nouvelle'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
label: 'Planning & Organisation',
|
||||
icon: 'pi pi-calendar',
|
||||
items: [
|
||||
{
|
||||
label: 'Vue Planning',
|
||||
icon: 'pi pi-fw pi-calendar',
|
||||
to: '/planning'
|
||||
},
|
||||
{
|
||||
label: 'Planning Hebdomadaire',
|
||||
icon: 'pi pi-fw pi-calendar-plus',
|
||||
to: '/planning/week'
|
||||
},
|
||||
{
|
||||
label: 'Planning Mensuel',
|
||||
icon: 'pi pi-fw pi-th',
|
||||
to: '/planning/month'
|
||||
},
|
||||
{
|
||||
label: 'Événements Planning',
|
||||
icon: 'pi pi-fw pi-list',
|
||||
to: '/planning/events'
|
||||
},
|
||||
{
|
||||
label: 'Conflits de Ressources',
|
||||
icon: 'pi pi-fw pi-exclamation-triangle',
|
||||
to: '/planning/conflicts'
|
||||
},
|
||||
{
|
||||
label: 'Vérifier Disponibilité',
|
||||
icon: 'pi pi-fw pi-search',
|
||||
to: '/planning/check-availability'
|
||||
},
|
||||
{
|
||||
label: 'Statistiques Planning',
|
||||
icon: 'pi pi-fw pi-chart-bar',
|
||||
to: '/planning/stats'
|
||||
}
|
||||
]
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
label: 'Documents & Photos',
|
||||
icon: 'pi pi-folder',
|
||||
items: [
|
||||
{
|
||||
label: 'Gestion Documents',
|
||||
icon: 'pi pi-fw pi-file',
|
||||
items: [
|
||||
{
|
||||
label: 'Tous les documents',
|
||||
icon: 'pi pi-fw pi-list',
|
||||
to: '/documents'
|
||||
},
|
||||
{
|
||||
label: 'Documents images',
|
||||
icon: 'pi pi-fw pi-image',
|
||||
to: '/documents/images'
|
||||
},
|
||||
{
|
||||
label: 'Documents PDF',
|
||||
icon: 'pi pi-fw pi-file-pdf',
|
||||
to: '/documents/pdfs'
|
||||
},
|
||||
{
|
||||
label: 'Documents publics',
|
||||
icon: 'pi pi-fw pi-globe',
|
||||
to: '/documents/publics'
|
||||
},
|
||||
{
|
||||
label: 'Documents récents',
|
||||
icon: 'pi pi-fw pi-clock',
|
||||
to: '/documents/recents'
|
||||
},
|
||||
{
|
||||
label: 'Documents orphelins',
|
||||
icon: 'pi pi-fw pi-question-circle',
|
||||
to: '/documents/orphelins'
|
||||
},
|
||||
{
|
||||
label: 'Statistiques documents',
|
||||
icon: 'pi pi-fw pi-chart-bar',
|
||||
to: '/documents/statistiques'
|
||||
},
|
||||
{
|
||||
label: 'Upload document',
|
||||
icon: 'pi pi-fw pi-upload',
|
||||
to: '/documents/upload'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Photos Chantiers',
|
||||
icon: 'pi pi-fw pi-camera',
|
||||
items: [
|
||||
{
|
||||
label: 'Toutes les photos',
|
||||
icon: 'pi pi-fw pi-images',
|
||||
to: '/photos'
|
||||
},
|
||||
{
|
||||
label: 'Photos récentes',
|
||||
icon: 'pi pi-fw pi-clock',
|
||||
to: '/photos/recentes'
|
||||
},
|
||||
{
|
||||
label: 'Par chantier',
|
||||
icon: 'pi pi-fw pi-building',
|
||||
to: '/photos/par-chantier'
|
||||
},
|
||||
{
|
||||
label: 'Par employé',
|
||||
icon: 'pi pi-fw pi-user',
|
||||
to: '/photos/par-employe'
|
||||
},
|
||||
{
|
||||
label: 'Galeries chantiers',
|
||||
icon: 'pi pi-fw pi-th-large',
|
||||
to: '/photos/galeries'
|
||||
},
|
||||
{
|
||||
label: 'Statistiques photos',
|
||||
icon: 'pi pi-fw pi-chart-line',
|
||||
to: '/photos/statistiques'
|
||||
},
|
||||
{
|
||||
label: 'Upload photos',
|
||||
icon: 'pi pi-fw pi-upload',
|
||||
to: '/photos/upload'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
label: 'Communication',
|
||||
icon: 'pi pi-comments',
|
||||
items: [
|
||||
{
|
||||
label: 'Messagerie',
|
||||
icon: 'pi pi-fw pi-envelope',
|
||||
items: [
|
||||
{
|
||||
label: 'Boîte de réception',
|
||||
icon: 'pi pi-fw pi-inbox',
|
||||
to: '/messages/boite-reception'
|
||||
},
|
||||
{
|
||||
label: 'Boîte d\'envoi',
|
||||
icon: 'pi pi-fw pi-send',
|
||||
to: '/messages/boite-envoi'
|
||||
},
|
||||
{
|
||||
label: 'Messages non lus',
|
||||
icon: 'pi pi-fw pi-exclamation-circle',
|
||||
to: '/messages/non-lus'
|
||||
},
|
||||
{
|
||||
label: 'Messages importants',
|
||||
icon: 'pi pi-fw pi-star',
|
||||
to: '/messages/importants'
|
||||
},
|
||||
{
|
||||
label: 'Messages archivés',
|
||||
icon: 'pi pi-fw pi-archive',
|
||||
to: '/messages/archives'
|
||||
},
|
||||
{
|
||||
label: 'Conversations',
|
||||
icon: 'pi pi-fw pi-comments',
|
||||
to: '/messages/conversations'
|
||||
},
|
||||
{
|
||||
label: 'Recherche messages',
|
||||
icon: 'pi pi-fw pi-search',
|
||||
to: '/messages/recherche'
|
||||
},
|
||||
{
|
||||
label: 'Statistiques messagerie',
|
||||
icon: 'pi pi-fw pi-chart-bar',
|
||||
to: '/messages/statistiques'
|
||||
},
|
||||
{
|
||||
label: 'Nouveau message',
|
||||
icon: 'pi pi-fw pi-plus',
|
||||
to: '/messages/nouveau'
|
||||
},
|
||||
{
|
||||
label: 'Diffuser message',
|
||||
icon: 'pi pi-fw pi-megaphone',
|
||||
to: '/messages/diffuser'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Notifications',
|
||||
icon: 'pi pi-fw pi-bell',
|
||||
items: [
|
||||
{
|
||||
label: 'Toutes les notifications',
|
||||
icon: 'pi pi-fw pi-list',
|
||||
to: '/notifications'
|
||||
},
|
||||
{
|
||||
label: 'Notifications non lues',
|
||||
icon: 'pi pi-fw pi-exclamation-circle',
|
||||
to: '/notifications/non-lues'
|
||||
},
|
||||
{
|
||||
label: 'Notifications récentes',
|
||||
icon: 'pi pi-fw pi-clock',
|
||||
to: '/notifications/recentes'
|
||||
},
|
||||
{
|
||||
label: 'Tableau de bord',
|
||||
icon: 'pi pi-fw pi-chart-line',
|
||||
to: '/notifications/tableau-bord'
|
||||
},
|
||||
{
|
||||
label: 'Statistiques',
|
||||
icon: 'pi pi-fw pi-chart-bar',
|
||||
to: '/notifications/statistiques'
|
||||
},
|
||||
{
|
||||
label: 'Diffuser notification',
|
||||
icon: 'pi pi-fw pi-megaphone',
|
||||
to: '/notifications/broadcast'
|
||||
},
|
||||
{
|
||||
label: 'Notifications automatiques',
|
||||
icon: 'pi pi-fw pi-cog',
|
||||
to: '/notifications/automatiques'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
label: 'Rapports & Analyses',
|
||||
icon: 'pi pi-chart-bar',
|
||||
items: [
|
||||
{
|
||||
label: 'Rapports Chantiers',
|
||||
icon: 'pi pi-fw pi-building',
|
||||
to: '/reports/chantiers'
|
||||
},
|
||||
{
|
||||
label: 'Rapport Maintenance',
|
||||
icon: 'pi pi-fw pi-wrench',
|
||||
to: '/reports/maintenance'
|
||||
},
|
||||
{
|
||||
label: 'Rapport RH',
|
||||
icon: 'pi pi-fw pi-users',
|
||||
to: '/reports/ressources-humaines'
|
||||
},
|
||||
{
|
||||
label: 'Rapport Financier',
|
||||
icon: 'pi pi-fw pi-dollar',
|
||||
to: '/reports/financier'
|
||||
},
|
||||
{
|
||||
label: 'Export CSV Chantiers',
|
||||
icon: 'pi pi-fw pi-file-excel',
|
||||
to: '/reports/export/csv/chantiers'
|
||||
},
|
||||
{
|
||||
label: 'Export CSV Maintenance',
|
||||
icon: 'pi pi-fw pi-file-excel',
|
||||
to: '/reports/export/csv/maintenance'
|
||||
}
|
||||
]
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
label: 'Admin & Utilisateurs',
|
||||
icon: 'pi pi-shield',
|
||||
requiredRoles: [UserRole.ADMIN, UserRole.MANAGER],
|
||||
items: [
|
||||
{
|
||||
label: 'Gestion Utilisateurs',
|
||||
icon: 'pi pi-fw pi-users',
|
||||
requiredRoles: [UserRole.ADMIN, UserRole.MANAGER],
|
||||
items: [
|
||||
{
|
||||
label: 'Tous les utilisateurs',
|
||||
icon: 'pi pi-fw pi-list',
|
||||
to: '/admin/utilisateurs',
|
||||
requiredRoles: [UserRole.ADMIN, UserRole.MANAGER]
|
||||
},
|
||||
{
|
||||
label: 'Demandes d\'accès',
|
||||
icon: 'pi pi-fw pi-clock',
|
||||
to: '/admin/demandes-acces',
|
||||
requiredRoles: [UserRole.ADMIN, UserRole.MANAGER]
|
||||
},
|
||||
{
|
||||
label: 'Attribution Gestionnaires',
|
||||
icon: 'pi pi-fw pi-sitemap',
|
||||
to: '/admin/attributions',
|
||||
requiredRoles: [UserRole.ADMIN, UserRole.MANAGER]
|
||||
},
|
||||
{
|
||||
label: 'Gestion des rôles',
|
||||
icon: 'pi pi-fw pi-shield',
|
||||
to: '/admin/roles',
|
||||
requiredRoles: [UserRole.ADMIN]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Mon Profil',
|
||||
icon: 'pi pi-fw pi-user',
|
||||
items: [
|
||||
{
|
||||
label: 'Voir mon profil',
|
||||
icon: 'pi pi-fw pi-id-card',
|
||||
to: '/auth/profile'
|
||||
},
|
||||
{
|
||||
label: 'Modifier profil',
|
||||
icon: 'pi pi-fw pi-user-edit',
|
||||
to: '/auth/profile/edit'
|
||||
},
|
||||
{
|
||||
label: 'Changer mot de passe',
|
||||
icon: 'pi pi-fw pi-key',
|
||||
to: '/auth/change-password'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const model = useMemo(() => filterMenuItems(baseModel), [baseModel]);
|
||||
|
||||
return <AppSubMenu model={model} />;
|
||||
};
|
||||
|
||||
export default AppMenu;
|
||||
187
layout/AppMenuitem.tsx
Normal file
187
layout/AppMenuitem.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import { Ripple } from 'primereact/ripple';
|
||||
import { classNames } from 'primereact/utils';
|
||||
import { useContext, useEffect, useRef } from 'react';
|
||||
import { LayoutContext } from './context/layoutcontext';
|
||||
import { MenuContext } from './context/menucontext';
|
||||
import { useSubmenuOverlayPosition } from './hooks/useSubmenuOverlayPosition';
|
||||
import { AppMenuItemProps } from '../types/layout';
|
||||
|
||||
const AppMenuitem = (props: AppMenuItemProps) => {
|
||||
const { activeMenu, setActiveMenu } = useContext(MenuContext);
|
||||
const { isSlim, isSlimPlus, isHorizontal, isDesktop, setLayoutState, layoutState, layoutConfig } = useContext(LayoutContext);
|
||||
const searchParams = useSearchParams();
|
||||
const pathname = usePathname();
|
||||
const submenuRef = useRef(null);
|
||||
const menuitemRef = useRef(null);
|
||||
const item = props.item;
|
||||
const key = props.parentKey ? props.parentKey + '-' + props.index : String(props.index);
|
||||
const isActiveRoute = item.to && pathname === item.to;
|
||||
const active = activeMenu === key || !!(activeMenu && activeMenu.startsWith(key + '-'));
|
||||
|
||||
useSubmenuOverlayPosition({
|
||||
target: menuitemRef.current,
|
||||
overlay: submenuRef.current,
|
||||
container: menuitemRef.current && menuitemRef.current.closest('.layout-menu-container'),
|
||||
when: props.root && active && (isSlim() || isSlimPlus() || isHorizontal()) && isDesktop()
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (layoutState.resetMenu) {
|
||||
setActiveMenu('');
|
||||
setLayoutState((prevLayoutState) => ({
|
||||
...prevLayoutState,
|
||||
resetMenu: false
|
||||
}));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [layoutState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!(isSlim() || isHorizontal() || isSlimPlus()) && isActiveRoute) {
|
||||
setActiveMenu(key);
|
||||
}
|
||||
}, [layoutConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
const url = pathname + searchParams.toString();
|
||||
|
||||
const onRouteChange = (url) => {
|
||||
if (!(isSlim() || isHorizontal() || isSlimPlus()) && item.to && item.to === url) {
|
||||
setActiveMenu(key);
|
||||
}
|
||||
};
|
||||
onRouteChange(url);
|
||||
}, [pathname, searchParams]);
|
||||
|
||||
const itemClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
//avoid processing disabled items
|
||||
if (item.disabled) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// navigate with hover
|
||||
if (props.root && (isSlim() || isHorizontal() || isSlimPlus())) {
|
||||
const isSubmenu = event.currentTarget.closest('.layout-root-menuitem.active-menuitem > ul') !== null;
|
||||
if (isSubmenu)
|
||||
setLayoutState((prevLayoutState) => ({
|
||||
...prevLayoutState,
|
||||
menuHoverActive: true
|
||||
}));
|
||||
else
|
||||
setLayoutState((prevLayoutState) => ({
|
||||
...prevLayoutState,
|
||||
menuHoverActive: !prevLayoutState.menuHoverActive
|
||||
}));
|
||||
}
|
||||
|
||||
//execute command
|
||||
if (item.command) {
|
||||
item.command({ originalEvent: event, item: item });
|
||||
}
|
||||
|
||||
// toggle active state
|
||||
if (item.items) {
|
||||
setActiveMenu(active ? props.parentKey : key);
|
||||
|
||||
if (props.root && !active && (isSlim() || isHorizontal() || isSlimPlus())) {
|
||||
setLayoutState((prevLayoutState) => ({
|
||||
...prevLayoutState,
|
||||
overlaySubmenuActive: true
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
if (!isDesktop()) {
|
||||
setLayoutState((prevLayoutState) => ({
|
||||
...prevLayoutState,
|
||||
staticMenuMobileActive: !prevLayoutState.staticMenuMobileActive
|
||||
}));
|
||||
}
|
||||
|
||||
if (isSlim() || isHorizontal() || isSlimPlus()) {
|
||||
setLayoutState((prevLayoutState) => ({
|
||||
...prevLayoutState,
|
||||
menuHoverActive: false
|
||||
}));
|
||||
}
|
||||
|
||||
setActiveMenu(key);
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseEnter = () => {
|
||||
// activate item on hover
|
||||
if (props.root && (isSlim() || isHorizontal() || isSlimPlus()) && isDesktop()) {
|
||||
if (!active && layoutState.menuHoverActive) {
|
||||
setActiveMenu(key);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const subMenu =
|
||||
item.items && item.visible !== false ? (
|
||||
<ul ref={submenuRef}>
|
||||
{item.items.map((child, i) => {
|
||||
return <AppMenuitem item={child} index={i} className={child.badgeClass} parentKey={key} key={child.label} />;
|
||||
})}
|
||||
</ul>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<li
|
||||
ref={menuitemRef}
|
||||
className={classNames({
|
||||
'layout-root-menuitem': props.root,
|
||||
'active-menuitem': active
|
||||
})}
|
||||
>
|
||||
{props.root && item.visible !== false && <div className="layout-menuitem-root-text">{item.label}</div>}
|
||||
{(!item.to || item.items) && item.visible !== false ? (
|
||||
<>
|
||||
<a
|
||||
href={item.url}
|
||||
onClick={(e) => itemClick(e)}
|
||||
className={classNames(item.class, 'p-ripple tooltip-target')}
|
||||
target={item.target}
|
||||
data-pr-tooltip={item.label}
|
||||
data-pr-disabled={!(isSlim() && props.root && !layoutState.menuHoverActive)}
|
||||
tabIndex={0}
|
||||
onMouseEnter={onMouseEnter}
|
||||
>
|
||||
<i className={classNames('layout-menuitem-icon', item.icon)}></i>
|
||||
<span className="layout-menuitem-text">{item.label}</span>
|
||||
{item.items && <i className="pi pi-fw pi-angle-down layout-submenu-toggler"></i>}
|
||||
<Ripple />
|
||||
</a>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{item.to && !item.items && item.visible !== false ? (
|
||||
<>
|
||||
<Link
|
||||
href={item.to}
|
||||
replace={item.replaceUrl}
|
||||
onClick={(e) => itemClick(e)}
|
||||
className={classNames(item.class, 'p-ripple ', {
|
||||
'active-route': isActiveRoute
|
||||
})}
|
||||
tabIndex={0}
|
||||
onMouseEnter={onMouseEnter}
|
||||
>
|
||||
<i className={classNames('layout-menuitem-icon', item.icon)}></i>
|
||||
<span className="layout-menuitem-text">{item.label}</span>
|
||||
{item.items && <i className="pi pi-fw pi-angle-down layout-submenu-toggler"></i>}
|
||||
<Ripple />
|
||||
</Link>
|
||||
</>
|
||||
) : null}
|
||||
{subMenu}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppMenuitem;
|
||||
87
layout/AppProfileMenu.tsx
Normal file
87
layout/AppProfileMenu.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { Sidebar } from 'primereact/sidebar';
|
||||
import { useContext, useState } from 'react';
|
||||
import { LayoutContext } from './context/layoutcontext';
|
||||
import { Calendar } from 'primereact/calendar';
|
||||
|
||||
const AppProfileSidebar = () => {
|
||||
const { layoutState, setLayoutState, layoutConfig } = useContext(LayoutContext);
|
||||
|
||||
const onRightMenuHide = () => {
|
||||
setLayoutState((prevLayoutState) => ({
|
||||
...prevLayoutState,
|
||||
rightMenuActive: false
|
||||
}));
|
||||
};
|
||||
const [date, setDate] = useState(null);
|
||||
const [visibleLeft, setVisibleLeft] = useState(false);
|
||||
|
||||
return (
|
||||
<Sidebar
|
||||
visible={layoutState.rightMenuVisible}
|
||||
position="right"
|
||||
onHide={() =>
|
||||
setLayoutState((prevState) => ({
|
||||
...prevState,
|
||||
rightMenuVisible: false
|
||||
}))
|
||||
}
|
||||
className={`layout-profile-sidebar w-full sm:w-28rem ${layoutState.rightMenuActive ? 'layout-rightmenu-active' : ''}`}
|
||||
>
|
||||
<div className="layout-rightmenu h-full overflow-y-auto overflow-x-hidden">
|
||||
<div className="user-detail-wrapper text-center" style={{ padding: '4.5rem 0 2rem 0' }}>
|
||||
<div className="user-detail-content mb-4">
|
||||
<img src="/layout/images/avatar/gene.png" alt="atlantis" className="user-image" />
|
||||
<span className="user-name text-2xl text-center block mt-4 mb-1">Gene Russell</span>
|
||||
<span className="user-number">(406) 555-0120</span>
|
||||
</div>
|
||||
<div className="user-tasks flex justify-content-between align-items-center py-4 px-3 border-bottom-1 surface-border">
|
||||
<div className="user-tasks-item in-progress font-medium">
|
||||
<a className="task-number text-red-500 flex justify-content-center align-items-center border-round" style={{ background: 'rgba(255, 255, 255, 0.05)', padding: '9px', width: '50px', height: '50px', fontSize: '30px' }}>
|
||||
23
|
||||
</a>
|
||||
<span className="task-name block mt-3">Progress</span>
|
||||
</div>
|
||||
<div className="user-tasks-item font-medium">
|
||||
<a className="task-number flex justify-content-center align-items-center border-round" style={{ background: 'rgba(255, 255, 255, 0.05)', padding: '9px', width: '50px', height: '50px', fontSize: '30px' }}>
|
||||
6
|
||||
</a>
|
||||
<span className="task-name block mt-3">Overdue</span>
|
||||
</div>
|
||||
<div className="user-tasks-item font-medium">
|
||||
<a className="task-number flex justify-content-center align-items-center border-round" style={{ background: 'rgba(255, 255, 255, 0.05)', padding: '9px', width: '50px', height: '50px', fontSize: '30px' }}>
|
||||
38
|
||||
</a>
|
||||
<span className="task-name block mt-3">All deals</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Calendar value={date} inline className="w-full p-0" onChange={(e) => setDate(e.value)} />
|
||||
</div>
|
||||
<div className="daily-plan-wrapper mt-5">
|
||||
<span className="today-date">14 Sunday, Jun 2020</span>
|
||||
<ul className="list-none overflow-hidden p-0 m-0">
|
||||
<li className="mt-3 border-round py-2 px-3" style={{ background: 'rgba(255, 255, 255, 0.05)' }}>
|
||||
<span className="event-time block font-semibold text-color-secondary">1:00 PM - 2:00 PM</span>
|
||||
<span className="event-topic block mt-2">Meeting with Alfredo Rhiel Madsen</span>
|
||||
</li>
|
||||
<li className="mt-3 border-round py-2 px-3" style={{ background: 'rgba(255, 255, 255, 0.05)' }}>
|
||||
<span className="event-time block font-semibold text-color-secondary">2:00 PM - 3:00 PM</span>
|
||||
<span className="event-topic block mt-2">Team Sync</span>
|
||||
</li>
|
||||
<li className="mt-3 border-round py-2 px-3" style={{ background: 'rgba(255, 255, 255, 0.05)' }}>
|
||||
<span className="event-time block font-semibold text-color-secondary">5:00 PM - 6:00 PM</span>
|
||||
<span className="event-topic block mt-2">Team Sync</span>
|
||||
</li>
|
||||
<li className="mt-3 border-round py-2 px-3" style={{ background: 'rgba(255, 255, 255, 0.05)' }}>
|
||||
<span className="event-time block font-semibold text-color-secondary">7:00 PM - 7:30 PM</span>
|
||||
<span className="event-topic block mt-2">Meeting with Engineering managers</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Sidebar>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppProfileSidebar;
|
||||
89
layout/AppSidebar.tsx
Normal file
89
layout/AppSidebar.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import AppMenu from './AppMenu';
|
||||
import { LayoutContext } from './context/layoutcontext';
|
||||
import { MenuProvider } from './context/menucontext';
|
||||
import { classNames } from 'primereact/utils';
|
||||
|
||||
const AppSidebar = (props: { sidebarRef: React.RefObject<HTMLDivElement> }) => {
|
||||
const { setLayoutState, layoutConfig, layoutState } = useContext(LayoutContext);
|
||||
const anchor = () => {
|
||||
setLayoutState((prevLayoutState) => ({
|
||||
...prevLayoutState,
|
||||
anchored: !prevLayoutState.anchored
|
||||
}));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
resetOverlay();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const resetOverlay = () => {
|
||||
if (layoutState.overlayMenuActive) {
|
||||
setLayoutState((prevLayoutState) => ({
|
||||
...prevLayoutState,
|
||||
overlayMenuActive: false
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
let timeout = null;
|
||||
|
||||
const onMouseEnter = () => {
|
||||
if (!layoutState.anchored) {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
setLayoutState((prevLayoutState) => ({
|
||||
...prevLayoutState,
|
||||
sidebarActive: true
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseLeave = () => {
|
||||
if (!layoutState.anchored) {
|
||||
if (!timeout) {
|
||||
timeout = setTimeout(
|
||||
() =>
|
||||
setLayoutState((prevLayoutState) => ({
|
||||
...prevLayoutState,
|
||||
sidebarActive: false
|
||||
})),
|
||||
300
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={props.sidebarRef} className="layout-sidebar" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||
<div className="sidebar-header">
|
||||
<Link href="/" className="app-logo">
|
||||
<div className="app-logo-small h-2rem">
|
||||
<img src={`/layout/images/logo/logo-${layoutConfig.colorScheme === 'light' ? 'dark' : 'light'}.png`} alt="BTPXpress Logo" />
|
||||
</div>
|
||||
<div className="app-logo-normal">
|
||||
<img className="h-2rem" src={`/layout/images/logo/logo-${layoutConfig.colorScheme === 'light' ? 'dark' : 'light'}.png`} alt="BTPXpress Logo" />
|
||||
<img className="h-2rem ml-3" src={`/layout/images/logo/appname-${layoutConfig.colorScheme === 'light' ? 'dark' : 'light'}.png`} alt="BTPXpress" />
|
||||
</div>
|
||||
</Link>
|
||||
<button className="layout-sidebar-anchor p-link z-2" type="button" onClick={anchor}></button>
|
||||
</div>
|
||||
<div className="layout-menu-container">
|
||||
<MenuProvider>
|
||||
<AppMenu />
|
||||
</MenuProvider>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppSidebar;
|
||||
57
layout/AppSubMenu.tsx
Normal file
57
layout/AppSubMenu.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import { Tooltip } from 'primereact/tooltip';
|
||||
import { useContext, useEffect, useRef } from 'react';
|
||||
import AppMenuitem from './AppMenuitem';
|
||||
import { LayoutContext } from './context/layoutcontext';
|
||||
import { MenuProvider } from './context/menucontext';
|
||||
import { Breadcrumb, BreadcrumbItem, MenuModal, MenuProps } from '../types/layout';
|
||||
|
||||
const AppSubMenu = (props: MenuProps) => {
|
||||
const { layoutState, setBreadcrumbs } = useContext(LayoutContext);
|
||||
const tooltipRef = useRef<Tooltip | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (tooltipRef.current) {
|
||||
tooltipRef.current.hide();
|
||||
(tooltipRef.current as any).updateTargetEvents();
|
||||
}
|
||||
}, [layoutState.overlaySubmenuActive]);
|
||||
|
||||
useEffect(() => {
|
||||
generateBreadcrumbs(props.model);
|
||||
}, []);
|
||||
|
||||
const generateBreadcrumbs = (model: MenuModal[]) => {
|
||||
let breadcrumbs: Breadcrumb[] = [];
|
||||
|
||||
const getBreadcrumb = (item: BreadcrumbItem, labels: string[] = []) => {
|
||||
const { label, to, items } = item;
|
||||
|
||||
label && labels.push(label);
|
||||
items &&
|
||||
items.forEach((_item) => {
|
||||
getBreadcrumb(_item, labels.slice());
|
||||
});
|
||||
to && breadcrumbs.push({ labels, to });
|
||||
};
|
||||
|
||||
model.forEach((item) => {
|
||||
getBreadcrumb(item);
|
||||
});
|
||||
setBreadcrumbs(breadcrumbs);
|
||||
};
|
||||
|
||||
return (
|
||||
<MenuProvider>
|
||||
<ul className="layout-menu">
|
||||
{props.model.map((item, i) => {
|
||||
return !item.separator ? <AppMenuitem item={item} root={true} index={i} key={item.label} /> : <li key={i} className="menu-separator"></li>;
|
||||
})}
|
||||
</ul>
|
||||
<Tooltip ref={tooltipRef} target="li:not(.active-menuitem)>.tooltip-target" />
|
||||
</MenuProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppSubMenu;
|
||||
173
layout/AppTopbar.tsx
Normal file
173
layout/AppTopbar.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
'use client';
|
||||
|
||||
import React, { forwardRef, useImperativeHandle, useContext, useRef, useState } from 'react';
|
||||
|
||||
import AppBreadCrumb from './AppBreadCrumb';
|
||||
import { LayoutContext } from './context/layoutcontext';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { StyleClass } from 'primereact/styleclass';
|
||||
import { Ripple } from 'primereact/ripple';
|
||||
import { Button } from 'primereact/button';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { classNames } from 'primereact/utils';
|
||||
import ConnectionStatusSimple from '../components/ConnectionStatusSimple';
|
||||
|
||||
const AppTopbar = forwardRef((props: { sidebarRef: React.RefObject<HTMLDivElement> }, ref) => {
|
||||
const [searchActive, setSearchActive] = useState(false);
|
||||
const { user, logout, isAuthenticated } = useAuth();
|
||||
|
||||
const btnRef1 = useRef(null);
|
||||
const btnRef2 = useRef(null);
|
||||
const menubutton = useRef(null);
|
||||
const menubuttonRef = useRef(null);
|
||||
const searchInput = useRef(null);
|
||||
const profileRef = useRef(null);
|
||||
const profileMenuRef = useRef(null);
|
||||
|
||||
const { onMenuToggle, showConfigSidebar, showSidebar, layoutConfig } = useContext(LayoutContext);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
menubutton: menubuttonRef.current
|
||||
}));
|
||||
|
||||
const activateSearch = () => {
|
||||
setSearchActive(true);
|
||||
|
||||
setTimeout(() => {
|
||||
(searchInput.current as any).focus();
|
||||
}, 1000);
|
||||
};
|
||||
const deactivateSearch = () => {
|
||||
setSearchActive(false);
|
||||
};
|
||||
const handleKeyDown = (event: any) => {
|
||||
if (event.key === 'Escape') {
|
||||
deactivateSearch();
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await logout();
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la déconnexion:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="layout-topbar">
|
||||
<div className="topbar-start">
|
||||
<button ref={btnRef1} type="button" className="p-ripple topbar-menubutton p-link p-trigger" onClick={onMenuToggle}>
|
||||
<i className="pi pi-bars"></i>
|
||||
<Ripple />
|
||||
</button>
|
||||
|
||||
<div className="topbar-breadcrumb">
|
||||
<AppBreadCrumb></AppBreadCrumb>
|
||||
</div>
|
||||
</div>
|
||||
<div className="topbar-end">
|
||||
<ul className="topbar-menu">
|
||||
<li className="hidden lg:block">
|
||||
<div className={classNames('topbar-search', { 'topbar-search-active': searchActive })}>
|
||||
<Button icon="pi pi-search" className="topbar-searchbutton p-button-text p-button-secondary text-color-secondary p-button-rounded flex-shrink-0" type="button" onClick={activateSearch}></Button>
|
||||
<div className="search-input-wrapper">
|
||||
<span className="p-input-icon-right">
|
||||
<InputText
|
||||
ref={searchInput}
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
onBlur={deactivateSearch}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') deactivateSearch();
|
||||
}}
|
||||
/>
|
||||
<i className="pi pi-search"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li className="profile-item topbar-item">
|
||||
<ConnectionStatusSimple
|
||||
showToasts={false}
|
||||
showIndicator={true}
|
||||
className="mr-2"
|
||||
/>
|
||||
</li>
|
||||
|
||||
<li className="profile-item topbar-item">
|
||||
<Button type="button" icon="pi pi-bell" className="p-button-text p-button-secondary text-color-secondary p-button-rounded flex-shrink-0"></Button>
|
||||
</li>
|
||||
|
||||
<li className="profile-item topbar-item">
|
||||
<Button type="button" icon="pi pi-comment" className="p-button-text p-button-secondary relative text-color-secondary p-button-rounded flex-shrink-0"></Button>
|
||||
</li>
|
||||
|
||||
<li className="ml-3">
|
||||
<Button type="button" icon="pi pi-cog" className="p-button-text p-button-secondary text-color-secondary p-button-rounded flex-shrink-0" onClick={showConfigSidebar}></Button>
|
||||
</li>
|
||||
|
||||
{isAuthenticated && user && (
|
||||
<li ref={profileMenuRef} className="profile-item topbar-item">
|
||||
<StyleClass nodeRef={profileRef} selector="@next" enterClassName="hidden" enterActiveClassName="px-scalein" leaveToClassName="hidden" leaveActiveClassName="px-fadeout" hideOnOutsideClick>
|
||||
<a className="p-ripple" ref={profileRef}>
|
||||
<img className="border-circle cursor-pointer" src="/layout/images/avatar/avatar-m-1.jpg" alt="avatar" />
|
||||
<span className="ml-2 font-medium hidden lg:block">{user.fullName || user.username}</span>
|
||||
<Ripple />
|
||||
</a>
|
||||
</StyleClass>
|
||||
|
||||
<ul className="topbar-menu active-topbar-menu p-4 w-20rem z-5 hidden border-round">
|
||||
<li role="menuitem" className="m-0 mb-3 pb-2 border-bottom-1 surface-border">
|
||||
<div className="flex flex-column">
|
||||
<span className="font-medium text-900">{user.fullName || user.username}</span>
|
||||
<span className="text-sm text-600">{user.email}</span>
|
||||
<span className="text-xs text-500 mt-1">Rôle: {user.highestRole || 'Utilisateur'}</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li role="menuitem" className="m-0 mb-3">
|
||||
<StyleClass nodeRef={menubutton} selector="@grandparent" enterClassName="hidden" enterActiveClassName="px-scalein" leaveToClassName="hidden" leaveActiveClassName="px-fadeout" hideOnOutsideClick>
|
||||
<a href="/profile" ref={menubutton} className="flex align-items-center hover:text-primary-500 transition-duration-200">
|
||||
<i className="pi pi-fw pi-user mr-2"></i>
|
||||
<span>Mon Profil</span>
|
||||
</a>
|
||||
</StyleClass>
|
||||
</li>
|
||||
|
||||
<li role="menuitem" className="m-0 mb-3">
|
||||
<StyleClass nodeRef={menubuttonRef} selector="@grandparent" enterClassName="hidden" enterActiveClassName="px-scalein" leaveToClassName="hidden" leaveActiveClassName="px-fadeout" hideOnOutsideClick>
|
||||
<a href="#" ref={menubuttonRef} className="flex align-items-center hover:text-primary-500 transition-duration-200">
|
||||
<i className="pi pi-fw pi-cog mr-2"></i>
|
||||
<span>Paramètres</span>
|
||||
</a>
|
||||
</StyleClass>
|
||||
</li>
|
||||
<li role="menuitem" className="m-0">
|
||||
<StyleClass nodeRef={btnRef2} selector="@grandparent" enterClassName="hidden" enterActiveClassName="px-scalein" leaveToClassName="hidden" leaveActiveClassName="px-fadeout" hideOnOutsideClick>
|
||||
<a href="#" ref={btnRef2} onClick={handleLogout} className="flex align-items-center hover:text-primary-500 transition-duration-200">
|
||||
<i className="pi pi-fw pi-sign-out mr-2"></i>
|
||||
<span>Se déconnecter</span>
|
||||
</a>
|
||||
</StyleClass>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
)}
|
||||
|
||||
<li className="right-panel-button relative hidden lg:block">
|
||||
<Button type="button" label="Today" style={{ width: '5.7rem' }} icon="pi pi-bookmark" className="layout-rightmenu-button md:block font-normal p-button-text p-button-rounded" onClick={showSidebar}></Button>
|
||||
<Button type="button" icon="pi pi-bookmark" className="layout-rightmenu-button block md:hidden font-normal p-button-text p-button-rounded" onClick={showSidebar}></Button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
export default AppTopbar;
|
||||
|
||||
AppTopbar.displayName = 'AppTopbar';
|
||||
161
layout/context/layoutcontext.tsx
Normal file
161
layout/context/layoutcontext.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
'use client';
|
||||
|
||||
import Head from 'next/head';
|
||||
import React, { useState } from 'react';
|
||||
import { Breadcrumb, LayoutConfig, LayoutContextProps } from '../../types/layout';
|
||||
import { ChildContainerProps } from '@/types';
|
||||
|
||||
export const LayoutContext = React.createContext({} as LayoutContextProps);
|
||||
|
||||
export const LayoutProvider = (props: ChildContainerProps) => {
|
||||
const [breadcrumbs, setBreadcrumbs] = useState<Breadcrumb[]>([]);
|
||||
const [layoutConfig, setLayoutConfig] = useState<LayoutConfig>({
|
||||
ripple: false,
|
||||
inputStyle: 'outlined',
|
||||
menuMode: 'static',
|
||||
colorScheme: 'dark',
|
||||
theme: 'magenta',
|
||||
scale: 14
|
||||
});
|
||||
|
||||
const [layoutState, setLayoutState] = useState({
|
||||
staticMenuDesktopInactive: false,
|
||||
overlayMenuActive: false,
|
||||
overlaySubmenuActive: false,
|
||||
rightMenuVisible: false,
|
||||
configSidebarVisible: false,
|
||||
staticMenuMobileActive: false,
|
||||
menuHoverActive: false,
|
||||
searchBarActive: false,
|
||||
resetMenu: false,
|
||||
sidebarActive: false,
|
||||
anchored: false,
|
||||
rightMenuActive: false
|
||||
});
|
||||
|
||||
const onMenuToggle = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (isOverlay()) {
|
||||
setLayoutState((prevLayoutState) => ({
|
||||
...prevLayoutState,
|
||||
overlayMenuActive: !prevLayoutState.overlayMenuActive
|
||||
}));
|
||||
}
|
||||
if (isDesktop()) {
|
||||
setLayoutState((prevLayoutState) => ({
|
||||
...prevLayoutState,
|
||||
staticMenuDesktopInactive: !prevLayoutState.staticMenuDesktopInactive
|
||||
}));
|
||||
} else {
|
||||
setLayoutState((prevLayoutState) => ({
|
||||
...prevLayoutState,
|
||||
staticMenuMobileActive: !prevLayoutState.staticMenuMobileActive
|
||||
}));
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const hideOverlayMenu = () => {
|
||||
setLayoutState((prevLayoutState) => ({
|
||||
...prevLayoutState,
|
||||
overlayMenuActive: false,
|
||||
staticMenuMobileActive: false
|
||||
}));
|
||||
};
|
||||
|
||||
const toggleSearch = () => {
|
||||
setLayoutState((prevLayoutState) => ({
|
||||
...prevLayoutState,
|
||||
searchBarActive: !layoutState.searchBarActive
|
||||
}));
|
||||
};
|
||||
|
||||
const onSearchHide = () => {
|
||||
setLayoutState((prevLayoutState) => ({
|
||||
...prevLayoutState,
|
||||
searchBarActive: false
|
||||
}));
|
||||
};
|
||||
|
||||
const showRightSidebar = () => {
|
||||
setLayoutState((prevLayoutState) => ({
|
||||
...prevLayoutState,
|
||||
rightMenuActive: true
|
||||
}));
|
||||
hideOverlayMenu();
|
||||
};
|
||||
|
||||
const showConfigSidebar = () => {
|
||||
setLayoutState((prevLayoutState) => ({
|
||||
...prevLayoutState,
|
||||
configSidebarVisible: true
|
||||
}));
|
||||
};
|
||||
const showSidebar = () => {
|
||||
setLayoutState((prevLayoutState) => ({
|
||||
...prevLayoutState,
|
||||
rightMenuVisible: true
|
||||
}));
|
||||
};
|
||||
|
||||
const isOverlay = () => {
|
||||
return layoutConfig.menuMode === 'overlay';
|
||||
};
|
||||
|
||||
const isSlim = () => {
|
||||
return layoutConfig.menuMode === 'slim';
|
||||
};
|
||||
|
||||
const isSlimPlus = () => {
|
||||
return layoutConfig.menuMode === 'slim-plus';
|
||||
};
|
||||
|
||||
const isHorizontal = () => {
|
||||
return layoutConfig.menuMode === 'horizontal';
|
||||
};
|
||||
|
||||
const isDesktop = () => {
|
||||
return window.innerWidth > 991;
|
||||
};
|
||||
|
||||
const value = {
|
||||
layoutConfig,
|
||||
setLayoutConfig,
|
||||
layoutState,
|
||||
setLayoutState,
|
||||
isSlim,
|
||||
isSlimPlus,
|
||||
isHorizontal,
|
||||
isDesktop,
|
||||
onMenuToggle,
|
||||
toggleSearch,
|
||||
onSearchHide,
|
||||
showRightSidebar,
|
||||
breadcrumbs,
|
||||
setBreadcrumbs,
|
||||
showConfigSidebar,
|
||||
showSidebar
|
||||
};
|
||||
|
||||
return (
|
||||
<LayoutContext.Provider value={value}>
|
||||
<>
|
||||
<Head>
|
||||
<title>PrimeReact - DIAMOND</title>
|
||||
<meta charSet="UTF-8" />
|
||||
<meta name="description" content="The ultimate collection of design-agnostic, flexible and accessible React UI Components." />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
<meta property="og:type" content="website"></meta>
|
||||
<meta property="og:title" content="Diamond by PrimeReact for NextJS"></meta>
|
||||
<meta property="og:url" content="https://diamond.primereact.org"></meta>
|
||||
<meta property="og:description" content="The ultimate collection of design-agnostic, flexible and accessible React UI Components." />
|
||||
<meta property="og:image" content="https://www.primefaces.org/static/social/diamond-react.png"></meta>
|
||||
<meta property="og:ttl" content="604800"></meta>
|
||||
<link rel="icon" href={`/favicon.ico`} type="image/x-icon"></link>
|
||||
</Head>
|
||||
{props.children}
|
||||
</>
|
||||
</LayoutContext.Provider>
|
||||
);
|
||||
};
|
||||
19
layout/context/menucontext.tsx
Normal file
19
layout/context/menucontext.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React, { createContext, useState } from 'react';
|
||||
import type { MenuContextProps } from '@/types';
|
||||
|
||||
export const MenuContext = createContext({} as MenuContextProps);
|
||||
|
||||
interface MenuProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const MenuProvider = (props: MenuProviderProps) => {
|
||||
const [activeMenu, setActiveMenu] = useState('');
|
||||
|
||||
const value = {
|
||||
activeMenu,
|
||||
setActiveMenu
|
||||
};
|
||||
|
||||
return <MenuContext.Provider value={value}>{props.children}</MenuContext.Provider>;
|
||||
};
|
||||
56
layout/hooks/useSubmenuOverlayPosition.tsx
Normal file
56
layout/hooks/useSubmenuOverlayPosition.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
'use client';
|
||||
|
||||
import { useEventListener } from 'primereact/hooks';
|
||||
import { DomHandler } from 'primereact/utils';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { LayoutContext } from '../context/layoutcontext';
|
||||
import { MenuContext } from '../context/menucontext';
|
||||
|
||||
export const useSubmenuOverlayPosition = ({ target, overlay, container, when }) => {
|
||||
const { isSlim, isSlimPlus, isHorizontal, setLayoutState, layoutState } = useContext(LayoutContext);
|
||||
const { activeMenu } = useContext(MenuContext);
|
||||
const [bindScrollListener, unbindScrollListener] = useEventListener({
|
||||
type: 'scroll',
|
||||
target: container,
|
||||
listener: () => {
|
||||
setLayoutState((prevLayoutState) => ({
|
||||
...prevLayoutState,
|
||||
overlayMenuActive: false,
|
||||
overlaySubmenuActive: false,
|
||||
staticMenuMobileActive: false,
|
||||
menuHoverActive: false,
|
||||
resetMenu: true
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
const calculatePosition = () => {
|
||||
if (overlay) {
|
||||
const { left, top } = target.getBoundingClientRect();
|
||||
const { height: vHeight } = DomHandler.getViewport();
|
||||
const oHeight = overlay.offsetHeight;
|
||||
|
||||
// reset
|
||||
overlay.style.top = overlay.style.left = '';
|
||||
|
||||
if (isHorizontal()) {
|
||||
overlay.style.left = `${left - 80}px`;
|
||||
} else if (isSlim() || isSlimPlus()) {
|
||||
const height = top + oHeight;
|
||||
overlay.style.top = vHeight < height ? `${top - (height - vHeight)}px` : `${top}px`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
when && bindScrollListener();
|
||||
|
||||
return () => {
|
||||
unbindScrollListener();
|
||||
};
|
||||
}, [when]);
|
||||
|
||||
useEffect(() => {
|
||||
when && calculatePosition();
|
||||
}, [when, activeMenu]);
|
||||
};
|
||||
147
layout/layout.tsx
Normal file
147
layout/layout.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
'use client';
|
||||
import React, { useCallback, useEffect, useRef, useContext } from 'react';
|
||||
import { classNames, DomHandler } from 'primereact/utils';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import { LayoutContext } from './context/layoutcontext';
|
||||
import { useEventListener, useMountEffect, useResizeListener, useUnmountEffect } from 'primereact/hooks';
|
||||
import AppTopbar from './AppTopbar';
|
||||
import AppConfig from './AppConfig';
|
||||
import AppBreadCrumb from './AppBreadCrumb';
|
||||
import { PrimeReactContext } from 'primereact/api';
|
||||
import { Tooltip } from 'primereact/tooltip';
|
||||
import { ChildContainerProps } from '@/types';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import AppProfileMenu from './AppProfileMenu';
|
||||
import AppSidebar from './AppSidebar';
|
||||
import GlobalErrorHandler from '../components/GlobalErrorHandler';
|
||||
|
||||
const Layout = (props: ChildContainerProps) => {
|
||||
const { layoutConfig, layoutState, setLayoutState, isSlim, isSlimPlus, isHorizontal, isDesktop } = useContext(LayoutContext);
|
||||
const { setRipple } = useContext(PrimeReactContext);
|
||||
const topbarRef = useRef(null);
|
||||
const sidebarRef = useRef(null);
|
||||
const copyTooltipRef = useRef(null);
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const [bindMenuOutsideClickListener, unbindMenuOutsideClickListener] = useEventListener({
|
||||
type: 'click',
|
||||
listener: (event) => {
|
||||
const isOutsideClicked = !(sidebarRef.current.isSameNode(event.target) || sidebarRef.current.contains(event.target) || topbarRef.current.menubutton.isSameNode(event.target) || topbarRef.current.menubutton.contains(event.target));
|
||||
|
||||
if (isOutsideClicked) {
|
||||
hideMenu();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const [bindDocumentResizeListener, unbindDocumentResizeListener] = useResizeListener({
|
||||
listener: () => {
|
||||
if (isDesktop() && !DomHandler.isTouchDevice()) {
|
||||
hideMenu();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const hideMenu = useCallback(() => {
|
||||
setLayoutState((prevLayoutState) => ({
|
||||
...prevLayoutState,
|
||||
overlayMenuActive: false,
|
||||
overlaySubmenuActive: false,
|
||||
staticMenuMobileActive: false,
|
||||
menuHoverActive: false,
|
||||
menuClick: false,
|
||||
resetMenu: (isSlim() || isSlimPlus() || isHorizontal()) && isDesktop()
|
||||
}));
|
||||
}, [isSlim, isHorizontal, isDesktop, setLayoutState]);
|
||||
|
||||
const blockBodyScroll = () => {
|
||||
if (document.body.classList) {
|
||||
document.body.classList.add('blocked-scroll');
|
||||
} else {
|
||||
document.body.className += ' blocked-scroll';
|
||||
}
|
||||
};
|
||||
|
||||
const unblockBodyScroll = () => {
|
||||
if (document.body.classList) {
|
||||
document.body.classList.remove('blocked-scroll');
|
||||
} else {
|
||||
document.body.className = document.body.className.replace(new RegExp('(^|\\b)' + 'blocked-scroll'.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
|
||||
}
|
||||
};
|
||||
useMountEffect(() => {
|
||||
setRipple(layoutConfig.ripple);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (layoutState.overlayMenuActive || layoutState.staticMenuMobileActive || layoutState.overlaySubmenuActive) {
|
||||
bindMenuOutsideClickListener();
|
||||
}
|
||||
|
||||
if (layoutState.staticMenuMobileActive) {
|
||||
blockBodyScroll();
|
||||
(isSlim() || isSlimPlus() || isHorizontal()) && bindDocumentResizeListener();
|
||||
}
|
||||
|
||||
return () => {
|
||||
unbindMenuOutsideClickListener();
|
||||
unbindDocumentResizeListener();
|
||||
unblockBodyScroll();
|
||||
};
|
||||
}, [layoutState.overlayMenuActive, layoutState.staticMenuMobileActive, layoutState.overlaySubmenuActive]);
|
||||
|
||||
useEffect(() => {
|
||||
const onRouteChange = () => {
|
||||
hideMenu();
|
||||
};
|
||||
onRouteChange();
|
||||
}, [pathname, searchParams]);
|
||||
|
||||
useUnmountEffect(() => {
|
||||
unbindMenuOutsideClickListener();
|
||||
});
|
||||
|
||||
const containerClassName = classNames('layout-wrapper', {
|
||||
'layout-light': layoutConfig.colorScheme === 'light',
|
||||
'layout-dark': layoutConfig.colorScheme === 'dark',
|
||||
'layout-overlay': layoutConfig.menuMode === 'overlay',
|
||||
'layout-static': layoutConfig.menuMode === 'static',
|
||||
'layout-slim': layoutConfig.menuMode === 'slim',
|
||||
'layout-slim-plus': layoutConfig.menuMode === 'slim-plus',
|
||||
'layout-horizontal': layoutConfig.menuMode === 'horizontal',
|
||||
'layout-reveal': layoutConfig.menuMode === 'reveal',
|
||||
'layout-drawer': layoutConfig.menuMode === 'drawer',
|
||||
'layout-static-inactive': layoutState.staticMenuDesktopInactive && layoutConfig.menuMode === 'static',
|
||||
'layout-overlay-active': layoutState.overlayMenuActive,
|
||||
'layout-mobile-active': layoutState.staticMenuMobileActive,
|
||||
'p-ripple-disabled': !layoutConfig.ripple,
|
||||
'layout-sidebar-active': layoutState.sidebarActive,
|
||||
'layout-sidebar-anchored': layoutState.anchored
|
||||
});
|
||||
|
||||
return (
|
||||
<GlobalErrorHandler>
|
||||
<div className={classNames('layout-container', containerClassName)} data-theme={layoutConfig.colorScheme}>
|
||||
<Tooltip ref={copyTooltipRef} target=".block-action-copy" position="bottom" content="Copied to clipboard" event="focus" />
|
||||
|
||||
<AppSidebar sidebarRef={sidebarRef} />
|
||||
|
||||
<div className="layout-content-wrapper">
|
||||
<AppTopbar ref={topbarRef} sidebarRef={sidebarRef} />
|
||||
<div className="content-breadcrumb">
|
||||
<AppBreadCrumb />
|
||||
</div>
|
||||
|
||||
<div className="layout-content">{props.children}</div>
|
||||
<div className="layout-mask"></div>
|
||||
</div>
|
||||
<AppProfileMenu />
|
||||
<AppConfig />
|
||||
|
||||
<Toast></Toast>
|
||||
</div>
|
||||
</GlobalErrorHandler>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
Reference in New Issue
Block a user