487 lines
19 KiB
TypeScript
487 lines
19 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { useParams, useLocation, useSearch } from "wouter";
|
|
import DashboardLayout from "@/components/DashboardLayout";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { trpc } from "@/lib/trpc";
|
|
import { ArrowLeft, Save, FileText } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
import { getBapReturnLocation } from "@/lib/bapNavigation";
|
|
|
|
export default function InvoiceDetail() {
|
|
const { id } = useParams<{ id: string }>();
|
|
const [, setLocation] = useLocation();
|
|
const detailSearch = useSearch();
|
|
const returnLocation = getBapReturnLocation(detailSearch);
|
|
const invoiceId = parseInt(id || "0");
|
|
|
|
const { data: invoice, isLoading } = trpc.invoices.getById.useQuery({ id: invoiceId });
|
|
const { data: departments } = trpc.departments.getByUser.useQuery();
|
|
const { data: allocations } = trpc.accountingAllocations.getByUser.useQuery();
|
|
const utils = trpc.useUtils();
|
|
|
|
const [isEditing, setIsEditing] = useState(false);
|
|
const [formData, setFormData] = useState({
|
|
supplierName: "",
|
|
invoiceNumber: "",
|
|
invoiceDate: "",
|
|
deliveryNoteNumber: "",
|
|
orderNumber: "",
|
|
totalAmount: "",
|
|
recipientName: "",
|
|
serviceConcerne: "",
|
|
typeAchat: "",
|
|
ventilationComptable: "",
|
|
});
|
|
|
|
// Initialize form data when invoice loads
|
|
useEffect(() => {
|
|
if (invoice) {
|
|
setFormData({
|
|
supplierName: invoice.supplierName || "",
|
|
invoiceNumber: invoice.invoiceNumber || "",
|
|
invoiceDate: invoice.invoiceDate
|
|
? new Date(invoice.invoiceDate).toISOString().split('T')[0]
|
|
: "",
|
|
deliveryNoteNumber: invoice.deliveryNoteNumber || "",
|
|
orderNumber: invoice.orderNumber || "",
|
|
totalAmount: invoice.totalAmount ? invoice.totalAmount.toString() : "",
|
|
recipientName: (invoice as any).recipientName || "",
|
|
serviceConcerne: invoice.serviceConcerne || "",
|
|
typeAchat: invoice.typeAchat || "",
|
|
ventilationComptable: invoice.ventilationComptable || "",
|
|
});
|
|
}
|
|
}, [invoice]);
|
|
|
|
const upsertLearningMutation = trpc.learnings.upsert.useMutation();
|
|
|
|
const updateMutation = trpc.invoices.update.useMutation({
|
|
onSuccess: () => {
|
|
toast.success("Facture mise à jour avec succès");
|
|
setIsEditing(false);
|
|
utils.invoices.getById.invalidate({ id: invoiceId });
|
|
utils.invoices.list.invalidate();
|
|
},
|
|
onError: (error) => {
|
|
toast.error(error.message || "Erreur lors de la mise à jour");
|
|
},
|
|
});
|
|
|
|
const handleSave = () => {
|
|
// Calculate new quality score based on filled fields
|
|
let filledFields = 0;
|
|
let totalFields = 6;
|
|
|
|
if (formData.supplierName) filledFields++;
|
|
if (formData.invoiceNumber) filledFields++;
|
|
if (formData.invoiceDate) filledFields++;
|
|
if (formData.deliveryNoteNumber) filledFields++;
|
|
if (formData.orderNumber) filledFields++;
|
|
if (formData.totalAmount) filledFields++;
|
|
|
|
const newQualityScore = Math.round((filledFields / totalFields) * 100);
|
|
|
|
// Apprentissage : enregistrer les corrections manuelles sur les champs clés
|
|
// On compare les valeurs actuelles (invoice) avec les nouvelles valeurs (formData)
|
|
if (invoice && formData.supplierName) {
|
|
const supplierName = formData.supplierName || invoice.supplierName || "";
|
|
if (!supplierName) return;
|
|
|
|
// isSubscription : géré séparément via le toggle dans InvoicesBAP
|
|
// typeAchat
|
|
if (formData.typeAchat !== (invoice.typeAchat || "")) {
|
|
upsertLearningMutation.mutate({
|
|
supplierName,
|
|
fieldName: "typeAchat",
|
|
originalValue: invoice.typeAchat || undefined,
|
|
correctedValue: formData.typeAchat,
|
|
});
|
|
}
|
|
// serviceConcerne
|
|
if (formData.serviceConcerne !== (invoice.serviceConcerne || "")) {
|
|
upsertLearningMutation.mutate({
|
|
supplierName,
|
|
fieldName: "serviceConcerne",
|
|
originalValue: invoice.serviceConcerne || undefined,
|
|
correctedValue: formData.serviceConcerne,
|
|
});
|
|
}
|
|
// ventilationComptable
|
|
if (formData.ventilationComptable !== (invoice.ventilationComptable || "")) {
|
|
upsertLearningMutation.mutate({
|
|
supplierName,
|
|
fieldName: "ventilationComptable",
|
|
originalValue: invoice.ventilationComptable || undefined,
|
|
correctedValue: formData.ventilationComptable,
|
|
});
|
|
}
|
|
}
|
|
|
|
updateMutation.mutate({
|
|
id: invoiceId,
|
|
data: {
|
|
supplierName: formData.supplierName || undefined,
|
|
invoiceNumber: formData.invoiceNumber || undefined,
|
|
invoiceDate: formData.invoiceDate ? new Date(formData.invoiceDate) : undefined,
|
|
deliveryNoteNumber: formData.deliveryNoteNumber || undefined,
|
|
orderNumber: formData.orderNumber || undefined,
|
|
totalAmount: formData.totalAmount ? formData.totalAmount : undefined,
|
|
recipientName: formData.recipientName || undefined,
|
|
serviceConcerne: formData.serviceConcerne || undefined,
|
|
typeAchat: (formData.typeAchat as "CAPEX" | "OPEX" | "") || undefined,
|
|
ventilationComptable: formData.ventilationComptable || undefined,
|
|
},
|
|
});
|
|
};
|
|
|
|
const getStatusBadge = (status: string) => {
|
|
switch (status) {
|
|
case "exported":
|
|
return <Badge className="bg-green-100 text-green-800 hover:bg-green-100">Exporté</Badge>;
|
|
case "not_exported":
|
|
return <Badge className="bg-blue-100 text-blue-800 hover:bg-blue-100">Non exporté</Badge>;
|
|
case "export_error":
|
|
return <Badge className="bg-red-100 text-red-800 hover:bg-red-100">Erreur export</Badge>;
|
|
default:
|
|
return <Badge variant="outline">{status}</Badge>;
|
|
}
|
|
};
|
|
|
|
const getQualityBadge = (score: number | null) => {
|
|
if (score === null) return <Badge variant="outline">-</Badge>;
|
|
if (score === 100) return <Badge className="bg-green-100 text-green-800 hover:bg-green-100">{score}%</Badge>;
|
|
if (score >= 80) return <Badge className="bg-yellow-100 text-yellow-800 hover:bg-yellow-100">{score}%</Badge>;
|
|
return <Badge className="bg-red-100 text-red-800 hover:bg-red-100">{score}%</Badge>;
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<DashboardLayout>
|
|
<div className="flex items-center justify-center h-96">
|
|
<div className="text-muted-foreground">Chargement...</div>
|
|
</div>
|
|
</DashboardLayout>
|
|
);
|
|
}
|
|
|
|
if (!invoice) {
|
|
return (
|
|
<DashboardLayout>
|
|
<div className="flex flex-col items-center justify-center h-96">
|
|
<FileText className="w-16 h-16 text-gray-300 mb-4" />
|
|
<h2 className="text-2xl font-bold mb-2">Facture introuvable</h2>
|
|
<p className="text-gray-500 mb-4">Cette facture n'existe pas ou a été supprimée</p>
|
|
<Button onClick={() => setLocation(returnLocation)}>
|
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
Retour aux factures
|
|
</Button>
|
|
</div>
|
|
</DashboardLayout>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<DashboardLayout>
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<Button variant="ghost" onClick={() => setLocation(returnLocation)}>
|
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
Retour
|
|
</Button>
|
|
<div>
|
|
<h1 className="text-3xl font-bold">Détail de la facture</h1>
|
|
<p className="text-gray-500 mt-1">
|
|
{invoice.supplierName || "Fournisseur inconnu"} • {invoice.invoiceNumber || "N° inconnu"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
{isEditing ? (
|
|
<>
|
|
<Button variant="outline" onClick={() => setIsEditing(false)}>
|
|
Annuler
|
|
</Button>
|
|
<Button onClick={handleSave} disabled={updateMutation.isPending}>
|
|
<Save className="w-4 h-4 mr-2" />
|
|
Enregistrer
|
|
</Button>
|
|
</>
|
|
) : (
|
|
<Button onClick={() => setIsEditing(true)}>
|
|
Modifier
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
{/* Left column - PDF Viewer */}
|
|
<div className="lg:col-span-2">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Aperçu du document</CardTitle>
|
|
<CardDescription>Fichier PDF de la facture</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="border rounded-lg overflow-hidden bg-gray-50">
|
|
<iframe
|
|
src={invoice.fileUrl}
|
|
className="w-full h-[800px]"
|
|
title="Aperçu de la facture"
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Right column - Metadata */}
|
|
<div className="space-y-6">
|
|
{/* Status Card */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Statut</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
<div>
|
|
<Label className="text-sm text-gray-500">Statut d'export</Label>
|
|
<div className="mt-1">{getStatusBadge(invoice.exportStatus || "not_exported")}</div>
|
|
</div>
|
|
<div>
|
|
<Label className="text-sm text-gray-500">Score de qualité</Label>
|
|
<div className="mt-1">{getQualityBadge(invoice.qualityScore)}</div>
|
|
</div>
|
|
{invoice.manuallyEdited === 1 && (
|
|
<div>
|
|
<Badge variant="outline" className="bg-orange-50 text-orange-700 border-orange-200">
|
|
Modifié manuellement
|
|
</Badge>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Metadata Card */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Métadonnées</CardTitle>
|
|
<CardDescription>
|
|
{isEditing ? "Modifiez les champs ci-dessous" : "Informations extraites de la facture"}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div>
|
|
<Label htmlFor="supplierName">Fournisseur</Label>
|
|
{isEditing ? (
|
|
<Input
|
|
id="supplierName"
|
|
value={formData.supplierName}
|
|
onChange={(e) => setFormData({ ...formData, supplierName: e.target.value })}
|
|
placeholder="Nom du fournisseur"
|
|
className={!formData.supplierName ? "border-red-500 border-2" : ""}
|
|
/>
|
|
) : (
|
|
<div className="text-sm mt-1">{invoice.supplierName || "-"}</div>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="invoiceNumber">Numéro de facture</Label>
|
|
{isEditing ? (
|
|
<Input
|
|
id="invoiceNumber"
|
|
value={formData.invoiceNumber}
|
|
onChange={(e) => setFormData({ ...formData, invoiceNumber: e.target.value })}
|
|
placeholder="N° de facture"
|
|
className={!formData.invoiceNumber ? "border-red-500 border-2" : ""}
|
|
/>
|
|
) : (
|
|
<div className="text-sm mt-1">{invoice.invoiceNumber || "-"}</div>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="invoiceDate">Date de facture</Label>
|
|
{isEditing ? (
|
|
<Input
|
|
id="invoiceDate"
|
|
type="date"
|
|
value={formData.invoiceDate}
|
|
onChange={(e) => setFormData({ ...formData, invoiceDate: e.target.value })}
|
|
className={!formData.invoiceDate ? "border-red-500 border-2" : ""}
|
|
/>
|
|
) : (
|
|
<div className="text-sm mt-1">
|
|
{invoice.invoiceDate
|
|
? new Date(invoice.invoiceDate).toLocaleDateString("fr-FR")
|
|
: "-"}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="deliveryNoteNumber">N° Bon de livraison</Label>
|
|
{isEditing ? (
|
|
<Input
|
|
id="deliveryNoteNumber"
|
|
value={formData.deliveryNoteNumber}
|
|
onChange={(e) => setFormData({ ...formData, deliveryNoteNumber: e.target.value })}
|
|
placeholder="N° de bon de livraison"
|
|
className={!formData.deliveryNoteNumber ? "border-red-500 border-2" : ""}
|
|
/>
|
|
) : (
|
|
<div className="text-sm mt-1">{invoice.deliveryNoteNumber || "-"}</div>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="orderNumber">N° de commande</Label>
|
|
{isEditing ? (
|
|
<Input
|
|
id="orderNumber"
|
|
value={formData.orderNumber}
|
|
onChange={(e) => setFormData({ ...formData, orderNumber: e.target.value })}
|
|
placeholder="N° de commande"
|
|
className={!formData.orderNumber ? "border-red-500 border-2" : ""}
|
|
/>
|
|
) : (
|
|
<div className="text-sm mt-1">{invoice.orderNumber || "-"}</div>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="totalAmount">Montant total</Label>
|
|
{isEditing ? (
|
|
<Input
|
|
id="totalAmount"
|
|
type="number"
|
|
step="0.01"
|
|
value={formData.totalAmount}
|
|
onChange={(e) => setFormData({ ...formData, totalAmount: e.target.value })}
|
|
placeholder="Montant en €"
|
|
className={!formData.totalAmount ? "border-red-500 border-2" : ""}
|
|
/>
|
|
) : (
|
|
<div className="text-sm mt-1">
|
|
{invoice.totalAmount
|
|
? `${parseFloat(invoice.totalAmount as string).toFixed(2)} €`
|
|
: "-"}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="recipientName">Destinataire</Label>
|
|
{isEditing ? (
|
|
<Input
|
|
id="recipientName"
|
|
value={formData.recipientName}
|
|
onChange={(e) => setFormData({ ...formData, recipientName: e.target.value })}
|
|
placeholder="Nom du destinataire"
|
|
/>
|
|
) : (
|
|
<div className="text-sm mt-1">{(invoice as any).recipientName || "-"}</div>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Business Fields Card */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Champs métier</CardTitle>
|
|
<CardDescription>
|
|
Informations de classification comptable
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div>
|
|
<Label htmlFor="serviceConcerne">Service concerné</Label>
|
|
{isEditing ? (
|
|
<select
|
|
value={formData.serviceConcerne}
|
|
onChange={(e) => setFormData({ ...formData, serviceConcerne: e.target.value })}
|
|
className="w-full px-3 py-2 border rounded-md"
|
|
>
|
|
<option value="">Sélectionner...</option>
|
|
{departments?.map((dept) => (
|
|
<option key={dept.id} value={dept.name}>{dept.name}</option>
|
|
))}
|
|
</select>
|
|
) : (
|
|
<div className="text-sm mt-1">{invoice.serviceConcerne || "-"}</div>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="typeAchat">Type d'achat</Label>
|
|
{isEditing ? (
|
|
<select
|
|
id="typeAchat"
|
|
value={formData.typeAchat}
|
|
onChange={(e) => setFormData({ ...formData, typeAchat: e.target.value })}
|
|
className="w-full px-3 py-2 border rounded-md"
|
|
>
|
|
<option value="">Sélectionner...</option>
|
|
<option value="CAPEX">CAPEX</option>
|
|
<option value="OPEX">OPEX</option>
|
|
</select>
|
|
) : (
|
|
<div className="text-sm mt-1">{invoice.typeAchat || "-"}</div>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="ventilationComptable">Ventilation comptable</Label>
|
|
{isEditing ? (
|
|
<select
|
|
value={formData.ventilationComptable}
|
|
onChange={(e) => setFormData({ ...formData, ventilationComptable: e.target.value })}
|
|
className="w-full px-3 py-2 border rounded-md"
|
|
>
|
|
<option value="">Sélectionner...</option>
|
|
{allocations?.map((alloc) => (
|
|
<option key={alloc.id} value={alloc.name}>{alloc.name}</option>
|
|
))}
|
|
</select>
|
|
) : (
|
|
<div className="text-sm mt-1">{invoice.ventilationComptable || "-"}</div>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Info Card */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Informations</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2 text-sm">
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-500">Créé le</span>
|
|
<span>{new Date(invoice.createdAt).toLocaleDateString("fr-FR")}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-500">Mis à jour le</span>
|
|
<span>{new Date(invoice.updatedAt).toLocaleDateString("fr-FR")}</span>
|
|
</div>
|
|
{invoice.exportedAt && (
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-500">Exporté le</span>
|
|
<span>{new Date(invoice.exportedAt).toLocaleDateString("fr-FR")}</span>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</DashboardLayout>
|
|
);
|
|
}
|