From cd15b391ac8b771c69dc61236fae5ac9df986d1e Mon Sep 17 00:00:00 2001 From: admin Date: Sun, 24 May 2026 16:38:41 +0200 Subject: [PATCH] Phase2: Invoicecomputation/AdminpricingUI/Ainvoicemgnt --- .../api/admin/billing/skill-pricing/route.ts | 6 +- .../admin/billing/pricing-editor.tsx | 70 ++++++-- src/lib/billing-i18n.ts | 157 ++++++++++++++++++ src/lib/billing-pdf.tsx | 4 + src/lib/billing.ts | 105 ++++++++---- src/lib/db.ts | 55 +++++- src/messages/de.json | 4 +- src/messages/en.json | 4 +- src/messages/fr.json | 4 +- src/messages/it.json | 4 +- src/types/index.ts | 8 + 11 files changed, 369 insertions(+), 52 deletions(-) create mode 100644 src/lib/billing-i18n.ts diff --git a/src/app/api/admin/billing/skill-pricing/route.ts b/src/app/api/admin/billing/skill-pricing/route.ts index 64e4359..d0305ca 100644 --- a/src/app/api/admin/billing/skill-pricing/route.ts +++ b/src/app/api/admin/billing/skill-pricing/route.ts @@ -24,6 +24,9 @@ import { safeError } from "@/lib/errors"; const upsertSchema = z.object({ skillId: z.string().min(1).max(100), dailyPriceChf: z.number().min(0).max(1_000_000), + // Optional with default 0 so existing API callers keep working. + // Setup fee fires once per (tenant, skill); see billing.ts. + setupFeeChf: z.number().min(0).max(1_000_000).optional().default(0), }); export async function GET() { @@ -63,7 +66,8 @@ export async function PUT(request: Request) { try { const row = await setSkillPricing( parsed.data.skillId, - parsed.data.dailyPriceChf + parsed.data.dailyPriceChf, + parsed.data.setupFeeChf ); return NextResponse.json(row); } catch (e) { diff --git a/src/components/admin/billing/pricing-editor.tsx b/src/components/admin/billing/pricing-editor.tsx index 6d8020a..8e17690 100644 --- a/src/components/admin/billing/pricing-editor.tsx +++ b/src/components/admin/billing/pricing-editor.tsx @@ -85,20 +85,27 @@ export function PricingEditor({ catalog.find((c) => c.category === "skill")?.id ?? "" ); const [newSkillPrice, setNewSkillPrice] = useState("0.10"); + const [newSkillSetupFee, setNewSkillSetupFee] = useState("0"); const [addingSkill, setAddingSkill] = useState(false); const [skillError, setSkillError] = useState(""); // 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) => { + // editors on existing rows. Kept event-free so callers can invoke it + // without synthesizing a fake form event. Both `dailyPriceChf` and + // `setupFeeChf` are written together because the API does a full + // upsert; partial updates would silently zero the other field. + const upsertSkillPrice = async ( + skillId: string, + dailyPriceChf: number, + setupFeeChf: 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, dailyPriceChf }), + body: JSON.stringify({ skillId, dailyPriceChf, setupFeeChf }), }); if (!res.ok) { const j = await res.json().catch(() => ({})); @@ -115,7 +122,11 @@ export function PricingEditor({ const onAddNewSkill = (e: React.FormEvent) => { e.preventDefault(); if (!newSkillId) return; - void upsertSkillPrice(newSkillId, Number(newSkillPrice)); + void upsertSkillPrice( + newSkillId, + Number(newSkillPrice), + Number(newSkillSetupFee) + ); }; const deleteSkill = async (skillId: string) => { @@ -234,6 +245,7 @@ export function PricingEditor({ {t("skillCol")} {t("dailyPriceCol")} + {t("setupFeeCol")} {t("actionsCol")} @@ -252,10 +264,26 @@ export function PricingEditor({ )} + {/* Inline edits write daily + setup together (full + upsert on the API side). The other field is + held constant from the snapshot here. */} upsertSkillPrice(sp.skillId, price)} + decimals={4} + onSave={(price) => + upsertSkillPrice(sp.skillId, price, sp.setupFeeChf) + } + /> + + + + upsertSkillPrice(sp.skillId, sp.dailyPriceChf, fee) + } /> @@ -292,9 +320,9 @@ export function PricingEditor({ ))} -