Files
craftvia/src/components/plans/plan-form.tsx
T
msolarczekandClaude Opus 5 06d320908f L17 Pakete: Stufen Basis/Profi, Lotse-Chat-Plätze und Kontingent
- Datenmodell: Tenant.tier (Default PROFI), lotseChatSeats, lotseChatHardLimit;
  Tabelle lotse_chat_seats (RLS, TENANT_MODELS, pii-fields), Index für die
  Monatszählung der Chat-Nachrichten (Migration 20260921100000_pakete)
- src/lib/plans.ts: Stufenregeln, 150 Chats je Platz, Mehrverbrauch in 100er-Paketen
- src/server/plan.ts: effektive Freischaltung = Stufe UND TenantModule, genutzt von
  requireModule, assertModuleEnabled, API, Sync (Offline-Op → rejected mit Klartext),
  Navigation, isLotseEnabled und planningAccess (Planung nur in Profi)
- Lotse-Chat: Platzprüfung (no_seat), Testphase ohne Platz, Kontingent mit
  hartem Limit (quota_exhausted); Platzvergabe durch den Mandanten-Admin
- Betreiber: Stufe/Plätze/hartes Limit im Mandantendetail mit Bestätigung
  und Plattform-Audit, Verbrauch laufender Monat/Vormonat, Stufe als Badge
- Demo-Seed: demo = Profi mit 3 Plätzen, demo2 = Basis
- Tests: test-pakete-{rules,gates,seats}

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 10:13:27 +02:00

79 lines
3.4 KiB
TypeScript

"use client";
import { useActionState } from "react";
import Link from "next/link";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { TENANT_TIERS, type TenantTier } from "@/lib/plans";
import type { PlatformPlanState } from "@/server/actions/plans-platform";
/** L17 Pakete: operator form (tier, seats, hard limit) with mandatory confirmation checkbox. */
export function PlanForm({
action,
tier,
seats,
hardLimit,
maxSeats,
closeHref,
}: {
action: (prev: PlatformPlanState, fd: FormData) => Promise<PlatformPlanState>;
tier: TenantTier;
seats: number;
hardLimit: boolean;
maxSeats: number;
closeHref: string;
}) {
const t = useTranslations("plans.platform");
const tt = useTranslations("plans");
const [state, formAction, pending] = useActionState<PlatformPlanState, FormData>(action, { status: "idle" });
return (
<form action={formAction} className="space-y-4">
<fieldset>
<legend className="text-sm font-semibold">{t("tier")}</legend>
<div className="mt-2 grid gap-2 sm:grid-cols-2">
{TENANT_TIERS.map((v) => (
<label key={v} className="flex min-h-11 cursor-pointer items-start gap-2 rounded-lg border p-3 text-sm has-[:checked]:border-[var(--ui-primary)] has-[:checked]:font-semibold">
<input type="radio" name="tier" value={v} defaultChecked={tier === v} className="mt-0.5 size-4" />
<span>
<span className="block">{tt(`tier.${v}`)}</span>
<span className="block text-[12px] font-normal text-muted-foreground">{t(`tierHint.${v}`)}</span>
</span>
</label>
))}
</div>
<p className="mt-2 text-[12px] text-muted-foreground">{t("downgradeHint")}</p>
</fieldset>
<div className="max-w-xs">
<Label htmlFor="plan-seats">{t("seatsInput")}</Label>
<Input id="plan-seats" name="lotseChatSeats" type="number" inputMode="numeric" min={0} max={maxSeats} step={1} required defaultValue={seats} className="mt-1 h-11" />
<p className="mt-1 text-[12px] text-muted-foreground">{t("seatsHint")}</p>
</div>
<label className="flex min-h-11 items-start gap-2 text-sm">
<input type="checkbox" name="lotseChatHardLimit" defaultChecked={hardLimit} className="mt-0.5 size-5" />
<span>
<span className="block font-semibold">{t("hardLimitInput")}</span>
<span className="block text-[12px] text-muted-foreground">{t("hardLimitHint")}</span>
</span>
</label>
<label className="flex min-h-11 items-center gap-2 text-sm">
<input type="checkbox" name="confirm" className="size-5" required /> {t("confirm")}
</label>
{state.status === "error" && (
<p role="alert" className="rounded-lg border border-[var(--risk)] px-3 py-2 text-sm text-[var(--risk)]">
{t.has(`errors.${state.code}`) ? t(`errors.${state.code}`) : t("errors.failed")}
</p>
)}
<div className="flex flex-wrap gap-2">
<Button type="submit" className="h-11" disabled={pending}>
{pending ? t("submitting") : t("submit")}
</Button>
<Button variant="outline" className="h-11" nativeButton={false} render={<Link href={closeHref} />}>
{t("close")}
</Button>
</div>
</form>
);
}