- 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>
24 lines
1004 B
TypeScript
24 lines
1004 B
TypeScript
import Link from "next/link";
|
|
import { getTranslations } from "next-intl/server";
|
|
import { CircleSlash } from "lucide-react";
|
|
import { card } from "@/components/field/ui";
|
|
|
|
/** „Lotse-Chat nicht verfügbar" (L16; L17: kein Platz / nicht im Paket) — text + icon, back to /m. */
|
|
export async function LotseChatUnavailable({ reason }: { reason: string }) {
|
|
const t = await getTranslations("lotse");
|
|
return (
|
|
<main className="space-y-3 p-4">
|
|
<section className={card}>
|
|
<h1 className="flex items-center gap-2 text-[20px]">
|
|
<CircleSlash className="size-5 text-muted-foreground" aria-hidden />
|
|
{t("chat.unavailable.title")}
|
|
</h1>
|
|
<p className="mt-2 text-[15px]">{t.has(`chat.errors.${reason}`) ? t(`chat.errors.${reason}`) : t("chat.errors.generic")}</p>
|
|
<Link href="/m" className="mt-3 inline-flex min-h-12 items-center font-semibold text-primary">
|
|
{t("chat.unavailable.back")}
|
|
</Link>
|
|
</section>
|
|
</main>
|
|
);
|
|
}
|