-
Notifications
You must be signed in to change notification settings - Fork 0
Explication de mon code cev3
PasRP-Theo edited this page Jun 5, 2025
·
1 revision
Le localStorage est une API Web qui permet de stocker des données localement dans le navigateur de l'utilisateur. Dans cette application, il est utilisé de manière stratégique pour persister l'historique des suppressions.
- Persistence des données supprimées : Contrairement aux données actives qui sont stockées en base de données, l'historique des suppressions est temporaire et personnel à chaque utilisateur
- Performance : Évite des requêtes serveur supplémentaires pour des données d'historique
- Expérience utilisateur : L'historique persiste même après fermeture/réouverture du navigateur
useEffect(() => {
// Chargement des travaux supprimés
const savedDeletedTravaux = localStorage.getItem('deletedTravaux');
if (savedDeletedTravaux) {
try {
setDeletedTravaux(JSON.parse(savedDeletedTravaux));
} catch (error) {
console.error("Erreur lors de la récupération de l'historique travaux :", error);
}
}
// Même logique pour les entretiens
const savedDeletedEntretiens = localStorage.getItem('deletedEntretiens');
if (savedDeletedEntretiens) {
try {
setDeletedEntretiens(JSON.parse(savedDeletedEntretiens));
} catch (error) {
console.error("Erreur lors de la récupération de l'historique entretiens :", error);
}
}
}, []);// Sauvegarde des travaux supprimés
useEffect(() => {
if (deletedTravaux.length > 0) {
localStorage.setItem('deletedTravaux', JSON.stringify(deletedTravaux));
} else {
localStorage.removeItem('deletedTravaux'); // Nettoyage si vide
}
}, [deletedTravaux]);
// Même logique pour les entretiens supprimés
useEffect(() => {
if (deletedEntretiens.length > 0) {
localStorage.setItem('deletedEntretiens', JSON.stringify(deletedEntretiens));
} else {
localStorage.removeItem('deletedEntretiens');
}
}, [deletedEntretiens]);const handleClearHistory = (type) => {
if (type === 'travaux') {
setDeletedTravaux([]);
localStorage.removeItem('deletedTravaux'); // Suppression explicite
} else {
setDeletedEntretiens([]);
localStorage.removeItem('deletedEntretiens');
}
};- Automatique : Sauvegarde/chargement transparent
- Sécurisée : Gestion des erreurs de parsing JSON
- Optimisée : Suppression des clés vides pour éviter l'encombrement
- Cohérente : Synchronisation permanente entre état React et localStorage
Cette application React permet de gérer deux types d'éléments : les travaux et les entretiens. Elle offre des fonctionnalités CRUD complètes (Create, Read, Update, Delete) avec une fonctionnalité unique de restauration d'éléments supprimés.
// Hooks React essentiels
import { useState, useEffect, useCallback } from 'react';
// Navigation
import { useNavigate } from 'react-router-dom';
// Requêtes HTTP
import axios from 'axios';
// Composants Material-UI
import {
TextField, Button, Typography, Box, Container,
CircularProgress, Accordion, AccordionSummary, AccordionDetails,
Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle,
Tabs, Tab,
} from '@mui/material';import {
calculateDuree, // Calcul de durée entre deux dates
formatDateForInput, // Formatage date pour input HTML
formatTimeForInput, // Formatage heure pour input HTML
formatDisplayDate, // Formatage date pour affichage
calculateTotalCout, // Calcul du coût total
createRequestData, // Création des données pour l'API
createDeletedTravailWithDate, // Ajout date de suppression aux travaux
createDeletedEntretiensWithDate, // Ajout date de suppression aux entretiens
validateForm // Validation des formulaires
} from '../utils/logicTravauxEntretiens';import TravauxTable from './TravauxTable'; // Tableau principal
import TravauxForm from './TravauxForm'; // Formulaire
import DeletedTable from './DeletedTable'; // Tableau historique suppressionsconst [travaux, setTravaux] = useState([]); // Liste des travaux actifs
const [entretiens, setEntretiens] = useState([]); // Liste des entretiens actifs
const [deletedTravaux, setDeletedTravaux] = useState([]); // Historique travaux supprimés
const [deletedEntretiens, setDeletedEntretiens] = useState([]); // Historique entretiens supprimésconst [currentTab, setCurrentTab] = useState(0); // Onglet actuel (0=Travaux, 1=Entretiens)
const [titre, setTitre] = useState('');
const [description, setDescription] = useState('');
const [dateDebut, setDateDebut] = useState('');
const [heureDebut, setHeureDebut] = useState('');
const [dateFin, setDateFin] = useState('');
const [cout, setCout] = useState('');const [editingTravauxId, setEditingTravauxId] = useState(null); // ID travaux en modification
const [editingEntretienId, setEditingEntretienId] = useState(null); // ID entretien en modification
const [loadingTravaux, setLoadingTravaux] = useState(false); // Loading travaux
const [loadingEntretiens, setLoadingEntretiens] = useState(false); // Loading entretiens
const [errorTravaux, setErrorTravaux] = useState(null); // Erreurs travaux
const [errorEntretiens, setErrorEntretiens] = useState(null); // Erreurs entretiens
const navigate = useNavigate(); // Navigation programmatique
// État pour dialogue de confirmation
const [confirmDialog, setConfirmDialog] = useState({
open: false,
title: '',
message: '',
confirmAction: null
});const fetchTravaux = useCallback(() => {
setLoadingTravaux(true);
axios.get('/api/travaux')
.then(response => {
if (Array.isArray(response.data)) {
setTravaux(response.data);
} else {
setErrorTravaux('Format de données incorrect');
}
})
.catch(error => {
console.error("Erreur travaux :", error);
setErrorTravaux('Erreur lors de la récupération des travaux: ' + (error.message || 'Erreur inconnue'));
})
.finally(() => setLoadingTravaux(false));
}, []);Points clés :
- useCallback : Évite les re-renders inutiles
- Validation : Vérifie que la réponse est un tableau
- Gestion d'erreurs : Messages explicites et fallback
- Loading state : Indicateur visuel pour l'utilisateur
Structure identique à fetchTravaux mais pour les entretiens :
- Endpoint
/api/entretiens - États spécifiques (
loadingEntretiens,errorEntretiens) - Même logique de validation et gestion d'erreurs
useEffect(() => {
fetchTravaux();
fetchEntretiens();
// Chargement des données supprimées depuis localStorage
const savedDeletedTravaux = localStorage.getItem('deletedTravaux');
if (savedDeletedTravaux) {
try {
setDeletedTravaux(JSON.parse(savedDeletedTravaux));
} catch (error) {
console.error("Erreur lors de la récupération de l'historique travaux :", error);
}
}
const savedDeletedEntretiens = localStorage.getItem('deletedEntretiens');
if (savedDeletedEntretiens) {
try {
setDeletedEntretiens(JSON.parse(savedDeletedEntretiens));
} catch (error) {
console.error("Erreur lors de la récupération de l'historique entretiens :", error);
}
}
}, [fetchTravaux, fetchEntretiens]);// Sauvegarde automatique des travaux supprimés
useEffect(() => {
if (deletedTravaux.length > 0) {
localStorage.setItem('deletedTravaux', JSON.stringify(deletedTravaux));
} else {
localStorage.removeItem('deletedTravaux');
}
}, [deletedTravaux]);
// Même logique pour les entretiens supprimés
useEffect(() => {
if (deletedEntretiens.length > 0) {
localStorage.setItem('deletedEntretiens', JSON.stringify(deletedEntretiens));
} else {
localStorage.removeItem('deletedEntretiens');
}
}, [deletedEntretiens]);useEffect(() => {
// Évite les doublons entre données actives et historique supprimé
const activeTravauxIDs = new Set(travaux.map(t => t.TravauxID));
const filteredDeletedTravaux = deletedTravaux.filter(dt => !activeTravauxIDs.has(dt.TravauxID));
if (filteredDeletedTravaux.length !== deletedTravaux.length ||
filteredDeletedTravaux.some((item, index) => item.TravauxID !== deletedTravaux[index].TravauxID)) {
setDeletedTravaux(filteredDeletedTravaux);
}
// Même logique pour les entretiens
const activeEntretienIDs = new Set(entretiens.map(e => e.EntretienID));
const filteredDeletedEntretiens = deletedEntretiens.filter(de => !activeEntretienIDs.has(de.EntretienID));
if (filteredDeletedEntretiens.length !== deletedEntretiens.length ||
filteredDeletedEntretiens.some((item, index) => item.EntretienID !== deletedEntretiens[index].EntretienID)) {
setDeletedEntretiens(filteredDeletedEntretiens);
}
}, [travaux, entretiens, deletedTravaux, deletedEntretiens]);const handleTabChange = (event, newValue) => {
setCurrentTab(newValue);
resetForm(); // Remet à zéro le formulaire
};const handleApiError = useCallback((error, setError) => {
if (error.response) {
if (error.response.status === 400) {
console.warn("Erreur 400 :", error.response.data);
setError("Erreur 400 : Données invalides ou incomplètes.");
} else if (error.response.status === 500) {
console.error("Erreur 500 :", error.response.data);
setError("Erreur 500 : Erreur serveur lors de l'enregistrement.");
} else {
setError(`Erreur ${error.response.status} : ${error.response.data?.message || 'Erreur inconnue.'}`);
}
} else {
console.error("Erreur réseau ou autre :", error);
setError("Erreur de connexion ou réponse inattendue.");
}
}, []);const handleSubmit = async (e) => {
e.preventDefault();
// 1. Validation des données
const validationError = validateForm({
titre, description, dateDebut, heureDebut, dateFin, cout
});
if (validationError) {
if (currentTab === 0) {
setErrorTravaux(validationError);
} else {
setErrorEntretiens(validationError);
}
return;
}
// 2. Préparation des données
const requestData = createRequestData(titre, description, dateDebut, heureDebut, dateFin, cout);
const isEditingTravaux = editingTravauxId !== null;
const isEditingEntretien = editingEntretienId !== null;
// 3. Envoi selon le type (travaux/entretiens)
if (currentTab === 0) {
setLoadingTravaux(true);
setErrorTravaux(null);
try {
let response;
if (isEditingTravaux) {
response = await axios.put(`/api/travaux/update/${editingTravauxId}`, requestData);
} else {
response = await axios.post('/api/travaux', requestData);
}
fetchTravaux(); // Rafraîchissement
resetForm(); // Reset du formulaire
} catch (error) {
handleApiError(error, setErrorTravaux);
} finally {
setLoadingTravaux(false);
}
} else {
// Logique identique pour les entretiens
setLoadingEntretiens(true);
setErrorEntretiens(null);
try {
let response;
if (isEditingEntretien) {
response = await axios.put(`/api/entretiens/update/${editingEntretienId}`, requestData);
} else {
response = await axios.post('/api/entretiens', requestData);
}
fetchEntretiens();
resetForm();
} catch (error) {
handleApiError(error, setErrorEntretiens);
} finally {
setLoadingEntretiens(false);
}
}
};const resetForm = () => {
setTitre('');
setDescription('');
setDateDebut('');
setHeureDebut('');
setDateFin('');
setCout('');
setEditingTravauxId(null);
setEditingEntretienId(null);
setErrorTravaux(null);
setErrorEntretiens(null);
};const handleDeleteClick = (id, type) => {
setConfirmDialog({
open: true,
title: "Confirmation de suppression",
message: `Êtes-vous sûr de vouloir supprimer ${type === 'travaux' ? 'ces travaux' : 'cet entretien'} ?`,
confirmAction: async () => {
setConfirmDialog(prev => ({ ...prev, open: false }));
if (type === 'travaux') {
await executeDeleteTravaux(id);
} else {
await executeDeleteEntretien(id);
}
}
});
};const executeDeleteTravaux = async (id) => {
try {
// 1. Récupération de l'élément à supprimer
const travauxToDelete = travaux.find(t => t.TravauxID === id);
if (travauxToDelete) {
// 2. Création d'une copie avec date de suppression
const travauxWithDeleteDate = createDeletedTravailWithDate(travauxToDelete);
// 3. Suppression de la liste active (optimistic update)
setTravaux(prev => prev.filter(t => t.TravauxID !== id));
// 4. Ajout à l'historique des suppressions
setDeletedTravaux(prev => [travauxWithDeleteDate, ...prev]);
}
// 5. Suppression côté serveur
await axios.delete(`/api/travaux/delete/${id}`);
} catch (error) {
handleDeleteError(error, setErrorTravaux, fetchTravaux);
}
};const handleDeleteError = (error, setError, refreshFunction) => {
if (error.response?.status === 404) {
console.warn("Élément non trouvé :", error.response.data);
setError("Erreur 404 : Élément introuvable.");
} else if (error.response?.status === 500) {
console.error("Erreur serveur :", error.response.data);
setError("Erreur 500 : Impossible de supprimer l'élément.");
} else {
console.error("Autre erreur :", error);
setError("Erreur inattendue lors de la suppression.");
}
refreshFunction(); // Resynchronisation avec le serveur
};const handleRestoreClick = async (deletedItem, type) => {
try {
// 1. Préparation des données pour la restauration
const itemToRestore = {
titre: deletedItem.Titre,
description: deletedItem.Description,
dateDebut: formatDateForInput(deletedItem.DateDebut),
heureDebut: formatTimeForInput(deletedItem.DateDebut),
dateFin: formatDateForInput(deletedItem.DateFin),
cout: deletedItem.Cout
};
// 2. Formatage des données pour l'API
const requestData = createRequestData(
itemToRestore.titre,
itemToRestore.description,
itemToRestore.dateDebut,
itemToRestore.heureDebut,
itemToRestore.dateFin,
itemToRestore.cout
);
if (type === 'travaux') {
// 3. Suppression de l'historique des suppressions
setDeletedTravaux(prev => prev.filter(t => t.TravauxID !== deletedItem.TravauxID));
// 4. Recréation côté serveur
await axios.post('/api/travaux', requestData);
// 5. Rafraîchissement des données
fetchTravaux();
} else {
// Même logique pour les entretiens
setDeletedEntretiens(prev => prev.filter(e => e.EntretienID !== deletedItem.EntretienID));
await axios.post('/api/entretiens', requestData);
fetchEntretiens();
}
} catch (error) {
console.error("Erreur lors de la restauration :", error);
if (type === 'travaux') {
setErrorTravaux("Erreur lors de la restauration des travaux.");
fetchTravaux(); // Resynchronisation en cas d'échec
} else {
setErrorEntretiens("Erreur lors de la restauration de l'entretien.");
fetchEntretiens();
}
}
};const handleClearHistory = (type) => {
setConfirmDialog({
open: true,
title: "Vider l'historique",
message: `Êtes-vous sûr de vouloir vider tout l'historique des suppressions ${type === 'travaux' ? 'des travaux' : 'des entretiens'} ? Cette action est irréversible.`,
confirmAction: () => {
setConfirmDialog(prev => ({ ...prev, open: false }));
if (type === 'travaux') {
setDeletedTravaux([]);
localStorage.removeItem('deletedTravaux');
} else {
setDeletedEntretiens([]);
localStorage.removeItem('deletedEntretiens');
}
}
});
};const handleEditTravaux = (travail) => {
setCurrentTab(0); // Basculer sur l'onglet Travaux
setTitre(travail.Titre || ''); // Remplir le formulaire
setDescription(travail.Description);
setDateDebut(formatDateForInput(travail.DateDebut));
setHeureDebut(formatTimeForInput(travail.DateDebut));
setDateFin(formatDateForInput(travail.DateFin));
setCout(travail.Cout);
setEditingTravauxId(travail.TravauxID); // Mode édition
setEditingEntretienId(null); // Reset autre type
setErrorTravaux(null); // Reset erreurs
setErrorEntretiens(null);
window.scrollTo({ top: 0, behavior: 'smooth' }); // Scroll vers le formulaire
};const handleEditEntretien = (entretien) => {
setCurrentTab(1); // Basculer sur l'onglet Entretiens
setTitre(entretien.Titre || ''); // Remplir le formulaire
setDescription(entretien.Description);
setDateDebut(formatDateForInput(entretien.DateDebut));
setHeureDebut(formatTimeForInput(entretien.DateDebut));
setDateFin(formatDateForInput(entretien.DateFin));
setCout(entretien.Cout);
setEditingEntretienId(entretien.EntretienID); // Mode édition
setEditingTravauxId(null); // Reset autre type
setErrorTravaux(null); // Reset erreurs
setErrorEntretiens(null);
window.scrollTo({ top: 0, behavior: 'smooth' }); // Scroll vers le formulaire
};const totalCoutTravaux = calculateTotalCout(travaux);
const totalCoutEntretiens = calculateTotalCout(entretiens);
const totalDeletedCoutTravaux = calculateTotalCout(deletedTravaux);
const totalDeletedCoutEntretiens = calculateTotalCout(deletedEntretiens);// Fusion de tous les éléments actifs avec type et tri par ID décroissant
const allItems = [
...travaux.map(t => ({ ...t, type: 'travaux', id: t.TravauxID })),
...entretiens.map(e => ({ ...e, type: 'entretiens', id: e.EntretienID }))
].sort((a, b) => b.id - a.id);
// Fusion de tous les éléments supprimés avec tri par date de suppression
const allDeletedItems = [
...deletedTravaux.map(t => ({ ...t, type: 'travaux', id: t.TravauxID })),
...deletedEntretiens.map(e => ({ ...e, type: 'entretiens', id: e.EntretienID }))
].sort((a, b) => new Date(b.dateSuppressionIso) - new Date(a.dateSuppressionIso));const isLoading = loadingTravaux || loadingEntretiens;
const currentError = currentTab === 0 ? errorTravaux : errorEntretiens;
const isEditing = editingTravauxId !== null || editingEntretienId !== null;return (
<Container maxWidth="lg">
{/* Titre principal */}
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<Typography variant="h4" gutterBottom>
Gestion des travaux et entretiens
</Typography>
</Box>
{/* Layout principal en deux colonnes */}
<Box sx={{ display: 'flex', gap: 4, flexDirection: { xs: 'column', md: 'row' } }}>
{/* Colonne gauche : Formulaire */}
<Box sx={{ flex: 1, bgcolor: 'white', padding: 3, borderRadius: 2, /* ... styles */ }}>
{/* Titre dynamique */}
<Typography variant="h5" gutterBottom>
{isEditing
? `Modifier ${currentTab === 0 ? 'des travaux' : 'un entretien'}`
: `Ajouter ${currentTab === 0 ? 'des travaux' : 'un entretien'}`}
</Typography>
{/* Onglets */}
<Tabs value={currentTab} onChange={handleTabChange} sx={{ marginBottom: 3 }}>
<Tab label="Travaux" />
<Tab label="Entretiens" />
</Tabs>
{/* Formulaire */}
<TravauxForm
titre={titre}
description={description}
dateDebut={dateDebut}
heureDebut={heureDebut}
dateFin={dateFin}
cout={cout}
setTitre={setTitre}
setDescription={setDescription}
setDateDebut={setDateDebut}
setHeureDebut={setHeureDebut}
setDateFin={setDateFin}
setCout={setCout}
handleSubmit={handleSubmit}
resetForm={resetForm}
isEditing={isEditing}
isLoading={isLoading}
currentTab={currentTab}
currentError={currentError}
/>
</Box>
{/* Colonne droite : Tableaux */}
<Box sx={{ flex: 1, bgcolor: 'white', padding: 3, borderRadius: 2, /* ... styles */ }}>
<Typography variant="h5" gutterBottom>
Tous les travaux et entretiens
</Typography>
{/* Gestion des états de chargement/erreur */}
{isLoading && allItems.length === 0 ? (
<Box sx={{ display: 'flex', justifyContent: 'center', padding: 4 }}>
<CircularProgress />
</Box>
) : currentError && allItems.length === 0 ? (
<Typography color="error">{currentError}</Typography>
) : allItems.length > 0 ? (
<>
{/* Tableau principal */}
<TravauxTable
items={allItems}
handleEditTravaux={handleEditTravaux}
handleEditEntretien={handleEditEntretien}
handleDeleteClick={handleDeleteClick}
calculateDuree={calculateDuree}
formatDisplayDate={formatDisplayDate}
/>
{/* Affichage des totaux */}
<Box sx={{ marginTop: 2, display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="h6">
Coût total des travaux : {totalCoutTravaux.toFixed(2)} €
</Typography>
<Typography variant="h6">
Coût total des entretiens : {totalCoutEntretiens.toFixed(2)} €
</Typography>
<Typography variant="h6" sx={{ fontWeight: 'bold', color: 'primary.main' }}>
Coût total général : {(totalCoutTravaux + totalCoutEntretiens).toFixed(2)} €
</Typography>
</Box>
</>
) : (
<Typography sx={{ padding: 2, textAlign: 'center' }}>
Aucun travaux ou entretiens enregistrés.
</Typography>
)}
{/* Tableau des éléments supprimés */}
<DeletedTable
allDeletedItems={allDeletedItems}
totalDeletedCoutTravaux={totalDeletedCoutTravaux}
totalDeletedCoutEntretiens={totalDeletedCoutEntretiens}
handleRestoreClick={handleRestoreClick}
handleClearHistory={handleClearHistory}
deletedTravauxLength={deletedTravaux.length}
deletedEntretiensLength={deletedEntretiens.length}
calculateDuree={calculateDuree}
formatDisplayDate={formatDisplayDate}
/>
</Box>
</Box>
{/* Dialogue de confirmation */}
<Dialog
open={confirmDialog.open}
onClose={handleCloseConfirmDialog}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">{confirmDialog.title}</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
{confirmDialog.message}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleCloseConfirmDialog} color="primary">
Annuler
</Button>
<Button
onClick={() => confirmDialog.confirmAction && confirmDialog.confirmAction()}
color="primary"
variant="contained"
autoFocus
>
Confirmer
</Button>
</DialogActions>
</Dialog>
</Container>
);- Persistence des données supprimées : L'historique des suppressions persiste entre les sessions
- Performance : Évite des requêtes serveur pour des données temporaires
- Expérience utilisateur : Données disponibles instantanément au chargement
useEffect(() => {
const savedDeletedTravaux = localStorage.getItem('deletedTravaux');
if (savedDeletedTravaux) {
try {
setDeletedTravaux(JSON.parse(savedDeletedTravaux));
} catch (error) {
console.error("Erreur parsing JSON :", error);
}
}
}, []);useEffect(() => {
if (deletedTravaux.length > 0) {
localStorage.setItem('deletedTravaux', JSON.stringify(deletedTravaux));
} else {
localStorage.removeItem('deletedTravaux'); // Nettoyage si vide
}
}, [