From 229bfea263d8380f00bc4f2f576c08a350096551 Mon Sep 17 00:00:00 2001 From: admin Date: Sun, 24 May 2026 17:51:09 +0200 Subject: [PATCH] Phase2.5: Skill SetUp Process --- .../admin/billing/pricing-editor.tsx | 82 +++++++++++++++---- src/components/packages/skill-cost-dialog.tsx | 13 ++- src/lib/packages.ts | 1 + src/messages/de.json | 24 +++--- src/messages/en.json | 24 +++--- src/messages/fr.json | 24 +++--- src/messages/it.json | 24 +++--- 7 files changed, 126 insertions(+), 66 deletions(-) diff --git a/src/components/admin/billing/pricing-editor.tsx b/src/components/admin/billing/pricing-editor.tsx index 8e17690..6f6f67e 100644 --- a/src/components/admin/billing/pricing-editor.tsx +++ b/src/components/admin/billing/pricing-editor.tsx @@ -34,6 +34,7 @@ export function PricingEditor({ catalog, }: Props) { const t = useTranslations("adminBilling"); + const tPackages = useTranslations("packages"); const router = useRouter(); // -- Platform pricing form ---------------------------------------------- @@ -78,12 +79,19 @@ export function PricingEditor({ } }; - // -- Skill pricing ------------------------------------------------------ + // -- Package pricing ---------------------------------------------------- // Server is authoritative — we don't keep an editable local copy of the // table; instead each action posts to the API and we router.refresh(). - const [newSkillId, setNewSkillId] = useState( - catalog.find((c) => c.category === "skill")?.id ?? "" - ); + // + // Naming carry-over: the underlying DB table is `skill_pricing` and the + // column is `skill_id`, dating from when only skills were priced. The + // model now applies to any PackageDef in the catalog regardless of + // category — core, channel, or skill. The state variable names below + // (newSkill*, addingSkill, etc.) retain the legacy "skill" prefix + // because renaming the entire surface for purely cosmetic reasons + // would create churn for no functional gain. Treat "skill" here as + // shorthand for "priced package". + const [newSkillId, setNewSkillId] = useState(catalog[0]?.id ?? ""); const [newSkillPrice, setNewSkillPrice] = useState("0.10"); const [newSkillSetupFee, setNewSkillSetupFee] = useState("0"); const [addingSkill, setAddingSkill] = useState(false); @@ -147,9 +155,16 @@ export function PricingEditor({ } }; - // Catalog filtered to skill-kind entries for the picker, but keeping - // existing pricing rows even if they reference non-skill packages. - const skillCatalogOptions = catalog.filter((c) => c.category === "skill"); + // Pricing applies to any catalog entry regardless of category. Grouped + // dropdown sorts options by category for visual scanning — core, + // channel, and skill in a single picker. + const skillCatalogOptions = [...catalog].sort((a, b) => { + const order = { core: 0, channel: 1, skill: 2 } as Record; + const ca = order[a.category] ?? 99; + const cb = order[b.category] ?? 99; + if (ca !== cb) return ca - cb; + return a.name.localeCompare(b.name); + }); const catalogIndex = new Map(catalog.map((c) => [c.id, c])); const pricedIds = new Set(initialSkillPricing.map((s) => s.skillId)); @@ -260,7 +275,12 @@ export function PricingEditor({
{sp.skillId}
{entry && ( -
{entry.name}
+
+ {entry.name} + + {entry.category} + +
)} @@ -311,13 +331,45 @@ export function PricingEditor({ onChange={(e) => setNewSkillId(e.target.value)} className="mt-1 w-full px-3 py-2 rounded-md border border-border bg-surface-2 text-sm" > - {skillCatalogOptions - .filter((c) => !pricedIds.has(c.id)) - .map((c) => ( - - ))} + {(() => { + // Group available options by category for the picker. + // Already-priced packages are filtered out (admin + // edits those inline above). + const available = skillCatalogOptions.filter( + (c) => !pricedIds.has(c.id) + ); + const byCat = new Map(); + for (const c of available) { + if (!byCat.has(c.category)) byCat.set(c.category, []); + byCat.get(c.category)!.push(c); + } + // Labels for the optgroups. Reuse the existing + // packages.categories.* scope which already has + // translations in all four locales. + const labels: Record = { + core: tPackages("categories.core"), + channel: tPackages("categories.channels"), + skill: tPackages("categories.skills"), + }; + const order: Array<"core" | "channel" | "skill"> = [ + "core", + "channel", + "skill", + ]; + return order.map((cat) => { + const items = byCat.get(cat); + if (!items || items.length === 0) return null; + return ( + + {items.map((c) => ( + + ))} + + ); + }); + })()}