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:
2026-09-21 10:13:27 +02:00
co-authored by Claude Opus 5
parent 4d5be72ebb
commit 06d320908f
60 changed files with 2076 additions and 91 deletions
+6 -2
View File
@@ -317,11 +317,15 @@
"transition_forbidden": "Dafür fehlt die Berechtigung.",
"not_editable": "Der Bericht ist eingereicht und kann nicht mehr geändert werden.",
"reason_required": "Der Grund fehlt.",
"use_billing_overview": "Nur über die Abrechnungsübersicht möglich."
"use_billing_overview": "Nur über die Abrechnungsübersicht möglich.",
"not_in_plan": "Der Lotse ist im Paket Profi enthalten und für diesen Betrieb nicht freigeschaltet.",
"no_seat": "Der Lotse-Chat ist für dich noch nicht freigeschaltet – frag im Büro nach.",
"quota_exhausted": "Das Chat-Kontingent des Betriebs ist für diesen Monat aufgebraucht."
},
"unavailable": {
"title": "Lotse-Chat nicht verfügbar",
"back": "Zur Übersicht"
}
},
"quotaExhausted": "Das Chat-Kontingent des Betriebs ist für diesen Monat aufgebraucht. Ab dem nächsten Monat kannst du wieder schreiben – bei Fragen hilft das Büro."
}
}
+3 -1
View File
@@ -92,7 +92,9 @@
"internal": "Technischer Fehler – wird automatisch erneut versucht.",
"timeOverlap": "Der Zeitnachtrag überschneidet sich mit einer bereits erfassten Zeit und wurde nicht übernommen.",
"timeWindow": "Der Zeitnachtrag liegt außerhalb der erlaubten 7 Tage, in der Zukunft oder ist zu lang und wurde nicht übernommen.",
"otherSession": "Auf einem anderen Auftrag lief noch eine Zeiterfassung – der Start wurde nicht übernommen. Bitte erneut starten und wechseln."
"otherSession": "Auf einem anderen Auftrag lief noch eine Zeiterfassung – der Start wurde nicht übernommen. Bitte erneut starten und wechseln.",
"notInPlan": "Im Paket Profi enthalten – für diesen Betrieb nicht freigeschaltet. Die Eingabe wurde nicht übernommen, bitte im Büro melden.",
"moduleDisabled": "Diese Funktion ist für diesen Betrieb ausgeschaltet. Die Eingabe wurde nicht übernommen, bitte im Büro melden."
},
"view": {
"title": "Offline-Ansicht",
+93
View File
@@ -0,0 +1,93 @@
{
"tier": {
"BASIS": "Basis",
"PROFI": "Profi"
},
"colTier": "Paket",
"notInTier": "Nur im Paket Profi",
"profiHint": {
"title": "Im Paket Profi enthalten",
"text": "Diese Funktion gehört zum Paket Profi. Ihr Betrieb nutzt das Paket Basis – für ein Upgrade wenden Sie sich bitte an Ihren Craftvia-Ansprechpartner."
},
"admin": {
"title": "Paket & Lotse-Chat",
"sub": "Paket des Betriebs, Chat-Plätze für Monteure und Verbrauch im laufenden Monat",
"package": "Paket",
"trial": "Testphase (alle Funktionen)",
"packageReadOnly": "Paket und Anzahl der Chat-Plätze legt der Anbieter fest. Hier vergeben Sie die gekauften Plätze an Ihre Monteure und Teamleiter.",
"basisHint": "Lotse und Lotse-Chat sind im Paket Profi enthalten. Bereits vergebene Plätze und Chatverläufe bleiben gespeichert und sind nach einem Wechsel zu Profi wieder verfügbar.",
"seatsTitle": "Chat-Plätze",
"seatsCount": "{assigned} von {purchased} Plätzen vergeben",
"trialSeats": "Während der Testphase dürfen alle Monteure und Teamleiter den Lotse-Chat nutzen – Plätze müssen nicht vergeben werden (Kontingent: {perSeat} Chats je Person und Monat).",
"noSeatsBought": "Noch keine Chat-Plätze gebucht. Plätze bucht der Anbieter für Ihren Betrieb.",
"overbooked": "Mehr Plätze vergeben als gebucht ({purchased}). Nur die zuerst vergebenen Plätze gelten – bitte Plätze entziehen.",
"noFieldUsers": "Keine aktiven Nutzer mit Feldrolle (Monteur, Teamleiter).",
"seatAssigned": "Platz vergeben",
"seatOverbooked": "Über der Platzanzahl",
"seatNone": "Kein Platz",
"assign": "Platz vergeben",
"revoke": "Platz entziehen",
"assignLabel": "Chat-Platz an {name} vergeben",
"revokeLabel": "Chat-Platz von {name} entziehen",
"saved": "Chat-Platz gespeichert.",
"usageTitle": "Verbrauch {month}",
"usageText": "{used} von {quota} Chats",
"quotaRule": "Kontingent: {perSeat} Chats je vergebenem Platz und Monat, gemeinsam für den Betrieb (Basis: {basis}). Ein Chat ist eine Nachricht an den Lotsen.",
"warn80": "{percent} % des Monatskontingents sind genutzt.",
"exhausted": "Das Monatskontingent ist ausgeschöpft. Weitere Chats werden als Mehrverbrauch in Paketen à {pack} berechnet.",
"hardLimitReached": "Das Monatskontingent ist ausgeschöpft. Der Lotse-Chat ist bis zum Monatsende gesperrt (hartes Limit).",
"overage": "Mehrverbrauch: {overage} Chats – entspricht {packs, plural, one {# Paket} other {# Paketen}} à {pack} Chats.",
"perUserCaption": "Chats je Nutzer im laufenden Monat",
"perUserName": "Nutzer",
"perUserChats": "Chats",
"errors": {
"no_seats_left": "Alle gebuchten Plätze sind vergeben. Bitte zuerst einen Platz entziehen oder weitere Plätze beim Anbieter buchen.",
"not_eligible": "Diese Person kann keinen Chat-Platz erhalten (keine aktive Feldrolle).",
"not_in_plan": "Der Lotse-Chat ist im Paket Profi enthalten.",
"disabled": "Lotse ist für diesen Betrieb ausgeschaltet.",
"forbidden": "Dafür fehlt die Berechtigung.",
"trial_expired": "Testphase abgelaufen – nur Lesezugriff.",
"failed": "Speichern nicht möglich."
}
},
"platform": {
"title": "Paket & Lotse-Chat",
"tier": "Paket",
"trialProfi": "Testphase = Profi",
"seats": "Chat-Plätze",
"seatsValue": "{assigned} vergeben / {purchased} gebucht",
"hardLimit": "Hartes Limit",
"hardLimitOn": "an – Sperre beim Kontingent",
"hardLimitOff": "aus – Mehrverbrauch wird gezählt",
"rule": "{perSeat} Chats je vergebenem Platz und Monat (Zeitzone des Mandanten); Mehrverbrauch in Paketen à {pack}.",
"currentMonth": "Laufender Monat · {month}",
"previousMonth": "Vormonat · {month}",
"chats": "{used} von {quota} Chats",
"overage": "Mehrverbrauch {overage} Chats → {packs, plural, one {# Paket} other {# Pakete}} à {pack}",
"noOverage": "Kein Mehrverbrauch",
"edit": "Paket ändern …",
"editTitle": "Paket & Chat-Plätze ändern",
"editText": "Die Änderung wirkt sofort für alle Nutzer des Mandanten und wird im Plattform-Audit protokolliert.",
"tierHint": {
"BASIS": "Ohne KI und Planung: kein Lotse, keine Plantafel/Live-Lage, keine Abrechnungsübersicht, kein Notdienst, kein Auftragsimport.",
"PROFI": "Alle Funktionen inkl. Planung, Abrechnungsübersicht, Notdienst, Import und Lotse."
},
"downgradeHint": "Wechsel zu Basis: Daten bleiben erhalten, nur der Zugriff endet. Zurück zu Profi ist alles wieder da.",
"seatsInput": "Gebuchte Chat-Plätze",
"seatsHint": "Der Mandanten-Admin vergibt die Plätze an Nutzer mit Feldrolle.",
"hardLimitInput": "Hartes Limit",
"hardLimitHint": "Nach Ausschöpfen des Monatskontingents wird der Chat gesperrt statt Mehrverbrauch zu zählen.",
"confirm": "Ich habe die Auswirkungen geprüft und bestätige die Änderung.",
"submit": "Speichern",
"submitting": "Speichern …",
"close": "Schließen",
"saved": "Paket gespeichert.",
"errors": {
"confirm_required": "Bitte die Änderung bestätigen.",
"invalid_input": "Bitte Eingaben prüfen (Plätze 0–1000).",
"tenant_archived": "Der Mandant ist archiviert.",
"platform_admin_required": "Nur Voll-Administratoren dürfen das Paket ändern.",
"failed": "Speichern nicht möglich."
}
}
}
+6 -2
View File
@@ -317,11 +317,15 @@
"transition_forbidden": "You lack the permission.",
"not_editable": "The report has been submitted and can no longer be changed.",
"reason_required": "The reason is missing.",
"use_billing_overview": "Only possible via the billing overview."
"use_billing_overview": "Only possible via the billing overview.",
"not_in_plan": "The Lotse is included in the Profi package and not activated for this company.",
"no_seat": "The Lotse chat is not activated for you yet – please ask the office.",
"quota_exhausted": "The company's chat quota for this month is used up."
},
"unavailable": {
"title": "Lotse chat not available",
"back": "Back to overview"
}
},
"quotaExhausted": "The company's chat quota for this month is used up. You can write again from next month – the office can help with questions."
}
}
+3 -1
View File
@@ -92,7 +92,9 @@
"internal": "Technical error – will retry automatically.",
"timeOverlap": "The added time overlaps with time already recorded and was not accepted.",
"timeWindow": "The added time is outside the allowed 7 days, in the future or too long and was not accepted.",
"otherSession": "Time was still running on another order – the start was not accepted. Please start again and switch."
"otherSession": "Time was still running on another order – the start was not accepted. Please start again and switch.",
"notInPlan": "Included in the Profi package – not activated for this company. The entry was not applied, please contact the office.",
"moduleDisabled": "This function is switched off for this company. The entry was not applied, please contact the office."
},
"view": {
"title": "Offline view",
+93
View File
@@ -0,0 +1,93 @@
{
"tier": {
"BASIS": "Basic",
"PROFI": "Profi"
},
"colTier": "Package",
"notInTier": "Profi package only",
"profiHint": {
"title": "Included in the Profi package",
"text": "This function is part of the Profi package. Your company uses the Basic package – please contact your Craftvia representative to upgrade."
},
"admin": {
"title": "Package & Lotse chat",
"sub": "Package of the company, chat seats for technicians and usage in the current month",
"package": "Package",
"trial": "Trial (all functions)",
"packageReadOnly": "The provider sets the package and the number of chat seats. Here you assign the purchased seats to your technicians and team leads.",
"basisHint": "Lotse and the Lotse chat are included in the Profi package. Assigned seats and chat histories are kept and available again after switching to Profi.",
"seatsTitle": "Chat seats",
"seatsCount": "{assigned} of {purchased} seats assigned",
"trialSeats": "During the trial all technicians and team leads may use the Lotse chat – no seats need to be assigned (quota: {perSeat} chats per person and month).",
"noSeatsBought": "No chat seats purchased yet. The provider books seats for your company.",
"overbooked": "More seats assigned than purchased ({purchased}). Only the seats assigned first are valid – please revoke seats.",
"noFieldUsers": "No active users with a field role (technician, team lead).",
"seatAssigned": "Seat assigned",
"seatOverbooked": "Above the seat number",
"seatNone": "No seat",
"assign": "Assign seat",
"revoke": "Revoke seat",
"assignLabel": "Assign chat seat to {name}",
"revokeLabel": "Revoke chat seat from {name}",
"saved": "Chat seat saved.",
"usageTitle": "Usage {month}",
"usageText": "{used} of {quota} chats",
"quotaRule": "Quota: {perSeat} chats per assigned seat and month, shared by the company (basis: {basis}). One chat is one message to the Lotse.",
"warn80": "{percent} % of the monthly quota are used.",
"exhausted": "The monthly quota is used up. Further chats are billed as overage in packs of {pack}.",
"hardLimitReached": "The monthly quota is used up. The Lotse chat is blocked until the end of the month (hard limit).",
"overage": "Overage: {overage} chats – equals {packs, plural, one {# pack} other {# packs}} of {pack} chats.",
"perUserCaption": "Chats per user in the current month",
"perUserName": "User",
"perUserChats": "Chats",
"errors": {
"no_seats_left": "All purchased seats are assigned. Please revoke a seat first or book more seats with the provider.",
"not_eligible": "This person cannot get a chat seat (no active field role).",
"not_in_plan": "The Lotse chat is included in the Profi package.",
"disabled": "Lotse is switched off for this company.",
"forbidden": "You are not allowed to do this.",
"trial_expired": "Trial expired – read-only access.",
"failed": "Could not save."
}
},
"platform": {
"title": "Package & Lotse chat",
"tier": "Package",
"trialProfi": "Trial = Profi",
"seats": "Chat seats",
"seatsValue": "{assigned} assigned / {purchased} purchased",
"hardLimit": "Hard limit",
"hardLimitOn": "on – blocked at the quota",
"hardLimitOff": "off – overage is counted",
"rule": "{perSeat} chats per assigned seat and month (tenant timezone); overage in packs of {pack}.",
"currentMonth": "Current month · {month}",
"previousMonth": "Previous month · {month}",
"chats": "{used} of {quota} chats",
"overage": "Overage {overage} chats → {packs, plural, one {# pack} other {# packs}} of {pack}",
"noOverage": "No overage",
"edit": "Change package …",
"editTitle": "Change package & chat seats",
"editText": "The change applies immediately to all users of the tenant and is logged in the platform audit.",
"tierHint": {
"BASIS": "Without AI and planning: no Lotse, no planning board/live situation, no billing overview, no emergency service, no order import.",
"PROFI": "All functions incl. planning, billing overview, emergency service, import and Lotse."
},
"downgradeHint": "Switching to Basic keeps all data, only access ends. Back on Profi everything is available again.",
"seatsInput": "Purchased chat seats",
"seatsHint": "The tenant admin assigns the seats to users with a field role.",
"hardLimitInput": "Hard limit",
"hardLimitHint": "When the monthly quota is used up the chat is blocked instead of counting overage.",
"confirm": "I checked the impact and confirm the change.",
"submit": "Save",
"submitting": "Saving …",
"close": "Close",
"saved": "Package saved.",
"errors": {
"confirm_required": "Please confirm the change.",
"invalid_input": "Please check the input (seats 0–1000).",
"tenant_archived": "The tenant is archived.",
"platform_admin_required": "Only full administrators may change the package.",
"failed": "Could not save."
}
}
}