Checkpoint: OPEX DSI : ajout d'un écran vide dédié pour les années sans données, avec bouton "Créer depuis N-1" (copie toutes les lignes de l'année précédente avec confirmation AlertDialog) et bouton "Ajouter une ligne manuellement". La fonction handleCreateFromN1 clone les lignes N-1 avec budget_n1 = montant N-1 pour que les tendances soient calculables dès la création.

This commit is contained in:
Manus
2026-06-04 13:48:45 +00:00
parent 7e360ca113
commit 1ce830d593

View File

@@ -454,6 +454,32 @@ export default function DsiOpex() {
toast.info('Montants réinitialisés', { description: 'Les valeurs ont été restaurées depuis les données sources.' }); toast.info('Montants réinitialisés', { description: 'Les valeurs ont été restaurées depuis les données sources.' });
}, [isValidated, annee]); }, [isValidated, annee]);
// Créer depuis l'année précédente (N-1)
const handleCreateFromN1 = useCallback(() => {
const anneeN1 = annee - 1;
const stateN1 = loadOpexState(anneeN1);
if (stateN1.lignes.length === 0) {
toast.error(`Aucune donnée disponible pour ${anneeN1}`, {
description: `Veuillez d'abord créer ou importer l'OPEX ${anneeN1}.`,
});
return;
}
// Cloner les lignes de N-1 en mettant budget_n1 = montant N-1 et montant = montant N-1
const lignesClonees: LigneOpex[] = stateN1.lignes.map(l => ({
...l,
id: l.isCustom ? genId() : l.id,
budget_n1: l.montant,
montant: l.montant,
}));
const newState: OpexAnneeState = { lignes: lignesClonees, validated: false, savedAt: new Date().toISOString() };
setOpexState(newState);
saveOpexState(annee, newState);
setIsDirty(false);
toast.success(`OPEX ${annee} créé depuis ${anneeN1}`, {
description: `${lignesClonees.length} postes de charges copiés. Vous pouvez maintenant les modifier.`,
});
}, [annee]);
// Valider définitivement // Valider définitivement
const handleValidate = useCallback(() => { const handleValidate = useCallback(() => {
const newState: OpexAnneeState = { const newState: OpexAnneeState = {
@@ -822,8 +848,54 @@ export default function DsiOpex() {
{/* Contenu principal */} {/* Contenu principal */}
<div className="flex-1 overflow-auto px-6 py-4"> <div className="flex-1 overflow-auto px-6 py-4">
{/* === ÉTAT VIDE : année sans données === */}
{opexState.lignes.length === 0 && (
<div className="flex flex-col items-center justify-center py-24 text-center">
<div className="w-16 h-16 rounded-full bg-muted flex items-center justify-center mb-4">
<BarChart3 className="w-8 h-8 text-muted-foreground" />
</div>
<h3 className="text-lg font-semibold text-foreground mb-1" style={{ fontFamily: 'Sora, sans-serif' }}>
Aucune donnée pour {annee}
</h3>
<p className="text-sm text-muted-foreground mb-6 max-w-sm">
L'OPEX {annee} n'a pas encore é créé. Vous pouvez le créer en copiant les charges de {annee - 1} ou en ajoutant les lignes manuellement.
</p>
<div className="flex items-center gap-3">
<AlertDialog>
<AlertDialogTrigger asChild>
<button className="flex items-center gap-2 px-5 py-2.5 rounded-lg bg-primary text-primary-foreground font-medium text-sm hover:bg-primary/90 transition-all shadow-sm">
<Plus className="w-4 h-4" />
Créer depuis {annee - 1}
</button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Créer l'OPEX {annee} depuis {annee - 1} ?</AlertDialogTitle>
<AlertDialogDescription>
Tous les postes de charges de {annee - 1} seront copiés comme base de travail pour {annee}. Les montants seront identiques et modifiables.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Annuler</AlertDialogCancel>
<AlertDialogAction onClick={handleCreateFromN1} className="bg-primary hover:bg-primary/90">
Confirmer
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<button
onClick={() => { setDialogMode('add'); setEditingLigne(null); }}
className="flex items-center gap-2 px-5 py-2.5 rounded-lg border border-border bg-card font-medium text-sm hover:bg-muted transition-colors"
>
<Plus className="w-4 h-4" />
Ajouter une ligne manuellement
</button>
</div>
</div>
)}
{/* === VUE PAR POSTE === */} {/* === VUE PAR POSTE === */}
{viewMode === 'postes' && ( {viewMode === 'postes' && opexState.lignes.length > 0 && (
<div className="space-y-2"> <div className="space-y-2">
{selectedCategorie !== 'Toutes' && ( {selectedCategorie !== 'Toutes' && (
<div className="flex items-center justify-between mb-3 px-1"> <div className="flex items-center justify-between mb-3 px-1">