- Correction des erreurs TypeScript dans userService.ts et workflowTester.ts - Ajout des propriétés manquantes aux objets User mockés - Conversion des dates de string vers objets Date - Correction des appels asynchrones et des types incompatibles - Ajout de dynamic rendering pour résoudre les erreurs useSearchParams - Enveloppement de useSearchParams dans Suspense boundary - Configuration de force-dynamic au niveau du layout principal Build réussi: 126 pages générées avec succès 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
371 lines
14 KiB
TypeScript
371 lines
14 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useContext } from 'react';
|
|
import { DataTable } from 'primereact/datatable';
|
|
import { Column } from 'primereact/column';
|
|
import { Tag } from 'primereact/tag';
|
|
import { Badge } from 'primereact/badge';
|
|
import { Button } from 'primereact/button';
|
|
import { Toolbar } from 'primereact/toolbar';
|
|
import { Dropdown } from 'primereact/dropdown';
|
|
import { InputSwitch } from 'primereact/inputswitch';
|
|
import { LayoutContext } from '../../layout/context/layoutcontext';
|
|
import type { PhaseChantier } from '../../types/btp-extended';
|
|
import phaseValidationService from '../../services/phaseValidationService';
|
|
|
|
interface AtlantisResponsivePhasesTableProps {
|
|
phases: PhaseChantier[];
|
|
onPhaseSelect?: (phase: PhaseChantier) => void;
|
|
onPhaseStart?: (phaseId: string) => void;
|
|
onPhaseValidate?: (phase: PhaseChantier) => void;
|
|
className?: string;
|
|
}
|
|
|
|
const AtlantisResponsivePhasesTable: React.FC<AtlantisResponsivePhasesTableProps> = ({
|
|
phases,
|
|
onPhaseSelect,
|
|
onPhaseStart,
|
|
onPhaseValidate,
|
|
className = ''
|
|
}) => {
|
|
const [selectedPhase, setSelectedPhase] = useState<PhaseChantier | null>(null);
|
|
const [globalFilter, setGlobalFilter] = useState<string>('');
|
|
const [filters, setFilters] = useState<any>({});
|
|
const { layoutConfig, isDesktop } = useContext(LayoutContext);
|
|
|
|
// Options de filtrage responsive
|
|
const [compactView, setCompactView] = useState(!isDesktop());
|
|
const [showSubPhases, setShowSubPhases] = useState(true);
|
|
|
|
const handlePhaseSelect = (phase: PhaseChantier) => {
|
|
setSelectedPhase(phase);
|
|
onPhaseSelect?.(phase);
|
|
};
|
|
|
|
// Template pour le nom des phases avec hiérarchie Atlantis
|
|
const nameBodyTemplate = (rowData: PhaseChantier) => {
|
|
const isSubPhase = !!rowData.phaseParent;
|
|
|
|
return (
|
|
<div className={`flex align-items-center gap-2 ${isSubPhase ? 'ml-4' : ''}`}>
|
|
{isSubPhase && (
|
|
<i className="pi pi-arrow-right text-color-secondary text-sm" />
|
|
)}
|
|
<span className={`${isSubPhase ? 'text-color-secondary' : 'font-semibold text-color'}`}>
|
|
{rowData.nom}
|
|
</span>
|
|
{rowData.critique && (
|
|
<Tag
|
|
value="Critique"
|
|
severity="danger"
|
|
className="text-xs"
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Template pour le statut avec Tag PrimeReact
|
|
const statusBodyTemplate = (rowData: PhaseChantier) => {
|
|
const getSeverity = () => {
|
|
switch (rowData.statut) {
|
|
case 'TERMINEE': return 'success';
|
|
case 'EN_COURS': return 'info';
|
|
default: return 'secondary';
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Tag
|
|
value={rowData.statut}
|
|
severity={getSeverity()}
|
|
icon={`pi pi-${rowData.statut === 'TERMINEE' ? 'check' : rowData.statut === 'EN_COURS' ? 'clock' : 'calendar'}`}
|
|
/>
|
|
);
|
|
};
|
|
|
|
// Template pour l'avancement avec style Atlantis
|
|
const progressBodyTemplate = (rowData: PhaseChantier) => {
|
|
const progress = rowData.pourcentageAvancement || 0;
|
|
|
|
return (
|
|
<div className="flex align-items-center gap-2">
|
|
<div className="w-full bg-surface-200 border-round" style={{ height: '8px' }}>
|
|
<div
|
|
className="bg-primary border-round h-full transition-all transition-duration-300"
|
|
style={{ width: `${progress}%` }}
|
|
/>
|
|
</div>
|
|
<span className="text-sm font-semibold text-color-secondary min-w-max">
|
|
{progress}%
|
|
</span>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Template pour la validation avec couleurs Atlantis
|
|
const validationBodyTemplate = (rowData: PhaseChantier) => {
|
|
const validation = phaseValidationService.validatePhaseStart(rowData, phases);
|
|
|
|
const getValidationButton = () => {
|
|
if (validation.readyToStart) {
|
|
return (
|
|
<Button
|
|
icon="pi pi-check-circle"
|
|
className="p-button-success p-button-rounded p-button-text"
|
|
onClick={() => onPhaseValidate?.(rowData)}
|
|
tooltip="Prête à démarrer"
|
|
/>
|
|
);
|
|
} else if (validation.canStart) {
|
|
return (
|
|
<Button
|
|
icon="pi pi-exclamation-triangle"
|
|
className="p-button-warning p-button-rounded p-button-text"
|
|
onClick={() => onPhaseValidate?.(rowData)}
|
|
tooltip="Peut démarrer avec précautions"
|
|
/>
|
|
);
|
|
} else {
|
|
return (
|
|
<Button
|
|
icon="pi pi-times-circle"
|
|
className="p-button-danger p-button-rounded p-button-text"
|
|
onClick={() => onPhaseValidate?.(rowData)}
|
|
tooltip="Ne peut pas démarrer"
|
|
/>
|
|
);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="flex align-items-center gap-2">
|
|
{getValidationButton()}
|
|
{validation.errors.length > 0 && (
|
|
<Badge value={validation.errors.length} severity="danger" />
|
|
)}
|
|
{validation.warnings.length > 0 && (
|
|
<Badge value={validation.warnings.length} severity="warning" />
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Template pour les actions
|
|
const actionsBodyTemplate = (rowData: PhaseChantier) => {
|
|
const validation = phaseValidationService.validatePhaseStart(rowData, phases);
|
|
|
|
return (
|
|
<div className="flex gap-1">
|
|
<Button
|
|
icon="pi pi-play"
|
|
className="p-button-success p-button-sm"
|
|
disabled={!validation.canStart || rowData.statut === 'TERMINEE'}
|
|
onClick={() => onPhaseStart?.(rowData.id!)}
|
|
tooltip="Démarrer"
|
|
/>
|
|
<Button
|
|
icon="pi pi-eye"
|
|
className="p-button-outlined p-button-sm"
|
|
onClick={() => onPhaseValidate?.(rowData)}
|
|
tooltip="Détails"
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Template pour les dates avec style Atlantis
|
|
const dateBodyTemplate = (field: string) => (rowData: PhaseChantier) => {
|
|
const date = rowData[field as keyof PhaseChantier] as string;
|
|
if (!date) return <span className="text-color-secondary">-</span>;
|
|
|
|
const formattedDate = new Date(date).toLocaleDateString('fr-FR');
|
|
const isOverdue = field.includes('Fin') && rowData.statut !== 'TERMINEE' && new Date(date) < new Date();
|
|
|
|
return (
|
|
<span className={isOverdue ? 'text-red-500 font-semibold' : 'text-color'}>
|
|
{formattedDate}
|
|
{isOverdue && <i className="pi pi-exclamation-triangle ml-2 text-red-500" />}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
// Barre d'outils responsive Atlantis
|
|
const toolbarStart = (
|
|
<div className="flex align-items-center gap-2">
|
|
<h5 className="m-0 text-color">Phases du chantier</h5>
|
|
{!isDesktop() && (
|
|
<Badge value={phases.length} className="ml-2" />
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
const toolbarEnd = (
|
|
<div className="flex align-items-center gap-2">
|
|
<div className="field-checkbox">
|
|
<InputSwitch
|
|
inputId="compactView"
|
|
checked={compactView}
|
|
onChange={(e) => setCompactView(e.value)}
|
|
/>
|
|
<label htmlFor="compactView" className="ml-2 text-sm">Vue compacte</label>
|
|
</div>
|
|
|
|
<div className="field-checkbox">
|
|
<InputSwitch
|
|
inputId="showSubPhases"
|
|
checked={showSubPhases}
|
|
onChange={(e) => setShowSubPhases(e.value)}
|
|
/>
|
|
<label htmlFor="showSubPhases" className="ml-2 text-sm">Sous-phases</label>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
// Filtrer les phases selon les options
|
|
const filteredPhases = phases.filter(phase => {
|
|
if (!showSubPhases && phase.phaseParent) return false;
|
|
return true;
|
|
});
|
|
|
|
// Déterminer les colonnes à afficher selon la taille d'écran
|
|
const getVisibleColumns = () => {
|
|
if (compactView) {
|
|
return ['nom', 'statut', 'pourcentageAvancement', 'actions'];
|
|
} else if (!isDesktop()) {
|
|
return ['nom', 'statut', 'pourcentageAvancement', 'validation', 'actions'];
|
|
} else {
|
|
return ['nom', 'statut', 'pourcentageAvancement', 'dateDebutPrevue', 'dateFinPrevue', 'validation', 'actions'];
|
|
}
|
|
};
|
|
|
|
const visibleColumns = getVisibleColumns();
|
|
|
|
return (
|
|
<div className={`card ${className}`}>
|
|
<Toolbar
|
|
start={toolbarStart}
|
|
end={toolbarEnd}
|
|
className="mb-4"
|
|
/>
|
|
|
|
<DataTable
|
|
value={filteredPhases}
|
|
selection={selectedPhase}
|
|
onSelectionChange={(e) => setSelectedPhase(e.value)}
|
|
selectionMode="single"
|
|
dataKey="id"
|
|
size={compactView ? 'small' : 'normal'}
|
|
stripedRows
|
|
responsiveLayout="scroll"
|
|
className="datatable-responsive"
|
|
emptyMessage="Aucune phase trouvée"
|
|
globalFilter={globalFilter}
|
|
header={
|
|
isDesktop() ? (
|
|
<div className="flex justify-content-between align-items-center">
|
|
<span className="text-xl font-semibold text-color">
|
|
Gestion des phases ({filteredPhases.length})
|
|
</span>
|
|
<span className="p-input-icon-left">
|
|
<i className="pi pi-search" />
|
|
<input
|
|
type="text"
|
|
className="p-inputtext p-component"
|
|
placeholder="Rechercher..."
|
|
value={globalFilter}
|
|
onChange={(e) => setGlobalFilter(e.target.value)}
|
|
/>
|
|
</span>
|
|
</div>
|
|
) : undefined
|
|
}
|
|
>
|
|
{visibleColumns.includes('nom') && (
|
|
<Column
|
|
field="nom"
|
|
header="Phase"
|
|
body={nameBodyTemplate}
|
|
sortable
|
|
style={{ minWidth: compactView ? '200px' : '250px' }}
|
|
/>
|
|
)}
|
|
|
|
{visibleColumns.includes('statut') && (
|
|
<Column
|
|
field="statut"
|
|
header="Statut"
|
|
body={statusBodyTemplate}
|
|
sortable
|
|
style={{ width: '120px' }}
|
|
/>
|
|
)}
|
|
|
|
{visibleColumns.includes('pourcentageAvancement') && (
|
|
<Column
|
|
field="pourcentageAvancement"
|
|
header="Avancement"
|
|
body={progressBodyTemplate}
|
|
sortable
|
|
style={{ width: compactView ? '120px' : '150px' }}
|
|
/>
|
|
)}
|
|
|
|
{visibleColumns.includes('dateDebutPrevue') && (
|
|
<Column
|
|
field="dateDebutPrevue"
|
|
header="Début prévu"
|
|
body={dateBodyTemplate('dateDebutPrevue')}
|
|
sortable
|
|
style={{ width: '130px' }}
|
|
/>
|
|
)}
|
|
|
|
{visibleColumns.includes('dateFinPrevue') && (
|
|
<Column
|
|
field="dateFinPrevue"
|
|
header="Fin prévue"
|
|
body={dateBodyTemplate('dateFinPrevue')}
|
|
sortable
|
|
style={{ width: '130px' }}
|
|
/>
|
|
)}
|
|
|
|
{visibleColumns.includes('validation') && (
|
|
<Column
|
|
header="Validation"
|
|
body={validationBodyTemplate}
|
|
style={{ width: '140px' }}
|
|
/>
|
|
)}
|
|
|
|
{visibleColumns.includes('actions') && (
|
|
<Column
|
|
header="Actions"
|
|
body={actionsBodyTemplate}
|
|
exportable={false}
|
|
style={{ width: '100px' }}
|
|
/>
|
|
)}
|
|
</DataTable>
|
|
|
|
{/* Informations sur la phase sélectionnée - Style Atlantis */}
|
|
{selectedPhase && (
|
|
<div className="card mt-3">
|
|
<div className="card-header">
|
|
<h6 className="m-0">Phase sélectionnée</h6>
|
|
</div>
|
|
<p className="m-0 mt-2">
|
|
<strong>{selectedPhase.nom}</strong> - {selectedPhase.statut} -
|
|
{selectedPhase.pourcentageAvancement || 0}% d'avancement
|
|
</p>
|
|
{selectedPhase.description && (
|
|
<p className="mt-2 mb-0 text-color-secondary">{selectedPhase.description}</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default AtlantisResponsivePhasesTable; |