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:
@@ -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 été 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">
|
||||||
|
|||||||
Reference in New Issue
Block a user