fix(admin-orders): make PDF download/view always available by generating PDF dynamically on the fly if missing
All checks were successful
Staging Build / build (push) Successful in 3m33s

This commit is contained in:
DanielS
2026-07-03 15:14:34 +02:00
parent 4c6fde326b
commit 553ec7eadd
2 changed files with 80 additions and 32 deletions

View File

@@ -1,5 +1,8 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { createAdminClient } from "@/lib/supabase/admin"; import { createAdminClient } from "@/lib/supabase/admin";
import { InvoicePDF } from "@/components/invoice-pdf";
import { renderToBuffer } from "@react-pdf/renderer";
import React from "react";
export async function GET( export async function GET(
request: NextRequest, request: NextRequest,
@@ -8,41 +11,92 @@ export async function GET(
try { try {
const { id } = await params; const { id } = await params;
const adminDb = createAdminClient(); const adminDb = createAdminClient();
const inline = request.nextUrl.searchParams.get("inline") === "true";
// 1. Get the order from database // 1. Get the order from database
const { data: order, error } = await adminDb const { data: order, error } = await adminDb
.from("orders") .from("orders")
.select("pdf_url, order_number") .select("customer_data, order_data, total_price, created_at, pdf_url, order_number")
.eq("id", id) .eq("id", id)
.single(); .single();
if (error || !order || !order.pdf_url) { if (error || !order) {
return new NextResponse("PDF not found", { status: 404 }); return new NextResponse("Order not found", { status: 404 });
} }
// 2. Determine file name in storage bucket
const storageFileName = `ab_${id}.pdf`; const storageFileName = `ab_${id}.pdf`;
let fileBuffer: Uint8Array | ArrayBuffer | null = null;
// 3. Download file from Supabase storage // 2. Try to download from storage if pdf_url is set
const { data: fileData, error: downloadError } = await adminDb if (order.pdf_url) {
.storage try {
.from("invoices") const { data: fileData, error: downloadError } = await adminDb
.download(storageFileName); .storage
.from("invoices")
.download(storageFileName);
if (downloadError || !fileData) { if (!downloadError && fileData) {
console.error("Failed to download PDF from storage:", downloadError); fileBuffer = await fileData.arrayBuffer();
return new NextResponse("Error downloading file from storage", { status: 500 }); }
} catch (storageErr) {
console.warn("Could not retrieve file from storage, will regenerate:", storageErr);
}
} }
// 4. Return as attachment // 3. If file not found or pdf_url not set, generate dynamically
const buffer = await fileData.arrayBuffer(); if (!fileBuffer) {
try {
const formattedDate = new Date(order.created_at).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
});
const buffer = await renderToBuffer(
React.createElement(InvoicePDF, {
orderNumber: order.order_number || `AE-${id.slice(0, 8)}`,
dateStr: formattedDate,
customer: order.customer_data,
orderData: order.order_data,
totalPrice: Number(order.total_price),
})
);
// Upload/Upsert to storage bucket
const { error: uploadError } = await adminDb.storage
.from("invoices")
.upload(storageFileName, Buffer.from(buffer), {
contentType: "application/pdf",
upsert: true,
});
if (uploadError) {
console.error("Failed to upload regenerated PDF:", uploadError);
} else {
// Update db with pdf_url
const path = `invoices/${storageFileName}`;
await adminDb.from("orders").update({ pdf_url: path }).eq("id", id);
}
fileBuffer = buffer;
} catch (genErr: any) {
console.error("PDF generation failed inside API:", genErr);
return new NextResponse("Error generating PDF: " + genErr.message, { status: 500 });
}
}
// 4. Return as PDF response
const downloadFileName = `Anfragebestaetigung_${order.order_number || id.slice(0, 8)}.pdf`; const downloadFileName = `Anfragebestaetigung_${order.order_number || id.slice(0, 8)}.pdf`;
const headers = new Headers(); const headers = new Headers();
headers.append("Content-Disposition", `attachment; filename="${downloadFileName}"`); if (inline) {
headers.append("Content-Disposition", "inline");
} else {
headers.append("Content-Disposition", `attachment; filename="${downloadFileName}"`);
}
headers.append("Content-Type", "application/pdf"); headers.append("Content-Type", "application/pdf");
return new NextResponse(buffer, { return new NextResponse(fileBuffer as any, {
status: 200, status: 200,
headers, headers,
}); });

View File

@@ -223,22 +223,16 @@ export function OrdersTable({ initialOrders }: OrdersTableProps) {
Bearbeiten Bearbeiten
</a> </a>
</Button> </Button>
{order.pdf_url ? ( <Button variant="outline" size="sm" asChild className="h-8 border-white/10 hover:bg-primary/20 text-xs">
<> <a href={`/api/admin/orders/${order.id}/download?inline=true`} target="_blank" rel="noopener noreferrer">
<Button variant="outline" size="sm" asChild className="h-8 border-white/10 hover:bg-primary/20 text-xs"> <ExternalLink className="w-3.5 h-3.5 mr-1" /> PDF
<a href={resolveSupabaseUrl(order.pdf_url)} target="_blank" rel="noopener noreferrer"> </a>
<ExternalLink className="w-3.5 h-3.5 mr-1" /> PDF </Button>
</a> <Button variant="secondary" size="sm" asChild className="h-8 text-xs">
</Button> <a href={`/api/admin/orders/${order.id}/download`}>
<Button variant="secondary" size="sm" asChild className="h-8 text-xs"> <Download className="w-3.5 h-3.5" />
<a href={`/api/admin/orders/${order.id}/download`}> </a>
<Download className="w-3.5 h-3.5" /> </Button>
</a>
</Button>
</>
) : (
<span className="text-xs text-slate-500 italic self-center px-2">Keine Bestätigung</span>
)}
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>