Phase2.5: Skill SetUp Process
All checks were successful
Build and Push / build (push) Successful in 1m39s

This commit is contained in:
2026-05-24 17:25:08 +02:00
parent cd15b391ac
commit 49b085e59e
22 changed files with 1666 additions and 14 deletions

View File

@@ -0,0 +1,108 @@
"use client";
import { useTranslations } from "next-intl";
import { Modal } from "@/components/ui/modal";
interface Props {
open: boolean;
onClose: () => void;
onConfirm: () => void;
skillName: string;
dailyPriceChf: number;
setupFeeChf: number;
busy?: boolean;
}
/**
* Cost-disclosure modal shown before activating a priced skill.
*
* Shows the daily rate and setup fee (each only if > 0) and
* requires an explicit Confirm before the activation request goes
* through. Rendered every time the user toggles on a priced skill,
* not once-and-remember — this is recurring-charge consent, not a
* one-time terms agreement.
*
* The setup fee is always shown when configured, with a note
* clarifying it's "one-time, charged on first activation". The
* backend (billing.ts tenantSkillHasBeenBilled) is the authority
* on whether the fee actually fires — we don't second-guess from
* the client. If you've previously activated this skill on this
* tenant, the fee won't appear on the next invoice even though
* the dialog mentions it.
*/
export function SkillCostDialog({
open,
onClose,
onConfirm,
skillName,
dailyPriceChf,
setupFeeChf,
busy = false,
}: Props) {
const t = useTranslations("skillCostDialog");
const showSetupFee = setupFeeChf > 0;
const showDaily = dailyPriceChf > 0;
// Nothing to disclose? Bail to confirm immediately — shouldn't
// normally be shown in this case but guard anyway.
if (!showSetupFee && !showDaily) {
return null;
}
return (
<Modal open={open} onClose={onClose} ariaLabel={t("title")}>
<div className="bg-surface-1 rounded-lg border border-border p-6 max-w-md w-full">
<h2 className="text-lg font-semibold mb-2">{t("title")}</h2>
<p className="text-sm text-text-secondary mb-4">
{t("intro", { skill: skillName })}
</p>
<div className="rounded-md bg-surface-2 border border-border p-4 mb-4 space-y-2">
{showSetupFee && (
<div className="flex justify-between items-baseline">
<div>
<div className="text-sm">{t("setupFeeLabel")}</div>
<div className="text-xs text-text-muted">
{t("setupFeeNote")}
</div>
</div>
<div className="text-sm font-mono">
CHF {setupFeeChf.toFixed(2)}
</div>
</div>
)}
{showDaily && (
<div className="flex justify-between items-baseline">
<div>
<div className="text-sm">{t("dailyPriceLabel")}</div>
<div className="text-xs text-text-muted">
{t("dailyPriceNote")}
</div>
</div>
<div className="text-sm font-mono">
CHF {dailyPriceChf.toFixed(2)} / {t("dayUnit")}
</div>
</div>
)}
</div>
<p className="text-xs text-text-muted mb-4">{t("disclaimer")}</p>
<div className="flex justify-end gap-2">
<button
onClick={onClose}
disabled={busy}
className="px-4 py-2 rounded-md border border-border text-sm disabled:opacity-50"
>
{t("cancel")}
</button>
<button
onClick={onConfirm}
disabled={busy}
className="px-4 py-2 rounded-md bg-accent text-white text-sm disabled:opacity-50"
>
{busy ? t("confirming") : t("confirm")}
</button>
</div>
</div>
</Modal>
);
}