639 lines
34 KiB
TypeScript
639 lines
34 KiB
TypeScript
import { useState, useEffect } from "react";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Label } from "@/components/ui/label";
|
||
import { Textarea } from "@/components/ui/textarea";
|
||
import { Calendar, ArrowLeft, Save, Plus, Trash2, AlertCircle } from "lucide-react";
|
||
import { useNavigate } from "react-router-dom";
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||
import { toast } from "sonner";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||
|
||
interface TypeConge {
|
||
id: number;
|
||
nom: string;
|
||
couleur: string;
|
||
}
|
||
|
||
interface LigneConge {
|
||
typeId: number;
|
||
nombreJours: number;
|
||
}
|
||
|
||
interface Employe {
|
||
id: number;
|
||
nom: string;
|
||
prenom: string;
|
||
email: string;
|
||
service: string;
|
||
soldeCP?: number;
|
||
soldeRTT?: number;
|
||
}
|
||
|
||
const SaisieManuelle = () => {
|
||
const navigate = useNavigate();
|
||
const [employes, setEmployes] = useState<Employe[]>([]);
|
||
const [typesConge, setTypesConge] = useState<TypeConge[]>([]);
|
||
const [loading, setLoading] = useState(false);
|
||
const [employeSelectionne, setEmployeSelectionne] = useState<Employe | null>(null);
|
||
|
||
const [formData, setFormData] = useState({
|
||
employeId: "",
|
||
dateDebut: "",
|
||
dateFin: "",
|
||
commentaire: ""
|
||
});
|
||
|
||
const [lignesConge, setLignesConge] = useState<LigneConge[]>([
|
||
{ typeId: 0, nombreJours: 0 }
|
||
]);
|
||
|
||
useEffect(() => {
|
||
chargerDonnees();
|
||
}, []);
|
||
|
||
const chargerDonnees = async () => {
|
||
try {
|
||
const token = localStorage.getItem('token');
|
||
|
||
// ✅ Charger les employés
|
||
const respEmployes = await fetch('/api/employes', {
|
||
headers: { 'Authorization': `Bearer ${token}` }
|
||
});
|
||
|
||
if (!respEmployes.ok) {
|
||
throw new Error(`Erreur HTTP ${respEmployes.status}`);
|
||
}
|
||
|
||
const dataEmployes = await respEmployes.json();
|
||
|
||
// ✅ LOG DÉBOGAGE
|
||
console.log('📊 Employés reçus:', dataEmployes);
|
||
console.log(' Type:', Array.isArray(dataEmployes) ? 'Array' : typeof dataEmployes);
|
||
console.log(' Longueur:', dataEmployes?.length);
|
||
|
||
// ✅ VÉRIFICATION : S'assurer que c'est un tableau
|
||
if (Array.isArray(dataEmployes)) {
|
||
// ✅ Filtrer les entrées invalides
|
||
const employesValides = dataEmployes.filter(emp =>
|
||
emp &&
|
||
emp.id !== undefined &&
|
||
emp.nom &&
|
||
emp.prenom
|
||
);
|
||
console.log(' Employés valides:', employesValides.length);
|
||
setEmployes(employesValides);
|
||
} else {
|
||
console.error('❌ dataEmployes n\'est pas un tableau:', dataEmployes);
|
||
toast.error("Format de données invalide");
|
||
setEmployes([]);
|
||
}
|
||
|
||
// ✅ Charger les types de congé
|
||
const respTypes = await fetch('/api/types-conge', {
|
||
headers: { 'Authorization': `Bearer ${token}` }
|
||
});
|
||
|
||
if (!respTypes.ok) {
|
||
throw new Error(`Erreur HTTP ${respTypes.status}`);
|
||
}
|
||
|
||
const dataTypes = await respTypes.json();
|
||
|
||
if (Array.isArray(dataTypes)) {
|
||
setTypesConge(dataTypes);
|
||
} else {
|
||
console.error('❌ dataTypes n\'est pas un tableau:', dataTypes);
|
||
setTypesConge([]);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur chargement données:', error);
|
||
toast.error("Erreur lors du chargement des données");
|
||
setEmployes([]);
|
||
setTypesConge([]);
|
||
}
|
||
};
|
||
|
||
const ajouterLigne = () => {
|
||
const joursOuvres = calculerJoursOuvres(formData.dateDebut, formData.dateFin);
|
||
setLignesConge([...lignesConge, { typeId: 0, nombreJours: joursOuvres }]);
|
||
};
|
||
|
||
const supprimerLigne = (index: number) => {
|
||
if (lignesConge.length > 1) {
|
||
setLignesConge(lignesConge.filter((_, i) => i !== index));
|
||
}
|
||
};
|
||
|
||
const modifierLigne = (index: number, field: keyof LigneConge, value: any) => {
|
||
const nouvelles = [...lignesConge];
|
||
nouvelles[index] = { ...nouvelles[index], [field]: value };
|
||
setLignesConge(nouvelles);
|
||
};
|
||
|
||
// Mettre à jour automatiquement tous les nombres de jours quand les dates changent
|
||
const handleDateChange = (field: 'dateDebut' | 'dateFin', value: string) => {
|
||
const nouvellesData = { ...formData, [field]: value };
|
||
setFormData(nouvellesData);
|
||
|
||
// Recalculer les jours pour toutes les lignes
|
||
if (nouvellesData.dateDebut && nouvellesData.dateFin) {
|
||
const joursOuvres = calculerJoursOuvres(nouvellesData.dateDebut, nouvellesData.dateFin);
|
||
const nouvellesLignes = lignesConge.map(ligne => ({
|
||
...ligne,
|
||
nombreJours: ligne.nombreJours === 0 ? joursOuvres : ligne.nombreJours
|
||
}));
|
||
setLignesConge(nouvellesLignes);
|
||
}
|
||
};
|
||
|
||
const calculerTotalJours = () => {
|
||
return lignesConge.reduce((total, ligne) => total + (ligne.nombreJours || 0), 0);
|
||
};
|
||
|
||
const calculerJoursOuvres = (debut: string, fin: string) => {
|
||
if (!debut || !fin) return 0;
|
||
|
||
const dateDebut = new Date(debut);
|
||
const dateFin = new Date(fin);
|
||
let jours = 0;
|
||
|
||
for (let d = new Date(dateDebut); d <= dateFin; d.setDate(d.getDate() + 1)) {
|
||
const jour = d.getDay();
|
||
if (jour !== 0 && jour !== 6) {
|
||
jours++;
|
||
}
|
||
}
|
||
return jours;
|
||
};
|
||
|
||
const handleEmployeChange = (employeId: string) => {
|
||
setFormData({ ...formData, employeId });
|
||
const employe = employes.find(e => e.id.toString() === employeId);
|
||
setEmployeSelectionne(employe || null);
|
||
};
|
||
|
||
const calculerJoursParType = (typeId: number) => {
|
||
return lignesConge
|
||
.filter(ligne => ligne.typeId === typeId)
|
||
.reduce((total, ligne) => total + (ligne.nombreJours || 0), 0);
|
||
};
|
||
|
||
const obtenirSoldeParType = (typeId: number): number => {
|
||
if (!employeSelectionne) return 0;
|
||
if (typeId === 1) return employeSelectionne.soldeCP ?? 0;
|
||
if (typeId === 2) return employeSelectionne.soldeRTT ?? 0;
|
||
return 0;
|
||
};
|
||
|
||
const obtenirNomType = (typeId: number): string => {
|
||
const type = typesConge.find(t => t.id === typeId);
|
||
return type?.nom || '';
|
||
};
|
||
|
||
const calculerRestantParType = (typeId: number): number => {
|
||
const solde = obtenirSoldeParType(typeId);
|
||
const saisis = calculerJoursParType(typeId);
|
||
return solde - saisis;
|
||
};
|
||
|
||
const verifierDepassement = () => {
|
||
if (!employeSelectionne) return false;
|
||
|
||
const joursCP = calculerJoursParType(1);
|
||
const joursRTT = calculerJoursParType(2);
|
||
|
||
return (joursCP > (employeSelectionne.soldeCP ?? 0)) ||
|
||
(joursRTT > (employeSelectionne.soldeRTT ?? 0));
|
||
};
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
|
||
if (!formData.employeId || !formData.dateDebut || !formData.dateFin) {
|
||
toast.error("Veuillez remplir tous les champs obligatoires");
|
||
return;
|
||
}
|
||
|
||
// Vérifier que au moins une ligne a des données valides
|
||
const lignesValides = lignesConge.filter(l => l.typeId > 0 && l.nombreJours > 0);
|
||
|
||
if (lignesValides.length === 0) {
|
||
toast.error("Veuillez remplir au moins une ligne avec un type de congé et un nombre de jours");
|
||
return;
|
||
}
|
||
|
||
if (verifierDepassement()) {
|
||
toast.warning("⚠️ Attention: dépassement des compteurs disponibles");
|
||
}
|
||
|
||
setLoading(true);
|
||
try {
|
||
const token = localStorage.getItem('token');
|
||
const response = await fetch('/api/demandes', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
collaborateurId: formData.employeId,
|
||
dateDebut: formData.dateDebut,
|
||
dateFin: formData.dateFin,
|
||
typesConge: lignesValides.map(l => ({
|
||
typeId: l.typeId,
|
||
nombreJours: l.nombreJours
|
||
})),
|
||
commentaire: formData.commentaire
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
toast.success("Demande enregistrée avec succès");
|
||
navigate("/dashboard");
|
||
} else {
|
||
throw new Error('Erreur lors de l\'enregistrement');
|
||
}
|
||
} catch (error) {
|
||
toast.error("Erreur lors de l'enregistrement");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const joursOuvres = calculerJoursOuvres(formData.dateDebut, formData.dateFin);
|
||
const totalJours = calculerTotalJours();
|
||
const aDepassement = verifierDepassement();
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gradient-subtle">
|
||
<header className="bg-card border-b border-border shadow-card">
|
||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center space-x-4">
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => navigate("/dashboard")}
|
||
className="transition-smooth"
|
||
>
|
||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||
Retour
|
||
</Button>
|
||
<div className="flex items-center space-x-3">
|
||
<div className="w-10 h-10 bg-gradient-primary rounded-xl flex items-center justify-center">
|
||
<Calendar className="w-5 h-5 text-primary-foreground" />
|
||
</div>
|
||
<h1 className="text-2xl font-bold text-foreground">Saisie manuelle</h1>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||
<Card className="shadow-elegant border-0">
|
||
<CardHeader>
|
||
<CardTitle>Nouvelle demande de congé</CardTitle>
|
||
<CardDescription>
|
||
Enregistrer manuellement une demande d'absence ou de récupération
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<form onSubmit={handleSubmit} className="space-y-6">
|
||
<div className="space-y-4">
|
||
<h3 className="text-lg font-semibold">Informations générales</h3>
|
||
|
||
{/* ✅ SECTION EMPLOYÉ CORRIGÉE */}
|
||
<div className="space-y-2">
|
||
<Label htmlFor="employe">
|
||
Collaborateur <span className="text-destructive">*</span>
|
||
</Label>
|
||
<Select
|
||
value={formData.employeId}
|
||
onValueChange={handleEmployeChange}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder="Sélectionner un collaborateur" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{employes && employes.length > 0 ? (
|
||
employes
|
||
.filter(emp => emp && emp.id && emp.prenom && emp.nom)
|
||
.map(emp => (
|
||
<SelectItem
|
||
key={emp.id}
|
||
value={emp.id.toString()}
|
||
>
|
||
{emp.prenom} {emp.nom} - {emp.service || 'Sans service'}
|
||
</SelectItem>
|
||
))
|
||
) : (
|
||
<div className="p-4 text-center text-sm text-muted-foreground">
|
||
{employes.length === 0
|
||
? "Chargement des collaborateurs..."
|
||
: "Aucun collaborateur disponible"}
|
||
</div>
|
||
)}
|
||
</SelectContent>
|
||
</Select>
|
||
|
||
{/* ✅ Indicateur de chargement */}
|
||
{employes.length === 0 && (
|
||
<p className="text-xs text-muted-foreground">
|
||
Chargement des collaborateurs...
|
||
</p>
|
||
)}
|
||
{employes.length > 0 && (
|
||
<p className="text-xs text-muted-foreground">
|
||
{employes.length} collaborateur{employes.length > 1 ? 's' : ''} disponible{employes.length > 1 ? 's' : ''}
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
{employeSelectionne && (
|
||
<div className="p-4 bg-primary/5 rounded-lg border border-primary/10">
|
||
<p className="text-sm font-medium mb-3">Compteurs disponibles</p>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="space-y-1">
|
||
<p className="text-xs text-muted-foreground">Congés Payés (CP)</p>
|
||
<div className="flex items-baseline gap-2">
|
||
<p className="text-2xl font-bold text-primary">
|
||
{employeSelectionne.soldeCP ?? 0}
|
||
</p>
|
||
<span className="text-sm text-muted-foreground">jours</span>
|
||
</div>
|
||
{calculerJoursParType(1) > 0 && (
|
||
<p className="text-xs text-muted-foreground">
|
||
Saisie en cours: <span className="font-semibold text-warning">{calculerJoursParType(1)}j</span>
|
||
{calculerJoursParType(1) > (employeSelectionne.soldeCP ?? 0) && (
|
||
<span className="text-destructive ml-1">⚠️ Dépassement!</span>
|
||
)}
|
||
</p>
|
||
)}
|
||
</div>
|
||
<div className="space-y-1">
|
||
<p className="text-xs text-muted-foreground">RTT</p>
|
||
<div className="flex items-baseline gap-2">
|
||
<p className="text-2xl font-bold text-primary">
|
||
{employeSelectionne.soldeRTT ?? 0}
|
||
</p>
|
||
<span className="text-sm text-muted-foreground">jours</span>
|
||
</div>
|
||
{calculerJoursParType(2) > 0 && (
|
||
<p className="text-xs text-muted-foreground">
|
||
Saisie en cours: <span className="font-semibold text-warning">{calculerJoursParType(2)}j</span>
|
||
{calculerJoursParType(2) > (employeSelectionne.soldeRTT ?? 0) && (
|
||
<span className="text-destructive ml-1">⚠️ Dépassement!</span>
|
||
)}
|
||
</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="dateDebut">Date de début <span className="text-destructive">*</span></Label>
|
||
<Input
|
||
id="dateDebut"
|
||
type="date"
|
||
value={formData.dateDebut}
|
||
onChange={(e) => handleDateChange('dateDebut', e.target.value)}
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="dateFin">Date de fin <span className="text-destructive">*</span></Label>
|
||
<Input
|
||
id="dateFin"
|
||
type="date"
|
||
value={formData.dateFin}
|
||
onChange={(e) => handleDateChange('dateFin', e.target.value)}
|
||
required
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{formData.dateDebut && formData.dateFin && joursOuvres > 0 && (
|
||
<div className="p-4 bg-primary/5 rounded-lg space-y-3">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-muted-foreground">Jours ouvrés de la période</p>
|
||
<p className="text-2xl font-bold">{joursOuvres}</p>
|
||
</div>
|
||
<div className="text-right">
|
||
<p className="text-sm text-muted-foreground">Total saisi</p>
|
||
<p className="text-2xl font-bold">{totalJours}</p>
|
||
</div>
|
||
</div>
|
||
|
||
{employeSelectionne && totalJours > 0 && (
|
||
<div className="pt-3 border-t space-y-2">
|
||
<p className="text-xs font-medium text-muted-foreground">Répartition par type</p>
|
||
|
||
{calculerJoursParType(1) > 0 && (
|
||
<div className="flex items-center justify-between text-sm">
|
||
<span className="text-muted-foreground">CP:</span>
|
||
<div className="flex items-center gap-2">
|
||
<Badge variant="outline" className="bg-blue-50 text-blue-700">
|
||
{calculerJoursParType(1)}j
|
||
</Badge>
|
||
<span className="text-xs">
|
||
Restants: <span className={calculerRestantParType(1) < 0 ? "font-bold text-destructive" : "font-bold text-success"}>
|
||
{calculerRestantParType(1)}j
|
||
</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{calculerJoursParType(2) > 0 && (
|
||
<div className="flex items-center justify-between text-sm">
|
||
<span className="text-muted-foreground">RTT:</span>
|
||
<div className="flex items-center gap-2">
|
||
<Badge variant="outline" className="bg-purple-50 text-purple-700">
|
||
{calculerJoursParType(2)}j
|
||
</Badge>
|
||
<span className="text-xs">
|
||
Restants: <span className={calculerRestantParType(2) < 0 ? "font-bold text-destructive" : "font-bold text-success"}>
|
||
{calculerRestantParType(2)}j
|
||
</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{lignesConge.filter(l => l.typeId > 2 && l.nombreJours > 0).map((ligne, idx) => {
|
||
const typeNom = obtenirNomType(ligne.typeId);
|
||
return typeNom ? (
|
||
<div key={idx} className="flex items-center justify-between text-sm">
|
||
<span className="text-muted-foreground">{typeNom}:</span>
|
||
<Badge variant="outline" className="bg-green-50 text-green-700">
|
||
{calculerJoursParType(ligne.typeId)}j
|
||
</Badge>
|
||
</div>
|
||
) : null;
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<h3 className="text-lg font-semibold">Types de congé</h3>
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={ajouterLigne}
|
||
>
|
||
<Plus className="w-4 h-4 mr-1" />
|
||
Ajouter un type
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="space-y-3">
|
||
{lignesConge.map((ligne, index) => {
|
||
const soldeDisponible = obtenirSoldeParType(ligne.typeId);
|
||
const dejaSaisi = calculerJoursParType(ligne.typeId);
|
||
const restant = soldeDisponible - dejaSaisi;
|
||
const enDepassement = restant < 0;
|
||
|
||
return (
|
||
<div key={index} className="space-y-2">
|
||
<div className="flex gap-3 items-end">
|
||
<div className="flex-1 space-y-2">
|
||
<Label>Type de congé</Label>
|
||
<Select
|
||
value={ligne.typeId.toString()}
|
||
onValueChange={(value) => modifierLigne(index, 'typeId', parseInt(value))}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder="Sélectionner" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{typesConge.map(type => (
|
||
<SelectItem key={type.id} value={type.id.toString()}>
|
||
{type.nom}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
<div className="w-40 space-y-2">
|
||
<Label>
|
||
Nombre de jours
|
||
{joursOuvres > 0 && (
|
||
<span className="text-xs text-muted-foreground ml-1">
|
||
(max: {joursOuvres})
|
||
</span>
|
||
)}
|
||
</Label>
|
||
<Input
|
||
type="number"
|
||
step="0.5"
|
||
min="0"
|
||
max={joursOuvres}
|
||
value={ligne.nombreJours || ''}
|
||
onChange={(e) => modifierLigne(index, 'nombreJours', parseFloat(e.target.value) || 0)}
|
||
placeholder={joursOuvres > 0 ? joursOuvres.toString() : "0"}
|
||
className={enDepassement && ligne.nombreJours > 0 ? "border-destructive" : ""}
|
||
/>
|
||
</div>
|
||
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="icon"
|
||
onClick={() => supprimerLigne(index)}
|
||
disabled={lignesConge.length === 1}
|
||
className="text-destructive hover:text-destructive hover:bg-destructive/10"
|
||
>
|
||
<Trash2 className="w-4 h-4" />
|
||
</Button>
|
||
</div>
|
||
|
||
{/* Indicateur de solde pour cette ligne */}
|
||
{employeSelectionne && ligne.typeId > 0 && ligne.nombreJours > 0 && (
|
||
<div className={`text-xs px-3 py-1.5 rounded-md ${enDepassement ? 'bg-destructive/10 text-destructive' : 'bg-success/10 text-success'}`}>
|
||
{obtenirNomType(ligne.typeId)}:
|
||
<span className="font-semibold ml-1">
|
||
{soldeDisponible}j disponibles
|
||
</span>
|
||
{dejaSaisi > 0 && (
|
||
<>
|
||
{' '}→ {dejaSaisi}j saisis =
|
||
<span className="font-bold ml-1">
|
||
{restant}j restants
|
||
{enDepassement && ' ⚠️'}
|
||
</span>
|
||
</>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
{aDepassement && (
|
||
<Alert variant="destructive">
|
||
<AlertCircle className="h-4 w-4" />
|
||
<AlertDescription>
|
||
<strong>Attention:</strong> La saisie dépasse les compteurs disponibles du collaborateur.
|
||
La demande sera tout de même enregistrée mais nécessitera une validation.
|
||
</AlertDescription>
|
||
</Alert>
|
||
)}
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="commentaire">Commentaire (optionnel)</Label>
|
||
<Textarea
|
||
id="commentaire"
|
||
placeholder="Préciser le motif, les circonstances..."
|
||
value={formData.commentaire}
|
||
onChange={(e) => setFormData({ ...formData, commentaire: e.target.value })}
|
||
rows={4}
|
||
className="resize-none"
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex gap-4 pt-4">
|
||
<Button
|
||
type="submit"
|
||
disabled={loading}
|
||
className="flex-1 bg-gradient-primary hover:opacity-90 transition-smooth shadow-elegant"
|
||
>
|
||
<Save className="w-4 h-4 mr-2" />
|
||
{loading ? 'Enregistrement...' : 'Enregistrer la demande'}
|
||
</Button>
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
onClick={() => navigate("/dashboard")}
|
||
disabled={loading}
|
||
className="transition-smooth"
|
||
>
|
||
Annuler
|
||
</Button>
|
||
</div>
|
||
</form>
|
||
</CardContent>
|
||
</Card>
|
||
</main>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default SaisieManuelle;
|