From ed2424cc68e6c9698d5c71e1582ea4cd8e29699b Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 12 May 2026 21:04:49 +0200 Subject: [PATCH] feat(invoices): remove client-facing PDF generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 / commit 11 of 14 — invoices are client-facing documents, and per the new "no CRM-generated client-facing PDFs" rule (see the design spec), the in-app pdfme rendering is removed entirely. Future invoice rendering will use the deferred AcroForm-fill admin- template feature: admin uploads a PDF template with named form fields, CRM fills them with invoice data via pdf-lib. Same pattern as the in-app EOI pathway. Tracked in BACKLOG.md. Deleted: - src/lib/services/invoices.ts:generateInvoicePdf (60 LOC) - src/lib/pdf/templates/invoice-template.ts (entire pdfme template) - src/app/api/v1/invoices/[id]/generate-pdf/route.ts - src/components/invoices/invoice-pdf-preview.tsx (regenerate UI) - "PDF Preview" tab on invoice detail page - 5 now-unused imports in invoices.ts (files, ports, buildStoragePath, getStorageBackend, env) sendInvoice() retained: still queues the send-invoice email job, still flips status to "sent", still emits the socket event. The PDF-attach step is gone — downstream consumers either render externally or wait for the AcroForm-fill feature. The `pdfFileId` column on invoices stays so existing rows don't break, just never gets written by this code path. 1319/1319 vitest green. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../v1/invoices/[id]/generate-pdf/route.ts | 21 --- src/components/invoices/invoice-detail.tsx | 7 - .../invoices/invoice-pdf-preview.tsx | 104 --------------- src/lib/pdf/templates/invoice-template.ts | 124 ------------------ src/lib/services/invoices.ts | 85 +----------- 5 files changed, 6 insertions(+), 335 deletions(-) delete mode 100644 src/app/api/v1/invoices/[id]/generate-pdf/route.ts delete mode 100644 src/components/invoices/invoice-pdf-preview.tsx delete mode 100644 src/lib/pdf/templates/invoice-template.ts diff --git a/src/app/api/v1/invoices/[id]/generate-pdf/route.ts b/src/app/api/v1/invoices/[id]/generate-pdf/route.ts deleted file mode 100644 index 8bebe9db..00000000 --- a/src/app/api/v1/invoices/[id]/generate-pdf/route.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { NextResponse } from 'next/server'; - -import { withAuth, withPermission } from '@/lib/api/helpers'; -import { errorResponse } from '@/lib/errors'; -import { generateInvoicePdf } from '@/lib/services/invoices'; - -export const POST = withAuth( - withPermission('invoices', 'edit', async (_req, ctx, params) => { - try { - const fileRecord = await generateInvoicePdf(params.id!, ctx.portId, { - userId: ctx.userId, - portId: ctx.portId, - ipAddress: ctx.ipAddress, - userAgent: ctx.userAgent, - }); - return NextResponse.json({ data: fileRecord }); - } catch (error) { - return errorResponse(error); - } - }), -); diff --git a/src/components/invoices/invoice-detail.tsx b/src/components/invoices/invoice-detail.tsx index c0dae673..b8b5611e 100644 --- a/src/components/invoices/invoice-detail.tsx +++ b/src/components/invoices/invoice-detail.tsx @@ -23,7 +23,6 @@ import { SelectValue, } from '@/components/ui/select'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { InvoicePdfPreview } from './invoice-pdf-preview'; import { apiFetch } from '@/lib/api/client'; import { toastError } from '@/lib/api/toast-error'; import { useMobileChrome } from '@/components/layout/mobile/mobile-layout-provider'; @@ -218,7 +217,6 @@ export function InvoiceDetail({ invoiceId }: InvoiceDetailProps) { Overview Linked Expenses - PDF Preview Payment @@ -360,11 +358,6 @@ export function InvoiceDetail({ invoiceId }: InvoiceDetailProps) { )} - {/* PDF Preview */} - - - - {/* Payment */} {invoice.status === 'paid' ? ( diff --git a/src/components/invoices/invoice-pdf-preview.tsx b/src/components/invoices/invoice-pdf-preview.tsx deleted file mode 100644 index ccc2708a..00000000 --- a/src/components/invoices/invoice-pdf-preview.tsx +++ /dev/null @@ -1,104 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { Loader2, RefreshCw, FileText } from 'lucide-react'; - -import { Button } from '@/components/ui/button'; -import { apiFetch } from '@/lib/api/client'; - -interface InvoicePdfPreviewProps { - invoiceId: string; - pdfFileId?: string | null; -} - -export function InvoicePdfPreview({ - invoiceId, - pdfFileId: initialPdfFileId, -}: InvoicePdfPreviewProps) { - const queryClient = useQueryClient(); - const [pdfFileId, setPdfFileId] = useState(initialPdfFileId); - - const { data: previewData, isLoading: previewLoading } = useQuery<{ - url: string; - mimeType: string; - }>({ - queryKey: ['file-preview', pdfFileId], - queryFn: () => apiFetch(`/api/v1/files/${pdfFileId}/preview`), - enabled: !!pdfFileId, - }); - - const regenerateMutation = useMutation({ - mutationFn: () => - apiFetch<{ data?: { id?: string } }>(`/api/v1/invoices/${invoiceId}/generate-pdf`, { - method: 'POST', - }), - onSuccess: (data) => { - const fileId = data?.data?.id; - if (fileId) { - setPdfFileId(fileId); - queryClient.invalidateQueries({ queryKey: ['invoices', invoiceId] }); - queryClient.invalidateQueries({ queryKey: ['file-preview', fileId] }); - } - }, - }); - - if (!pdfFileId) { - return ( -
- -

No PDF generated yet

- -
- ); - } - - return ( -
-
- -
- - {previewLoading ? ( -
- -
- ) : previewData?.url ? ( -