diff --git a/src/app/api/admin/billing/invoices/[id]/pdf/route.ts b/src/app/api/admin/billing/invoices/[id]/pdf/route.ts index d540fab..1ce0b8b 100644 --- a/src/app/api/admin/billing/invoices/[id]/pdf/route.ts +++ b/src/app/api/admin/billing/invoices/[id]/pdf/route.ts @@ -26,9 +26,16 @@ export async function GET( if (!pdf) { return new NextResponse("Not found", { status: 404 }); } - // Construct a response that the browser will render inline (PDF - // viewer) but also offer to download with the right filename. - return new NextResponse(pdf.data, { + // Web `Response`'s `BodyInit` doesn't include Node's `Buffer` — pg + // returns bytea as Buffer but Next/the runtime want a BufferSource. + // Wrap into a zero-copy Uint8Array view (Buffer extends Uint8Array + // under the hood, but TypeScript treats them as distinct). + const body = new Uint8Array( + pdf.data.buffer, + pdf.data.byteOffset, + pdf.data.byteLength + ); + return new NextResponse(body, { status: 200, headers: { "Content-Type": "application/pdf", diff --git a/src/components/admin/billing/invoice-detail-view.tsx b/src/components/admin/billing/invoice-detail-view.tsx index ba28386..ea5d5c7 100644 --- a/src/components/admin/billing/invoice-detail-view.tsx +++ b/src/components/admin/billing/invoice-detail-view.tsx @@ -301,7 +301,7 @@ function StatusPill({ status }: { status: InvoiceStatus }) { - {t(`status_${status}` as any)} + {t(`status_${status}`)} ); } diff --git a/src/components/admin/billing/invoices-table.tsx b/src/components/admin/billing/invoices-table.tsx index 8141933..ebce4d4 100644 --- a/src/components/admin/billing/invoices-table.tsx +++ b/src/components/admin/billing/invoices-table.tsx @@ -73,7 +73,7 @@ export function InvoicesTable({ initialInvoices }: Props) { > {STATUS_FILTERS.map((s) => ( ))} @@ -177,7 +177,7 @@ function StatusPill({ status }: { status: InvoiceStatus }) { - {t(`status_${status}` as any)} + {t(`status_${status}`)} ); } diff --git a/src/components/admin/billing/pricing-editor.tsx b/src/components/admin/billing/pricing-editor.tsx index 884c993..6d8020a 100644 --- a/src/components/admin/billing/pricing-editor.tsx +++ b/src/components/admin/billing/pricing-editor.tsx @@ -88,22 +88,17 @@ export function PricingEditor({ const [addingSkill, setAddingSkill] = useState(false); const [skillError, setSkillError] = useState(""); - const addOrUpdateSkill = async ( - e: React.FormEvent, - overrideId?: string, - overridePrice?: string - ) => { - e.preventDefault(); + // Core upsert — used by both the "add new skill" form and the inline + // editor on existing rows. Kept event-free so callers can invoke it + // without synthesizing a fake form event. + const upsertSkillPrice = async (skillId: string, dailyPriceChf: number) => { setAddingSkill(true); setSkillError(""); try { const res = await fetch("/api/admin/billing/skill-pricing", { method: "PUT", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - skillId: overrideId ?? newSkillId, - dailyPriceChf: Number(overridePrice ?? newSkillPrice), - }), + body: JSON.stringify({ skillId, dailyPriceChf }), }); if (!res.ok) { const j = await res.json().catch(() => ({})); @@ -117,6 +112,12 @@ export function PricingEditor({ } }; + const onAddNewSkill = (e: React.FormEvent) => { + e.preventDefault(); + if (!newSkillId) return; + void upsertSkillPrice(newSkillId, Number(newSkillPrice)); + }; + const deleteSkill = async (skillId: string) => { if (!confirm(t("confirmDeleteSkillPrice", { skill: skillId }))) return; setSkillError(""); @@ -254,13 +255,7 @@ export function PricingEditor({ - addOrUpdateSkill( - new Event("submit") as any, - sp.skillId, - String(price) - ) - } + onSave={(price) => upsertSkillPrice(sp.skillId, price)} /> @@ -280,10 +275,7 @@ export function PricingEditor({

{t("noSkillsPriced")}

)} -
addOrUpdateSkill(e)} - className="flex items-end gap-3" - > +