diff --git a/client/src/pages/VentilationFreePro.tsx b/client/src/pages/VentilationFreePro.tsx index e075233..775d7ee 100644 --- a/client/src/pages/VentilationFreePro.tsx +++ b/client/src/pages/VentilationFreePro.tsx @@ -34,6 +34,7 @@ import { Hash, BarChart3, Euro, + Share2, } from "lucide-react"; import jsPDF from "jspdf"; import autoTable from "jspdf-autotable"; @@ -82,16 +83,22 @@ function typeBadgeColor(type: string): string { return "bg-orange-100 text-orange-800 border-orange-200"; } -// ── Export PDF ───────────────────────────────────────────────────────────── +/** Formatage montant pour PDF : sans espace insécable pour éviter les artefacts jsPDF */ +function formatMontantPdf(centimes: number): string { + const val = centimes / 100; + const parts = val.toFixed(2).split("."); + // Séparateur de milliers = espace simple (pas \u202f qui cause le slash) + const intPart = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, " "); + return `${intPart},${parts[1]} EUR`; +} -function exportToPdf( - importRecord: ImportRecord, - lines: VentilationLine[] -) { - // A4 paysage : 297mm × 210mm - const doc = new jsPDF({ orientation: "landscape", unit: "mm", format: "a4" }); - const pageW = 297; - const pageH = 210; +// ── Construction du PDF (partagée entre export local et SharePoint) ───────── + +function buildPdfDoc(importRecord: ImportRecord, lines: VentilationLine[]): jsPDF { + // A4 portrait : 210mm × 297mm + const doc = new jsPDF({ orientation: "portrait", unit: "mm", format: "a4" }); + const pageW = 210; + const pageH = 297; const margin = 14; // En-tête @@ -107,7 +114,7 @@ function exportToPdf( // Date d'édition (haut droite) const now = new Date(); - const editDate = `Edité le ${now.toLocaleDateString("fr-FR")} - ${now.toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" })}`; + const editDate = `Edite le ${now.toLocaleDateString("fr-FR")} - ${now.toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" })}`; doc.setFontSize(8); doc.setFont("helvetica", "normal"); doc.text(editDate, pageW - margin, 9, { align: "right" }); @@ -119,35 +126,33 @@ function exportToPdf( doc.setFont("helvetica", "normal"); doc.text(importRecord.refPiece ?? "", margin + 25, 29); - // Tableau principal + // Données tableau const tableData = lines.map((l) => [ l.structure ?? "(vide)", l.type, - formatMontant(l.montantCentimes), + formatMontantPdf(l.montantCentimes), ]); - - // Total général const totalCentimes = lines.reduce((s, l) => s + l.montantCentimes, 0); - // Calcul dynamique des largeurs pour tenir sur 1 page - const usableW = pageW - margin * 2; // ~269mm - const colStructure = usableW * 0.50; // ~134mm - const colType = usableW * 0.30; // ~81mm - const colMontant = usableW * 0.20; // ~54mm + // Largeurs colonnes A4 portrait (~182mm utiles) + const usableW = pageW - margin * 2; + const colStructure = usableW * 0.45; + const colType = usableW * 0.32; + const colMontant = usableW * 0.23; - // Calcul de la taille de police pour tenir sur 1 page - // A4 paysage : ~170mm de hauteur utile (210 - 35 header - 10 footer) - const nbRows = lines.length + 2; // +1 header +1 footer - const availH = pageH - 38 - 12; // zone tableau + // Taille de police dynamique pour tenir sur 1 page + const nbRows = lines.length + 2; + const availH = pageH - 38 - 15; const rowH = Math.min(8, Math.floor(availH / nbRows)); - const fontSize = rowH >= 7 ? 9 : rowH >= 6 ? 8 : 7; + const fontSize = rowH >= 7 ? 9 : rowH >= 6 ? 8 : rowH >= 5 ? 7 : 6; + const cellPad = rowH >= 7 ? 2 : 1.2; autoTable(doc, { startY: 33, head: [["Structure", "Type", "Montant TTC"]], body: tableData, - foot: [["Total général", "", formatMontant(totalCentimes)]], - styles: { fontSize, cellPadding: rowH >= 7 ? 2 : 1.5, overflow: "linebreak" }, + foot: [["Total general", "", formatMontantPdf(totalCentimes)]], + styles: { fontSize, cellPadding: cellPad, overflow: "linebreak" }, headStyles: { fillColor: [255, 255, 255], textColor: [0, 0, 0], fontStyle: "bold", lineWidth: 0.3, lineColor: [0, 0, 0], @@ -163,18 +168,28 @@ function exportToPdf( 2: { cellWidth: colMontant, halign: "right" }, }, alternateRowStyles: { fillColor: [248, 248, 248] }, - // Forcer tout sur 1 page pageBreak: "avoid", rowPageBreak: "avoid", margin: { left: margin, right: margin }, tableWidth: usableW, }); + return doc; +} + +function exportToPdf(importRecord: ImportRecord, lines: VentilationLine[]) { + const doc = buildPdfDoc(importRecord, lines); + const [moisStr, anneeStr] = importRecord.moisLabel.split("/"); const moisPad = moisStr.padStart(2, "0"); const anneeCourt = anneeStr.slice(2); doc.save(`FreePro - ventilation facture ${moisPad}.${anneeCourt}.pdf`); } +function buildPdfBase64(importRecord: ImportRecord, lines: VentilationLine[]): string { + const doc = buildPdfDoc(importRecord, lines); + return doc.output("datauristring").split(",")[1]; +} + // ── Composant principal ──────────────────────────────────────────────────── function VentilationFreeProContent() { @@ -206,7 +221,7 @@ function VentilationFreeProContent() { const importMutation = trpc.freepro.import.useMutation({ onSuccess: (data) => { utils.freepro.list.invalidate(); - toast.success(`Import réussi — ${data.nbLignes} lignes traitées — Total TTC : ${formatTotalTtc(data.totalTtc)}`); + toast.success(`Import reussi — ${data.nbLignes} lignes traitees — Total TTC : ${formatTotalTtc(data.totalTtc)}`); setSelectedImportId(data.importId); setView("detail"); }, @@ -219,15 +234,36 @@ function VentilationFreeProContent() { onSuccess: () => { utils.freepro.list.invalidate(); if (view === "detail") setView("list"); - toast.success("Import supprimé"); + toast.success("Import supprime"); }, }); + const [isExportingSP, setIsExportingSP] = useState(false); + const exportToSharePointMutation = trpc.freepro.exportToSharePoint.useMutation({ + onSuccess: (data) => { + setIsExportingSP(false); + toast.success(`Fichier depose sur SharePoint : ${data.fileName}`); + }, + onError: (err) => { + setIsExportingSP(false); + toast.error(`Erreur SharePoint : ${err.message}`); + }, + }); + + const handleExportSharePoint = useCallback(() => { + if (!selectedImportId || !detail) return; + const imp = detail.import as ImportRecord; + const lines = (detail.lines ?? []) as VentilationLine[]; + const pdfBase64 = buildPdfBase64(imp, lines); + setIsExportingSP(true); + exportToSharePointMutation.mutate({ id: selectedImportId, pdfBase64 }); + }, [selectedImportId, detail, exportToSharePointMutation]); + // Gestion du fichier const handleFile = useCallback( (file: File) => { if (!file.name.match(/\.(xlsx|xls|csv)$/i)) { - toast.error("Format invalide : veuillez sélectionner un fichier Excel (.xlsx, .xls) ou CSV (.csv)"); + toast.error("Format invalide : veuillez selectionner un fichier Excel (.xlsx, .xls) ou CSV (.csv)"); return; } const reader = new FileReader(); @@ -237,7 +273,7 @@ function VentilationFreeProContent() { }; reader.readAsDataURL(file); }, - [moisLabel, importMutation, toast] + [moisLabel, importMutation] ); const handleDrop = useCallback( @@ -251,23 +287,22 @@ function VentilationFreeProContent() { ); // ── Rendu liste ────────────────────────────────────────────────────────── - if (view === "list") { return (
Import et ventilation des factures Free Pro par structure
+Import et ventilation des factures Free Pro par structure
+Glissez votre fichier FreePro ici
+ou cliquez pour sélectionner
+Formats acceptés : .xlsx, .xls, .csv
{ const f = e.target.files?.[0]; if (f) handleFile(f); }} /> - {importMutation.isPending ? ( -Traitement en cours…
-- Glissez-déposez le fichier Excel FreePro ici -
-ou cliquez pour sélectionner (.xlsx, .xls, .csv)
-