Files
demat-facturation/client/src/pages/InvoiceDetail.tsx

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>
);
}