- 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>
43 lines
1.9 KiB
TypeScript
43 lines
1.9 KiB
TypeScript
import { can, ServiceError, type ServiceCtx } from "@/server/services/context";
|
|
import { assertPlanFeature } from "@/server/plan";
|
|
|
|
/**
|
|
* Who may use the planning views (L13):
|
|
* - `work_order:read_all` (backoffice, admin): all teams; scheduling needs `work_order:assign` + `work_order:write`.
|
|
* - `work_order:read_team` + leader of at least one active team (Teamleiter): read-only, own teams only.
|
|
* - everyone else (Monteur): forbidden.
|
|
* L17 Pakete: planning is a PROFI feature — Basis → `forbidden` (reason `not_in_plan`) for board, live
|
|
* situation, recommendations, freed capacity, dashboard tiles and the planning API.
|
|
*/
|
|
export type PlanningAccess = {
|
|
all: boolean;
|
|
/** null = all teams of the tenant */
|
|
teamIds: string[] | null;
|
|
canSchedule: boolean;
|
|
canManageTeams: boolean;
|
|
};
|
|
|
|
export async function planningAccess(ctx: ServiceCtx): Promise<PlanningAccess> {
|
|
await assertPlanFeature(ctx.tenantId, "planning");
|
|
if (can(ctx, "work_order:read_all")) {
|
|
return {
|
|
all: true,
|
|
teamIds: null,
|
|
canSchedule: can(ctx, "work_order:assign") && can(ctx, "work_order:write"),
|
|
canManageTeams: can(ctx, "team:manage"),
|
|
};
|
|
}
|
|
if (can(ctx, "work_order:read_team")) {
|
|
const led = await ctx.db.team.findMany({ where: { leaderUserId: ctx.userId, status: "active", deletedAt: null }, select: { id: true } });
|
|
if (led.length > 0) return { all: false, teamIds: led.map((t) => t.id), canSchedule: false, canManageTeams: false };
|
|
}
|
|
throw new ServiceError("forbidden", "planning not allowed");
|
|
}
|
|
|
|
/** Recommendations and scheduling are backoffice functions (all teams + assign right). */
|
|
export async function assertCanPlanAll(ctx: ServiceCtx): Promise<PlanningAccess> {
|
|
const access = await planningAccess(ctx);
|
|
if (!access.all || !can(ctx, "work_order:assign")) throw new ServiceError("forbidden", "missing permission work_order:assign");
|
|
return access;
|
|
}
|