Checkpoint: Apprentissage déclenché depuis les selects serviceConcerne/typeAchat/ventilation dans la liste Factures BAP. Page Apprentissages IA refaite avec indicateur de confiance (Confirmé/En observation) et seuil configurable.
This commit is contained in:
@@ -158,6 +158,8 @@ export default function InvoicesBAP() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const upsertLearningMutation = trpc.learnings.upsert.useMutation();
|
||||||
|
|
||||||
const reprocessMutation = trpc.invoices.reprocessSelected.useMutation({
|
const reprocessMutation = trpc.invoices.reprocessSelected.useMutation({
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
if (data.errors > 0) {
|
if (data.errors > 0) {
|
||||||
@@ -624,10 +626,20 @@ export default function InvoicesBAP() {
|
|||||||
// Remove field from autoFilledFields when manually edited
|
// Remove field from autoFilledFields when manually edited
|
||||||
const autoFilledFields = invoice.autoFilledFields ? JSON.parse(invoice.autoFilledFields) : [];
|
const autoFilledFields = invoice.autoFilledFields ? JSON.parse(invoice.autoFilledFields) : [];
|
||||||
const updatedAutoFields = autoFilledFields.filter((f: string) => f !== "serviceConcerne");
|
const updatedAutoFields = autoFilledFields.filter((f: string) => f !== "serviceConcerne");
|
||||||
|
const newVal = e.target.value || undefined;
|
||||||
|
// Apprentissage : mémoriser la correction manuelle
|
||||||
|
if (invoice.supplierName && newVal !== (invoice.serviceConcerne || undefined)) {
|
||||||
|
upsertLearningMutation.mutate({
|
||||||
|
supplierName: invoice.supplierName,
|
||||||
|
fieldName: 'serviceConcerne',
|
||||||
|
originalValue: invoice.serviceConcerne || undefined,
|
||||||
|
correctedValue: newVal || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
updateFieldMutation.mutate({
|
updateFieldMutation.mutate({
|
||||||
id: invoice.id,
|
id: invoice.id,
|
||||||
data: {
|
data: {
|
||||||
serviceConcerne: e.target.value || undefined,
|
serviceConcerne: newVal,
|
||||||
autoFilledFields: updatedAutoFields.length > 0 ? JSON.stringify(updatedAutoFields) : null
|
autoFilledFields: updatedAutoFields.length > 0 ? JSON.stringify(updatedAutoFields) : null
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -649,10 +661,20 @@ export default function InvoicesBAP() {
|
|||||||
// Remove field from autoFilledFields when manually edited
|
// Remove field from autoFilledFields when manually edited
|
||||||
const autoFilledFields = invoice.autoFilledFields ? JSON.parse(invoice.autoFilledFields) : [];
|
const autoFilledFields = invoice.autoFilledFields ? JSON.parse(invoice.autoFilledFields) : [];
|
||||||
const updatedAutoFields = autoFilledFields.filter((f: string) => f !== "typeAchat");
|
const updatedAutoFields = autoFilledFields.filter((f: string) => f !== "typeAchat");
|
||||||
|
const newTypeAchat = e.target.value as "CAPEX" | "OPEX" | undefined;
|
||||||
|
// Apprentissage : mémoriser la correction manuelle
|
||||||
|
if (invoice.supplierName && newTypeAchat !== (invoice.typeAchat || undefined)) {
|
||||||
|
upsertLearningMutation.mutate({
|
||||||
|
supplierName: invoice.supplierName,
|
||||||
|
fieldName: 'typeAchat',
|
||||||
|
originalValue: invoice.typeAchat || undefined,
|
||||||
|
correctedValue: newTypeAchat || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
updateFieldMutation.mutate({
|
updateFieldMutation.mutate({
|
||||||
id: invoice.id,
|
id: invoice.id,
|
||||||
data: {
|
data: {
|
||||||
typeAchat: e.target.value as "CAPEX" | "OPEX" | undefined,
|
typeAchat: newTypeAchat,
|
||||||
autoFilledFields: updatedAutoFields.length > 0 ? JSON.stringify(updatedAutoFields) : null
|
autoFilledFields: updatedAutoFields.length > 0 ? JSON.stringify(updatedAutoFields) : null
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -677,10 +699,20 @@ export default function InvoicesBAP() {
|
|||||||
// Remove field from autoFilledFields when manually edited
|
// Remove field from autoFilledFields when manually edited
|
||||||
const autoFilledFields = invoice.autoFilledFields ? JSON.parse(invoice.autoFilledFields) : [];
|
const autoFilledFields = invoice.autoFilledFields ? JSON.parse(invoice.autoFilledFields) : [];
|
||||||
const updatedAutoFields = autoFilledFields.filter((f: string) => f !== "ventilationComptable");
|
const updatedAutoFields = autoFilledFields.filter((f: string) => f !== "ventilationComptable");
|
||||||
|
const newVentil = e.target.value || undefined;
|
||||||
|
// Apprentissage : mémoriser la correction manuelle
|
||||||
|
if (invoice.supplierName && newVentil !== (invoice.ventilationComptable || undefined)) {
|
||||||
|
upsertLearningMutation.mutate({
|
||||||
|
supplierName: invoice.supplierName,
|
||||||
|
fieldName: 'ventilationComptable',
|
||||||
|
originalValue: invoice.ventilationComptable || undefined,
|
||||||
|
correctedValue: newVentil || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
updateFieldMutation.mutate({
|
updateFieldMutation.mutate({
|
||||||
id: invoice.id,
|
id: invoice.id,
|
||||||
data: {
|
data: {
|
||||||
ventilationComptable: e.target.value || undefined,
|
ventilationComptable: newVentil,
|
||||||
autoFilledFields: updatedAutoFields.length > 0 ? JSON.stringify(updatedAutoFields) : null
|
autoFilledFields: updatedAutoFields.length > 0 ? JSON.stringify(updatedAutoFields) : null
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import DashboardLayout from "@/components/DashboardLayout";
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
import { trpc } from "@/lib/trpc";
|
import { trpc } from "@/lib/trpc";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Brain, Trash2, RefreshCw, AlertTriangle } from "lucide-react";
|
import { Brain, Trash2, RefreshCw, CheckCircle2, Eye, Settings2 } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@@ -32,9 +34,14 @@ const FIELD_COLORS: Record<string, string> = {
|
|||||||
isSubscription: "bg-orange-100 text-orange-800",
|
isSubscription: "bg-orange-100 text-orange-800",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DEFAULT_CONFIDENCE_THRESHOLD = 2;
|
||||||
|
|
||||||
export default function LearningSettings() {
|
export default function LearningSettings() {
|
||||||
const utils = trpc.useUtils();
|
const utils = trpc.useUtils();
|
||||||
const { data: learnings, isLoading } = trpc.learnings.list.useQuery();
|
const { data: learnings, isLoading } = trpc.learnings.list.useQuery();
|
||||||
|
const [confidenceThreshold, setConfidenceThreshold] = useState(DEFAULT_CONFIDENCE_THRESHOLD);
|
||||||
|
const [editingThreshold, setEditingThreshold] = useState(false);
|
||||||
|
const [thresholdInput, setThresholdInput] = useState(String(DEFAULT_CONFIDENCE_THRESHOLD));
|
||||||
|
|
||||||
const deleteMutation = trpc.learnings.delete.useMutation({
|
const deleteMutation = trpc.learnings.delete.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -62,6 +69,36 @@ export default function LearningSettings() {
|
|||||||
|
|
||||||
const supplierCount = Object.keys(grouped).length;
|
const supplierCount = Object.keys(grouped).length;
|
||||||
const totalCount = learnings?.length || 0;
|
const totalCount = learnings?.length || 0;
|
||||||
|
const confirmedCount = (learnings || []).filter(l => (l?.applyCount || 0) >= confidenceThreshold).length;
|
||||||
|
const observationCount = totalCount - confirmedCount;
|
||||||
|
|
||||||
|
const getConfidenceBadge = (applyCount: number) => {
|
||||||
|
if (applyCount >= confidenceThreshold) {
|
||||||
|
return (
|
||||||
|
<span className="flex items-center gap-1 text-xs text-emerald-700 bg-emerald-50 border border-emerald-200 rounded-full px-2 py-0.5 font-medium">
|
||||||
|
<CheckCircle2 className="w-3 h-3" />
|
||||||
|
Confirmé · {applyCount}×
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span className="flex items-center gap-1 text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded-full px-2 py-0.5 font-medium">
|
||||||
|
<Eye className="w-3 h-3" />
|
||||||
|
Observation · {applyCount}×
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveThreshold = () => {
|
||||||
|
const val = parseInt(thresholdInput, 10);
|
||||||
|
if (isNaN(val) || val < 1) {
|
||||||
|
toast.error("Le seuil doit être un entier ≥ 1");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setConfidenceThreshold(val);
|
||||||
|
setEditingThreshold(false);
|
||||||
|
toast.success(`Seuil de confiance mis à jour : ${val} application(s)`);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
@@ -110,7 +147,7 @@ export default function LearningSettings() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
<div className="text-3xl font-bold text-indigo-600">{supplierCount}</div>
|
<div className="text-3xl font-bold text-indigo-600">{supplierCount}</div>
|
||||||
@@ -123,8 +160,65 @@ export default function LearningSettings() {
|
|||||||
<div className="text-sm text-muted-foreground mt-1">Correction(s) mémorisée(s)</div>
|
<div className="text-sm text-muted-foreground mt-1">Correction(s) mémorisée(s)</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="text-3xl font-bold text-emerald-600">{confirmedCount}</div>
|
||||||
|
<div className="text-sm text-muted-foreground mt-1">Confirmée(s) (≥ {confidenceThreshold}×)</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="text-3xl font-bold text-amber-500">{observationCount}</div>
|
||||||
|
<div className="text-sm text-muted-foreground mt-1">En observation (< {confidenceThreshold}×)</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Seuil de confiance */}
|
||||||
|
<Card className="border-indigo-200">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-base flex items-center gap-2">
|
||||||
|
<Settings2 className="w-4 h-4 text-indigo-600" />
|
||||||
|
Seuil de confiance
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Un apprentissage est marqué <strong>Confirmé</strong> quand il a été appliqué au moins{" "}
|
||||||
|
<strong>{confidenceThreshold} fois</strong>. En dessous, il est <strong>En observation</strong>.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{editingThreshold ? (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Label htmlFor="threshold" className="text-sm whitespace-nowrap">
|
||||||
|
Nombre minimum d'applications :
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="threshold"
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={100}
|
||||||
|
value={thresholdInput}
|
||||||
|
onChange={(e) => setThresholdInput(e.target.value)}
|
||||||
|
className="w-24"
|
||||||
|
/>
|
||||||
|
<Button size="sm" onClick={handleSaveThreshold}>Appliquer</Button>
|
||||||
|
<Button size="sm" variant="ghost" onClick={() => { setEditingThreshold(false); setThresholdInput(String(confidenceThreshold)); }}>
|
||||||
|
Annuler
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Seuil actuel : <strong>{confidenceThreshold} application(s)</strong>
|
||||||
|
</span>
|
||||||
|
<Button size="sm" variant="outline" onClick={() => { setEditingThreshold(true); setThresholdInput(String(confidenceThreshold)); }}>
|
||||||
|
Modifier
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* Comment ça fonctionne */}
|
{/* Comment ça fonctionne */}
|
||||||
<Card className="border-indigo-200 bg-indigo-50/50">
|
<Card className="border-indigo-200 bg-indigo-50/50">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
@@ -136,13 +230,17 @@ export default function LearningSettings() {
|
|||||||
<CardContent className="text-sm text-muted-foreground space-y-1">
|
<CardContent className="text-sm text-muted-foreground space-y-1">
|
||||||
<p>
|
<p>
|
||||||
Quand vous modifiez manuellement le <strong>type d'achat</strong>, le <strong>service concerné</strong> ou
|
Quand vous modifiez manuellement le <strong>type d'achat</strong>, le <strong>service concerné</strong> ou
|
||||||
la <strong>ventilation comptable</strong> d'une facture dans sa page de détail, le système mémorise cette
|
la <strong>ventilation comptable</strong> d'une facture (dans le détail ou dans la liste BAP), le système mémorise cette
|
||||||
correction pour ce fournisseur.
|
correction pour ce fournisseur.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Lors des prochains imports, si une facture du même fournisseur est détectée, les corrections mémorisées
|
Lors des prochains imports, si une facture du même fournisseur est détectée, les corrections mémorisées
|
||||||
sont appliquées automatiquement après l'extraction IA et les règles d'automatisme.
|
sont appliquées automatiquement après l'extraction IA et les règles d'automatisme.
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
Le compteur <strong>Appliqué N×</strong> indique combien de fois cette correction a été utilisée lors d'imports.
|
||||||
|
Une correction <strong>Confirmée</strong> a atteint le seuil de confiance configuré.
|
||||||
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -165,81 +263,96 @@ export default function LearningSettings() {
|
|||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{Object.entries(grouped).map(([supplierKey, entries]) => (
|
{Object.entries(grouped).map(([supplierKey, entries]) => {
|
||||||
<Card key={supplierKey}>
|
const supplierConfirmed = (entries || []).filter(l => (l?.applyCount || 0) >= confidenceThreshold).length;
|
||||||
<CardHeader className="pb-3">
|
const supplierObservation = (entries?.length || 0) - supplierConfirmed;
|
||||||
<div className="flex items-center justify-between">
|
return (
|
||||||
<CardTitle className="text-base capitalize">{supplierKey}</CardTitle>
|
<Card key={supplierKey}>
|
||||||
<Badge variant="outline" className="text-xs">
|
<CardHeader className="pb-3">
|
||||||
{entries?.length} correction(s)
|
<div className="flex items-center justify-between flex-wrap gap-2">
|
||||||
</Badge>
|
<CardTitle className="text-base capitalize">{supplierKey}</CardTitle>
|
||||||
</div>
|
<div className="flex items-center gap-2">
|
||||||
</CardHeader>
|
{supplierConfirmed > 0 && (
|
||||||
<CardContent>
|
<Badge className="text-xs bg-emerald-100 text-emerald-800 border-emerald-200">
|
||||||
<div className="space-y-2">
|
{supplierConfirmed} confirmée(s)
|
||||||
{entries?.map((learning) => (
|
|
||||||
<div
|
|
||||||
key={learning.id}
|
|
||||||
className="flex items-center justify-between p-3 rounded-lg border bg-muted/30"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
|
||||||
<Badge className={`text-xs shrink-0 ${FIELD_COLORS[learning.fieldName] || "bg-gray-100 text-gray-800"}`}>
|
|
||||||
{FIELD_LABELS[learning.fieldName] || learning.fieldName}
|
|
||||||
</Badge>
|
</Badge>
|
||||||
<div className="flex items-center gap-2 text-sm min-w-0">
|
)}
|
||||||
{learning.originalValue && (
|
{supplierObservation > 0 && (
|
||||||
<>
|
<Badge className="text-xs bg-amber-100 text-amber-800 border-amber-200">
|
||||||
<span className="text-muted-foreground line-through truncate">
|
{supplierObservation} en observation
|
||||||
{learning.originalValue}
|
</Badge>
|
||||||
</span>
|
)}
|
||||||
<span className="text-muted-foreground">→</span>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)}
|
</CardHeader>
|
||||||
<span className="font-medium text-foreground truncate">
|
<CardContent>
|
||||||
{learning.correctedValue || <em className="text-muted-foreground">vide</em>}
|
<div className="space-y-2">
|
||||||
</span>
|
{entries?.map((learning) => (
|
||||||
|
<div
|
||||||
|
key={learning.id}
|
||||||
|
className={`flex items-center justify-between p-3 rounded-lg border ${
|
||||||
|
(learning.applyCount || 0) >= confidenceThreshold
|
||||||
|
? "bg-emerald-50/40 border-emerald-200"
|
||||||
|
: "bg-amber-50/40 border-amber-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||||
|
<Badge className={`text-xs shrink-0 ${FIELD_COLORS[learning.fieldName] || "bg-gray-100 text-gray-800"}`}>
|
||||||
|
{FIELD_LABELS[learning.fieldName] || learning.fieldName}
|
||||||
|
</Badge>
|
||||||
|
<div className="flex items-center gap-2 text-sm min-w-0">
|
||||||
|
{learning.originalValue && (
|
||||||
|
<>
|
||||||
|
<span className="text-muted-foreground line-through truncate">
|
||||||
|
{learning.originalValue}
|
||||||
|
</span>
|
||||||
|
<span className="text-muted-foreground">→</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<span className="font-medium text-foreground truncate">
|
||||||
|
{learning.correctedValue || <em className="text-muted-foreground">vide</em>}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3 shrink-0 ml-3">
|
||||||
|
{getConfidenceBadge(learning.applyCount || 0)}
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-7 w-7 text-red-400 hover:text-red-600 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3.5 h-3.5" />
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Supprimer cet apprentissage ?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
La correction « {FIELD_LABELS[learning.fieldName] || learning.fieldName} → {learning.correctedValue} »
|
||||||
|
pour le fournisseur « {supplierKey} » ne sera plus appliquée automatiquement.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Annuler</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
className="bg-red-600 hover:bg-red-700"
|
||||||
|
onClick={() => deleteMutation.mutate({ id: learning.id })}
|
||||||
|
>
|
||||||
|
Supprimer
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 shrink-0 ml-3">
|
))}
|
||||||
<span className="text-xs text-muted-foreground">
|
</div>
|
||||||
Appliqué {learning.applyCount}×
|
</CardContent>
|
||||||
</span>
|
</Card>
|
||||||
<AlertDialog>
|
);
|
||||||
<AlertDialogTrigger asChild>
|
})}
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-7 w-7 text-red-400 hover:text-red-600 hover:bg-red-50"
|
|
||||||
>
|
|
||||||
<Trash2 className="w-3.5 h-3.5" />
|
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>Supprimer cet apprentissage ?</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
La correction « {FIELD_LABELS[learning.fieldName] || learning.fieldName} → {learning.correctedValue} »
|
|
||||||
pour le fournisseur « {supplierKey} » ne sera plus appliquée automatiquement.
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Annuler</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
className="bg-red-600 hover:bg-red-700"
|
|
||||||
onClick={() => deleteMutation.mutate({ id: learning.id })}
|
|
||||||
>
|
|
||||||
Supprimer
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user