Initial commit
This commit is contained in:
371
components/phases/AtlantisResponsivePhasesTable.tsx
Normal file
371
components/phases/AtlantisResponsivePhasesTable.tsx
Normal file
@@ -0,0 +1,371 @@
|
||||
'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';
|
||||
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;
|
||||
Reference in New Issue
Block a user