fix: PDF A4 portrait, total sans slash, bouton SharePoint FreePro
This commit is contained in:
@@ -34,6 +34,7 @@ import {
|
|||||||
Hash,
|
Hash,
|
||||||
BarChart3,
|
BarChart3,
|
||||||
Euro,
|
Euro,
|
||||||
|
Share2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import jsPDF from "jspdf";
|
import jsPDF from "jspdf";
|
||||||
import autoTable from "jspdf-autotable";
|
import autoTable from "jspdf-autotable";
|
||||||
@@ -82,16 +83,22 @@ function typeBadgeColor(type: string): string {
|
|||||||
return "bg-orange-100 text-orange-800 border-orange-200";
|
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(
|
// ── Construction du PDF (partagée entre export local et SharePoint) ─────────
|
||||||
importRecord: ImportRecord,
|
|
||||||
lines: VentilationLine[]
|
function buildPdfDoc(importRecord: ImportRecord, lines: VentilationLine[]): jsPDF {
|
||||||
) {
|
// A4 portrait : 210mm × 297mm
|
||||||
// A4 paysage : 297mm × 210mm
|
const doc = new jsPDF({ orientation: "portrait", unit: "mm", format: "a4" });
|
||||||
const doc = new jsPDF({ orientation: "landscape", unit: "mm", format: "a4" });
|
const pageW = 210;
|
||||||
const pageW = 297;
|
const pageH = 297;
|
||||||
const pageH = 210;
|
|
||||||
const margin = 14;
|
const margin = 14;
|
||||||
|
|
||||||
// En-tête
|
// En-tête
|
||||||
@@ -107,7 +114,7 @@ function exportToPdf(
|
|||||||
|
|
||||||
// Date d'édition (haut droite)
|
// Date d'édition (haut droite)
|
||||||
const now = new Date();
|
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.setFontSize(8);
|
||||||
doc.setFont("helvetica", "normal");
|
doc.setFont("helvetica", "normal");
|
||||||
doc.text(editDate, pageW - margin, 9, { align: "right" });
|
doc.text(editDate, pageW - margin, 9, { align: "right" });
|
||||||
@@ -119,35 +126,33 @@ function exportToPdf(
|
|||||||
doc.setFont("helvetica", "normal");
|
doc.setFont("helvetica", "normal");
|
||||||
doc.text(importRecord.refPiece ?? "", margin + 25, 29);
|
doc.text(importRecord.refPiece ?? "", margin + 25, 29);
|
||||||
|
|
||||||
// Tableau principal
|
// Données tableau
|
||||||
const tableData = lines.map((l) => [
|
const tableData = lines.map((l) => [
|
||||||
l.structure ?? "(vide)",
|
l.structure ?? "(vide)",
|
||||||
l.type,
|
l.type,
|
||||||
formatMontant(l.montantCentimes),
|
formatMontantPdf(l.montantCentimes),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Total général
|
|
||||||
const totalCentimes = lines.reduce((s, l) => s + l.montantCentimes, 0);
|
const totalCentimes = lines.reduce((s, l) => s + l.montantCentimes, 0);
|
||||||
|
|
||||||
// Calcul dynamique des largeurs pour tenir sur 1 page
|
// Largeurs colonnes A4 portrait (~182mm utiles)
|
||||||
const usableW = pageW - margin * 2; // ~269mm
|
const usableW = pageW - margin * 2;
|
||||||
const colStructure = usableW * 0.50; // ~134mm
|
const colStructure = usableW * 0.45;
|
||||||
const colType = usableW * 0.30; // ~81mm
|
const colType = usableW * 0.32;
|
||||||
const colMontant = usableW * 0.20; // ~54mm
|
const colMontant = usableW * 0.23;
|
||||||
|
|
||||||
// Calcul de la taille de police pour tenir sur 1 page
|
// Taille de police dynamique pour tenir sur 1 page
|
||||||
// A4 paysage : ~170mm de hauteur utile (210 - 35 header - 10 footer)
|
const nbRows = lines.length + 2;
|
||||||
const nbRows = lines.length + 2; // +1 header +1 footer
|
const availH = pageH - 38 - 15;
|
||||||
const availH = pageH - 38 - 12; // zone tableau
|
|
||||||
const rowH = Math.min(8, Math.floor(availH / nbRows));
|
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, {
|
autoTable(doc, {
|
||||||
startY: 33,
|
startY: 33,
|
||||||
head: [["Structure", "Type", "Montant TTC"]],
|
head: [["Structure", "Type", "Montant TTC"]],
|
||||||
body: tableData,
|
body: tableData,
|
||||||
foot: [["Total général", "", formatMontant(totalCentimes)]],
|
foot: [["Total general", "", formatMontantPdf(totalCentimes)]],
|
||||||
styles: { fontSize, cellPadding: rowH >= 7 ? 2 : 1.5, overflow: "linebreak" },
|
styles: { fontSize, cellPadding: cellPad, overflow: "linebreak" },
|
||||||
headStyles: {
|
headStyles: {
|
||||||
fillColor: [255, 255, 255], textColor: [0, 0, 0],
|
fillColor: [255, 255, 255], textColor: [0, 0, 0],
|
||||||
fontStyle: "bold", lineWidth: 0.3, lineColor: [0, 0, 0],
|
fontStyle: "bold", lineWidth: 0.3, lineColor: [0, 0, 0],
|
||||||
@@ -163,18 +168,28 @@ function exportToPdf(
|
|||||||
2: { cellWidth: colMontant, halign: "right" },
|
2: { cellWidth: colMontant, halign: "right" },
|
||||||
},
|
},
|
||||||
alternateRowStyles: { fillColor: [248, 248, 248] },
|
alternateRowStyles: { fillColor: [248, 248, 248] },
|
||||||
// Forcer tout sur 1 page
|
|
||||||
pageBreak: "avoid",
|
pageBreak: "avoid",
|
||||||
rowPageBreak: "avoid",
|
rowPageBreak: "avoid",
|
||||||
margin: { left: margin, right: margin },
|
margin: { left: margin, right: margin },
|
||||||
tableWidth: usableW,
|
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 moisPad = moisStr.padStart(2, "0");
|
||||||
const anneeCourt = anneeStr.slice(2);
|
const anneeCourt = anneeStr.slice(2);
|
||||||
doc.save(`FreePro - ventilation facture ${moisPad}.${anneeCourt}.pdf`);
|
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 ────────────────────────────────────────────────────
|
// ── Composant principal ────────────────────────────────────────────────────
|
||||||
|
|
||||||
function VentilationFreeProContent() {
|
function VentilationFreeProContent() {
|
||||||
@@ -206,7 +221,7 @@ function VentilationFreeProContent() {
|
|||||||
const importMutation = trpc.freepro.import.useMutation({
|
const importMutation = trpc.freepro.import.useMutation({
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
utils.freepro.list.invalidate();
|
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);
|
setSelectedImportId(data.importId);
|
||||||
setView("detail");
|
setView("detail");
|
||||||
},
|
},
|
||||||
@@ -219,15 +234,36 @@ function VentilationFreeProContent() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
utils.freepro.list.invalidate();
|
utils.freepro.list.invalidate();
|
||||||
if (view === "detail") setView("list");
|
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
|
// Gestion du fichier
|
||||||
const handleFile = useCallback(
|
const handleFile = useCallback(
|
||||||
(file: File) => {
|
(file: File) => {
|
||||||
if (!file.name.match(/\.(xlsx|xls|csv)$/i)) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
@@ -237,7 +273,7 @@ function VentilationFreeProContent() {
|
|||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
},
|
},
|
||||||
[moisLabel, importMutation, toast]
|
[moisLabel, importMutation]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDrop = useCallback(
|
const handleDrop = useCallback(
|
||||||
@@ -251,23 +287,22 @@ function VentilationFreeProContent() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ── Rendu liste ──────────────────────────────────────────────────────────
|
// ── Rendu liste ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
if (view === "list") {
|
if (view === "list") {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* En-tête */}
|
{/* En-tête */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="p-2 rounded-lg bg-blue-50 border border-blue-200">
|
|
||||||
<BarChart3 className="h-6 w-6 text-blue-600" />
|
<BarChart3 className="h-6 w-6 text-blue-600" />
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-foreground">Ventilation FreePro</h1>
|
<h1 className="text-2xl font-bold">Ventilation FreePro</h1>
|
||||||
<p className="text-sm text-muted-foreground">Import et ventilation des factures Free Pro par structure</p>
|
<p className="text-sm text-muted-foreground">Import et ventilation des factures Free Pro par structure</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Zone d'import */}
|
{/* Zone d'import */}
|
||||||
<Card className="border-2 border-dashed border-blue-200 bg-blue-50/30">
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-base flex items-center gap-2">
|
<CardTitle className="text-base flex items-center gap-2">
|
||||||
<Upload className="h-4 w-4 text-blue-600" />
|
<Upload className="h-4 w-4 text-blue-600" />
|
||||||
@@ -277,31 +312,31 @@ function VentilationFreeProContent() {
|
|||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
{/* Sélecteur de mois */}
|
{/* Sélecteur de mois */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<label className="text-sm font-medium text-foreground w-28">Mois de facturation</label>
|
<label className="text-sm font-medium text-muted-foreground whitespace-nowrap">Mois de facturation :</label>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Calendar className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={moisLabel}
|
value={moisLabel}
|
||||||
onChange={(e) => setMoisLabel(e.target.value)}
|
onChange={(e) => setMoisLabel(e.target.value)}
|
||||||
placeholder="MM/YYYY"
|
placeholder="MM/AAAA"
|
||||||
className="border rounded px-3 py-1.5 text-sm w-28 focus:outline-none focus:ring-2 focus:ring-blue-400"
|
className="border rounded px-3 py-1.5 text-sm w-32 font-mono focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
pattern="\d{2}/\d{4}"
|
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-muted-foreground">Format : MM/YYYY</span>
|
<span className="text-xs text-muted-foreground">Format : MM/AAAA (ex : 06/2025)</span>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Zone de dépôt */}
|
{/* Zone de dépôt */}
|
||||||
<div
|
<div
|
||||||
className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-all ${
|
className={`border-2 border-dashed rounded-lg p-8 text-center transition-colors cursor-pointer ${
|
||||||
isDragging ? "border-blue-500 bg-blue-100" : "border-blue-300 hover:border-blue-400 hover:bg-blue-50"
|
isDragging ? "border-blue-500 bg-blue-50" : "border-muted-foreground/30 hover:border-blue-400 hover:bg-blue-50/30"
|
||||||
} ${importMutation.isPending ? "opacity-50 pointer-events-none" : ""}`}
|
}`}
|
||||||
onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }}
|
onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }}
|
||||||
onDragLeave={() => setIsDragging(false)}
|
onDragLeave={() => setIsDragging(false)}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
>
|
>
|
||||||
|
<FileSpreadsheet className="h-10 w-10 mx-auto mb-3 text-blue-400" />
|
||||||
|
<p className="text-sm font-medium">Glissez votre fichier FreePro ici</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">ou cliquez pour sélectionner</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-2">Formats acceptés : .xlsx, .xls, .csv</p>
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
@@ -309,21 +344,14 @@ function VentilationFreeProContent() {
|
|||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={(e) => { const f = e.target.files?.[0]; if (f) handleFile(f); }}
|
onChange={(e) => { const f = e.target.files?.[0]; if (f) handleFile(f); }}
|
||||||
/>
|
/>
|
||||||
{importMutation.isPending ? (
|
|
||||||
<div className="flex flex-col items-center gap-2">
|
|
||||||
<div className="h-8 w-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
|
||||||
<p className="text-sm text-blue-600 font-medium">Traitement en cours…</p>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div className="flex flex-col items-center gap-2">
|
{importMutation.isPending && (
|
||||||
<FileSpreadsheet className="h-10 w-10 text-blue-400" />
|
<div className="flex items-center gap-2 text-sm text-blue-600">
|
||||||
<p className="text-sm font-medium text-foreground">
|
<div className="h-4 w-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
||||||
Glissez-déposez le fichier Excel FreePro ici
|
Traitement en cours…
|
||||||
</p>
|
|
||||||
<p className="text-xs text-muted-foreground">ou cliquez pour sélectionner (.xlsx, .xls, .csv)</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -436,8 +464,7 @@ function VentilationFreeProContent() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Rendu détail ─────────────────────────────────────────────────────────
|
// ── Rendu détail ──────────────────────────────────────────────────────────
|
||||||
// (suite ci-dessous)
|
|
||||||
|
|
||||||
const imp = detail?.import as ImportRecord | undefined;
|
const imp = detail?.import as ImportRecord | undefined;
|
||||||
const lines = (detail?.lines ?? []) as VentilationLine[];
|
const lines = (detail?.lines ?? []) as VentilationLine[];
|
||||||
@@ -466,6 +493,7 @@ function VentilationFreeProContent() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{imp && (
|
{imp && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => exportToPdf(imp, lines)}
|
onClick={() => exportToPdf(imp, lines)}
|
||||||
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white"
|
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white"
|
||||||
@@ -473,6 +501,20 @@ function VentilationFreeProContent() {
|
|||||||
<Download className="h-4 w-4" />
|
<Download className="h-4 w-4" />
|
||||||
Exporter PDF
|
Exporter PDF
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleExportSharePoint}
|
||||||
|
disabled={isExportingSP}
|
||||||
|
variant="outline"
|
||||||
|
className="flex items-center gap-2 border-green-600 text-green-700 hover:bg-green-50"
|
||||||
|
>
|
||||||
|
{isExportingSP ? (
|
||||||
|
<div className="h-4 w-4 border-2 border-green-600 border-t-transparent rounded-full animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Share2 className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
{isExportingSP ? "Envoi…" : "Envoyer vers SharePoint"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -521,9 +563,9 @@ function VentilationFreeProContent() {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="bg-muted/30">
|
<TableRow className="bg-muted/30">
|
||||||
<TableHead className="font-bold">structure</TableHead>
|
<TableHead className="font-bold">Structure</TableHead>
|
||||||
<TableHead className="font-bold">Type</TableHead>
|
<TableHead className="font-bold">Type</TableHead>
|
||||||
<TableHead className="text-right font-bold">Montant</TableHead>
|
<TableHead className="text-right font-bold">Montant TTC</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
|||||||
@@ -2268,6 +2268,52 @@ export const appRouter = router({
|
|||||||
await deleteFreeproImport(input.id);
|
await deleteFreeproImport(input.id);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
/** Exporte la ventilation FreePro vers SharePoint */
|
||||||
|
exportToSharePoint: protectedProcedure
|
||||||
|
.input(z.object({ id: z.number(), pdfBase64: z.string() }))
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const data = await getFreeproImportWithLines(input.id);
|
||||||
|
if (!data) throw new TRPCError({ code: "NOT_FOUND" });
|
||||||
|
if (data.import.userId !== ctx.user.id)
|
||||||
|
throw new TRPCError({ code: "FORBIDDEN" });
|
||||||
|
|
||||||
|
// Récupérer les paramètres SharePoint depuis importSettings
|
||||||
|
const settings = await getImportSettingsByUser(ctx.user.id);
|
||||||
|
const tenantId = (settings as any)?.azureTenantId || '';
|
||||||
|
const clientId = (settings as any)?.azureClientId || '';
|
||||||
|
const clientSecret = (settings as any)?.azureClientSecret || '';
|
||||||
|
const sharepointUrl = (settings as any)?.exportFolder || '';
|
||||||
|
|
||||||
|
if (!tenantId || !clientId || !clientSecret) {
|
||||||
|
throw new TRPCError({ code: "BAD_REQUEST", message: "Credentials Azure AD non configurés dans les paramètres d'export" });
|
||||||
|
}
|
||||||
|
if (!sharepointUrl) {
|
||||||
|
throw new TRPCError({ code: "BAD_REQUEST", message: "URL SharePoint non configurée dans le dossier d'export" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construire le nom du fichier
|
||||||
|
const [moisStr, anneeStr] = data.import.moisLabel.split('/');
|
||||||
|
const moisPad = moisStr.padStart(2, '0');
|
||||||
|
const anneeCourt = anneeStr.slice(2);
|
||||||
|
const fileName = `FreePro - ventilation facture ${moisPad}.${anneeCourt}.pdf`;
|
||||||
|
|
||||||
|
// Convertir le PDF base64 en buffer
|
||||||
|
const pdfBuffer = Buffer.from(input.pdfBase64, 'base64');
|
||||||
|
|
||||||
|
const { uploadToSharePoint } = await import('./sharepoint');
|
||||||
|
const result = await uploadToSharePoint(
|
||||||
|
{ tenantId, clientId, clientSecret, sharepointUrl },
|
||||||
|
pdfBuffer,
|
||||||
|
fileName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: result.error || 'Erreur upload SharePoint' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, webUrl: result.webUrl, fileName };
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
export type AppRouter = typeof appRouter;
|
export type AppRouter = typeof appRouter;
|
||||||
|
|||||||
Reference in New Issue
Block a user