Checkpoint: Ajout des filtres par statut d'export, export Excel et page de détail avec édition manuelle.
Nouvelles fonctionnalités : 1. Filtres par statut d'export : - Boutons de filtre rapide (Tous/Exportés/Non exportés/Erreurs) - Compteurs dynamiques pour chaque statut - Couleurs distinctives (vert/bleu/rouge) 2. Export Excel : - Nouveau bouton "Excel" dans la page Factures - Génération automatique de fichier .xlsx avec toutes les métadonnées - Colonnes : Fournisseur, N° Facture, Date, N° BL, N° Commande, Montant, Score, Statut, Dates 3. Page de détail de facture (/invoices/:id) : - Visualisation PDF intégrée dans iframe - Affichage des métadonnées extraites - Mode édition avec formulaire complet - Recalcul automatique du score de qualité après édition - Marquage "Modifié manuellement" - Badges de statut et qualité - Lien cliquable sur le nom du fournisseur dans la liste L'application offre maintenant une expérience complète de gestion des factures avec filtrage avancé, exports multiples et édition manuelle.
This commit is contained in:
346
client/src/pages/InvoiceDetail.tsx
Normal file
346
client/src/pages/InvoiceDetail.tsx
Normal file
@@ -0,0 +1,346 @@
|
||||
import { useState } from "react";
|
||||
import { useParams, useLocation } 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";
|
||||
|
||||
export default function InvoiceDetail() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [, setLocation] = useLocation();
|
||||
const invoiceId = parseInt(id || "0");
|
||||
|
||||
const { data: invoice, isLoading } = trpc.invoices.getById.useQuery({ id: invoiceId });
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
supplierName: "",
|
||||
invoiceNumber: "",
|
||||
invoiceDate: "",
|
||||
deliveryNoteNumber: "",
|
||||
orderNumber: "",
|
||||
totalAmount: "",
|
||||
});
|
||||
|
||||
// Initialize form data when invoice loads
|
||||
useState(() => {
|
||||
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() : "",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
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("/invoices")}>
|
||||
<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("/invoices")}>
|
||||
<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"
|
||||
/>
|
||||
) : (
|
||||
<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"
|
||||
/>
|
||||
) : (
|
||||
<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 })}
|
||||
/>
|
||||
) : (
|
||||
<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"
|
||||
/>
|
||||
) : (
|
||||
<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"
|
||||
/>
|
||||
) : (
|
||||
<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 €"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-sm mt-1">
|
||||
{invoice.totalAmount
|
||||
? `${parseFloat(invoice.totalAmount as string).toFixed(2)} €`
|
||||
: "-"}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user