Files
pieced-portal/src/components/packages/skill-cost-dialog.tsx
admin 229bfea263
All checks were successful
Build and Push / build (push) Successful in 1m39s
Phase2.5: Skill SetUp Process
2026-05-24 17:51:09 +02:00

116 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 && (
/* Display reference monthly cost (daily × 30) plus the
actual daily rate as a sub-note. Billing is always
per UTC day enabled — partial months prorate to that
same daily rate, full months land at roughly the
figure shown (varies ±~3% by month length). */
<div className="flex justify-between items-baseline">
<div>
<div className="text-sm">{t("monthlyPriceLabel")}</div>
<div className="text-xs text-text-muted">
{t("monthlyPriceNote", {
daily: dailyPriceChf.toFixed(2),
})}
</div>
</div>
<div className="text-sm font-mono">
CHF {(dailyPriceChf * 30).toFixed(2)} / {t("monthUnit")}
</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>
);
}