Initial commit

This commit is contained in:
dahoud
2025-10-01 01:39:07 +00:00
commit b430bf3b96
826 changed files with 255287 additions and 0 deletions

94
layout/AppBreadCrumb.tsx Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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';

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

View 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>;
};

View 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
View 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;