Phase2.5: Skill SetUp Process
All checks were successful
Build and Push / build (push) Successful in 1m39s
All checks were successful
Build and Push / build (push) Successful in 1m39s
This commit is contained in:
@@ -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<string, number>;
|
||||
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({
|
||||
<td className="py-2">
|
||||
<div className="font-mono text-xs">{sp.skillId}</div>
|
||||
{entry && (
|
||||
<div className="text-xs text-text-muted">{entry.name}</div>
|
||||
<div className="text-xs text-text-muted flex items-center gap-2">
|
||||
<span>{entry.name}</span>
|
||||
<span className="text-[10px] uppercase tracking-wider bg-surface-3 px-1.5 py-0.5 rounded">
|
||||
{entry.category}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2 text-right">
|
||||
@@ -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) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.name} ({c.id})
|
||||
</option>
|
||||
))}
|
||||
{(() => {
|
||||
// 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<string, typeof available>();
|
||||
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<string, string> = {
|
||||
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 (
|
||||
<optgroup key={cat} label={labels[cat] ?? cat}>
|
||||
{items.map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.name} ({c.id})
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
);
|
||||
});
|
||||
})()}
|
||||
</select>
|
||||
</label>
|
||||
<label className="w-28">
|
||||
|
||||
Reference in New Issue
Block a user