- 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>
32 lines
1.5 KiB
TypeScript
32 lines
1.5 KiB
TypeScript
import { z } from "zod";
|
|
import { writeAuditLog } from "@/server/audit";
|
|
import { assertCan, ServiceError, type ServiceCtx } from "@/server/services/context";
|
|
import { assertPlanFeature } from "@/server/plan";
|
|
import { parseInput } from "@/server/services/work-orders/_shared";
|
|
|
|
/** Planning settings of a team = crew (L13): crew working day in minutes + working days bit mask. */
|
|
|
|
const settingsSchema = z.object({
|
|
dailyCapacityMinutes: z.coerce.number().int().min(0).max(24 * 60),
|
|
workingDays: z.coerce.number().int().min(0).max(127),
|
|
});
|
|
export type TeamPlanningSettingsInput = z.input<typeof settingsSchema>;
|
|
|
|
export async function updateTeamPlanningSettings(ctx: ServiceCtx, teamId: string, raw: TeamPlanningSettingsInput) {
|
|
assertCan(ctx, "team:manage");
|
|
await assertPlanFeature(ctx.tenantId, "planning"); // L17 Pakete: Kolonnenkapazität gehört zur Planung
|
|
const input = parseInput(settingsSchema, raw);
|
|
const before = await ctx.db.team.findFirst({
|
|
where: { id: teamId, deletedAt: null },
|
|
select: { id: true, dailyCapacityMinutes: true, workingDays: true },
|
|
});
|
|
if (!before) throw new ServiceError("not_found", "team_not_found");
|
|
const after = await ctx.db.team.update({
|
|
where: { id: teamId },
|
|
data: input,
|
|
select: { id: true, dailyCapacityMinutes: true, workingDays: true },
|
|
});
|
|
await writeAuditLog({ tenantId: ctx.tenantId, actorId: ctx.userId, action: "update", entity: "team", entityId: teamId, before, after });
|
|
return after;
|
|
}
|