Checkpoint: Ajout d'un bouton "Détail" dans la liste des factures pour afficher le texte complet extrait du PDF. Le texte est maintenant demandé à Mistral AI lors de l'extraction et sauvegardé dans la base de données. Une boîte de dialogue affiche le texte complet avec défilement.

This commit is contained in:
Manus
2026-02-11 09:30:58 -05:00
parent 70d1cf23e5
commit 14c50fc8fd
10 changed files with 1150 additions and 2 deletions

View File

@@ -48,6 +48,8 @@ export default function Invoices() {
const [addDialogType, setAddDialogType] = useState<"service" | "ventilation" | null>(null); const [addDialogType, setAddDialogType] = useState<"service" | "ventilation" | null>(null);
const [newItemName, setNewItemName] = useState(""); const [newItemName, setNewItemName] = useState("");
const [pendingInvoiceId, setPendingInvoiceId] = useState<number | null>(null); const [pendingInvoiceId, setPendingInvoiceId] = useState<number | null>(null);
const [textDialogOpen, setTextDialogOpen] = useState(false);
const [selectedInvoiceText, setSelectedInvoiceText] = useState<string | null>(null);
const { data: invoices, isLoading } = trpc.invoices.list.useQuery(); const { data: invoices, isLoading } = trpc.invoices.list.useQuery();
const { data: departments } = trpc.departments.getByUser.useQuery(); const { data: departments } = trpc.departments.getByUser.useQuery();
const { data: allocations } = trpc.accountingAllocations.getByUser.useQuery(); const { data: allocations } = trpc.accountingAllocations.getByUser.useQuery();
@@ -511,11 +513,24 @@ export default function Invoices() {
<TableCell>{getExportStatusBadge(invoice.exportStatus || "not_exported")}</TableCell> <TableCell>{getExportStatusBadge(invoice.exportStatus || "not_exported")}</TableCell>
<TableCell> <TableCell>
<div className="flex gap-2"> <div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => {
setSelectedInvoiceText(invoice.extractedText || "Aucun texte extrait disponible");
setTextDialogOpen(true);
}}
className="h-8 px-2"
title="Voir le texte extrait"
>
<FileText className="h-4 w-4" />
</Button>
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
onClick={() => setLocation(`/invoices/${invoice.id}`)} onClick={() => setLocation(`/invoices/${invoice.id}`)}
className="h-8 px-2" className="h-8 px-2"
title="Modifier"
> >
<Edit className="h-4 w-4" /> <Edit className="h-4 w-4" />
</Button> </Button>
@@ -527,6 +542,7 @@ export default function Invoices() {
setDeleteDialogOpen(true); setDeleteDialogOpen(true);
}} }}
className="h-8 px-2 text-red-600 hover:text-red-700 hover:bg-red-50" className="h-8 px-2 text-red-600 hover:text-red-700 hover:bg-red-50"
title="Supprimer"
> >
<Trash className="h-4 w-4" /> <Trash className="h-4 w-4" />
</Button> </Button>
@@ -625,6 +641,24 @@ export default function Invoices() {
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* Extracted Text Dialog */}
<Dialog open={textDialogOpen} onOpenChange={setTextDialogOpen}>
<DialogContent className="max-w-3xl max-h-[80vh]">
<DialogHeader>
<DialogTitle>Texte extrait de la facture</DialogTitle>
<DialogDescription>
Texte complet extrait du PDF lors de l'import
</DialogDescription>
</DialogHeader>
<div className="overflow-y-auto max-h-[60vh] p-4 bg-gray-50 rounded border">
<pre className="whitespace-pre-wrap text-sm font-mono">{selectedInvoiceText}</pre>
</div>
<DialogFooter>
<Button onClick={() => setTextDialogOpen(false)}>Fermer</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</DashboardLayout> </DashboardLayout>
); );
} }

View File

@@ -0,0 +1 @@
ALTER TABLE `invoices` ADD `extractedText` text;

File diff suppressed because it is too large Load Diff

View File

@@ -36,6 +36,13 @@
"when": 1770814574366, "when": 1770814574366,
"tag": "0004_white_the_hunter", "tag": "0004_white_the_hunter",
"breakpoints": true "breakpoints": true
},
{
"idx": 5,
"version": "5",
"when": 1770819973462,
"tag": "0005_small_roxanne_simpson",
"breakpoints": true
} }
] ]
} }

View File

@@ -89,6 +89,9 @@ export const invoices = mysqlTable("invoices", {
typeAchat: mysqlEnum("typeAchat", ["CAPEX", "OPEX"]), // Type d'achat typeAchat: mysqlEnum("typeAchat", ["CAPEX", "OPEX"]), // Type d'achat
ventilationComptable: varchar("ventilationComptable", { length: 100 }), // Ventilation comptable (TOUS, PA, HEP, etc.) ventilationComptable: varchar("ventilationComptable", { length: 100 }), // Ventilation comptable (TOUS, PA, HEP, etc.)
// Extracted text from PDF
extractedText: text("extractedText"), // Full text extracted from the invoice PDF
// SFTP Export tracking // SFTP Export tracking
exportedAt: timestamp("exportedAt"), exportedAt: timestamp("exportedAt"),
exportMode: mysqlEnum("exportMode", ["manual", "automatic"]), exportMode: mysqlEnum("exportMode", ["manual", "automatic"]),

View File

@@ -154,6 +154,7 @@ async function processEmailAttachment(
totalAmount: invoiceData.totalAmount?.toString(), totalAmount: invoiceData.totalAmount?.toString(),
pageRange: invoiceData.pageRange, pageRange: invoiceData.pageRange,
qualityScore: invoiceData.qualityScore, qualityScore: invoiceData.qualityScore,
extractedText: invoiceData.extractedText,
metadataFileKey: metadataKey, metadataFileKey: metadataKey,
metadataFileUrl: metadataUrl, metadataFileUrl: metadataUrl,
status: "completed", status: "completed",

View File

@@ -155,6 +155,7 @@ async function processFolderFile(
totalAmount: invoiceData.totalAmount?.toString(), totalAmount: invoiceData.totalAmount?.toString(),
pageRange: invoiceData.pageRange, pageRange: invoiceData.pageRange,
qualityScore: invoiceData.qualityScore, qualityScore: invoiceData.qualityScore,
extractedText: invoiceData.extractedText,
metadataFileKey: metadataKey, metadataFileKey: metadataKey,
metadataFileUrl: metadataUrl, metadataFileUrl: metadataUrl,
status: "completed", status: "completed",

View File

@@ -11,6 +11,7 @@ export interface ExtractedInvoiceData {
totalAmount: number | null; totalAmount: number | null;
pageRange: string; pageRange: string;
qualityScore: number; // 0-100 qualityScore: number; // 0-100
extractedText: string | null; // Full text extracted from the invoice PDF
} }
export interface MultiInvoiceResult { export interface MultiInvoiceResult {
@@ -143,7 +144,8 @@ Pour chaque facture trouvée, extrais les informations suivantes:
- orderNumber: Numéro de commande client (si présent) - orderNumber: Numéro de commande client (si présent)
- totalAmount: Montant total TTC (nombre décimal) - totalAmount: Montant total TTC (nombre décimal)
- pageRange: Plage de pages de cette facture (ex: "1-2" ou "5") - pageRange: Plage de pages de cette facture (ex: "1-2" ou "5")
- qualityScore: Score de qualité de l'extraction de 0 à 100 (100 = toutes les informations trouvées et claires)${keywordsHint} - qualityScore: Score de qualité de l'extraction de 0 à 100 (100 = toutes les informations trouvées et claires)
- extractedText: Texte complet extrait de la facture (tout le texte visible sur les pages de cette facture)${keywordsHint}
Réponds UNIQUEMENT avec un objet JSON valide au format suivant: Réponds UNIQUEMENT avec un objet JSON valide au format suivant:
{ {
@@ -158,7 +160,8 @@ Réponds UNIQUEMENT avec un objet JSON valide au format suivant:
"orderNumber": "...", "orderNumber": "...",
"totalAmount": 123.45, "totalAmount": 123.45,
"pageRange": "1-2", "pageRange": "1-2",
"qualityScore": 85 "qualityScore": 85,
"extractedText": "Texte complet de la facture..."
} }
] ]
} }

View File

@@ -234,6 +234,7 @@ export const appRouter = router({
totalAmount: invoiceData.totalAmount?.toString(), totalAmount: invoiceData.totalAmount?.toString(),
pageRange: invoiceData.pageRange, pageRange: invoiceData.pageRange,
qualityScore: invoiceData.qualityScore, qualityScore: invoiceData.qualityScore,
extractedText: invoiceData.extractedText,
metadataFileKey: metadataKey, metadataFileKey: metadataKey,
metadataFileUrl: metadataUrl, metadataFileUrl: metadataUrl,
status: "completed", status: "completed",

View File

@@ -238,3 +238,12 @@
- [x] Sauvegarder la nouvelle entrée dans la base de données (createDepartmentMutation, createAllocationMutation) - [x] Sauvegarder la nouvelle entrée dans la base de données (createDepartmentMutation, createAllocationMutation)
- [x] Sélectionner automatiquement la nouvelle valeur pour la facture - [x] Sélectionner automatiquement la nouvelle valeur pour la facture
- [x] Rafraîchir les listes après ajout (invalidate queries) - [x] Rafraîchir les listes après ajout (invalidate queries)
## Affichage du texte extrait de la facture
- [x] Ajouter une colonne extractedText dans la table invoices
- [x] Modifier le prompt Mistral pour demander le texte extrait
- [x] Modifier le type ExtractedInvoiceData pour inclure extractedText
- [x] Modifier routers.ts, emailImportService.ts et folderImportService.ts pour sauvegarder extractedText
- [x] Ajouter un bouton "Détail" (icône FileText) dans la liste des factures
- [x] Créer une boîte de dialogue pour afficher le texte complet
- [ ] Tester l'affichage du texte extrait avec une nouvelle facture importée