From 9c0164c4feeaee39759ba999042c40dee1a34a39 Mon Sep 17 00:00:00 2001 From: DanielS Date: Fri, 1 May 2026 00:52:37 +0200 Subject: [PATCH] feat: add automated PDF invoice generation and storage in Supabase --- shop/components/invoice-pdf.tsx | 168 ++++++++++++++++++ shop/lib/actions/orders.ts | 72 +++++++- .../20260430222242_create_invoice_bucket.sql | 9 + 3 files changed, 241 insertions(+), 8 deletions(-) create mode 100644 shop/components/invoice-pdf.tsx create mode 100644 shop/supabase/migrations/20260430222242_create_invoice_bucket.sql diff --git a/shop/components/invoice-pdf.tsx b/shop/components/invoice-pdf.tsx new file mode 100644 index 0000000..5514cde --- /dev/null +++ b/shop/components/invoice-pdf.tsx @@ -0,0 +1,168 @@ +import { Document, Page, Text, View, StyleSheet, Font } from '@react-pdf/renderer'; + +// Create styles +const styles = StyleSheet.create({ + page: { + padding: 50, + fontSize: 10, + color: '#333', + fontFamily: 'Helvetica', + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: 40, + }, + companyInfo: { + textAlign: 'right', + }, + title: { + fontSize: 24, + color: '#000', + marginBottom: 20, + }, + section: { + marginBottom: 20, + }, + label: { + fontSize: 8, + color: '#999', + textTransform: 'uppercase', + marginBottom: 2, + }, + table: { + display: 'table', + width: 'auto', + borderStyle: 'solid', + borderWidth: 1, + borderColor: '#eee', + borderRightWidth: 0, + borderBottomWidth: 0, + marginBottom: 20, + }, + tableRow: { + margin: 'auto', + flexDirection: 'row', + }, + tableCol: { + width: '60%', + borderStyle: 'solid', + borderWidth: 1, + borderColor: '#eee', + borderLeftWidth: 0, + borderTopWidth: 0, + padding: 8, + }, + tableColPrice: { + width: '40%', + borderStyle: 'solid', + borderWidth: 1, + borderColor: '#eee', + borderLeftWidth: 0, + borderTopWidth: 0, + padding: 8, + textAlign: 'right', + }, + tableCell: { + margin: 'auto', + marginTop: 5, + fontSize: 10, + }, + total: { + flexDirection: 'row', + justifyContent: 'flex-end', + marginTop: 20, + borderTop: 1, + borderColor: '#000', + paddingTop: 10, + }, + totalLabel: { + fontSize: 14, + fontWeight: 'bold', + }, + footer: { + position: 'absolute', + bottom: 30, + left: 50, + right: 50, + textAlign: 'center', + color: '#999', + fontSize: 8, + borderTop: 1, + borderColor: '#eee', + paddingTop: 10, + }, +}); + +export const InvoicePDF = ({ order, product, modules, customer }: any) => ( + + + + + CASPOS + Software Solutions + + + CASPOS GmbH + Musterstraße 1 + 12345 Musterstadt + Deutschland + + + + Bestellbestätigung / Rechnung + + + Rechnungsempfänger + {customer.company_name} + {customer.first_name} {customer.last_name} + {customer.address} + {customer.zip_code} {customer.city} + USt-IdNr: {customer.vat_id} + + + + Bestelldetails + Bestellnummer: {order.id} + Datum: {new Date().toLocaleDateString('de-DE')} + + + + + Beschreibung + Preis (mtl.) + + + + {product.name} (Basispaket) + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(product.base_price)} + + + {modules.map((mod: any) => ( + + {mod.name} + +{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(mod.additional_price)} + + ))} + + + + + + Zwischensumme: + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(order.total_amount)} + + + Gesamtbetrag: + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(order.total_amount)} + + + + + + Vielen Dank für Ihre Bestellung! + Dies ist ein automatisch generiertes Dokument. + + + +); diff --git a/shop/lib/actions/orders.ts b/shop/lib/actions/orders.ts index 8baf39e..fd48449 100644 --- a/shop/lib/actions/orders.ts +++ b/shop/lib/actions/orders.ts @@ -2,7 +2,9 @@ import { createClient } from '@/lib/supabase/server' import { revalidatePath } from 'next/cache' -import { Order } from '../types' +import { InvoicePDF } from '@/components/invoice-pdf' +import { renderToBuffer } from '@react-pdf/renderer' +import React from 'react' export async function submitOrder(orderData: { product_id: string; @@ -15,23 +17,77 @@ export async function submitOrder(orderData: { const { data: { user } } = await supabase.auth.getUser() if (!user) throw new Error('Not authenticated') - const { data, error } = await supabase + // 1. Fetch Product and Module Details for Invoice + const { data: product } = await supabase + .from('products') + .select('*') + .eq('id', orderData.product_id) + .single() + + const { data: modules } = await supabase + .from('product_modules') + .select('*') + .in('id', orderData.selected_modules) + + if (!product) throw new Error('Product not found') + + // 2. Create the Order in DB + const { data: order, error: orderError } = await supabase .from('orders') .insert([{ user_id: user.id, - total_amount: orderData.total_amount, - items: { + total_price: orderData.total_amount, // Matched column name in initial_schema.sql + order_data: { product_id: orderData.product_id, modules: orderData.selected_modules }, - billing_details: orderData.customer_details, + customer_data: orderData.customer_details, status: 'pending' }]) .select() .single() - if (error) throw error + if (orderError) throw orderError - revalidatePath('/admin/orders') // If there was an admin orders page - return data + // 3. Generate PDF + try { + const buffer = await renderToBuffer( + React.createElement(InvoicePDF, { + order: order, + product: product, + modules: modules || [], + customer: orderData.customer_details + }) + ) + + // 4. Upload to Supabase Storage + const fileName = `invoice_${order.id}.pdf` + const { data: uploadData, error: uploadError } = await supabase + .storage + .from('invoices') + .upload(fileName, buffer, { + contentType: 'application/pdf', + upsert: true + }) + + if (uploadError) { + console.error('PDF Upload Error:', uploadError) + } else { + // 5. Update Order with PDF URL + const { data: publicUrlData } = supabase + .storage + .from('invoices') + .getPublicUrl(fileName) + + await supabase + .from('orders') + .update({ pdf_url: publicUrlData.publicUrl }) + .eq('id', order.id) + } + } catch (pdfError) { + console.error('PDF Generation Error:', pdfError) + } + + revalidatePath('/protected') + return order } diff --git a/shop/supabase/migrations/20260430222242_create_invoice_bucket.sql b/shop/supabase/migrations/20260430222242_create_invoice_bucket.sql new file mode 100644 index 0000000..615fe1d --- /dev/null +++ b/shop/supabase/migrations/20260430222242_create_invoice_bucket.sql @@ -0,0 +1,9 @@ +-- Create bucket for invoices +INSERT INTO storage.buckets (id, name, public) +VALUES ('invoices', 'invoices', true) +ON CONFLICT (id) DO NOTHING; + +-- Policies for invoices bucket +-- Allow public access to invoices (simplified for this demo, usually should be restricted) +CREATE POLICY "Public Access" ON storage.objects FOR SELECT USING (bucket_id = 'invoices'); +CREATE POLICY "Authenticated Upload" ON storage.objects FOR INSERT WITH CHECK (bucket_id = 'invoices' AND auth.role() = 'authenticated');