RELEASE V.1.2.1 - Voici les fonctionnalités :

->Refonte visuelle + Prise en charge du mode sombre par l'ensemble des composants.
This commit is contained in:
mathis committed 2026-09-22 22:14:21 +02:00
1 parent d61422ddbf
commit 37ab831aac
44 files changed
+17261 -7636

No files matched your search

+339 -150
View File
@@ -8,13 +8,13 @@ import './Travaux.css';
import RichTextEditor from '../gares/RichTextEditor';
const JOURS_SEMAINE = [
{ value: 'lundi', label: 'Lundi' },
{ value: 'mardi', label: 'Mardi' },
{ value: 'mercredi', label: 'Mercredi' },
{ value: 'jeudi', label: 'Jeudi' },
{ value: 'vendredi', label: 'Vendredi' },
{ value: 'samedi', label: 'Samedi' },
{ value: 'dimanche', label: 'Dimanche' }
{ value: 'lundi', label: 'Lundi', short: 'Lun' },
{ value: 'mardi', label: 'Mardi', short: 'Mar' },
{ value: 'mercredi', label: 'Mercredi', short: 'Mer' },
{ value: 'jeudi', label: 'Jeudi', short: 'Jeu' },
{ value: 'vendredi', label: 'Vendredi', short: 'Ven' },
{ value: 'samedi', label: 'Samedi', short: 'Sam' },
{ value: 'dimanche', label: 'Dimanche', short: 'Dim' }
];
export default function CreerTravaux() {
@@ -25,9 +25,9 @@ export default function CreerTravaux() {
const [description, setDescription] = useState('');
const [dateDebut, setDateDebut] = useState('');
const [dateFin, setDateFin] = useState('');
const [joursTravaux, setJoursTravaux] = useState([]);
const [heureDebut, setHeureDebut] = useState('');
const [heureFin, setHeureFin] = useState('');
const [joursTravaux, setJoursTravaux] = useState(['lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi', 'dimanche']);
const [heureDebut, setHeureDebut] = useState('22:00');
const [heureFin, setHeureFin] = useState('06:00');
const [actif, setActif] = useState(true);
// Substitutions
@@ -96,21 +96,33 @@ export default function CreerTravaux() {
}
};
const selectAllDays = () => {
setJoursTravaux(JOURS_SEMAINE.map(j => j.value));
};
const selectWeekdays = () => {
setJoursTravaux(['lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi']);
};
const selectWeekend = () => {
setJoursTravaux(['samedi', 'dimanche']);
};
const formatHeure = (heure) => heure ? heure.substring(0, 5) : '—';
const navigate = (path) => { window.location.href = path; };
const handleCreate = async () => {
if (!canCreateTravaux) {
alert('Vous n\'avez pas les droits pour créer des travaux.');
alert("Vous n'avez pas les droits pour créer des travaux.");
return;
}
if (!titre.trim() || !dateDebut || !dateFin || joursTravaux.length === 0 || !heureDebut || !heureFin) {
alert('Veuillez remplir tous les champs obligatoires');
alert('Veuillez remplir tous les champs obligatoires (*)');
return;
}
if (new Date(dateFin) < new Date(dateDebut)) {
alert('La date de fin doit être postérieure à la date de début');
alert('La date de fin doit être postérieure ou égale à la date de début.');
return;
}
@@ -134,171 +146,348 @@ export default function CreerTravaux() {
const { error } = await createTravaux(payload, substitutionsIds);
if (error) {
alert('Erreur lors de la création des travaux');
alert('Erreur lors de la création des travaux : ' + (error.message || 'Erreur'));
} else {
navigate('/travaux');
}
} catch (err) {
alert('Une erreur inattendue s\'est produite');
alert("Une erreur inattendue s'est produite");
} finally {
setSaving(false);
}
};
return (
<div className="sncf-groupe panel-layout">
<div className="panel-nav"><NavPanel /></div>
<div className="panel-content travaux-form-page-content">
<div className="sncf-groupe travaux-page-root">
<div className="panel-layout">
<div className="panel-nav">
<NavPanel />
</div>
<header className="travaux-page-header" style={{ borderBottom: 'none', padding: '0 0 30px 0', background: 'transparent' }}>
<div>
<p className="travaux-page-kicker">Création</p>
<h1>Créer des travaux</h1>
<p className="travaux-page-description">Planifiez de nouveaux travaux et leurs substitutions.</p>
</div>
<wcs-button className="wcs-secondary" onClick={() => navigate('/travaux')}>
<wcs-mat-icon icon="close"></wcs-mat-icon> Annuler
</wcs-button>
</header>
<div className="travaux-glass-card">
<h2 className="travaux-section-title"><wcs-mat-icon icon="info"></wcs-mat-icon> Informations Générales</h2>
<p className="travaux-section-subtitle">Saisissez un titre clair et une description détaillée pour ces travaux.</p>
<div className="premium-input-group">
<label className="premium-label">Titre des travaux *</label>
<input type="text" className="premium-input" placeholder="Ex: Maintenance des voies Paris-Lyon" value={titre} onChange={(e) => setTitre(e.target.value)} />
</div>
<div className="premium-input-group">
<label className="premium-label">Description détaillée</label>
<RichTextEditor value={description} onChange={setDescription} placeholder="Contexte opérationnel, impacts prévus..." />
</div>
<div className="premium-input-group">
<label className="premium-label">Ligne ferroviaire associée</label>
<select
className="premium-input"
value={selectedLigneId}
onChange={(e) => setSelectedLigneId(e.target.value)}
disabled={loadingLignes}
>
<option value="">Sélectionner une ligne (optionnel)</option>
{lignes.map(l => (
<option key={l.id} value={l.id}>{l.nom}</option>
))}
</select>
</div>
</div>
<div className="travaux-glass-card">
<h2 className="travaux-section-title"><wcs-mat-icon icon="event"></wcs-mat-icon> Planification</h2>
<p className="travaux-section-subtitle">Définissez la période, les jours concernés et la plage horaire d'intervention.</p>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '20px' }}>
<div className="premium-input-group">
<label className="premium-label">Date de début *</label>
<input type="date" className="premium-input" value={dateDebut} onChange={(e) => setDateDebut(e.target.value)} />
<div className="panel-content travaux-form-page-content">
{/* Header Banner */}
<header className="travaux-form-header-modern">
<div className="form-header-main">
<button type="button" className="btn-back-link" onClick={() => navigate('/travaux')}>
<wcs-mat-icon icon="arrow_back"></wcs-mat-icon>
<span>Retour aux travaux</span>
</button>
<div className="form-title-group">
<span className="travaux-kicker-pill">
<wcs-mat-icon icon="add_circle"></wcs-mat-icon>
Nouveau Chantier
</span>
<h1>Déclarer des travaux sur le réseau</h1>
<p>Planifiez une coupure, des restrictions de circulation et les substitutions associées.</p>
</div>
</div>
<div className="premium-input-group">
<label className="premium-label">Date de fin *</label>
<input type="date" className="premium-input" value={dateFin} onChange={(e) => setDateFin(e.target.value)} />
</div>
<div className="premium-input-group">
<label className="premium-label">Heure de début *</label>
<input type="time" className="premium-input" value={heureDebut} onChange={(e) => setHeureDebut(e.target.value)} />
</div>
<div className="premium-input-group">
<label className="premium-label">Heure de fin *</label>
<input type="time" className="premium-input" value={heureFin} onChange={(e) => setHeureFin(e.target.value)} />
</div>
</div>
<div className="premium-input-group" style={{ marginTop: '10px' }}>
<label className="premium-label">Jours de travaux *</label>
<div className="premium-checkbox-group">
{JOURS_SEMAINE.map((jour) => (
<label key={jour.value} className={`premium-checkbox-label ${joursTravaux.includes(jour.value) ? 'selected' : ''}`}>
<input type="checkbox" checked={joursTravaux.includes(jour.value)} onChange={() => toggleJour(jour.value)} />
{jour.label}
</label>
))}
<div className="form-header-actions">
<wcs-button className="wcs-secondary" mode="stroked" onClick={() => navigate('/travaux')}>
Annuler
</wcs-button>
<wcs-button
className="wcs-primary"
onClick={handleCreate}
disabled={saving || !canCreateTravaux}
>
{saving ? (
<>
<wcs-spinner mode="border" style={{ width: '16px', height: '16px', marginRight: '6px' }}></wcs-spinner>
<span>Enregistrement...</span>
</>
) : (
<>
<wcs-mat-icon icon="save" style={{ marginRight: '6px' }}></wcs-mat-icon>
<span>Créer le chantier</span>
</>
)}
</wcs-button>
</div>
</div>
</div>
</header>
<div className="travaux-glass-card">
<h2 className="travaux-section-title"><wcs-mat-icon icon="swap_horiz"></wcs-mat-icon> Substitutions (Optionnel)</h2>
<p className="travaux-section-subtitle">Gérez les trains ou cars de substitution associés à ces travaux.</p>
<label className={`premium-checkbox-label ${substitutionsActive ? 'selected' : ''}`} style={{ display: 'inline-flex', marginBottom: '20px' }}>
<input type="checkbox" checked={substitutionsActive} onChange={(e) => setSubstitutionsActive(e.target.checked)} />
Activer la recherche de substitutions
</label>
{substitutionsActive && (
<div>
<div className="premium-input-group" style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<input type="text" className="premium-input" placeholder="Numéro de train, gare..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} onKeyPress={(e) => { if (e.key === 'Enter') handleSearchHoraires(); }} />
<wcs-button className="wcs-primary" onClick={handleSearchHoraires} style={{ flexShrink: 0, height: '46px' }}>Rechercher</wcs-button>
<div className="travaux-form-grid">
{/* Section 1 : Informations générales */}
<div className="travaux-glass-card">
<div className="section-title-wrap">
<div className="section-icon-box">
<wcs-mat-icon icon="info"></wcs-mat-icon>
</div>
<div>
<h2 className="travaux-section-title">Informations Générales</h2>
<p className="travaux-section-subtitle">Définissez l'intitulé, la ligne concernée et les consignes d'exploitation.</p>
</div>
</div>
<div className="premium-input-group">
<label className="premium-label">Titre des travaux *</label>
<input
type="text"
className="premium-input"
placeholder="Ex: Rénovation caténaire et voies Paris vers Lyon"
value={titre}
onChange={(e) => setTitre(e.target.value)}
/>
</div>
{horairesSelectionnes.length > 0 && (
<div style={{ marginTop: '20px', background: '#eff6ff', padding: '15px', borderRadius: '10px', border: '1px solid #bfdbfe' }}>
<h4 style={{ margin: '0 0 10px 0', color: '#1d4ed8' }}>{horairesSelectionnes.length} Substitution(s) Sélectionnée(s)</h4>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{horairesSelectionnes.map(h => (
<div key={h.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: '#fff', padding: '10px', borderRadius: '8px', border: '1px solid #dbeafe' }}>
<div>
<strong style={{ color: '#1e293b' }}>{h.numero_train}</strong> <span style={{ color: '#64748b', fontSize: '0.9rem' }}>— {h.gare_depart_nom} → {h.gare_arrivee_nom}</span>
</div>
<wcs-button className="wcs-danger wcs-small" onClick={() => toggleHoraireSelection(h)}><wcs-mat-icon icon="close"></wcs-mat-icon></wcs-button>
</div>
))}
<div className="premium-input-group">
<label className="premium-label">Ligne ferroviaire associée</label>
<select
className="premium-input"
value={selectedLigneId}
onChange={(e) => setSelectedLigneId(e.target.value)}
disabled={loadingLignes}
>
<option value="">Sélectionner une ligne (optionnel)</option>
{lignes.map(l => (
<option key={l.id} value={l.id}>{l.nom || l.numero || `Ligne ${l.id}`}</option>
))}
</select>
</div>
<div className="premium-input-group">
<label className="premium-label">Description détaillée & Informations voyageurs</label>
<RichTextEditor
value={description}
onChange={setDescription}
placeholder="Détaillez les impacts prévus, motifs techniques, gares concernées..."
/>
</div>
</div>
{/* Section 2 : Planification & Calendrier */}
<div className="travaux-glass-card">
<div className="section-title-wrap">
<div className="section-icon-box">
<wcs-mat-icon icon="calendar_month"></wcs-mat-icon>
</div>
<div>
<h2 className="travaux-section-title">Planification & Horaires</h2>
<p className="travaux-section-subtitle">Période de validité du chantier, jours impactés et créneaux journaliers.</p>
</div>
</div>
<div className="form-columns-2">
<div className="premium-input-group">
<label className="premium-label">Date de début *</label>
<input type="date" className="premium-input" value={dateDebut} onChange={(e) => setDateDebut(e.target.value)} />
</div>
<div className="premium-input-group">
<label className="premium-label">Date de fin *</label>
<input type="date" className="premium-input" value={dateFin} onChange={(e) => setDateFin(e.target.value)} />
</div>
</div>
<div className="form-columns-2">
<div className="premium-input-group">
<label className="premium-label">Heure de début journalière *</label>
<input type="time" className="premium-input" value={heureDebut} onChange={(e) => setHeureDebut(e.target.value)} />
</div>
<div className="premium-input-group">
<label className="premium-label">Heure de fin journalière *</label>
<input type="time" className="premium-input" value={heureFin} onChange={(e) => setHeureFin(e.target.value)} />
</div>
</div>
<div className="premium-input-group" style={{ marginTop: '12px' }}>
<div className="days-header-row">
<label className="premium-label">Jours impactés dans la semaine *</label>
<div className="days-shortcuts">
<button type="button" className="btn-shortcut" onClick={selectAllDays}>Tous les jours</button>
<button type="button" className="btn-shortcut" onClick={selectWeekdays}>Semaine</button>
<button type="button" className="btn-shortcut" onClick={selectWeekend}>Week-end</button>
</div>
</div>
)}
{showResults && (
<div style={{ marginTop: '20px' }}>
<h4 style={{ marginBottom: '10px', color: '#334155' }}>Résultats ({horairesDisponibles.length})</h4>
{loadingHoraires ? (
<div style={{ padding: '20px', textAlign: 'center' }}><wcs-spinner></wcs-spinner></div>
) : (
<div style={{ maxHeight: '300px', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '8px' }}>
{horairesDisponibles.map(h => {
const isSelected = horairesSelectionnes.some(hs => hs.id === h.id);
return (
<div key={h.id} onClick={() => toggleHoraireSelection(h)} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px', background: isSelected ? '#eff6ff' : '#f8fafc', border: `1px solid ${isSelected ? '#3b82f6' : '#e2e8f0'}`, borderRadius: '8px', cursor: 'pointer' }}>
<div>
<strong style={{ color: '#1e293b', marginRight: '8px' }}>{h.numero_train}</strong>
{h.type_train && <span style={{ fontSize: '0.8rem', background: '#e2e8f0', padding: '2px 6px', borderRadius: '4px', marginRight: '8px' }}>{h.type_train}</span>}
<span style={{ color: '#64748b', fontSize: '0.9rem' }}>{h.gare_depart_nom} ({formatHeure(h.heure_depart)}) → {h.gare_arrivee_nom} ({formatHeure(h.heure_arrivee)})</span>
<div className="premium-checkbox-group">
{JOURS_SEMAINE.map((jour) => (
<label key={jour.value} className={`premium-checkbox-label ${joursTravaux.includes(jour.value) ? 'selected' : ''}`}>
<input type="checkbox" checked={joursTravaux.includes(jour.value)} onChange={() => toggleJour(jour.value)} />
<span>{jour.label}</span>
</label>
))}
</div>
</div>
</div>
{/* Section 3 : Substitutions */}
<div className="travaux-glass-card">
<div className="section-title-wrap">
<div className="section-icon-box">
<wcs-mat-icon icon="swap_horiz"></wcs-mat-icon>
</div>
<div>
<h2 className="travaux-section-title">Plan de Transport de Substitution</h2>
<p className="travaux-section-subtitle">Associez des cars ou trains spéciaux pour remplacer les circulations supprimées.</p>
</div>
</div>
<label className={`premium-toggle-switch ${substitutionsActive ? 'active' : ''}`}>
<input
type="checkbox"
checked={substitutionsActive}
onChange={(e) => setSubstitutionsActive(e.target.checked)}
/>
<span className="switch-slider"></span>
<span className="switch-text">
<strong>Activer le plan de substitution pour ce chantier</strong>
<small>Permet de relier des horaires de cars ou trains de substitution actifs</small>
</span>
</label>
{substitutionsActive && (
<div className="substitutions-module-body">
<div className="substitutions-search-bar">
<div className="search-input-wrap">
<wcs-mat-icon icon="search" className="search-icon"></wcs-mat-icon>
<input
type="text"
className="premium-input"
placeholder="Rechercher par numéro de train, gare de départ ou terminus..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') handleSearchHoraires(); }}
/>
</div>
<wcs-button
className="wcs-primary"
onClick={handleSearchHoraires}
disabled={loadingHoraires}
>
<wcs-mat-icon icon="search" style={{ marginRight: '6px' }}></wcs-mat-icon>
<span>Rechercher</span>
</wcs-button>
</div>
{/* Horaires sélectionnés */}
{horairesSelectionnes.length > 0 && (
<div className="selected-subs-container">
<div className="selected-subs-header">
<h4>
<wcs-mat-icon icon="check_circle" style={{ color: '#0088ce', marginRight: '6px' }}></wcs-mat-icon>
{horairesSelectionnes.length} circulation{horairesSelectionnes.length > 1 ? 's' : ''} de substitution sélectionnée{horairesSelectionnes.length > 1 ? 's' : ''}
</h4>
</div>
<div className="selected-subs-list">
{horairesSelectionnes.map(h => (
<div key={h.id} className="selected-sub-card">
<div className="sub-main-col">
<div className="sub-num-badge">
<wcs-mat-icon icon="directions_bus"></wcs-mat-icon>
<strong>{h.numero_train || 'Train'}</strong>
{h.type_train && <wcs-badge className="wcs-info" shape="round">{h.type_train}</wcs-badge>}
</div>
<div className="sub-route">
<span>{h.gare_depart_nom} ({formatHeure(h.heure_depart)})</span>
<wcs-mat-icon icon="arrow_forward" className="arrow-icon"></wcs-mat-icon>
<span>{h.gare_arrivee_nom} ({formatHeure(h.heure_arrivee)})</span>
</div>
</div>
{isSelected && <wcs-mat-icon icon="check_circle" style={{ color: '#3b82f6' }}></wcs-mat-icon>}
<button
type="button"
className="btn-remove-sub"
onClick={() => toggleHoraireSelection(h)}
title="Retirer cette substitution"
>
<wcs-mat-icon icon="close"></wcs-mat-icon>
</button>
</div>
);
})}
))}
</div>
</div>
)}
{/* Résultats de recherche */}
{showResults && (
<div className="search-results-section">
<div className="search-results-header">
<h4>Résultats disponibles ({horairesDisponibles.length})</h4>
<span className="results-hint">Cliquez sur une circulation pour l'ajouter ou la retirer</span>
</div>
{loadingHoraires ? (
<div className="search-loading-box">
<wcs-spinner mode="border"></wcs-spinner>
<p>Recherche des circulations en cours...</p>
</div>
) : horairesDisponibles.length === 0 ? (
<div className="search-empty-box">
<p>Aucun horaire de substitution trouvé pour « {searchQuery} ».</p>
</div>
) : (
<div className="subs-results-list">
{horairesDisponibles.map(h => {
const isSelected = horairesSelectionnes.some(hs => hs.id === h.id);
return (
<div
key={h.id}
className={`sub-result-card ${isSelected ? 'is-selected' : ''}`}
onClick={() => toggleHoraireSelection(h)}
>
<div className="result-info">
<div className="result-badge-row">
<strong>{h.numero_train}</strong>
{h.type_train && <wcs-badge className="wcs-info" shape="round">{h.type_train}</wcs-badge>}
{h.ligne_nom && <wcs-badge className="wcs-secondary" shape="round">{h.ligne_nom}</wcs-badge>}
</div>
<div className="result-route-row">
<span>{h.gare_depart_nom} ({formatHeure(h.heure_depart)})</span>
<wcs-mat-icon icon="arrow_forward" className="arrow-icon"></wcs-mat-icon>
<span>{h.gare_arrivee_nom} ({formatHeure(h.heure_arrivee)})</span>
</div>
</div>
<div className="result-checkbox">
<wcs-mat-icon icon={isSelected ? "check_box" : "check_box_outline_blank"} className={isSelected ? "icon-checked" : ""}></wcs-mat-icon>
</div>
</div>
);
})}
</div>
)}
</div>
)}
</div>
)}
</div>
)}
</div>
{/* Bottom Action Bar */}
<div className="form-sticky-bottom-bar">
<label className={`premium-checkbox-label ${actif ? 'selected' : ''}`}>
<input type="checkbox" checked={actif} onChange={(e) => setActif(e.target.checked)} />
<span style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<wcs-badge className={actif ? "wcs-success" : "wcs-secondary"} shape="round">
<wcs-mat-icon icon={actif ? "visibility" : "visibility_off"} style={{ fontSize: '13px', marginRight: '4px' }}></wcs-mat-icon>
{actif ? 'Chantier Actif et visible' : 'Chantier Inactif / Masqué'}
</wcs-badge>
</span>
</label>
<div className="bottom-bar-btns">
<wcs-button className="wcs-secondary" mode="stroked" onClick={() => navigate('/travaux')}>
Annuler
</wcs-button>
<wcs-button
className="wcs-primary"
onClick={handleCreate}
disabled={saving || !canCreateTravaux}
>
{saving ? (
<>
<wcs-spinner mode="border" style={{ width: '16px', height: '16px', marginRight: '6px' }}></wcs-spinner>
<span>Enregistrement...</span>
</>
) : (
<>
<wcs-mat-icon icon="check" style={{ marginRight: '6px' }}></wcs-mat-icon>
<span>Enregistrer les travaux</span>
</>
)}
</wcs-button>
</div>
</div>
</div>
<div className="premium-actions">
<label className={`premium-checkbox-label ${actif ? 'selected' : ''}`} style={{ marginRight: 'auto' }}>
<input type="checkbox" checked={actif} onChange={(e) => setActif(e.target.checked)} />
{actif ? '✔ Travaux Actifs' : 'Travaux Inactifs'}
</label>
<wcs-button className="wcs-secondary" onClick={() => navigate('/travaux')}>Annuler</wcs-button>
<wcs-button className="wcs-primary" onClick={handleCreate} disabled={saving || !canCreateTravaux}>
{saving ? <wcs-spinner></wcs-spinner> : 'Enregistrer les travaux'}
</wcs-button>
</div>
</div>
</div>
);