Initial commit
This commit is contained in:
497
components/phases/wizard/TemplateSelectionStep.tsx
Normal file
497
components/phases/wizard/TemplateSelectionStep.tsx
Normal file
@@ -0,0 +1,497 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* Étape 1: Sélection et configuration du template de phases
|
||||
* Interface de choix du type de chantier avec preview des phases incluses
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card } from 'primereact/card';
|
||||
import { Button } from 'primereact/button';
|
||||
import { Tag } from 'primereact/tag';
|
||||
import { Badge } from 'primereact/badge';
|
||||
import { Skeleton } from 'primereact/skeleton';
|
||||
import { ScrollPanel } from 'primereact/scrollpanel';
|
||||
import { Divider } from 'primereact/divider';
|
||||
import { ProgressBar } from 'primereact/progressbar';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
import { Message } from 'primereact/message';
|
||||
import { TypeChantierTemplate, WizardConfiguration } from '../PhaseGenerationWizard';
|
||||
import { Chantier } from '../../../types/btp';
|
||||
|
||||
interface TemplateSelectionStepProps {
|
||||
templates: TypeChantierTemplate[];
|
||||
loading: boolean;
|
||||
configuration: WizardConfiguration;
|
||||
onConfigurationChange: (config: WizardConfiguration) => void;
|
||||
chantier?: Chantier;
|
||||
}
|
||||
|
||||
const TemplateSelectionStep: React.FC<TemplateSelectionStepProps> = ({
|
||||
templates,
|
||||
loading,
|
||||
configuration,
|
||||
onConfigurationChange,
|
||||
chantier
|
||||
}) => {
|
||||
const [searchFilter, setSearchFilter] = useState('');
|
||||
const [categorieFilter, setCategorieFilter] = useState('');
|
||||
const [complexiteFilter, setComplexiteFilter] = useState('');
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<TypeChantierTemplate | null>(configuration.typeChantier);
|
||||
|
||||
// Options de filtrage
|
||||
const categorieOptions = [
|
||||
{ label: 'Toutes catégories', value: '' },
|
||||
{ label: 'Résidentiel', value: 'RESIDENTIEL' },
|
||||
{ label: 'Commercial', value: 'COMMERCIAL' },
|
||||
{ label: 'Industriel', value: 'INDUSTRIEL' },
|
||||
{ label: 'Infrastructure', value: 'INFRASTRUCTURE' },
|
||||
{ label: 'Rénovation', value: 'RENOVATION' }
|
||||
];
|
||||
|
||||
const complexiteOptions = [
|
||||
{ label: 'Toutes complexités', value: '' },
|
||||
{ label: 'Simple', value: 'SIMPLE' },
|
||||
{ label: 'Moyenne', value: 'MOYENNE' },
|
||||
{ label: 'Complexe', value: 'COMPLEXE' },
|
||||
{ label: 'Expert', value: 'EXPERT' }
|
||||
];
|
||||
|
||||
// Filtrer les templates
|
||||
const filteredTemplates = templates.filter(template => {
|
||||
let matches = true;
|
||||
|
||||
if (searchFilter) {
|
||||
const search = searchFilter.toLowerCase();
|
||||
matches = matches && (
|
||||
template.nom.toLowerCase().includes(search) ||
|
||||
template.description.toLowerCase().includes(search) ||
|
||||
template.tags.some(tag => tag.toLowerCase().includes(search))
|
||||
);
|
||||
}
|
||||
|
||||
if (categorieFilter) {
|
||||
matches = matches && template.categorie === categorieFilter;
|
||||
}
|
||||
|
||||
if (complexiteFilter) {
|
||||
matches = matches && template.complexiteMetier === complexiteFilter;
|
||||
}
|
||||
|
||||
return matches;
|
||||
});
|
||||
|
||||
// Sélectionner un template
|
||||
const selectTemplate = (template: TypeChantierTemplate) => {
|
||||
setSelectedTemplate(template);
|
||||
|
||||
const newConfig = {
|
||||
...configuration,
|
||||
typeChantier: template,
|
||||
phasesSelectionnees: template.phases, // Par défaut, toutes les phases sont sélectionnées
|
||||
// Préserver les valeurs du chantier si elles existent, sinon utiliser celles du template
|
||||
budgetGlobal: configuration.budgetGlobal || template.budgetGlobalEstime,
|
||||
dureeGlobale: configuration.dureeGlobale || template.dureeGlobaleEstimee
|
||||
};
|
||||
|
||||
onConfigurationChange(newConfig);
|
||||
};
|
||||
|
||||
// Template de carte de template
|
||||
const templateCard = (template: TypeChantierTemplate) => {
|
||||
const isSelected = selectedTemplate?.id === template.id;
|
||||
|
||||
const complexiteSeverityMap = {
|
||||
'SIMPLE': 'success',
|
||||
'MOYENNE': 'info',
|
||||
'COMPLEXE': 'warning',
|
||||
'EXPERT': 'danger'
|
||||
} as const;
|
||||
|
||||
const cardHeader = (
|
||||
<div className="flex justify-content-between align-items-start">
|
||||
<div className="flex-1">
|
||||
<h5 className="m-0 mb-2">{template.nom}</h5>
|
||||
<p className="text-color-secondary text-sm m-0 mb-3 line-height-3">
|
||||
{template.description}
|
||||
</p>
|
||||
</div>
|
||||
{isSelected && (
|
||||
<i className="pi pi-check-circle text-green-500 text-2xl ml-2"></i>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const cardFooter = (
|
||||
<div className="flex justify-content-between align-items-center">
|
||||
<div className="flex align-items-center gap-2">
|
||||
<Tag
|
||||
value={template.categorie}
|
||||
severity="info"
|
||||
className="text-xs"
|
||||
/>
|
||||
<Tag
|
||||
value={template.complexiteMetier}
|
||||
severity={complexiteSeverityMap[template.complexiteMetier]}
|
||||
className="text-xs"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
label={isSelected ? "Sélectionné" : "Sélectionner"}
|
||||
icon={isSelected ? "pi pi-check" : "pi pi-arrow-right"}
|
||||
className={isSelected ? "p-button-text p-button-rounded p-button-success" : "p-button-text p-button-rounded"}
|
||||
size="small"
|
||||
onClick={() => selectTemplate(template)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={template.id}
|
||||
header={cardHeader}
|
||||
footer={cardFooter}
|
||||
className={`h-full cursor-pointer transition-all duration-200 ${
|
||||
isSelected
|
||||
? 'border-primary-500 shadow-4'
|
||||
: 'hover:shadow-2 border-transparent'
|
||||
}`}
|
||||
onClick={() => selectTemplate(template)}
|
||||
>
|
||||
<div className="grid">
|
||||
<div className="col-12 md:col-6">
|
||||
<div className="flex align-items-center gap-2 mb-2">
|
||||
<i className="pi pi-sitemap text-primary"></i>
|
||||
<span className="font-semibold">{template.nombreTotalPhases} phases</span>
|
||||
</div>
|
||||
<div className="flex align-items-center gap-2 mb-2">
|
||||
<i className="pi pi-clock text-orange-500"></i>
|
||||
<span>{template.dureeGlobaleEstimee} jours</span>
|
||||
</div>
|
||||
<div className="flex align-items-center gap-2">
|
||||
<i className="pi pi-euro text-green-500"></i>
|
||||
<span>
|
||||
{new Intl.NumberFormat('fr-FR', {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
minimumFractionDigits: 0
|
||||
}).format(template.budgetGlobalEstime)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 md:col-6">
|
||||
<div className="flex align-items-center gap-1 mb-2">
|
||||
<span className="text-sm text-color-secondary">Catégories:</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{template.phases.slice(0, 3).map((phase, index) => (
|
||||
<Badge
|
||||
key={index}
|
||||
value={phase.categorieMetier}
|
||||
severity="info"
|
||||
className="text-xs"
|
||||
/>
|
||||
))}
|
||||
{template.phases.length > 3 && (
|
||||
<Badge
|
||||
value={`+${template.phases.length - 3}`}
|
||||
severity="secondary"
|
||||
className="text-xs"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{template.tags.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{template.tags.slice(0, 2).map((tag, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-block bg-blue-50 text-blue-700 px-2 py-1 border-round text-xs"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
{template.tags.length > 2 && (
|
||||
<span className="inline-block bg-gray-100 text-gray-600 px-2 py-1 border-round text-xs">
|
||||
+{template.tags.length - 2}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
// Prévisualisation du template sélectionné
|
||||
const previewPanel = () => {
|
||||
if (!selectedTemplate) return null;
|
||||
|
||||
return (
|
||||
<Card className="mt-4">
|
||||
<div className="flex align-items-center gap-3 mb-4">
|
||||
<i className="pi pi-eye text-primary text-2xl"></i>
|
||||
<h5 className="m-0">Prévisualisation: {selectedTemplate.nom}</h5>
|
||||
</div>
|
||||
|
||||
<div className="grid">
|
||||
<div className="col-12 md:col-4">
|
||||
<Card className="bg-blue-50 h-full">
|
||||
<div className="text-center">
|
||||
<i className="pi pi-sitemap text-blue-600 text-3xl mb-3"></i>
|
||||
<h6 className="text-blue-800 m-0 mb-2">Phases incluses</h6>
|
||||
<div className="text-blue-900 font-bold text-2xl">
|
||||
{selectedTemplate.nombreTotalPhases}
|
||||
</div>
|
||||
<small className="text-blue-600">
|
||||
phases + sous-phases
|
||||
</small>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="col-12 md:col-4">
|
||||
<Card className="bg-orange-50 h-full">
|
||||
<div className="text-center">
|
||||
<i className="pi pi-clock text-orange-600 text-3xl mb-3"></i>
|
||||
<h6 className="text-orange-800 m-0 mb-2">Durée estimée</h6>
|
||||
<div className="text-orange-900 font-bold text-2xl">
|
||||
{selectedTemplate.dureeGlobaleEstimee}
|
||||
</div>
|
||||
<small className="text-orange-600">
|
||||
jours ouvrés
|
||||
</small>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="col-12 md:col-4">
|
||||
<Card className="bg-green-50 h-full">
|
||||
<div className="text-center">
|
||||
<i className="pi pi-euro text-green-600 text-3xl mb-3"></i>
|
||||
<h6 className="text-green-800 m-0 mb-2">Budget estimé</h6>
|
||||
<div className="text-green-900 font-bold text-xl">
|
||||
{new Intl.NumberFormat('fr-FR', {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
minimumFractionDigits: 0
|
||||
}).format(selectedTemplate.budgetGlobalEstime)}
|
||||
</div>
|
||||
<small className="text-green-600">
|
||||
estimation globale
|
||||
</small>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div className="grid">
|
||||
<div className="col-12 md:col-6">
|
||||
<h6 className="text-primary mb-3">
|
||||
<i className="pi pi-list mr-2"></i>
|
||||
Phases principales ({selectedTemplate.phases.length})
|
||||
</h6>
|
||||
<ScrollPanel style={{ width: '100%', height: '200px' }}>
|
||||
{selectedTemplate.phases.map((phase, index) => (
|
||||
<div key={phase.id} className="flex align-items-center gap-2 mb-2 p-2 bg-gray-50 border-round">
|
||||
<Badge value={index + 1} className="bg-primary" />
|
||||
<div className="flex-1">
|
||||
<div className="font-semibold text-sm">{phase.nom}</div>
|
||||
<small className="text-color-secondary">
|
||||
{phase.dureeEstimee}j • {phase.sousPhases.length} sous-phases
|
||||
</small>
|
||||
</div>
|
||||
<Tag
|
||||
value={phase.categorieMetier}
|
||||
severity="info"
|
||||
className="text-xs"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</ScrollPanel>
|
||||
</div>
|
||||
<div className="col-12 md:col-6">
|
||||
<h6 className="text-primary mb-3">
|
||||
<i className="pi pi-tags mr-2"></i>
|
||||
Caractéristiques du template
|
||||
</h6>
|
||||
<div className="flex flex-column gap-3">
|
||||
<div>
|
||||
<span className="font-semibold">Catégorie:</span>
|
||||
<Tag value={selectedTemplate.categorie} severity="info" className="ml-2" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">Complexité:</span>
|
||||
<Tag
|
||||
value={selectedTemplate.complexiteMetier}
|
||||
severity={
|
||||
selectedTemplate.complexiteMetier === 'SIMPLE' ? 'success' :
|
||||
selectedTemplate.complexiteMetier === 'MOYENNE' ? 'info' :
|
||||
selectedTemplate.complexiteMetier === 'COMPLEXE' ? 'warning' : 'danger'
|
||||
}
|
||||
className="ml-2"
|
||||
/>
|
||||
</div>
|
||||
{selectedTemplate.tags.length > 0 && (
|
||||
<div>
|
||||
<span className="font-semibold mb-2 block">Tags:</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{selectedTemplate.tags.map((tag, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-block bg-blue-50 text-blue-700 px-2 py-1 border-round text-xs"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex align-items-center gap-3 mb-4">
|
||||
<i className="pi pi-th-large text-primary text-2xl"></i>
|
||||
<h4 className="m-0">Sélection du Template de Chantier</h4>
|
||||
</div>
|
||||
|
||||
{/* Informations du chantier */}
|
||||
{chantier && (
|
||||
<Card className="mb-4" style={{ backgroundColor: 'var(--surface-50)', border: '1px solid var(--primary-200)' }}>
|
||||
<div className="flex align-items-center gap-3 mb-3">
|
||||
<i className="pi pi-info-circle text-primary"></i>
|
||||
<h6 className="m-0 text-color">Informations du chantier</h6>
|
||||
</div>
|
||||
<div className="grid">
|
||||
<div className="col-12 md:col-6">
|
||||
<div className="flex flex-column gap-2">
|
||||
<div className="text-color">
|
||||
<span className="font-semibold">Nom :</span> {chantier.nom}
|
||||
</div>
|
||||
{chantier.typeChantier && (
|
||||
<div className="text-color">
|
||||
<span className="font-semibold">Type :</span> {chantier.typeChantier}
|
||||
</div>
|
||||
)}
|
||||
{chantier.surface && (
|
||||
<div className="text-color">
|
||||
<span className="font-semibold">Surface :</span> {chantier.surface} m²
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 md:col-6">
|
||||
<div className="flex flex-column gap-2">
|
||||
{chantier.montantPrevu && (
|
||||
<div className="text-color">
|
||||
<span className="font-semibold">Budget prévu :</span> {chantier.montantPrevu.toLocaleString('fr-FR', {
|
||||
style: 'currency',
|
||||
currency: 'EUR'
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{chantier.dateDebut && (
|
||||
<div className="text-color">
|
||||
<span className="font-semibold">Date de début :</span> {new Date(chantier.dateDebut).toLocaleDateString('fr-FR')}
|
||||
</div>
|
||||
)}
|
||||
{chantier.dateFinPrevue && (
|
||||
<div className="text-color">
|
||||
<span className="font-semibold">Date de fin prévue :</span> {new Date(chantier.dateFinPrevue).toLocaleDateString('fr-FR')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 p-2 surface-ground border-round">
|
||||
<small className="text-primary">
|
||||
<i className="pi pi-lightbulb mr-1"></i>
|
||||
Ces informations seront utilisées pour personnaliser les phases générées
|
||||
</small>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Message
|
||||
severity="info"
|
||||
text="Choisissez le type de chantier qui correspond le mieux à votre projet. Le template sélectionné déterminera les phases qui seront générées automatiquement."
|
||||
className="mb-4"
|
||||
/>
|
||||
|
||||
{/* Filtres */}
|
||||
<Card className="mb-4">
|
||||
<div className="grid">
|
||||
<div className="col-12 md:col-4">
|
||||
<span className="p-input-icon-left w-full">
|
||||
<i className="pi pi-search" />
|
||||
<InputText
|
||||
value={searchFilter}
|
||||
onChange={(e) => setSearchFilter(e.target.value)}
|
||||
placeholder="Rechercher un template..."
|
||||
className="w-full"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-12 md:col-4">
|
||||
<Dropdown
|
||||
value={categorieFilter}
|
||||
options={categorieOptions}
|
||||
onChange={(e) => setCategorieFilter(e.value)}
|
||||
placeholder="Filtrer par catégorie"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 md:col-4">
|
||||
<Dropdown
|
||||
value={complexiteFilter}
|
||||
options={complexiteOptions}
|
||||
onChange={(e) => setComplexiteFilter(e.value)}
|
||||
placeholder="Filtrer par complexité"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Liste des templates */}
|
||||
{loading ? (
|
||||
<div className="grid">
|
||||
{[1, 2, 3, 4].map((item) => (
|
||||
<div key={item} className="col-12 md:col-6 xl:col-4">
|
||||
<Card>
|
||||
<Skeleton width="100%" height="200px" />
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : filteredTemplates.length === 0 ? (
|
||||
<Message
|
||||
severity="warn"
|
||||
text="Aucun template trouvé avec les critères de recherche actuels."
|
||||
/>
|
||||
) : (
|
||||
<div className="grid">
|
||||
{filteredTemplates.map((template) => (
|
||||
<div key={template.id} className="col-12 md:col-6 xl:col-4">
|
||||
{templateCard(template)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Prévisualisation */}
|
||||
{previewPanel()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateSelectionStep;
|
||||
Reference in New Issue
Block a user