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>
This commit is contained in:
@@ -41,6 +41,7 @@ export function LotseChat({
|
||||
transcription,
|
||||
locale,
|
||||
timeZone,
|
||||
quotaExhausted = false,
|
||||
}: {
|
||||
initial: ChatView;
|
||||
workOrderId: string | null;
|
||||
@@ -48,6 +49,8 @@ export function LotseChat({
|
||||
transcription: boolean;
|
||||
locale: string;
|
||||
timeZone: string;
|
||||
/** L17 Pakete: monthly chat quota used up with hard limit → hint instead of the input */
|
||||
quotaExhausted?: boolean;
|
||||
}) {
|
||||
const t = useTranslations("lotse");
|
||||
const router = useRouter();
|
||||
@@ -128,6 +131,7 @@ export function LotseChat({
|
||||
}
|
||||
|
||||
const lastAssistant = [...view.messages].reverse().find((m) => m.role !== "user");
|
||||
const sendLocked = quotaExhausted || error === "quota_exhausted";
|
||||
const errorText = error ? (t.has(`chat.errors.${error}`) ? t(`chat.errors.${error}`) : t("chat.errors.generic")) : null;
|
||||
|
||||
function notice(m: ChatMessageView): string | null {
|
||||
@@ -235,7 +239,7 @@ export function LotseChat({
|
||||
{m.id === lastAssistant?.id && m.content.chips && m.content.chips.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2" role="group" aria-label={t("chat.chipsLabel")}>
|
||||
{m.content.chips.map((c) => (
|
||||
<button key={c.value} type="button" className={chip(false)} disabled={pending !== null || !configured} onClick={() => send(c.value)}>
|
||||
<button key={c.value} type="button" className={chip(false)} disabled={pending !== null || !configured || sendLocked} onClick={() => send(c.value)}>
|
||||
{c.label}
|
||||
</button>
|
||||
))}
|
||||
@@ -270,13 +274,19 @@ export function LotseChat({
|
||||
{t("chat.offline")}
|
||||
</p>
|
||||
)}
|
||||
{errorText && (
|
||||
{errorText && error !== "quota_exhausted" && (
|
||||
<p className={noticeError} role="alert">
|
||||
<TriangleAlert className="mt-0.5 size-4.5 shrink-0" aria-hidden />
|
||||
{errorText}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{sendLocked ? (
|
||||
<p className={noticeWarn} role="status">
|
||||
<Info className="mt-0.5 size-4.5 shrink-0" aria-hidden />
|
||||
{t("chat.quotaExhausted")}
|
||||
</p>
|
||||
) : (
|
||||
<form
|
||||
className="space-y-2 rounded-xl border bg-card p-3 shadow-card"
|
||||
onSubmit={(e) => {
|
||||
@@ -315,6 +325,7 @@ export function LotseChat({
|
||||
</div>
|
||||
{transcription && <p className="text-[12.5px] text-muted-foreground">{t("chat.mic.hint")}</p>}
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user