Initial commit
This commit is contained in:
584
components/phases/BudgetExecutionDialog.tsx
Normal file
584
components/phases/BudgetExecutionDialog.tsx
Normal file
@@ -0,0 +1,584 @@
|
||||
/**
|
||||
* Dialog d'exécution budgétaire pour le suivi des dépenses réelles
|
||||
* Permet la comparaison budget prévu vs coût réel avec analyse des écarts
|
||||
*/
|
||||
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { Button } from 'primereact/button';
|
||||
import { InputNumber } from 'primereact/inputnumber';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
import { DataTable } from 'primereact/datatable';
|
||||
import { Column } from 'primereact/column';
|
||||
import { Card } from 'primereact/card';
|
||||
import { TabView, TabPanel } from 'primereact/tabview';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { Divider } from 'primereact/divider';
|
||||
import { Tag } from 'primereact/tag';
|
||||
import { ProgressBar } from 'primereact/progressbar';
|
||||
import { Calendar } from 'primereact/calendar';
|
||||
import { Chart } from 'primereact/chart';
|
||||
import { PhaseChantier } from '../../types/btp-extended';
|
||||
|
||||
interface DepenseReelle {
|
||||
id?: string;
|
||||
date: string;
|
||||
categorie: 'MATERIEL' | 'MAIN_OEUVRE' | 'SOUS_TRAITANCE' | 'TRANSPORT' | 'AUTRES';
|
||||
designation: string;
|
||||
montant: number;
|
||||
fournisseur?: string;
|
||||
numeroPiece?: string; // Numéro de facture, bon de commande, etc.
|
||||
notes?: string;
|
||||
valide: boolean;
|
||||
validePar?: string;
|
||||
dateValidation?: string;
|
||||
}
|
||||
|
||||
interface AnalyseEcart {
|
||||
categorieId: string;
|
||||
categorieNom: string;
|
||||
budgetPrevu: number;
|
||||
depenseReelle: number;
|
||||
ecart: number;
|
||||
ecartPourcentage: number;
|
||||
statut: 'CONFORME' | 'ALERTE' | 'DEPASSEMENT';
|
||||
}
|
||||
|
||||
interface BudgetExecutionDialogProps {
|
||||
visible: boolean;
|
||||
onHide: () => void;
|
||||
phase: PhaseChantier | null;
|
||||
onSave: (executionData: any) => void;
|
||||
}
|
||||
|
||||
export const BudgetExecutionDialog: React.FC<BudgetExecutionDialogProps> = ({
|
||||
visible,
|
||||
onHide,
|
||||
phase,
|
||||
onSave
|
||||
}) => {
|
||||
const toast = useRef<Toast>(null);
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const [depenses, setDepenses] = useState<DepenseReelle[]>([]);
|
||||
const [nouvelleDepense, setNouvelleDepense] = useState<DepenseReelle>({
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
categorie: 'MATERIEL',
|
||||
designation: '',
|
||||
montant: 0,
|
||||
valide: false
|
||||
});
|
||||
|
||||
const categories = [
|
||||
{ label: 'Matériel', value: 'MATERIEL', color: '#007ad9' },
|
||||
{ label: 'Main d\'œuvre', value: 'MAIN_OEUVRE', color: '#22c55e' },
|
||||
{ label: 'Sous-traitance', value: 'SOUS_TRAITANCE', color: '#f97316' },
|
||||
{ label: 'Transport', value: 'TRANSPORT', color: '#8b5cf6' },
|
||||
{ label: 'Autres', value: 'AUTRES', color: '#6b7280' }
|
||||
];
|
||||
|
||||
// Simuler le budget prévu par catégorie (normalement récupéré de l'analyse budgétaire)
|
||||
const budgetParCategorie = {
|
||||
MATERIEL: phase?.budgetPrevu ? phase.budgetPrevu * 0.4 : 0,
|
||||
MAIN_OEUVRE: phase?.budgetPrevu ? phase.budgetPrevu * 0.3 : 0,
|
||||
SOUS_TRAITANCE: phase?.budgetPrevu ? phase.budgetPrevu * 0.2 : 0,
|
||||
TRANSPORT: phase?.budgetPrevu ? phase.budgetPrevu * 0.05 : 0,
|
||||
AUTRES: phase?.budgetPrevu ? phase.budgetPrevu * 0.05 : 0
|
||||
};
|
||||
|
||||
// Charger les dépenses existantes au montage du composant
|
||||
useEffect(() => {
|
||||
if (phase && visible) {
|
||||
loadDepenses();
|
||||
}
|
||||
}, [phase, visible]);
|
||||
|
||||
const loadDepenses = async () => {
|
||||
// Simuler le chargement des dépenses depuis l'API
|
||||
// En réalité, ceci ferait appel à une API
|
||||
const depensesSimulees: DepenseReelle[] = [
|
||||
{
|
||||
id: '1',
|
||||
date: '2025-01-15',
|
||||
categorie: 'MATERIEL',
|
||||
designation: 'Béton C25/30',
|
||||
montant: 1500,
|
||||
fournisseur: 'Béton Express',
|
||||
numeroPiece: 'FC-2025-001',
|
||||
valide: true,
|
||||
validePar: 'Chef de projet',
|
||||
dateValidation: '2025-01-16'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
date: '2025-01-20',
|
||||
categorie: 'MAIN_OEUVRE',
|
||||
designation: 'Équipe de maçonnerie - 2 jours',
|
||||
montant: 800,
|
||||
numeroPiece: 'TS-2025-005',
|
||||
valide: true,
|
||||
validePar: 'Chef de projet',
|
||||
dateValidation: '2025-01-21'
|
||||
}
|
||||
];
|
||||
setDepenses(depensesSimulees);
|
||||
};
|
||||
|
||||
// Ajouter une nouvelle dépense
|
||||
const ajouterDepense = () => {
|
||||
if (!nouvelleDepense.designation.trim() || nouvelleDepense.montant <= 0) {
|
||||
toast.current?.show({
|
||||
severity: 'warn',
|
||||
summary: 'Champs requis',
|
||||
detail: 'Veuillez remplir la désignation et le montant',
|
||||
life: 3000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const nouvelleDepenseAvecId = {
|
||||
...nouvelleDepense,
|
||||
id: `dep_${Date.now()}`
|
||||
};
|
||||
|
||||
setDepenses([...depenses, nouvelleDepenseAvecId]);
|
||||
|
||||
// Réinitialiser le formulaire
|
||||
setNouvelleDepense({
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
categorie: 'MATERIEL',
|
||||
designation: '',
|
||||
montant: 0,
|
||||
valide: false
|
||||
});
|
||||
|
||||
toast.current?.show({
|
||||
severity: 'success',
|
||||
summary: 'Dépense ajoutée',
|
||||
detail: 'La dépense a été enregistrée',
|
||||
life: 3000
|
||||
});
|
||||
};
|
||||
|
||||
// Valider une dépense
|
||||
const validerDepense = (depenseId: string) => {
|
||||
setDepenses(depenses.map(d =>
|
||||
d.id === depenseId
|
||||
? {
|
||||
...d,
|
||||
valide: true,
|
||||
validePar: 'Utilisateur actuel',
|
||||
dateValidation: new Date().toISOString()
|
||||
}
|
||||
: d
|
||||
));
|
||||
};
|
||||
|
||||
// Supprimer une dépense
|
||||
const supprimerDepense = (depenseId: string) => {
|
||||
setDepenses(depenses.filter(d => d.id !== depenseId));
|
||||
};
|
||||
|
||||
// Calculer l'analyse des écarts
|
||||
const getAnalyseEcarts = (): AnalyseEcart[] => {
|
||||
return categories.map(cat => {
|
||||
const budgetPrevu = budgetParCategorie[cat.value as keyof typeof budgetParCategorie];
|
||||
const depenseReelle = depenses
|
||||
.filter(d => d.categorie === cat.value && d.valide)
|
||||
.reduce((sum, d) => sum + d.montant, 0);
|
||||
|
||||
const ecart = depenseReelle - budgetPrevu;
|
||||
const ecartPourcentage = budgetPrevu > 0 ? (ecart / budgetPrevu) * 100 : 0;
|
||||
|
||||
let statut: 'CONFORME' | 'ALERTE' | 'DEPASSEMENT' = 'CONFORME';
|
||||
if (ecartPourcentage > 10) statut = 'DEPASSEMENT';
|
||||
else if (ecartPourcentage > 5) statut = 'ALERTE';
|
||||
|
||||
return {
|
||||
categorieId: cat.value,
|
||||
categorieNom: cat.label,
|
||||
budgetPrevu,
|
||||
depenseReelle,
|
||||
ecart,
|
||||
ecartPourcentage,
|
||||
statut
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Préparer les données pour le graphique
|
||||
const getChartData = () => {
|
||||
const analyses = getAnalyseEcarts();
|
||||
return {
|
||||
labels: analyses.map(a => a.categorieNom),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Budget prévu',
|
||||
data: analyses.map(a => a.budgetPrevu),
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.6)',
|
||||
borderColor: 'rgba(54, 162, 235, 1)',
|
||||
borderWidth: 1
|
||||
},
|
||||
{
|
||||
label: 'Dépense réelle',
|
||||
data: analyses.map(a => a.depenseReelle),
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.6)',
|
||||
borderColor: 'rgba(255, 99, 132, 1)',
|
||||
borderWidth: 1
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top' as const
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Budget prévu vs Dépenses réelles'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
callback: function(value: any) {
|
||||
return new Intl.NumberFormat('fr-FR', {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
minimumFractionDigits: 0
|
||||
}).format(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Templates pour le DataTable
|
||||
const categorieTemplate = (rowData: DepenseReelle) => {
|
||||
const category = categories.find(cat => cat.value === rowData.categorie);
|
||||
const severityMap = {
|
||||
'MATERIEL': 'info',
|
||||
'MAIN_OEUVRE': 'success',
|
||||
'SOUS_TRAITANCE': 'warning',
|
||||
'TRANSPORT': 'help',
|
||||
'AUTRES': 'secondary'
|
||||
} as const;
|
||||
|
||||
return <Tag value={category?.label} severity={severityMap[rowData.categorie]} />;
|
||||
};
|
||||
|
||||
const montantTemplate = (rowData: DepenseReelle) => {
|
||||
return new Intl.NumberFormat('fr-FR', {
|
||||
style: 'currency',
|
||||
currency: 'EUR'
|
||||
}).format(rowData.montant);
|
||||
};
|
||||
|
||||
const validationTemplate = (rowData: DepenseReelle) => {
|
||||
return rowData.valide ? (
|
||||
<Tag value="Validé" severity="success" icon="pi pi-check" />
|
||||
) : (
|
||||
<Tag value="En attente" severity="warning" icon="pi pi-clock" />
|
||||
);
|
||||
};
|
||||
|
||||
const actionsTemplate = (rowData: DepenseReelle) => {
|
||||
return (
|
||||
<div className="flex gap-1">
|
||||
{!rowData.valide && (
|
||||
<Button
|
||||
icon="pi pi-check"
|
||||
className="p-button-success p-button-text p-button-sm"
|
||||
tooltip="Valider"
|
||||
onClick={() => validerDepense(rowData.id!)}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
icon="pi pi-trash"
|
||||
className="p-button-danger p-button-text p-button-sm"
|
||||
tooltip="Supprimer"
|
||||
onClick={() => supprimerDepense(rowData.id!)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const analysesEcarts = getAnalyseEcarts();
|
||||
const totalBudgetPrevu = analysesEcarts.reduce((sum, a) => sum + a.budgetPrevu, 0);
|
||||
const totalDepenseReelle = analysesEcarts.reduce((sum, a) => sum + a.depenseReelle, 0);
|
||||
const ecartTotal = totalDepenseReelle - totalBudgetPrevu;
|
||||
const ecartTotalPourcentage = totalBudgetPrevu > 0 ? (ecartTotal / totalBudgetPrevu) * 100 : 0;
|
||||
|
||||
const dialogFooter = (
|
||||
<div className="flex justify-content-between">
|
||||
<Button
|
||||
label="Fermer"
|
||||
icon="pi pi-times"
|
||||
onClick={onHide}
|
||||
className="p-button-text"
|
||||
/>
|
||||
<Button
|
||||
label="Enregistrer l'exécution"
|
||||
icon="pi pi-save"
|
||||
onClick={() => {
|
||||
const executionData = {
|
||||
depenses: depenses.filter(d => d.valide),
|
||||
analyse: analysesEcarts,
|
||||
coutTotal: totalDepenseReelle,
|
||||
ecartTotal,
|
||||
ecartPourcentage: ecartTotalPourcentage,
|
||||
dateAnalyse: new Date().toISOString()
|
||||
};
|
||||
onSave(executionData);
|
||||
onHide();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toast ref={toast} />
|
||||
<Dialog
|
||||
header={`Exécution budgétaire - ${phase?.nom || 'Phase'}`}
|
||||
visible={visible}
|
||||
onHide={onHide}
|
||||
footer={dialogFooter}
|
||||
style={{ width: '95vw', maxWidth: '1200px' }}
|
||||
modal
|
||||
maximizable
|
||||
>
|
||||
<TabView activeIndex={activeIndex} onTabChange={(e) => setActiveIndex(e.index)}>
|
||||
<TabPanel header="Saisie des dépenses" leftIcon="pi pi-plus">
|
||||
<div className="grid">
|
||||
{/* Formulaire d'ajout de dépense */}
|
||||
<div className="col-12">
|
||||
<Card title="Ajouter une dépense" className="mb-4">
|
||||
<div className="grid">
|
||||
<div className="col-12 md:col-2">
|
||||
<label htmlFor="dateDepense" className="font-semibold">Date</label>
|
||||
<Calendar
|
||||
id="dateDepense"
|
||||
value={nouvelleDepense.date ? new Date(nouvelleDepense.date) : null}
|
||||
onChange={(e) => setNouvelleDepense({
|
||||
...nouvelleDepense,
|
||||
date: e.value ? e.value.toISOString().split('T')[0] : ''
|
||||
})}
|
||||
className="w-full"
|
||||
dateFormat="dd/mm/yy"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 md:col-2">
|
||||
<label htmlFor="categorieDepense" className="font-semibold">Catégorie</label>
|
||||
<Dropdown
|
||||
id="categorieDepense"
|
||||
value={nouvelleDepense.categorie}
|
||||
options={categories}
|
||||
onChange={(e) => setNouvelleDepense({...nouvelleDepense, categorie: e.value})}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 md:col-4">
|
||||
<label htmlFor="designationDepense" className="font-semibold">Désignation</label>
|
||||
<InputText
|
||||
id="designationDepense"
|
||||
value={nouvelleDepense.designation}
|
||||
onChange={(e) => setNouvelleDepense({...nouvelleDepense, designation: e.target.value})}
|
||||
className="w-full"
|
||||
placeholder="Description de la dépense"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 md:col-2">
|
||||
<label htmlFor="montantDepense" className="font-semibold">Montant (€)</label>
|
||||
<InputNumber
|
||||
id="montantDepense"
|
||||
value={nouvelleDepense.montant}
|
||||
onValueChange={(e) => setNouvelleDepense({...nouvelleDepense, montant: e.value || 0})}
|
||||
className="w-full"
|
||||
mode="currency"
|
||||
currency="EUR"
|
||||
locale="fr-FR"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 md:col-2">
|
||||
<label className="font-semibold"> </label>
|
||||
<Button
|
||||
icon="pi pi-plus"
|
||||
onClick={ajouterDepense}
|
||||
className="w-full"
|
||||
tooltip="Ajouter cette dépense"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid mt-3">
|
||||
<div className="col-12 md:col-4">
|
||||
<label htmlFor="fournisseurDepense" className="font-semibold">Fournisseur (optionnel)</label>
|
||||
<InputText
|
||||
id="fournisseurDepense"
|
||||
value={nouvelleDepense.fournisseur || ''}
|
||||
onChange={(e) => setNouvelleDepense({...nouvelleDepense, fournisseur: e.target.value})}
|
||||
className="w-full"
|
||||
placeholder="Nom du fournisseur"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 md:col-3">
|
||||
<label htmlFor="numeroPiece" className="font-semibold">N° pièce</label>
|
||||
<InputText
|
||||
id="numeroPiece"
|
||||
value={nouvelleDepense.numeroPiece || ''}
|
||||
onChange={(e) => setNouvelleDepense({...nouvelleDepense, numeroPiece: e.target.value})}
|
||||
className="w-full"
|
||||
placeholder="N° facture, bon..."
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 md:col-5">
|
||||
<label htmlFor="notesDepense" className="font-semibold">Notes</label>
|
||||
<InputText
|
||||
id="notesDepense"
|
||||
value={nouvelleDepense.notes || ''}
|
||||
onChange={(e) => setNouvelleDepense({...nouvelleDepense, notes: e.target.value})}
|
||||
className="w-full"
|
||||
placeholder="Notes supplémentaires"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Liste des dépenses */}
|
||||
<div className="col-12">
|
||||
<DataTable
|
||||
value={depenses}
|
||||
emptyMessage="Aucune dépense enregistrée"
|
||||
size="small"
|
||||
header="Dépenses enregistrées"
|
||||
>
|
||||
<Column field="date" header="Date" style={{ width: '8rem' }} />
|
||||
<Column field="categorie" header="Catégorie" body={categorieTemplate} style={{ width: '10rem' }} />
|
||||
<Column field="designation" header="Désignation" style={{ minWidth: '15rem' }} />
|
||||
<Column field="montant" header="Montant" body={montantTemplate} style={{ width: '8rem' }} />
|
||||
<Column field="fournisseur" header="Fournisseur" style={{ width: '10rem' }} />
|
||||
<Column field="numeroPiece" header="N° pièce" style={{ width: '8rem' }} />
|
||||
<Column field="valide" header="Statut" body={validationTemplate} style={{ width: '8rem' }} />
|
||||
<Column header="Actions" body={actionsTemplate} style={{ width: '8rem' }} />
|
||||
</DataTable>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel header="Analyse des écarts" leftIcon="pi pi-chart-line">
|
||||
<div className="grid">
|
||||
<div className="col-12 lg:col-8">
|
||||
<Card title="Comparaison budget/réalisé">
|
||||
<div style={{ height: '300px' }}>
|
||||
<Chart type="bar" data={getChartData()} options={chartOptions} />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="col-12 lg:col-4">
|
||||
<Card title="Synthèse globale">
|
||||
<div className="flex justify-content-between align-items-center mb-3">
|
||||
<span>Budget total prévu:</span>
|
||||
<span className="font-semibold">
|
||||
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(totalBudgetPrevu)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-content-between align-items-center mb-3">
|
||||
<span>Dépenses réelles:</span>
|
||||
<span className="font-semibold">
|
||||
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(totalDepenseReelle)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div className="flex justify-content-between align-items-center mb-2">
|
||||
<span>Écart total:</span>
|
||||
<span className={`font-bold ${ecartTotal > 0 ? 'text-red-500' : 'text-green-500'}`}>
|
||||
{ecartTotal > 0 ? '+' : ''}{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(ecartTotal)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-content-between align-items-center mb-3">
|
||||
<span>Écart (%):</span>
|
||||
<span className={`font-bold ${ecartTotalPourcentage > 0 ? 'text-red-500' : 'text-green-500'}`}>
|
||||
{ecartTotalPourcentage > 0 ? '+' : ''}{ecartTotalPourcentage.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ProgressBar
|
||||
value={totalBudgetPrevu > 0 ? (totalDepenseReelle / totalBudgetPrevu) * 100 : 0}
|
||||
className="mb-2"
|
||||
color={ecartTotalPourcentage > 10 ? '#dc3545' : ecartTotalPourcentage > 5 ? '#ffc107' : '#22c55e'}
|
||||
/>
|
||||
|
||||
<small className="text-color-secondary">
|
||||
Taux de consommation budgétaire
|
||||
</small>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Détail par catégorie */}
|
||||
<div className="col-12">
|
||||
<Card title="Analyse détaillée par catégorie">
|
||||
<DataTable value={analysesEcarts} size="small">
|
||||
<Column field="categorieNom" header="Catégorie" />
|
||||
<Column
|
||||
field="budgetPrevu"
|
||||
header="Budget prévu"
|
||||
body={(rowData) => new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(rowData.budgetPrevu)}
|
||||
/>
|
||||
<Column
|
||||
field="depenseReelle"
|
||||
header="Dépense réelle"
|
||||
body={(rowData) => new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(rowData.depenseReelle)}
|
||||
/>
|
||||
<Column
|
||||
field="ecart"
|
||||
header="Écart"
|
||||
body={(rowData) => (
|
||||
<span className={rowData.ecart > 0 ? 'text-red-500 font-semibold' : 'text-green-500'}>
|
||||
{rowData.ecart > 0 ? '+' : ''}{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(rowData.ecart)}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
<Column
|
||||
field="ecartPourcentage"
|
||||
header="Écart %"
|
||||
body={(rowData) => (
|
||||
<span className={rowData.ecartPourcentage > 10 ? 'text-red-500 font-semibold' : rowData.ecartPourcentage > 5 ? 'text-orange-500' : 'text-green-500'}>
|
||||
{rowData.ecartPourcentage > 0 ? '+' : ''}{rowData.ecartPourcentage.toFixed(1)}%
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
<Column
|
||||
field="statut"
|
||||
header="Statut"
|
||||
body={(rowData) => {
|
||||
const severityMap = {
|
||||
'CONFORME': 'success',
|
||||
'ALERTE': 'warning',
|
||||
'DEPASSEMENT': 'danger'
|
||||
} as const;
|
||||
return <Tag value={rowData.statut} severity={severityMap[rowData.statut]} />;
|
||||
}}
|
||||
/>
|
||||
</DataTable>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</TabView>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BudgetExecutionDialog;
|
||||
Reference in New Issue
Block a user