1802 lines
85 KiB
TypeScript
1802 lines
85 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useRef, useEffect } from 'react';
|
|
import { DataTable } from 'primereact/datatable';
|
|
import { Column } from 'primereact/column';
|
|
import { Button } from 'primereact/button';
|
|
import { Tag } from 'primereact/tag';
|
|
import { Dialog } from 'primereact/dialog';
|
|
import { InputTextarea } from 'primereact/inputtextarea';
|
|
import { Toast } from 'primereact/toast';
|
|
import { Toolbar } from 'primereact/toolbar';
|
|
import { Card } from 'primereact/card';
|
|
import { ProgressBar } from 'primereact/progressbar';
|
|
import { Badge } from 'primereact/badge';
|
|
import { Dropdown } from 'primereact/dropdown';
|
|
import { Calendar } from 'primereact/calendar';
|
|
import { InputText } from 'primereact/inputtext';
|
|
import { Panel } from 'primereact/panel';
|
|
import { TabView, TabPanel } from 'primereact/tabview';
|
|
import { Timeline } from 'primereact/timeline';
|
|
import { Steps } from 'primereact/steps';
|
|
import { MenuItem } from 'primereact/menuitem';
|
|
import { Accordion, AccordionTab } from 'primereact/accordion';
|
|
import { Divider } from 'primereact/divider';
|
|
import { Avatar } from 'primereact/avatar';
|
|
import { AvatarGroup } from 'primereact/avatargroup';
|
|
import { Chip } from 'primereact/chip';
|
|
import { Message } from 'primereact/message';
|
|
import { Messages } from 'primereact/messages';
|
|
import { Skeleton } from 'primereact/skeleton';
|
|
import { SpeedDial } from 'primereact/speeddial';
|
|
import { ScrollPanel } from 'primereact/scrollpanel';
|
|
import { Splitter, SplitterPanel } from 'primereact/splitter';
|
|
import { FileUpload } from 'primereact/fileupload';
|
|
import { Image } from 'primereact/image';
|
|
import { Galleria } from 'primereact/galleria';
|
|
import { DataView } from 'primereact/dataview';
|
|
import { OrderList } from 'primereact/orderlist';
|
|
import { PickList } from 'primereact/picklist';
|
|
import { OverlayPanel } from 'primereact/overlaypanel';
|
|
import { Sidebar } from 'primereact/sidebar';
|
|
import { ContextMenu } from 'primereact/contextmenu';
|
|
import { Menu } from 'primereact/menu';
|
|
import { TieredMenu } from 'primereact/tieredmenu';
|
|
import { ConfirmDialog, confirmDialog } from 'primereact/confirmdialog';
|
|
import { BlockUI } from 'primereact/blockui';
|
|
import { Chart } from 'primereact/chart';
|
|
import { Rating } from 'primereact/rating';
|
|
import { Knob } from 'primereact/knob';
|
|
import { InputSwitch } from 'primereact/inputswitch';
|
|
import { MultiSelect } from 'primereact/multiselect';
|
|
import { Checkbox } from 'primereact/checkbox';
|
|
import { RadioButton } from 'primereact/radiobutton';
|
|
import { Slider } from 'primereact/slider';
|
|
import { ToggleButton } from 'primereact/togglebutton';
|
|
import { SelectButton } from 'primereact/selectbutton';
|
|
import { InputNumber } from 'primereact/inputnumber';
|
|
import { ColorPicker } from 'primereact/colorpicker';
|
|
import { ListBox } from 'primereact/listbox';
|
|
import { Fieldset } from 'primereact/fieldset';
|
|
// import { ProgressBar } from 'primereact/progressbar'; // Module not available
|
|
import { TreeTable } from 'primereact/treetable';
|
|
import { Tree } from 'primereact/tree';
|
|
import { ScrollTop } from 'primereact/scrolltop';
|
|
import { VirtualScroller } from 'primereact/virtualscroller';
|
|
import { DeferredContent } from 'primereact/deferredcontent';
|
|
import { Carousel } from 'primereact/carousel';
|
|
import { Terminal } from 'primereact/terminal';
|
|
|
|
interface DemandeAcces {
|
|
id: string;
|
|
userId: string;
|
|
nom: string;
|
|
prenom: string;
|
|
email: string;
|
|
telephone: string;
|
|
entreprise: string;
|
|
siret: string;
|
|
secteurActivite: string;
|
|
effectif?: number;
|
|
role: string;
|
|
status: 'PENDING' | 'APPROVED' | 'REJECTED' | 'SUSPENDED';
|
|
dateCreation: string;
|
|
dateModification?: string;
|
|
dateTraitement?: string;
|
|
commentaireDemandeur?: string;
|
|
commentaireAdmin?: string;
|
|
kbisUploade?: boolean;
|
|
assuranceRCUploade?: boolean;
|
|
assuranceDecennaleUploade?: boolean;
|
|
attestationUrssafUploade?: boolean;
|
|
pourcentageCompletion?: number;
|
|
validateurNom?: string;
|
|
}
|
|
|
|
const DemandesAccesAdmin = () => {
|
|
const [demandes, setDemandes] = useState<DemandeAcces[]>([]);
|
|
const [selectedDemande, setSelectedDemande] = useState<DemandeAcces | null>(null);
|
|
const [detailDialog, setDetailDialog] = useState(false);
|
|
const [validationDialog, setValidationDialog] = useState(false);
|
|
const [bulkDialog, setBulkDialog] = useState(false);
|
|
const [workflowDialog, setWorkflowDialog] = useState(false);
|
|
const [analyticsDialog, setAnalyticsDialog] = useState(false);
|
|
const [actionType, setActionType] = useState<'APPROVE' | 'REJECT'>('APPROVE');
|
|
const [commentaire, setCommentaire] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
const [blocked, setBlocked] = useState(false);
|
|
const [globalFilter, setGlobalFilter] = useState('');
|
|
const [statusFilter, setStatusFilter] = useState<string>('');
|
|
const [dateFilter, setDateFilter] = useState<Date | null>(null);
|
|
const [sectorFilter, setSectorFilter] = useState<string>('');
|
|
const [selectedDemandes, setSelectedDemandes] = useState<DemandeAcces[]>([]);
|
|
const [viewMode, setViewMode] = useState<'table' | 'grid' | 'timeline'>('table');
|
|
const [autoRefresh, setAutoRefresh] = useState(true);
|
|
const [refreshInterval, setRefreshInterval] = useState(30);
|
|
const [sidebarVisible, setSidebarVisible] = useState(false);
|
|
const [activeTab, setActiveTab] = useState(0);
|
|
const [processingQueue, setProcessingQueue] = useState<DemandeAcces[]>([]);
|
|
const [approvalWorkflow, setApprovalWorkflow] = useState([]);
|
|
const [auditLog, setAuditLog] = useState([]);
|
|
const [kpiData, setKpiData] = useState({
|
|
totalDemandes: 0,
|
|
enAttente: 0,
|
|
approuvees: 0,
|
|
rejetees: 0,
|
|
tauxApprobation: 0,
|
|
delaiMoyenTraitement: 0
|
|
});
|
|
const [chartData, setChartData] = useState({});
|
|
const [timelineData, setTimelineData] = useState([]);
|
|
const [validationRules, setValidationRules] = useState([]);
|
|
const [notifications, setNotifications] = useState([]);
|
|
const [documentPreview, setDocumentPreview] = useState(null);
|
|
const [compareMode, setCompareMode] = useState(false);
|
|
const [selectedForComparison, setSelectedForComparison] = useState<DemandeAcces[]>([]);
|
|
const [exportProgress, setExportProgress] = useState(0);
|
|
const [aiRecommendations, setAiRecommendations] = useState([]);
|
|
const [riskScore, setRiskScore] = useState(0);
|
|
const [complianceCheck, setComplianceCheck] = useState(true);
|
|
|
|
const toast = useRef<Toast>(null);
|
|
const messages = useRef<Messages>(null);
|
|
const overlayPanel = useRef<OverlayPanel>(null);
|
|
const contextMenu = useRef<ContextMenu>(null);
|
|
const terminal = useRef<Terminal>(null);
|
|
const dt = useRef<DataTable<any>>(null);
|
|
|
|
const statusOptions = [
|
|
{ label: 'Tous', value: '', icon: 'pi pi-list' },
|
|
{ label: 'En attente', value: 'PENDING', icon: 'pi pi-clock', color: '#f59e0b' },
|
|
{ label: 'Approuvé', value: 'APPROVED', icon: 'pi pi-check', color: '#10b981' },
|
|
{ label: 'Rejeté', value: 'REJECTED', icon: 'pi pi-times', color: '#ef4444' },
|
|
{ label: 'Suspendu', value: 'SUSPENDED', icon: 'pi pi-pause', color: '#6b7280' },
|
|
{ label: 'En révision', value: 'UNDER_REVIEW', icon: 'pi pi-eye', color: '#3b82f6' },
|
|
{ label: 'Incomplet', value: 'INCOMPLETE', icon: 'pi pi-exclamation-triangle', color: '#f97316' }
|
|
];
|
|
|
|
const sectorOptions = [
|
|
{ label: 'Tous les secteurs', value: '' },
|
|
{ label: 'BTP Général', value: 'BTP_GENERAL' },
|
|
{ label: 'Électricité', value: 'ELECTRICITE' },
|
|
{ label: 'Plomberie', value: 'PLOMBERIE' },
|
|
{ label: 'Maçonnerie', value: 'MACONNERIE' },
|
|
{ label: 'Menuiserie', value: 'MENUISERIE' },
|
|
{ label: 'Peinture', value: 'PEINTURE' },
|
|
{ label: 'Couverture', value: 'COUVERTURE' },
|
|
{ label: 'Terrassement', value: 'TERRASSEMENT' }
|
|
];
|
|
|
|
const viewModeOptions = [
|
|
{ label: 'Tableau', value: 'table', icon: 'pi pi-table' },
|
|
{ label: 'Grille', value: 'grid', icon: 'pi pi-th-large' },
|
|
{ label: 'Timeline', value: 'timeline', icon: 'pi pi-calendar' }
|
|
];
|
|
|
|
const bulkActions = [
|
|
{ label: 'Approuver sélection', icon: 'pi pi-check', command: () => processBulkAction('APPROVE') },
|
|
{ label: 'Rejeter sélection', icon: 'pi pi-times', command: () => processBulkAction('REJECT') },
|
|
{ label: 'Marquer comme incomplet', icon: 'pi pi-exclamation-triangle', command: () => processBulkAction('INCOMPLETE') },
|
|
{ label: 'Exporter sélection', icon: 'pi pi-download', command: () => exportSelected() },
|
|
{ label: 'Assigner à un validateur', icon: 'pi pi-user', command: () => assignValidator() },
|
|
{ label: 'Envoyer rappel', icon: 'pi pi-send', command: () => sendReminder() }
|
|
];
|
|
|
|
const quickFilters = [
|
|
{ label: 'Nouvelles demandes (24h)', filter: () => filterByDate(1), icon: 'pi pi-clock' },
|
|
{ label: 'En attente > 7 jours', filter: () => filterByDelay(7), icon: 'pi pi-exclamation-triangle' },
|
|
{ label: 'Documents manquants', filter: () => filterByDocuments(), icon: 'pi pi-file' },
|
|
{ label: 'Score de risque élevé', filter: () => filterByRisk(), icon: 'pi pi-shield' },
|
|
{ label: 'Grandes entreprises', filter: () => filterBySize('large'), icon: 'pi pi-building' }
|
|
];
|
|
|
|
const workflowSteps = [
|
|
{ label: 'Soumission', icon: 'pi pi-send' },
|
|
{ label: 'Vérification automatique', icon: 'pi pi-cog' },
|
|
{ label: 'Contrôle documents', icon: 'pi pi-file-check' },
|
|
{ label: 'Validation métier', icon: 'pi pi-user-check' },
|
|
{ label: 'Approbation finale', icon: 'pi pi-check-circle' }
|
|
];
|
|
|
|
const validationCriteria = [
|
|
{ name: 'Informations entreprise', weight: 25, status: 'complete' },
|
|
{ name: 'Documents légaux', weight: 30, status: 'incomplete' },
|
|
{ name: 'Assurances', weight: 20, status: 'complete' },
|
|
{ name: 'Références clients', weight: 15, status: 'pending' },
|
|
{ name: 'Capacité financière', weight: 10, status: 'complete' }
|
|
];
|
|
|
|
useEffect(() => {
|
|
loadDemandes();
|
|
}, []);
|
|
|
|
const loadDemandes = async () => {
|
|
setLoading(true);
|
|
try {
|
|
// TODO: Remplacer par un appel API réel pour charger les demandes d'accès
|
|
// Exemple: const response = await fetch('/api/admin/demandes-acces');
|
|
// const demandesData = await response.json();
|
|
const mockData: DemandeAcces[] = [];
|
|
setDemandes(mockData);
|
|
} catch (error) {
|
|
toast.current?.show({
|
|
severity: 'error',
|
|
summary: 'Erreur',
|
|
detail: 'Impossible de charger les demandes',
|
|
life: 3000
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const getStatusSeverity = (status: string) => {
|
|
switch (status) {
|
|
case 'PENDING': return 'warning';
|
|
case 'APPROVED': return 'success';
|
|
case 'REJECTED': return 'danger';
|
|
case 'SUSPENDED': return 'info';
|
|
default: return 'info';
|
|
}
|
|
};
|
|
|
|
const getStatusLabel = (status: string) => {
|
|
switch (status) {
|
|
case 'PENDING': return 'En attente';
|
|
case 'APPROVED': return 'Approuvé';
|
|
case 'REJECTED': return 'Rejeté';
|
|
case 'SUSPENDED': return 'Suspendu';
|
|
default: return status;
|
|
}
|
|
};
|
|
|
|
// Filtrage des demandes
|
|
const filteredDemandes = demandes.filter(demande => {
|
|
const matchesGlobal = !globalFilter ||
|
|
demande.nom.toLowerCase().includes(globalFilter.toLowerCase()) ||
|
|
demande.prenom.toLowerCase().includes(globalFilter.toLowerCase()) ||
|
|
demande.email.toLowerCase().includes(globalFilter.toLowerCase()) ||
|
|
demande.entreprise.toLowerCase().includes(globalFilter.toLowerCase());
|
|
|
|
const matchesStatus = !statusFilter || demande.status === statusFilter;
|
|
|
|
return matchesGlobal && matchesStatus;
|
|
});
|
|
|
|
const statusBodyTemplate = (rowData: DemandeAcces) => {
|
|
return <Tag value={getStatusLabel(rowData.status)} severity={getStatusSeverity(rowData.status)} />;
|
|
};
|
|
|
|
const entrepriseBodyTemplate = (rowData: DemandeAcces) => {
|
|
return (
|
|
<div>
|
|
<div className="font-bold">{rowData.entreprise}</div>
|
|
<div className="text-sm text-500">SIRET: {rowData.siret}</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const contactBodyTemplate = (rowData: DemandeAcces) => {
|
|
return (
|
|
<div>
|
|
<div className="font-bold">{rowData.prenom} {rowData.nom}</div>
|
|
<div className="text-sm text-500">{rowData.email}</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const documentsBodyTemplate = (rowData: DemandeAcces) => {
|
|
return (
|
|
<div className="flex align-items-center gap-2">
|
|
<ProgressBar
|
|
value={rowData.pourcentageCompletion || 0}
|
|
style={{ width: '60px', height: '6px' }}
|
|
/>
|
|
<span className="text-sm">{rowData.pourcentageCompletion || 0}%</span>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const dateBodyTemplate = (rowData: DemandeAcces) => {
|
|
return new Date(rowData.dateCreation).toLocaleDateString('fr-FR');
|
|
};
|
|
|
|
const actionsBodyTemplate = (rowData: DemandeAcces) => {
|
|
return (
|
|
<div className="flex gap-2">
|
|
<Button
|
|
icon="pi pi-eye"
|
|
className="p-button-rounded p-button-text"
|
|
onClick={() => {
|
|
setSelectedDemande(rowData);
|
|
setDetailDialog(true);
|
|
}}
|
|
tooltip="Voir détails"
|
|
/>
|
|
{rowData.status === 'PENDING' && (
|
|
<>
|
|
<Button
|
|
icon="pi pi-check"
|
|
className="p-button-rounded p-button-success p-button-text"
|
|
onClick={() => openValidationDialog(rowData, 'APPROVE')}
|
|
tooltip="Approuver"
|
|
/>
|
|
<Button
|
|
icon="pi pi-times"
|
|
className="p-button-rounded p-button-danger p-button-text"
|
|
onClick={() => openValidationDialog(rowData, 'REJECT')}
|
|
tooltip="Rejeter"
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const openValidationDialog = (demande: DemandeAcces, action: 'APPROVE' | 'REJECT') => {
|
|
setSelectedDemande(demande);
|
|
setActionType(action);
|
|
setCommentaire('');
|
|
setValidationDialog(true);
|
|
};
|
|
|
|
const handleValidation = async () => {
|
|
if (!selectedDemande || !commentaire.trim()) {
|
|
toast.current?.show({
|
|
severity: 'warn',
|
|
summary: 'Attention',
|
|
detail: 'Veuillez saisir un commentaire',
|
|
life: 3000
|
|
});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setLoading(true);
|
|
|
|
// Simuler l'appel API
|
|
const updatedDemandes = demandes.map(d =>
|
|
d.id === selectedDemande.id
|
|
? { ...d, status: actionType === 'APPROVE' ? 'APPROVED' as const : 'REJECTED' as const, commentaireAdmin: commentaire, dateTraitement: new Date().toISOString() }
|
|
: d
|
|
);
|
|
setDemandes(updatedDemandes);
|
|
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Succès',
|
|
detail: `Demande ${actionType === 'APPROVE' ? 'approuvée' : 'rejetée'} avec succès`,
|
|
life: 3000
|
|
});
|
|
|
|
setValidationDialog(false);
|
|
} catch (error) {
|
|
toast.current?.show({
|
|
severity: 'error',
|
|
summary: 'Erreur',
|
|
detail: 'Erreur lors de la validation',
|
|
life: 3000
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
loadDemandes();
|
|
loadKPIData();
|
|
loadAnalytics();
|
|
|
|
if (autoRefresh) {
|
|
const interval = setInterval(() => {
|
|
loadDemandes();
|
|
loadKPIData();
|
|
}, refreshInterval * 1000);
|
|
return () => clearInterval(interval);
|
|
}
|
|
}, [autoRefresh, refreshInterval]);
|
|
|
|
const loadKPIData = async () => {
|
|
// TODO: Remplacer par un appel API réel
|
|
const mockKPI = {
|
|
totalDemandes: 156,
|
|
enAttente: 23,
|
|
approuvees: 98,
|
|
rejetees: 35,
|
|
tauxApprobation: 73.6,
|
|
delaiMoyenTraitement: 4.2
|
|
};
|
|
setKpiData(mockKPI);
|
|
};
|
|
|
|
const loadAnalytics = async () => {
|
|
// TODO: Remplacer par un appel API réel pour les analytics
|
|
const mockChartData = {
|
|
labels: ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun'],
|
|
datasets: [
|
|
{
|
|
label: 'Demandes reçues',
|
|
data: [25, 32, 28, 45, 38, 42],
|
|
backgroundColor: '#3B82F6',
|
|
borderColor: '#1D4ED8'
|
|
},
|
|
{
|
|
label: 'Demandes approuvées',
|
|
data: [18, 24, 21, 32, 28, 31],
|
|
backgroundColor: '#10B981',
|
|
borderColor: '#047857'
|
|
}
|
|
]
|
|
};
|
|
setChartData(mockChartData);
|
|
|
|
const mockTimeline = [
|
|
{ date: new Date(), event: 'Nouvelle demande reçue', type: 'info', user: 'Système' },
|
|
{ date: new Date(), event: 'Demande approuvée', type: 'success', user: 'Admin' },
|
|
{ date: new Date(), event: 'Documents manquants signalés', type: 'warning', user: 'Validateur' }
|
|
];
|
|
setTimelineData(mockTimeline);
|
|
};
|
|
|
|
const processBulkAction = (action: string) => {
|
|
if (selectedDemandes.length === 0) {
|
|
toast.current?.show({
|
|
severity: 'warn',
|
|
summary: 'Attention',
|
|
detail: 'Veuillez sélectionner au moins une demande',
|
|
life: 3000
|
|
});
|
|
return;
|
|
}
|
|
|
|
confirmDialog({
|
|
message: `Voulez-vous ${action === 'APPROVE' ? 'approuver' : 'rejeter'} ${selectedDemandes.length} demande(s) ?`,
|
|
header: 'Action groupée',
|
|
icon: 'pi pi-question-circle',
|
|
acceptLabel: 'Confirmer',
|
|
rejectLabel: 'Annuler',
|
|
accept: () => {
|
|
setBlocked(true);
|
|
setTimeout(() => {
|
|
setBlocked(false);
|
|
setSelectedDemandes([]);
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Action terminée',
|
|
detail: `${selectedDemandes.length} demandes traitées avec succès`,
|
|
life: 3000
|
|
});
|
|
}, 2000);
|
|
}
|
|
});
|
|
};
|
|
|
|
const exportSelected = () => {
|
|
if (selectedDemandes.length === 0) {
|
|
toast.current?.show({
|
|
severity: 'warn',
|
|
summary: 'Attention',
|
|
detail: 'Veuillez sélectionner au moins une demande',
|
|
life: 3000
|
|
});
|
|
return;
|
|
}
|
|
|
|
setExportProgress(0);
|
|
const interval = setInterval(() => {
|
|
setExportProgress(prev => {
|
|
if (prev >= 100) {
|
|
clearInterval(interval);
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Export terminé',
|
|
detail: `${selectedDemandes.length} demandes exportées`,
|
|
life: 3000
|
|
});
|
|
return 100;
|
|
}
|
|
return prev + 10;
|
|
});
|
|
}, 200);
|
|
};
|
|
|
|
const assignValidator = () => {
|
|
toast.current?.show({
|
|
severity: 'info',
|
|
summary: 'Assignation',
|
|
detail: 'Fonction d\'assignation en cours de développement',
|
|
life: 3000
|
|
});
|
|
};
|
|
|
|
const sendReminder = () => {
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Rappels envoyés',
|
|
detail: `Rappels envoyés pour ${selectedDemandes.length} demandes`,
|
|
life: 3000
|
|
});
|
|
};
|
|
|
|
const filterByDate = (days: number) => {
|
|
const cutoffDate = new Date();
|
|
cutoffDate.setDate(cutoffDate.getDate() - days);
|
|
// Implémentation du filtre
|
|
toast.current?.show({
|
|
severity: 'info',
|
|
summary: 'Filtre appliqué',
|
|
detail: `Affichage des demandes des ${days} derniers jours`,
|
|
life: 3000
|
|
});
|
|
};
|
|
|
|
const filterByDelay = (days: number) => {
|
|
toast.current?.show({
|
|
severity: 'info',
|
|
summary: 'Filtre appliqué',
|
|
detail: `Affichage des demandes en attente depuis plus de ${days} jours`,
|
|
life: 3000
|
|
});
|
|
};
|
|
|
|
const filterByDocuments = () => {
|
|
toast.current?.show({
|
|
severity: 'info',
|
|
summary: 'Filtre appliqué',
|
|
detail: 'Affichage des demandes avec documents manquants',
|
|
life: 3000
|
|
});
|
|
};
|
|
|
|
const filterByRisk = () => {
|
|
toast.current?.show({
|
|
severity: 'info',
|
|
summary: 'Filtre appliqué',
|
|
detail: 'Affichage des demandes à risque élevé',
|
|
life: 3000
|
|
});
|
|
};
|
|
|
|
const filterBySize = (size: string) => {
|
|
toast.current?.show({
|
|
severity: 'info',
|
|
summary: 'Filtre appliqué',
|
|
detail: `Affichage des ${size === 'large' ? 'grandes' : 'petites'} entreprises`,
|
|
life: 3000
|
|
});
|
|
};
|
|
|
|
const advancedToolbar = (
|
|
<div className="flex flex-wrap align-items-center justify-content-between gap-3 p-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white border-round">
|
|
<div className="flex align-items-center gap-3">
|
|
<Avatar icon="pi pi-users" className="bg-white-alpha-20" style={{ color: 'white' }} />
|
|
<div>
|
|
<div className="text-xl font-bold">Gestion des Demandes d'Accès</div>
|
|
<div className="text-sm opacity-90">
|
|
{kpiData.totalDemandes} demandes | {kpiData.enAttente} en attente | Taux d'approbation: {kpiData.tauxApprobation}%
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex align-items-center gap-2">
|
|
<SelectButton
|
|
value={viewMode}
|
|
options={viewModeOptions}
|
|
onChange={(e) => setViewMode(e.value)}
|
|
optionLabel="label"
|
|
optionValue="value"
|
|
className="view-mode-selector"
|
|
/>
|
|
|
|
<ToggleButton
|
|
checked={autoRefresh}
|
|
onChange={(e) => setAutoRefresh(e.value)}
|
|
onIcon="pi pi-pause"
|
|
offIcon="pi pi-play"
|
|
onLabel=""
|
|
offLabel=""
|
|
className="bg-white-alpha-20"
|
|
tooltip="Auto-refresh"
|
|
/>
|
|
|
|
<Button
|
|
icon="pi pi-chart-bar"
|
|
rounded
|
|
text
|
|
onClick={() => setAnalyticsDialog(true)}
|
|
tooltip="Analytics"
|
|
className="text-white"
|
|
/>
|
|
|
|
<Button
|
|
icon="pi pi-cog"
|
|
rounded
|
|
text
|
|
onClick={() => setSidebarVisible(true)}
|
|
tooltip="Configuration"
|
|
className="text-white"
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const searchToolbar = (
|
|
<div className="flex flex-wrap align-items-center justify-content-between gap-3 p-3 bg-gray-50 border-round mb-3">
|
|
<div className="flex flex-wrap align-items-center gap-2">
|
|
<span className="p-input-icon-left">
|
|
<i className="pi pi-search" />
|
|
<InputText
|
|
type="search"
|
|
placeholder="Rechercher par nom, email, entreprise..."
|
|
value={globalFilter}
|
|
onChange={(e) => setGlobalFilter(e.target.value)}
|
|
className="w-20rem"
|
|
/>
|
|
</span>
|
|
|
|
<Dropdown
|
|
value={statusFilter}
|
|
options={statusOptions}
|
|
onChange={(e) => setStatusFilter(e.value)}
|
|
placeholder="Statut"
|
|
className="w-12rem"
|
|
showClear
|
|
/>
|
|
|
|
<Dropdown
|
|
value={sectorFilter}
|
|
options={sectorOptions}
|
|
onChange={(e) => setSectorFilter(e.value)}
|
|
placeholder="Secteur"
|
|
className="w-12rem"
|
|
showClear
|
|
/>
|
|
|
|
<Calendar
|
|
value={dateFilter}
|
|
onChange={(e) => setDateFilter(e.value)}
|
|
placeholder="Date création"
|
|
showIcon
|
|
className="w-12rem"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex align-items-center gap-2">
|
|
<Badge value={filteredDemandes.length} className="mr-2" />
|
|
<span className="text-sm text-600">résultats</span>
|
|
|
|
<Button
|
|
icon="pi pi-filter-slash"
|
|
label="Effacer"
|
|
size="small"
|
|
outlined
|
|
onClick={() => {
|
|
setGlobalFilter('');
|
|
setStatusFilter('');
|
|
setSectorFilter('');
|
|
setDateFilter(null);
|
|
}}
|
|
/>
|
|
|
|
<Button
|
|
icon="pi pi-refresh"
|
|
label="Actualiser"
|
|
size="small"
|
|
onClick={loadDemandes}
|
|
loading={loading}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
|
|
|
|
|
|
const renderKPIDashboard = () => (
|
|
<div className="grid mb-4">
|
|
<div className="col-12 md:col-3">
|
|
<Card className="bg-gradient-to-r from-blue-500 to-blue-600 text-white">
|
|
<div className="flex align-items-center justify-content-between">
|
|
<div>
|
|
<div className="text-2xl font-bold">{kpiData.totalDemandes}</div>
|
|
<div className="text-sm opacity-90">Total demandes</div>
|
|
</div>
|
|
<Avatar icon="pi pi-users" size="large" className="bg-white-alpha-20" />
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-3">
|
|
<Card className="bg-gradient-to-r from-orange-500 to-orange-600 text-white">
|
|
<div className="flex align-items-center justify-content-between">
|
|
<div>
|
|
<div className="text-2xl font-bold">{kpiData.enAttente}</div>
|
|
<div className="text-sm opacity-90">En attente</div>
|
|
</div>
|
|
<Avatar icon="pi pi-clock" size="large" className="bg-white-alpha-20" />
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-3">
|
|
<Card className="bg-gradient-to-r from-green-500 to-green-600 text-white">
|
|
<div className="flex align-items-center justify-content-between">
|
|
<div>
|
|
<div className="text-2xl font-bold">{kpiData.tauxApprobation}%</div>
|
|
<div className="text-sm opacity-90">Taux approbation</div>
|
|
</div>
|
|
<Avatar icon="pi pi-check-circle" size="large" className="bg-white-alpha-20" />
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-3">
|
|
<Card className="bg-gradient-to-r from-purple-500 to-purple-600 text-white">
|
|
<div className="flex align-items-center justify-content-between">
|
|
<div>
|
|
<div className="text-2xl font-bold">{kpiData.delaiMoyenTraitement}j</div>
|
|
<div className="text-sm opacity-90">Délai moyen</div>
|
|
</div>
|
|
<Avatar icon="pi pi-calendar" size="large" className="bg-white-alpha-20" />
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const renderQuickFilters = () => (
|
|
<Card className="mb-4">
|
|
<h6 className="mb-3">Filtres rapides</h6>
|
|
<div className="flex flex-wrap gap-2">
|
|
{quickFilters.map((filter, index) => (
|
|
<Chip
|
|
key={index}
|
|
label={filter.label}
|
|
icon={filter.icon}
|
|
className="cursor-pointer hover:bg-primary-50"
|
|
onClick={filter.filter}
|
|
/>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
);
|
|
|
|
const renderTableView = () => (
|
|
<Card>
|
|
{selectedDemandes.length > 0 && (
|
|
<div className="mb-3 p-3 bg-blue-50 border-round">
|
|
<div className="flex align-items-center justify-content-between">
|
|
<div className="flex align-items-center gap-2">
|
|
<i className="pi pi-check-circle text-blue-500" />
|
|
<span>{selectedDemandes.length} demande(s) sélectionnée(s)</span>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
label="Actions groupées"
|
|
icon="pi pi-chevron-down"
|
|
size="small"
|
|
onClick={(e) => overlayPanel.current?.toggle(e)}
|
|
/>
|
|
<Button
|
|
icon="pi pi-times"
|
|
size="small"
|
|
text
|
|
onClick={() => setSelectedDemandes([])}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<DataTable
|
|
ref={dt}
|
|
value={filteredDemandes}
|
|
selection={selectedDemandes}
|
|
onSelectionChange={(e) => setSelectedDemandes(e.value)}
|
|
paginator
|
|
rows={10}
|
|
rowsPerPageOptions={[10, 25, 50, 100]}
|
|
loading={loading}
|
|
globalFilter={globalFilter}
|
|
emptyMessage="Aucune demande trouvée"
|
|
responsiveLayout="stack"
|
|
breakpoint="768px"
|
|
className="p-datatable-striped"
|
|
showGridlines
|
|
sortMode="multiple"
|
|
removableSort
|
|
reorderableColumns
|
|
resizableColumns
|
|
columnResizeMode="expand"
|
|
contextMenuSelection={selectedDemande}
|
|
onContextMenuSelectionChange={(e) => setSelectedDemande(e.value)}
|
|
onContextMenu={(e) => contextMenu.current?.show(e.originalEvent)}
|
|
>
|
|
<Column selectionMode="multiple" headerStyle={{ width: '3rem' }} />
|
|
<Column
|
|
field="entreprise"
|
|
header="Entreprise"
|
|
body={entrepriseBodyTemplate}
|
|
sortable
|
|
filter
|
|
filterPlaceholder="Rechercher..."
|
|
/>
|
|
<Column
|
|
field="contact"
|
|
header="Contact"
|
|
body={contactBodyTemplate}
|
|
sortable
|
|
/>
|
|
<Column
|
|
field="secteurActivite"
|
|
header="Secteur"
|
|
sortable
|
|
filter
|
|
filterElement={sectorFilterTemplate}
|
|
/>
|
|
<Column
|
|
field="role"
|
|
header="Rôle"
|
|
sortable
|
|
/>
|
|
<Column
|
|
field="status"
|
|
header="Statut"
|
|
body={statusBodyTemplate}
|
|
sortable
|
|
filter
|
|
filterElement={statusFilterTemplate}
|
|
/>
|
|
<Column
|
|
field="documents"
|
|
header="Documents"
|
|
body={documentsBodyTemplate}
|
|
/>
|
|
<Column
|
|
field="dateCreation"
|
|
header="Date création"
|
|
body={dateBodyTemplate}
|
|
sortable
|
|
filter
|
|
filterElement={dateFilterTemplate}
|
|
/>
|
|
<Column
|
|
field="riskScore"
|
|
header="Risque"
|
|
body={riskScoreTemplate}
|
|
sortable
|
|
/>
|
|
<Column
|
|
header="Actions"
|
|
body={actionsBodyTemplate}
|
|
exportable={false}
|
|
frozen
|
|
alignFrozen="right"
|
|
/>
|
|
</DataTable>
|
|
</Card>
|
|
);
|
|
|
|
const renderGridView = () => (
|
|
<div className="grid">
|
|
{filteredDemandes.map((demande) => (
|
|
<div key={demande.id} className="col-12 md:col-6 lg:col-4">
|
|
<Card className="h-full hover:shadow-lg transition-shadow">
|
|
<div className="flex align-items-center justify-content-between mb-3">
|
|
<Avatar
|
|
label={demande.prenom.charAt(0) + demande.nom.charAt(0)}
|
|
className="mr-2"
|
|
style={{ backgroundColor: '#DEE2E6', color: '#495057' }}
|
|
/>
|
|
<Tag
|
|
value={getStatusLabel(demande.status)}
|
|
severity={getStatusSeverity(demande.status)}
|
|
/>
|
|
</div>
|
|
|
|
<h6 className="mb-2">{demande.prenom} {demande.nom}</h6>
|
|
<p className="text-sm text-600 mb-2">{demande.email}</p>
|
|
<p className="font-bold mb-2">{demande.entreprise}</p>
|
|
<p className="text-sm text-500 mb-3">{demande.secteurActivite}</p>
|
|
|
|
<div className="flex align-items-center mb-3">
|
|
<ProgressBar
|
|
value={demande.pourcentageCompletion || 0}
|
|
className="flex-1 mr-2"
|
|
style={{ height: '6px' }}
|
|
/>
|
|
<span className="text-sm">{demande.pourcentageCompletion || 0}%</span>
|
|
</div>
|
|
|
|
<div className="flex justify-content-between">
|
|
<Button
|
|
icon="pi pi-eye"
|
|
size="small"
|
|
outlined
|
|
onClick={() => {
|
|
setSelectedDemande(demande);
|
|
setDetailDialog(true);
|
|
}}
|
|
/>
|
|
{demande.status === 'PENDING' && (
|
|
<div className="flex gap-1">
|
|
<Button
|
|
icon="pi pi-check"
|
|
size="small"
|
|
severity="success"
|
|
onClick={() => openValidationDialog(demande, 'APPROVE')}
|
|
/>
|
|
<Button
|
|
icon="pi pi-times"
|
|
size="small"
|
|
severity="danger"
|
|
onClick={() => openValidationDialog(demande, 'REJECT')}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
|
|
const renderTimelineView = () => (
|
|
<Card>
|
|
<Timeline
|
|
value={filteredDemandes}
|
|
content={(item) => (
|
|
<Card className="ml-3">
|
|
<div className="flex align-items-center justify-content-between mb-2">
|
|
<div className="flex align-items-center">
|
|
<Avatar
|
|
label={item.prenom.charAt(0) + item.nom.charAt(0)}
|
|
className="mr-2"
|
|
size="normal"
|
|
/>
|
|
<div>
|
|
<div className="font-bold">{item.prenom} {item.nom}</div>
|
|
<div className="text-sm text-600">{item.entreprise}</div>
|
|
</div>
|
|
</div>
|
|
<Tag
|
|
value={getStatusLabel(item.status)}
|
|
severity={getStatusSeverity(item.status)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="text-sm text-500 mb-2">
|
|
{new Date(item.dateCreation).toLocaleDateString('fr-FR')}
|
|
</div>
|
|
|
|
<div className="flex justify-content-end gap-2">
|
|
<Button
|
|
icon="pi pi-eye"
|
|
size="small"
|
|
text
|
|
onClick={() => {
|
|
setSelectedDemande(item);
|
|
setDetailDialog(true);
|
|
}}
|
|
/>
|
|
</div>
|
|
</Card>
|
|
)}
|
|
className="w-full"
|
|
/>
|
|
</Card>
|
|
);
|
|
|
|
const sectorFilterTemplate = (options: any) => (
|
|
<Dropdown
|
|
value={options.value}
|
|
options={sectorOptions}
|
|
onChange={(e) => options.filterCallback(e.value)}
|
|
placeholder="Tous"
|
|
className="p-column-filter"
|
|
showClear
|
|
/>
|
|
);
|
|
|
|
const statusFilterTemplate = (options: any) => (
|
|
<Dropdown
|
|
value={options.value}
|
|
options={statusOptions}
|
|
onChange={(e) => options.filterCallback(e.value)}
|
|
placeholder="Tous"
|
|
className="p-column-filter"
|
|
showClear
|
|
/>
|
|
);
|
|
|
|
const dateFilterTemplate = (options: any) => (
|
|
<Calendar
|
|
value={options.value}
|
|
onChange={(e) => options.filterCallback(e.value)}
|
|
placeholder="Sélectionner"
|
|
className="p-column-filter"
|
|
showIcon
|
|
/>
|
|
);
|
|
|
|
const riskScoreTemplate = (rowData: DemandeAcces) => {
|
|
const score = Math.floor(Math.random() * 40) + 30; // Score entre 30-70
|
|
const getSeverity = () => {
|
|
if (score >= 60) return 'danger';
|
|
if (score >= 40) return 'warning';
|
|
return 'success';
|
|
};
|
|
|
|
return (
|
|
<div className="flex align-items-center">
|
|
<Badge value={score} severity={getSeverity()} className="mr-2" />
|
|
<div className="w-4rem">
|
|
<ProgressBar value={score} style={{ height: '4px' }} />
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="access-requests-management">
|
|
<BlockUI blocked={blocked}>
|
|
<Toast ref={toast} />
|
|
<Messages ref={messages} />
|
|
<ConfirmDialog />
|
|
|
|
{advancedToolbar}
|
|
|
|
{exportProgress > 0 && exportProgress < 100 && (
|
|
<Card className="mb-4">
|
|
<div className="flex align-items-center gap-3">
|
|
<i className="pi pi-spin pi-download text-blue-500" />
|
|
<div className="flex-1">
|
|
<p className="m-0 font-bold">Export en cours...</p>
|
|
<ProgressBar value={exportProgress} className="mt-2" />
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
)}
|
|
|
|
<TabView activeIndex={activeTab} onTabChange={(e) => setActiveTab(e.index)}>
|
|
<TabPanel header="Demandes" leftIcon="pi pi-list mr-2">
|
|
{renderKPIDashboard()}
|
|
{renderQuickFilters()}
|
|
{searchToolbar}
|
|
|
|
{viewMode === 'table' && renderTableView()}
|
|
{viewMode === 'grid' && renderGridView()}
|
|
{viewMode === 'timeline' && renderTimelineView()}
|
|
</TabPanel>
|
|
|
|
<TabPanel header="Workflow" leftIcon="pi pi-sitemap mr-2">
|
|
<Card title="Processus d'approbation">
|
|
<Steps
|
|
model={workflowSteps}
|
|
activeIndex={2}
|
|
className="mb-4"
|
|
/>
|
|
|
|
<div className="grid">
|
|
<div className="col-12 md:col-6">
|
|
<h6>File d'attente de traitement</h6>
|
|
<DataView
|
|
value={processingQueue}
|
|
itemTemplate={(item) => (
|
|
<div className="p-3 border-bottom-1 surface-border">
|
|
<div className="flex align-items-center justify-content-between">
|
|
<div>
|
|
<div className="font-bold">{item.entreprise}</div>
|
|
<div className="text-sm text-600">{item.prenom} {item.nom}</div>
|
|
</div>
|
|
<Badge value="En cours" severity="info" />
|
|
</div>
|
|
</div>
|
|
)}
|
|
emptyMessage="Aucune demande en cours de traitement"
|
|
/>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-6">
|
|
<h6>Règles de validation</h6>
|
|
{validationCriteria.map((criteria, index) => (
|
|
<div key={index} className="flex align-items-center justify-content-between mb-2 p-2 border-round bg-gray-50">
|
|
<span className="text-sm">{criteria.name}</span>
|
|
<div className="flex align-items-center gap-2">
|
|
<Badge value={`${criteria.weight}%`} />
|
|
<Tag
|
|
value={criteria.status}
|
|
severity={
|
|
criteria.status === 'complete' ? 'success' :
|
|
criteria.status === 'incomplete' ? 'danger' : 'warning'
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</TabPanel>
|
|
|
|
<TabPanel header="Analytics" leftIcon="pi pi-chart-bar mr-2">
|
|
<div className="grid">
|
|
<div className="col-12 md:col-8">
|
|
<Card title="Tendances des demandes">
|
|
<Chart type="bar" data={chartData} className="w-full" />
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-4">
|
|
<Card title="Répartition par secteur">
|
|
<Chart
|
|
type="doughnut"
|
|
data={{
|
|
labels: ['BTP Général', 'Électricité', 'Plomberie', 'Autres'],
|
|
datasets: [{
|
|
data: [45, 25, 20, 10],
|
|
backgroundColor: ['#3B82F6', '#10B981', '#F59E0B', '#8B5CF6']
|
|
}]
|
|
}}
|
|
options={{
|
|
plugins: {
|
|
legend: { position: 'bottom' }
|
|
}
|
|
}}
|
|
/>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12">
|
|
<Card title="Activité récente">
|
|
<Timeline
|
|
value={timelineData}
|
|
content={(item) => (
|
|
<div className="flex align-items-center">
|
|
<Badge
|
|
value={item.type}
|
|
severity={
|
|
item.type === 'success' ? 'success' :
|
|
item.type === 'warning' ? 'warning' : 'info'
|
|
}
|
|
className="mr-2"
|
|
/>
|
|
<div>
|
|
<div className="font-bold">{item.event}</div>
|
|
<div className="text-sm text-600">{item.user} - {item.date.toLocaleTimeString()}</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
/>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</TabPanel>
|
|
|
|
<TabPanel header="Audit" leftIcon="pi pi-shield mr-2">
|
|
<Card>
|
|
<Terminal
|
|
ref={terminal}
|
|
welcomeMessage="Console d'audit - Tapez 'help' pour les commandes disponibles"
|
|
prompt="audit $"
|
|
className="bg-gray-900 text-white"
|
|
/>
|
|
</Card>
|
|
</TabPanel>
|
|
</TabView>
|
|
|
|
<SpeedDial
|
|
model={bulkActions}
|
|
radius={80}
|
|
type="semi-circle"
|
|
direction="up-left"
|
|
style={{ left: 'calc(50% - 2rem)', bottom: 0 }}
|
|
buttonClassName="p-button-help"
|
|
maskClassName="bg-black-alpha-30"
|
|
visible={selectedDemandes.length > 0}
|
|
/>
|
|
|
|
<OverlayPanel ref={overlayPanel}>
|
|
<div className="flex flex-column gap-2">
|
|
{bulkActions.map((action, index) => (
|
|
<Button
|
|
key={index}
|
|
label={action.label}
|
|
icon={action.icon}
|
|
text
|
|
className="justify-content-start"
|
|
onClick={() => {
|
|
action.command();
|
|
overlayPanel.current?.hide();
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
</OverlayPanel>
|
|
|
|
<ContextMenu
|
|
ref={contextMenu}
|
|
model={[
|
|
{ label: 'Voir détails', icon: 'pi pi-eye', command: () => setDetailDialog(true) },
|
|
{ label: 'Approuver', icon: 'pi pi-check', command: () => openValidationDialog(selectedDemande, 'APPROVE') },
|
|
{ label: 'Rejeter', icon: 'pi pi-times', command: () => openValidationDialog(selectedDemande, 'REJECT') },
|
|
{ separator: true },
|
|
{ label: 'Dupliquer', icon: 'pi pi-copy' },
|
|
{ label: 'Exporter', icon: 'pi pi-download' }
|
|
]}
|
|
/>
|
|
|
|
<Sidebar
|
|
visible={sidebarVisible}
|
|
onHide={() => setSidebarVisible(false)}
|
|
position="right"
|
|
className="w-25rem"
|
|
>
|
|
<h3>Configuration avancée</h3>
|
|
|
|
<Accordion multiple>
|
|
<AccordionTab header="Paramètres d'affichage">
|
|
<div className="field">
|
|
<label>Mode d'affichage</label>
|
|
<SelectButton
|
|
value={viewMode}
|
|
options={viewModeOptions}
|
|
onChange={(e) => setViewMode(e.value)}
|
|
optionLabel="label"
|
|
optionValue="value"
|
|
className="w-full mt-2"
|
|
/>
|
|
</div>
|
|
|
|
<div className="field">
|
|
<div className="flex align-items-center gap-2">
|
|
<InputSwitch
|
|
checked={autoRefresh}
|
|
onChange={(e) => setAutoRefresh(e.value)}
|
|
/>
|
|
<label>Auto-refresh</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="field">
|
|
<label>Intervalle de refresh (secondes)</label>
|
|
<Slider
|
|
value={refreshInterval}
|
|
onChange={(e) => setRefreshInterval(e.value as number)}
|
|
min={10}
|
|
max={300}
|
|
step={10}
|
|
className="w-full mt-2"
|
|
/>
|
|
</div>
|
|
</AccordionTab>
|
|
|
|
<AccordionTab header="Notifications">
|
|
<div className="field">
|
|
<div className="flex align-items-center gap-2 mb-2">
|
|
<Checkbox checked />
|
|
<label>Nouvelles demandes</label>
|
|
</div>
|
|
<div className="flex align-items-center gap-2 mb-2">
|
|
<Checkbox checked />
|
|
<label>Demandes approuvées</label>
|
|
</div>
|
|
<div className="flex align-items-center gap-2">
|
|
<Checkbox checked={false} onChange={() => {}} />
|
|
<label>Demandes en retard</label>
|
|
</div>
|
|
</div>
|
|
</AccordionTab>
|
|
|
|
<AccordionTab header="Validation automatique">
|
|
<div className="field">
|
|
<div className="flex align-items-center gap-2 mb-3">
|
|
<InputSwitch checked={complianceCheck} onChange={(e) => setComplianceCheck(e.value)} />
|
|
<label>Vérification conformité</label>
|
|
</div>
|
|
|
|
<div className="field">
|
|
<label>Seuil de risque</label>
|
|
<Knob
|
|
value={riskScore}
|
|
onChange={(e) => setRiskScore(e.value)}
|
|
min={0}
|
|
max={100}
|
|
size={80}
|
|
className="mt-2"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</AccordionTab>
|
|
</Accordion>
|
|
</Sidebar>
|
|
|
|
<ScrollTop threshold={600} className="w-3rem h-3rem border-round bg-primary" />
|
|
|
|
{/* Dialog de détails ultra-avancé */}
|
|
<Dialog
|
|
visible={detailDialog}
|
|
onHide={() => setDetailDialog(false)}
|
|
header={(
|
|
<div className="flex align-items-center gap-3">
|
|
<Avatar
|
|
label={selectedDemande?.prenom?.charAt(0) + selectedDemande?.nom?.charAt(0)}
|
|
size="large"
|
|
style={{ backgroundColor: '#3B82F6', color: 'white' }}
|
|
/>
|
|
<div>
|
|
<div className="text-lg font-bold">Détails de la demande d'accès</div>
|
|
<div className="text-sm text-600">
|
|
{selectedDemande?.entreprise} - {selectedDemande?.secteurActivite}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
style={{ width: '95vw', maxWidth: '1200px', height: '90vh' }}
|
|
modal
|
|
maximizable
|
|
>
|
|
{selectedDemande && (
|
|
<Splitter style={{ height: '70vh' }}>
|
|
<SplitterPanel size={70}>
|
|
<ScrollPanel style={{ width: '100%', height: '100%' }}>
|
|
<TabView>
|
|
<TabPanel header="Informations générales" leftIcon="pi pi-user mr-2">
|
|
<div className="grid">
|
|
<div className="col-12 md:col-6">
|
|
<Fieldset legend="Contact">
|
|
<div className="grid">
|
|
<div className="col-6">
|
|
<div className="field">
|
|
<label className="font-bold text-sm">Prénom</label>
|
|
<InputText value={selectedDemande.prenom} disabled className="w-full" />
|
|
</div>
|
|
</div>
|
|
<div className="col-6">
|
|
<div className="field">
|
|
<label className="font-bold text-sm">Nom</label>
|
|
<InputText value={selectedDemande.nom} disabled className="w-full" />
|
|
</div>
|
|
</div>
|
|
<div className="col-12">
|
|
<div className="field">
|
|
<label className="font-bold text-sm">Email</label>
|
|
<InputText value={selectedDemande.email} disabled className="w-full" />
|
|
</div>
|
|
</div>
|
|
<div className="col-12">
|
|
<div className="field">
|
|
<label className="font-bold text-sm">Téléphone</label>
|
|
<InputText value={selectedDemande.telephone} disabled className="w-full" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Fieldset>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-6">
|
|
<Fieldset legend="Entreprise">
|
|
<div className="grid">
|
|
<div className="col-12">
|
|
<div className="field">
|
|
<label className="font-bold text-sm">Raison sociale</label>
|
|
<InputText value={selectedDemande.entreprise} disabled className="w-full" />
|
|
</div>
|
|
</div>
|
|
<div className="col-8">
|
|
<div className="field">
|
|
<label className="font-bold text-sm">SIRET</label>
|
|
<InputText value={selectedDemande.siret} disabled className="w-full" />
|
|
</div>
|
|
</div>
|
|
<div className="col-4">
|
|
<div className="field">
|
|
<label className="font-bold text-sm">Effectif</label>
|
|
<InputNumber value={selectedDemande.effectif} disabled className="w-full" />
|
|
</div>
|
|
</div>
|
|
<div className="col-12">
|
|
<div className="field">
|
|
<label className="font-bold text-sm">Secteur d'activité</label>
|
|
<InputText value={selectedDemande.secteurActivite} disabled className="w-full" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Fieldset>
|
|
</div>
|
|
</div>
|
|
</TabPanel>
|
|
|
|
<TabPanel header="Documents" leftIcon="pi pi-file mr-2">
|
|
<div className="grid">
|
|
<div className="col-12">
|
|
<Card title="État des documents">
|
|
<div className="grid">
|
|
<div className="col-12 md:col-3">
|
|
<div className="text-center p-3 border-round bg-gray-50">
|
|
<i className={`pi ${selectedDemande.kbisUploade ? 'pi-check-circle text-green-500' : 'pi-times-circle text-red-500'} text-4xl mb-2`}></i>
|
|
<div className="font-bold">KBIS</div>
|
|
<Tag
|
|
value={selectedDemande.kbisUploade ? 'Fourni' : 'Manquant'}
|
|
severity={selectedDemande.kbisUploade ? 'success' : 'danger'}
|
|
className="mt-2"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-3">
|
|
<div className="text-center p-3 border-round bg-gray-50">
|
|
<i className={`pi ${selectedDemande.assuranceRCUploade ? 'pi-check-circle text-green-500' : 'pi-times-circle text-red-500'} text-4xl mb-2`}></i>
|
|
<div className="font-bold">Assurance RC</div>
|
|
<Tag
|
|
value={selectedDemande.assuranceRCUploade ? 'Fournie' : 'Manquante'}
|
|
severity={selectedDemande.assuranceRCUploade ? 'success' : 'danger'}
|
|
className="mt-2"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-3">
|
|
<div className="text-center p-3 border-round bg-gray-50">
|
|
<i className={`pi ${selectedDemande.assuranceDecennaleUploade ? 'pi-check-circle text-green-500' : 'pi-times-circle text-red-500'} text-4xl mb-2`}></i>
|
|
<div className="font-bold">Décennale</div>
|
|
<Tag
|
|
value={selectedDemande.assuranceDecennaleUploade ? 'Fournie' : 'Manquante'}
|
|
severity={selectedDemande.assuranceDecennaleUploade ? 'success' : 'danger'}
|
|
className="mt-2"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-3">
|
|
<div className="text-center p-3 border-round bg-gray-50">
|
|
<i className={`pi ${selectedDemande.attestationUrssafUploade ? 'pi-check-circle text-green-500' : 'pi-times-circle text-red-500'} text-4xl mb-2`}></i>
|
|
<div className="font-bold">URSSAF</div>
|
|
<Tag
|
|
value={selectedDemande.attestationUrssafUploade ? 'Fournie' : 'Manquante'}
|
|
severity={selectedDemande.attestationUrssafUploade ? 'success' : 'danger'}
|
|
className="mt-2"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Divider />
|
|
|
|
<div className="flex align-items-center justify-content-between">
|
|
<div>
|
|
<label className="font-bold">Progression globale</label>
|
|
<div className="text-sm text-600">Complétude du dossier</div>
|
|
</div>
|
|
<div className="text-center">
|
|
<Knob
|
|
value={selectedDemande.pourcentageCompletion || 0}
|
|
readOnly
|
|
size={60}
|
|
strokeWidth={8}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</TabPanel>
|
|
|
|
<TabPanel header="Historique" leftIcon="pi pi-history mr-2">
|
|
<Timeline
|
|
value={[
|
|
{ date: selectedDemande.dateCreation, event: 'Demande soumise', status: 'info' },
|
|
{ date: selectedDemande.dateModification, event: 'Documents vérifiés', status: 'success' },
|
|
{ date: selectedDemande.dateTraitement, event: 'En cours de validation', status: 'warning' }
|
|
].filter(item => item.date)}
|
|
content={(item) => (
|
|
<div className="flex align-items-center">
|
|
<Badge
|
|
value={item.status}
|
|
severity={item.status === 'success' ? 'success' : item.status === 'warning' ? 'warning' : 'info'}
|
|
className="mr-2"
|
|
/>
|
|
<div>
|
|
<div className="font-bold">{item.event}</div>
|
|
<div className="text-sm text-600">{new Date(item.date).toLocaleString('fr-FR')}</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
/>
|
|
</TabPanel>
|
|
|
|
<TabPanel header="Analyse" leftIcon="pi pi-chart-line mr-2">
|
|
<div className="grid">
|
|
<div className="col-12 md:col-6">
|
|
<Card title="Score de conformité">
|
|
<div className="text-center">
|
|
<Rating
|
|
value={4}
|
|
readOnly
|
|
stars={5}
|
|
className="mb-3"
|
|
/>
|
|
<div className="text-lg font-bold text-green-600">Excellent</div>
|
|
<div className="text-sm text-600">Toutes les exigences sont respectées</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-6">
|
|
<Card title="Recommandations IA">
|
|
<div className="flex flex-column gap-2">
|
|
<Message
|
|
severity="success"
|
|
text="Dossier complet - Approbation recommandée"
|
|
className="w-full"
|
|
/>
|
|
<Message
|
|
severity="info"
|
|
text="Entreprise avec historique positif"
|
|
className="w-full"
|
|
/>
|
|
<Message
|
|
severity="warn"
|
|
text="Vérifier la validité des assurances"
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</TabPanel>
|
|
</TabView>
|
|
</ScrollPanel>
|
|
</SplitterPanel>
|
|
|
|
<SplitterPanel size={30}>
|
|
<ScrollPanel style={{ width: '100%', height: '100%' }}>
|
|
<div className="p-3">
|
|
<h6>Actions rapides</h6>
|
|
|
|
<div className="flex flex-column gap-2 mb-4">
|
|
<Button
|
|
label="Approuver"
|
|
icon="pi pi-check"
|
|
severity="success"
|
|
className="w-full"
|
|
onClick={() => openValidationDialog(selectedDemande, 'APPROVE')}
|
|
/>
|
|
<Button
|
|
label="Rejeter"
|
|
icon="pi pi-times"
|
|
severity="danger"
|
|
className="w-full"
|
|
onClick={() => openValidationDialog(selectedDemande, 'REJECT')}
|
|
/>
|
|
<Button
|
|
label="Demander complément"
|
|
icon="pi pi-exclamation-triangle"
|
|
severity="warning"
|
|
className="w-full"
|
|
outlined
|
|
/>
|
|
<Button
|
|
label="Programmer entretien"
|
|
icon="pi pi-calendar"
|
|
className="w-full"
|
|
outlined
|
|
/>
|
|
</div>
|
|
|
|
<Divider />
|
|
|
|
<h6>Informations système</h6>
|
|
<div className="text-sm text-600 mb-2">
|
|
<div><strong>ID:</strong> {selectedDemande.id}</div>
|
|
<div><strong>Créée le:</strong> {new Date(selectedDemande.dateCreation).toLocaleDateString('fr-FR')}</div>
|
|
<div><strong>Statut:</strong> {getStatusLabel(selectedDemande.status)}</div>
|
|
{selectedDemande.validateurNom && (
|
|
<div><strong>Validateur:</strong> {selectedDemande.validateurNom}</div>
|
|
)}
|
|
</div>
|
|
|
|
<Divider />
|
|
|
|
{selectedDemande.commentaireDemandeur && (
|
|
<div className="mb-3">
|
|
<h6>Commentaire demandeur</h6>
|
|
<Panel>
|
|
<p className="text-sm">{selectedDemande.commentaireDemandeur}</p>
|
|
</Panel>
|
|
</div>
|
|
)}
|
|
|
|
{selectedDemande.commentaireAdmin && (
|
|
<div>
|
|
<h6>Commentaire admin</h6>
|
|
<Panel>
|
|
<p className="text-sm">{selectedDemande.commentaireAdmin}</p>
|
|
{selectedDemande.validateurNom && (
|
|
<div className="text-xs text-500 mt-2">Par: {selectedDemande.validateurNom}</div>
|
|
)}
|
|
</Panel>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</ScrollPanel>
|
|
</SplitterPanel>
|
|
</Splitter>
|
|
)}
|
|
</Dialog>
|
|
|
|
{/* Dialog de validation amélioré */}
|
|
<Dialog
|
|
visible={validationDialog}
|
|
onHide={() => setValidationDialog(false)}
|
|
header={(
|
|
<div className="flex align-items-center gap-2">
|
|
<i className={`pi ${actionType === 'APPROVE' ? 'pi-check text-green-500' : 'pi-times text-red-500'} text-xl`} />
|
|
<span>{actionType === 'APPROVE' ? 'Approuver' : 'Rejeter'} la demande</span>
|
|
</div>
|
|
)}
|
|
style={{ width: '500px' }}
|
|
modal
|
|
>
|
|
<div className="formgrid grid">
|
|
<div className="field col-12">
|
|
<Message
|
|
severity={actionType === 'APPROVE' ? 'success' : 'warn'}
|
|
text={actionType === 'APPROVE'
|
|
? 'Cette action accordera l\'accès à l\'entreprise'
|
|
: 'Cette action refusera définitivement l\'accès'}
|
|
className="w-full mb-3"
|
|
/>
|
|
</div>
|
|
|
|
<div className="field col-12">
|
|
<label htmlFor="commentaire" className="font-bold">
|
|
Commentaire {actionType === 'REJECT' ? '(obligatoire)' : ''}
|
|
</label>
|
|
<InputTextarea
|
|
id="commentaire"
|
|
value={commentaire}
|
|
onChange={(e) => setCommentaire(e.target.value)}
|
|
rows={4}
|
|
placeholder={actionType === 'APPROVE'
|
|
? 'Commentaire d\'approbation...'
|
|
: 'Motif du rejet...'
|
|
}
|
|
className="w-full"
|
|
autoResize
|
|
/>
|
|
</div>
|
|
|
|
{actionType === 'APPROVE' && (
|
|
<div className="field col-12">
|
|
<div className="flex align-items-center gap-2">
|
|
<Checkbox checked />
|
|
<label className="text-sm">Envoyer un email de bienvenue</label>
|
|
</div>
|
|
<div className="flex align-items-center gap-2 mt-2">
|
|
<Checkbox checked={false} onChange={() => {}} />
|
|
<label className="text-sm">Programmer une formation</label>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex justify-content-end gap-2 mt-4">
|
|
<Button
|
|
label="Annuler"
|
|
icon="pi pi-times"
|
|
outlined
|
|
onClick={() => setValidationDialog(false)}
|
|
/>
|
|
<Button
|
|
label={actionType === 'APPROVE' ? 'Approuver' : 'Rejeter'}
|
|
icon={actionType === 'APPROVE' ? 'pi pi-check' : 'pi pi-times'}
|
|
severity={actionType === 'APPROVE' ? 'success' : 'danger'}
|
|
onClick={handleValidation}
|
|
loading={loading}
|
|
/>
|
|
</div>
|
|
</Dialog>
|
|
</BlockUI>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default DemandesAccesAdmin;
|
|
|
|
// Styles CSS personnalisés pour l'interface ultra-professionnelle
|
|
const customStyles = `
|
|
.access-requests-management .p-tabview-nav {
|
|
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
|
border-radius: 10px 10px 0 0;
|
|
}
|
|
|
|
.access-requests-management .p-tabview-nav li .p-tabview-nav-link {
|
|
color: white;
|
|
border: none;
|
|
}
|
|
|
|
.access-requests-management .p-tabview-nav li.p-highlight .p-tabview-nav-link {
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border-radius: 8px;
|
|
margin: 4px;
|
|
}
|
|
|
|
.view-mode-selector .p-button {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
color: white;
|
|
}
|
|
|
|
.view-mode-selector .p-button.p-highlight {
|
|
background: rgba(255, 255, 255, 0.3);
|
|
}
|
|
|
|
.p-datatable-striped .p-datatable-tbody > tr:nth-child(odd) {
|
|
background: rgba(0, 0, 0, 0.02);
|
|
}
|
|
|
|
.p-datatable .p-datatable-tbody > tr:hover {
|
|
background: rgba(59, 130, 246, 0.1);
|
|
}
|
|
|
|
.hover\\:shadow-lg:hover {
|
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
transition: box-shadow 0.3s ease;
|
|
}
|
|
|
|
.transition-shadow {
|
|
transition: box-shadow 0.3s ease;
|
|
}
|
|
|
|
.cursor-pointer {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.bg-gradient-to-r {
|
|
background: linear-gradient(90deg, var(--tw-gradient-stops));
|
|
}
|
|
|
|
.from-blue-500 {
|
|
--tw-gradient-from: #3b82f6;
|
|
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(59, 130, 246, 0));
|
|
}
|
|
|
|
.to-purple-600 {
|
|
--tw-gradient-to: #9333ea;
|
|
}
|
|
|
|
.from-orange-500 {
|
|
--tw-gradient-from: #f97316;
|
|
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(249, 115, 22, 0));
|
|
}
|
|
|
|
.to-orange-600 {
|
|
--tw-gradient-to: #ea580c;
|
|
}
|
|
|
|
.from-green-500 {
|
|
--tw-gradient-from: #22c55e;
|
|
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(34, 197, 94, 0));
|
|
}
|
|
|
|
.to-green-600 {
|
|
--tw-gradient-to: #16a34a;
|
|
}
|
|
|
|
.from-purple-500 {
|
|
--tw-gradient-from: #a855f7;
|
|
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(168, 85, 247, 0));
|
|
}
|
|
|
|
.to-purple-600 {
|
|
--tw-gradient-to: #9333ea;
|
|
}
|
|
`;
|
|
|
|
// Injection du style dans le document
|
|
if (typeof document !== 'undefined') {
|
|
const style = document.createElement('style');
|
|
style.textContent = customStyles;
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
|