first commit
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@d4rk-tablet/shared",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./dto": "./src/dto/index.ts",
|
||||
"./events": "./src/events/index.ts",
|
||||
"./rbac": "./src/rbac/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { z } from 'zod';
|
||||
import { isoDate } from './common';
|
||||
|
||||
/** Beitrag am Schwarzen Brett (Tabelle mdt_announcements). */
|
||||
export const announcementSchema = z.object({
|
||||
id: z.number().int(),
|
||||
title: z.string(),
|
||||
body: z.string(),
|
||||
pinned: z.boolean(),
|
||||
important: z.boolean(),
|
||||
authorCitizenid: z.string().nullable(),
|
||||
authorName: z.string().nullable(),
|
||||
createdAt: isoDate,
|
||||
});
|
||||
export type Announcement = z.infer<typeof announcementSchema>;
|
||||
|
||||
export const createAnnouncementSchema = z.object({
|
||||
title: z.string().min(1),
|
||||
body: z.string().default(''),
|
||||
important: z.boolean().default(false),
|
||||
pinned: z.boolean().default(false),
|
||||
});
|
||||
export type CreateAnnouncementInput = z.infer<typeof createAnnouncementSchema>;
|
||||
|
||||
export const updateAnnouncementSchema = z.object({
|
||||
title: z.string().min(1).optional(),
|
||||
body: z.string().optional(),
|
||||
important: z.boolean().optional(),
|
||||
pinned: z.boolean().optional(),
|
||||
});
|
||||
export type UpdateAnnouncementInput = z.infer<typeof updateAnnouncementSchema>;
|
||||
@@ -0,0 +1,40 @@
|
||||
import { z } from 'zod';
|
||||
import { citizenId } from './common';
|
||||
import { ROLES, DEPARTMENTS } from '../rbac';
|
||||
import { permissionSchema } from './rank';
|
||||
|
||||
/** JWT-Payload (in Web-App aus Token dekodiert). */
|
||||
export const authUserSchema = z.object({
|
||||
sub: z.string(), // interne mdt_users id
|
||||
citizenid: citizenId.nullable(),
|
||||
discordId: z.string().nullable(),
|
||||
name: z.string(),
|
||||
roles: z.array(z.enum(ROLES)),
|
||||
/** Effektive Rechte (server-seitig aus Rang-Matrix + Overrides berechnet). */
|
||||
permissions: z.array(permissionSchema),
|
||||
/** Behörde + Rang (aus QBox-Job), für Branding & Anzeige. */
|
||||
department: z.enum(DEPARTMENTS).nullable(),
|
||||
grade: z.number().int().nullable(),
|
||||
/** Behörden-Keys, denen der User angehört (aus QBox-Job über die Registry). */
|
||||
authorities: z.array(z.string()),
|
||||
});
|
||||
export type AuthUser = z.infer<typeof authUserSchema>;
|
||||
|
||||
/** Body von /auth/fivem — von der fivem-bridge (Server) gesendet, HMAC-signiert. */
|
||||
export const fivemAuthRequestSchema = z.object({
|
||||
license: z.string().min(1),
|
||||
citizenid: citizenId,
|
||||
name: z.string(),
|
||||
job: z.string(),
|
||||
jobGrade: z.string().optional(), // Rang-Bezeichnung (Anzeige)
|
||||
gradeLevel: z.coerce.number().int().min(0).default(0), // QBox job.grade.level
|
||||
callsign: z.string().nullable().optional(),
|
||||
});
|
||||
export type FivemAuthRequest = z.infer<typeof fivemAuthRequestSchema>;
|
||||
|
||||
export const authTokenResponseSchema = z.object({
|
||||
token: z.string(),
|
||||
refreshToken: z.string(),
|
||||
user: authUserSchema,
|
||||
});
|
||||
export type AuthTokenResponse = z.infer<typeof authTokenResponseSchema>;
|
||||
@@ -0,0 +1,34 @@
|
||||
import { z } from 'zod';
|
||||
import { DEPARTMENTS } from '../rbac';
|
||||
|
||||
/**
|
||||
* Eine Behörde/Fraktion (LSPD, BCSO, DOJ, SAMS, LSFD …).
|
||||
* `jobs` = QBox-Job-Namen, die zu dieser Behörde gehören.
|
||||
* `department` = Basis-Kategorie (police/ems/fire) für Branding & Rang-Matrix.
|
||||
*/
|
||||
export const authoritySchema = z.object({
|
||||
id: z.number().int(),
|
||||
key: z.string(), // kurzer eindeutiger Schlüssel, z. B. "lspd"
|
||||
name: z.string(),
|
||||
color: z.string(),
|
||||
department: z.enum(DEPARTMENTS),
|
||||
jobs: z.array(z.string()),
|
||||
});
|
||||
export type Authority = z.infer<typeof authoritySchema>;
|
||||
|
||||
export const createAuthoritySchema = z.object({
|
||||
key: z.string().min(1).regex(/^[a-z0-9_]+$/, 'nur a-z, 0-9, _'),
|
||||
name: z.string().min(1),
|
||||
color: z.string().min(1).default('#2f6df6'),
|
||||
department: z.enum(DEPARTMENTS),
|
||||
jobs: z.array(z.string()).default([]),
|
||||
});
|
||||
export type CreateAuthorityInput = z.infer<typeof createAuthoritySchema>;
|
||||
|
||||
export const updateAuthoritySchema = z.object({
|
||||
name: z.string().min(1).optional(),
|
||||
color: z.string().min(1).optional(),
|
||||
department: z.enum(DEPARTMENTS).optional(),
|
||||
jobs: z.array(z.string()).optional(),
|
||||
});
|
||||
export type UpdateAuthorityInput = z.infer<typeof updateAuthoritySchema>;
|
||||
@@ -0,0 +1,69 @@
|
||||
import { z } from 'zod';
|
||||
import { citizenId, isoDate } from './common';
|
||||
|
||||
/** RSVP-Status eines Teilnehmers. */
|
||||
export const attendeeStatus = z.enum(['invited', 'accepted', 'declined', 'maybe']);
|
||||
export type AttendeeStatus = z.infer<typeof attendeeStatus>;
|
||||
|
||||
/** Kategorie/Typ eines Termins (Farbe im Frontend). */
|
||||
export const eventCategory = z.enum(['dienst', 'schulung', 'meeting', 'event', 'sonstiges']);
|
||||
export type EventCategory = z.infer<typeof eventCategory>;
|
||||
|
||||
export const calendarAttendeeSchema = z.object({
|
||||
id: z.number().int(),
|
||||
citizenid: citizenId,
|
||||
name: z.string(),
|
||||
status: attendeeStatus,
|
||||
self: z.boolean().default(false), // hat sich selbst eingetragen (kein Invite)
|
||||
});
|
||||
export type CalendarAttendee = z.infer<typeof calendarAttendeeSchema>;
|
||||
|
||||
export const calendarEventSchema = z.object({
|
||||
id: z.number().int(),
|
||||
title: z.string(),
|
||||
description: z.string().default(''), // HTML (Tiptap)
|
||||
category: eventCategory.default('sonstiges'),
|
||||
startAt: isoDate,
|
||||
endAt: isoDate.nullable(),
|
||||
allDay: z.boolean().default(false),
|
||||
location: z.string().nullable(),
|
||||
openSignup: z.boolean().default(true), // dürfen sich Nicht-Eingeladene selbst eintragen?
|
||||
organizerCitizenid: citizenId.nullable(),
|
||||
organizerName: z.string().nullable(),
|
||||
attendees: z.array(calendarAttendeeSchema).default([]),
|
||||
createdAt: isoDate,
|
||||
});
|
||||
export type CalendarEvent = z.infer<typeof calendarEventSchema>;
|
||||
|
||||
/** Einzuladende Person (Snapshot). */
|
||||
export const inviteeSchema = z.object({ citizenid: citizenId, name: z.string() });
|
||||
export type Invitee = z.infer<typeof inviteeSchema>;
|
||||
|
||||
export const createEventSchema = z.object({
|
||||
title: z.string().trim().min(1),
|
||||
description: z.string().default(''),
|
||||
category: eventCategory.default('sonstiges'),
|
||||
startAt: z.string(), // ISO
|
||||
endAt: z.string().nullable().default(null),
|
||||
allDay: z.boolean().default(false),
|
||||
location: z.string().trim().nullable().default(null),
|
||||
openSignup: z.boolean().default(true),
|
||||
invitees: z.array(inviteeSchema).default([]),
|
||||
});
|
||||
export type CreateEventInput = z.infer<typeof createEventSchema>;
|
||||
|
||||
export const updateEventSchema = z.object({
|
||||
title: z.string().trim().min(1).optional(),
|
||||
description: z.string().optional(),
|
||||
category: eventCategory.optional(),
|
||||
startAt: z.string().optional(),
|
||||
endAt: z.string().nullable().optional(),
|
||||
allDay: z.boolean().optional(),
|
||||
location: z.string().trim().nullable().optional(),
|
||||
openSignup: z.boolean().optional(),
|
||||
});
|
||||
export type UpdateEventInput = z.infer<typeof updateEventSchema>;
|
||||
|
||||
/** Eigene Teilnahme setzen (Selbst-Eintragung / RSVP). */
|
||||
export const rsvpSchema = z.object({ status: attendeeStatus });
|
||||
export type RsvpInput = z.infer<typeof rsvpSchema>;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { z } from 'zod';
|
||||
import { citizenId, isoDate } from './common';
|
||||
import { caseChargeSchema } from './charge';
|
||||
|
||||
export const caseStatus = z.enum(['open', 'closed', 'dismissed']);
|
||||
export type CaseStatus = z.infer<typeof caseStatus>;
|
||||
|
||||
/** Strafakte (Tabelle mdt_cases + mdt_case_charges). */
|
||||
export const caseSchema = z.object({
|
||||
id: z.number().int(),
|
||||
title: z.string(),
|
||||
suspectCitizenid: citizenId,
|
||||
suspectName: z.string().nullable(),
|
||||
status: caseStatus.default('open'),
|
||||
charges: z.array(caseChargeSchema).default([]),
|
||||
totalFine: z.number().int().default(0),
|
||||
totalJailTime: z.number().int().default(0),
|
||||
narrative: z.string().default(''),
|
||||
officerCitizenid: citizenId.nullable(),
|
||||
officerName: z.string().nullable(),
|
||||
createdAt: isoDate,
|
||||
updatedAt: isoDate.nullable(),
|
||||
});
|
||||
export type Case = z.infer<typeof caseSchema>;
|
||||
|
||||
export const createCaseSchema = z.object({
|
||||
title: z.string().min(1),
|
||||
suspectCitizenid: citizenId,
|
||||
narrative: z.string().default(''),
|
||||
charges: z
|
||||
.array(
|
||||
z.object({
|
||||
catalogId: z.number().int(),
|
||||
count: z.number().int().min(1).default(1),
|
||||
}),
|
||||
)
|
||||
.default([]),
|
||||
});
|
||||
export type CreateCaseInput = z.infer<typeof createCaseSchema>;
|
||||
|
||||
export const updateCaseSchema = z.object({
|
||||
title: z.string().min(1).optional(),
|
||||
status: caseStatus.optional(),
|
||||
narrative: z.string().optional(),
|
||||
charges: z
|
||||
.array(z.object({ catalogId: z.number().int(), count: z.number().int().min(1).default(1) }))
|
||||
.optional(),
|
||||
});
|
||||
export type UpdateCaseInput = z.infer<typeof updateCaseSchema>;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/** Eintrag im Strafenkatalog (Tabelle mdt_charges_catalog). */
|
||||
export const chargeCatalogItemSchema = z.object({
|
||||
id: z.number().int(),
|
||||
code: z.string(), // Paragraph, z. B. "§ 242"
|
||||
title: z.string(),
|
||||
category: z.string(),
|
||||
fine: z.number().int().default(0), // Bußgeld in €
|
||||
jailTime: z.number().int().default(0), // Haftzeit in Monaten
|
||||
points: z.number().int().default(0), // Strafpunkte
|
||||
active: z.boolean().default(true),
|
||||
});
|
||||
export type ChargeCatalogItem = z.infer<typeof chargeCatalogItemSchema>;
|
||||
|
||||
export const upsertChargeCatalogItemSchema = chargeCatalogItemSchema
|
||||
.omit({ id: true })
|
||||
.partial({ active: true, fine: true, jailTime: true, points: true });
|
||||
export type UpsertChargeCatalogItemInput = z.infer<typeof upsertChargeCatalogItemSchema>;
|
||||
|
||||
/** Eine konkret in einer Akte verhängte Anklage (mdt_case_charges), mit Multiplikator. */
|
||||
export const caseChargeSchema = z.object({
|
||||
catalogId: z.number().int(),
|
||||
code: z.string(),
|
||||
title: z.string(),
|
||||
count: z.number().int().min(1).default(1),
|
||||
fine: z.number().int(),
|
||||
jailTime: z.number().int(),
|
||||
points: z.number().int(),
|
||||
});
|
||||
export type CaseCharge = z.infer<typeof caseChargeSchema>;
|
||||
@@ -0,0 +1,35 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/** ISO-Datumsstring (z. B. Geburtsdatum, Timestamps). */
|
||||
export const isoDate = z.string();
|
||||
|
||||
/** QBox citizenid (Primärschlüssel für Personen). */
|
||||
export const citizenId = z.string().min(1);
|
||||
|
||||
/** Kennzeichen. QBox speichert i. d. R. mit trailing spaces → wir trimmen. */
|
||||
export const plate = z.string().min(1).transform((s) => s.trim().toUpperCase());
|
||||
|
||||
/** Koordinaten (Live-Map / Dispatch). */
|
||||
export const coordsSchema = z.object({
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
z: z.number(),
|
||||
heading: z.number().optional(),
|
||||
});
|
||||
export type Coords = z.infer<typeof coordsSchema>;
|
||||
|
||||
/** Standard-Paginierung für Listen-Endpoints. */
|
||||
export const paginationSchema = z.object({
|
||||
page: z.coerce.number().int().min(1).default(1),
|
||||
pageSize: z.coerce.number().int().min(1).max(100).default(25),
|
||||
});
|
||||
export type Pagination = z.infer<typeof paginationSchema>;
|
||||
|
||||
export function paginated<T extends z.ZodTypeAny>(item: T) {
|
||||
return z.object({
|
||||
items: z.array(item),
|
||||
total: z.number().int(),
|
||||
page: z.number().int(),
|
||||
pageSize: z.number().int(),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { z } from 'zod';
|
||||
import { citizenId, isoDate, coordsSchema } from './common';
|
||||
import { DEPARTMENTS } from '../rbac';
|
||||
|
||||
export const dispatchStatus = z.enum(['pending', 'assigned', 'enroute', 'onscene', 'closed']);
|
||||
export type DispatchStatus = z.infer<typeof dispatchStatus>;
|
||||
|
||||
export const dispatchPriority = z.enum(['low', 'medium', 'high']);
|
||||
export type DispatchPriority = z.infer<typeof dispatchPriority>;
|
||||
|
||||
/** Live-Einsatz (Tabelle mdt_dispatch_calls + Realtime via Socket). */
|
||||
export const dispatchCallSchema = z.object({
|
||||
id: z.number().int(),
|
||||
code: z.string(), // 10-Code / Kategorie
|
||||
title: z.string(),
|
||||
description: z.string().default(''),
|
||||
department: z.enum(DEPARTMENTS),
|
||||
priority: dispatchPriority.default('medium'),
|
||||
status: dispatchStatus.default('pending'),
|
||||
location: z.string().nullable(),
|
||||
coords: coordsSchema.nullable(),
|
||||
callerCitizenid: citizenId.nullable(),
|
||||
assignedOfficers: z.array(citizenId).default([]),
|
||||
createdAt: isoDate,
|
||||
updatedAt: isoDate.nullable(),
|
||||
});
|
||||
export type DispatchCall = z.infer<typeof dispatchCallSchema>;
|
||||
|
||||
export const createDispatchCallSchema = z.object({
|
||||
code: z.string().min(1),
|
||||
title: z.string().min(1),
|
||||
description: z.string().default(''),
|
||||
department: z.enum(DEPARTMENTS).default('police'),
|
||||
priority: dispatchPriority.default('medium'),
|
||||
location: z.string().nullable().default(null),
|
||||
coords: coordsSchema.nullable().default(null),
|
||||
callerCitizenid: citizenId.nullable().default(null),
|
||||
});
|
||||
export type CreateDispatchCallInput = z.infer<typeof createDispatchCallSchema>;
|
||||
|
||||
export const assignDispatchSchema = z.object({
|
||||
officers: z.array(citizenId).min(1),
|
||||
status: dispatchStatus.optional(),
|
||||
});
|
||||
export type AssignDispatchInput = z.infer<typeof assignDispatchSchema>;
|
||||
@@ -0,0 +1,81 @@
|
||||
import { z } from 'zod';
|
||||
import { isoDate } from './common';
|
||||
|
||||
/** Ein Freigabe-Eintrag: wer (Subjekt) darf was (Level). */
|
||||
export const aclSubjectType = z.enum(['authority', 'group', 'person']);
|
||||
export type AclSubjectType = z.infer<typeof aclSubjectType>;
|
||||
|
||||
export const aclLevel = z.enum(['read', 'write']);
|
||||
export type AclLevel = z.infer<typeof aclLevel>;
|
||||
|
||||
export const aclEntrySchema = z.object({
|
||||
type: aclSubjectType,
|
||||
id: z.string(), // authority-key | group-id | citizenid
|
||||
label: z.string(), // Anzeigename (Snapshot)
|
||||
level: aclLevel,
|
||||
});
|
||||
export type AclEntry = z.infer<typeof aclEntrySchema>;
|
||||
|
||||
/** Freigabe-Zustand eines Ordners/Dokuments. */
|
||||
export const accessSchema = z.object({
|
||||
public: z.boolean().default(false), // öffentlich lesbar (jeder mit documents.view)
|
||||
acl: z.array(aclEntrySchema).default([]),
|
||||
});
|
||||
export type Access = z.infer<typeof accessSchema>;
|
||||
|
||||
export const docFolderSchema = z.object({
|
||||
id: z.number().int(),
|
||||
name: z.string(),
|
||||
public: z.boolean(),
|
||||
acl: z.array(aclEntrySchema),
|
||||
docCount: z.number().int().default(0),
|
||||
});
|
||||
export type DocFolder = z.infer<typeof docFolderSchema>;
|
||||
|
||||
/** Kurzform für Listen. */
|
||||
export const documentSummarySchema = z.object({
|
||||
id: z.number().int(),
|
||||
reference: z.string(), // DO-2026-07-06-001
|
||||
title: z.string(),
|
||||
folderId: z.number().int().nullable(),
|
||||
public: z.boolean(),
|
||||
acl: z.array(aclEntrySchema),
|
||||
pinned: z.boolean(),
|
||||
canWrite: z.boolean(), // darf der aktuelle Betrachter bearbeiten?
|
||||
authorName: z.string().nullable(),
|
||||
updatedAt: isoDate,
|
||||
});
|
||||
export type DocumentSummary = z.infer<typeof documentSummarySchema>;
|
||||
|
||||
/** Volles Dokument inkl. Rich-Text-Body (HTML aus Tiptap). */
|
||||
export const documentSchema = documentSummarySchema.extend({
|
||||
content: z.string(), // HTML
|
||||
createdAt: isoDate,
|
||||
});
|
||||
export type MdtDocument = z.infer<typeof documentSchema>;
|
||||
|
||||
export const createDocumentSchema = z.object({
|
||||
title: z.string().min(1),
|
||||
content: z.string().default(''),
|
||||
folderId: z.number().int().nullable().default(null),
|
||||
public: z.boolean().default(false),
|
||||
acl: z.array(aclEntrySchema).default([]),
|
||||
});
|
||||
export type CreateDocumentInput = z.infer<typeof createDocumentSchema>;
|
||||
|
||||
export const updateDocumentSchema = z.object({
|
||||
title: z.string().min(1).optional(),
|
||||
content: z.string().optional(),
|
||||
folderId: z.number().int().nullable().optional(),
|
||||
public: z.boolean().optional(),
|
||||
acl: z.array(aclEntrySchema).optional(),
|
||||
pinned: z.boolean().optional(),
|
||||
});
|
||||
export type UpdateDocumentInput = z.infer<typeof updateDocumentSchema>;
|
||||
|
||||
export const createFolderSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
public: z.boolean().default(false),
|
||||
acl: z.array(aclEntrySchema).default([]),
|
||||
});
|
||||
export type CreateFolderInput = z.infer<typeof createFolderSchema>;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { z } from 'zod';
|
||||
import { citizenId } from './common';
|
||||
|
||||
export const groupMemberSchema = z.object({
|
||||
citizenid: citizenId,
|
||||
name: z.string(),
|
||||
});
|
||||
export type GroupMember = z.infer<typeof groupMemberSchema>;
|
||||
|
||||
export const groupSchema = z.object({
|
||||
id: z.number().int(),
|
||||
name: z.string(),
|
||||
color: z.string(),
|
||||
memberCount: z.number().int().default(0),
|
||||
});
|
||||
export type Group = z.infer<typeof groupSchema>;
|
||||
|
||||
export const groupDetailSchema = groupSchema.extend({
|
||||
members: z.array(groupMemberSchema),
|
||||
});
|
||||
export type GroupDetail = z.infer<typeof groupDetailSchema>;
|
||||
|
||||
export const createGroupSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
color: z.string().min(1).default('#8b5cf6'),
|
||||
});
|
||||
export type CreateGroupInput = z.infer<typeof createGroupSchema>;
|
||||
|
||||
export const updateGroupSchema = z.object({
|
||||
name: z.string().min(1).optional(),
|
||||
color: z.string().min(1).optional(),
|
||||
});
|
||||
export type UpdateGroupInput = z.infer<typeof updateGroupSchema>;
|
||||
|
||||
export const addGroupMemberSchema = z.object({
|
||||
citizenid: citizenId,
|
||||
name: z.string().min(1),
|
||||
});
|
||||
export type AddGroupMemberInput = z.infer<typeof addGroupMemberSchema>;
|
||||
@@ -0,0 +1,17 @@
|
||||
export * from './common';
|
||||
export * from './person';
|
||||
export * from './vehicle';
|
||||
export * from './charge';
|
||||
export * from './case';
|
||||
export * from './treatment';
|
||||
export * from './law';
|
||||
export * from './calendar';
|
||||
export * from './warrant';
|
||||
export * from './dispatch';
|
||||
export * from './officer';
|
||||
export * from './rank';
|
||||
export * from './announcement';
|
||||
export * from './document';
|
||||
export * from './authority';
|
||||
export * from './group';
|
||||
export * from './auth';
|
||||
@@ -0,0 +1,28 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/** Gesetzes-/Paragraphen-Eintrag (Strafgesetzbuch-Nachschlagewerk). */
|
||||
export const lawSchema = z.object({
|
||||
id: z.number().int(),
|
||||
category: z.string(), // z. B. "Eigentumsdelikte"
|
||||
paragraph: z.string(), // z. B. "§ 242"
|
||||
title: z.string(),
|
||||
description: z.string().default(''), // HTML (Tiptap)
|
||||
fine: z.number().int().nullable().default(null),
|
||||
jailTime: z.number().int().nullable().default(null), // Monate
|
||||
sortOrder: z.number().int().default(0),
|
||||
});
|
||||
export type Law = z.infer<typeof lawSchema>;
|
||||
|
||||
export const createLawSchema = z.object({
|
||||
category: z.string().trim().min(1),
|
||||
paragraph: z.string().trim().min(1),
|
||||
title: z.string().trim().min(1),
|
||||
description: z.string().default(''),
|
||||
fine: z.number().int().nullable().default(null),
|
||||
jailTime: z.number().int().nullable().default(null),
|
||||
sortOrder: z.number().int().default(0),
|
||||
});
|
||||
export type CreateLawInput = z.infer<typeof createLawSchema>;
|
||||
|
||||
export const updateLawSchema = createLawSchema.partial();
|
||||
export type UpdateLawInput = z.infer<typeof updateLawSchema>;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { z } from 'zod';
|
||||
import { citizenId, coordsSchema } from './common';
|
||||
import { DEPARTMENTS } from '../rbac';
|
||||
|
||||
/** Live-Zustand eines Officers (nur RAM/Socket, nicht persistiert). */
|
||||
export const officerSchema = z.object({
|
||||
citizenid: citizenId,
|
||||
name: z.string(),
|
||||
callsign: z.string().nullable(),
|
||||
department: z.enum(DEPARTMENTS),
|
||||
onDuty: z.boolean().default(false),
|
||||
coords: coordsSchema.nullable().default(null),
|
||||
updatedAt: z.number().int(), // epoch ms
|
||||
});
|
||||
export type Officer = z.infer<typeof officerSchema>;
|
||||
|
||||
/** Position-Update, das die fivem-bridge (throttled) an das Backend pusht. */
|
||||
export const officerPositionUpdateSchema = z.object({
|
||||
citizenid: citizenId,
|
||||
coords: coordsSchema,
|
||||
});
|
||||
export type OfficerPositionUpdate = z.infer<typeof officerPositionUpdateSchema>;
|
||||
|
||||
export const officerDutyUpdateSchema = z.object({
|
||||
citizenid: citizenId,
|
||||
onDuty: z.boolean(),
|
||||
});
|
||||
export type OfficerDutyUpdate = z.infer<typeof officerDutyUpdateSchema>;
|
||||
@@ -0,0 +1,108 @@
|
||||
import { z } from 'zod';
|
||||
import { citizenId, isoDate } from './common';
|
||||
import { aclEntrySchema } from './document';
|
||||
|
||||
/** Aufenthaltsstatus — u. a. für illegale Einreise (Behörden-Führung, nicht QBox). */
|
||||
export const legalStatusValues = ['citizen', 'resident', 'visitor', 'illegal', 'unknown'] as const;
|
||||
export const legalStatusSchema = z.enum(legalStatusValues);
|
||||
export type LegalStatus = z.infer<typeof legalStatusSchema>;
|
||||
|
||||
export const genderValues = ['male', 'female', 'divers', 'unknown'] as const;
|
||||
export const genderSchema = z.enum(genderValues);
|
||||
export type Gender = z.infer<typeof genderSchema>;
|
||||
|
||||
/** Führerschein-/Schein-Eintrag (editierbar). */
|
||||
export const licenseSchema = z.object({
|
||||
type: z.string(),
|
||||
label: z.string(),
|
||||
active: z.boolean().default(true),
|
||||
});
|
||||
export type License = z.infer<typeof licenseSchema>;
|
||||
|
||||
export const plateStatusValues = ['registered', 'forged', 'stolen', 'unknown'] as const;
|
||||
export const plateStatusSchema = z.enum(plateStatusValues);
|
||||
export type PlateStatus = z.infer<typeof plateStatusSchema>;
|
||||
|
||||
/** Auf einen Bürger geführtes Fahrzeug (aus mdt_vehicles). */
|
||||
export const ownedVehicleSchema = z.object({
|
||||
id: z.number().int().optional(),
|
||||
plate: z.string(),
|
||||
model: z.string().nullable(),
|
||||
plateStatus: plateStatusSchema.default('registered'),
|
||||
isStolen: z.boolean().default(false),
|
||||
});
|
||||
export type OwnedVehicle = z.infer<typeof ownedVehicleSchema>;
|
||||
|
||||
/** Kurzform für Registry-Liste + Suche. */
|
||||
export const personSummarySchema = z.object({
|
||||
citizenid: citizenId,
|
||||
firstname: z.string(),
|
||||
lastname: z.string(),
|
||||
dob: z.string().nullable(),
|
||||
phone: z.string().nullable(),
|
||||
isWanted: z.boolean().default(false),
|
||||
mugshotUrl: z.string().nullable().default(null),
|
||||
legalStatus: legalStatusSchema.default('citizen'),
|
||||
// Freigabe/Sichtbarkeit — welche Behörden/Gruppen/Personen die Akte sehen dürfen
|
||||
public: z.boolean().default(true),
|
||||
acl: z.array(aclEntrySchema).default([]),
|
||||
});
|
||||
export type PersonSummary = z.infer<typeof personSummarySchema>;
|
||||
|
||||
/** Volle, editierbare Behörden-Bürgerakte (MDT-eigen, unabhängig von QBox). */
|
||||
export const personSchema = personSummarySchema.extend({
|
||||
gender: genderSchema.default('unknown'),
|
||||
nationality: z.string().nullable().default(null),
|
||||
address: z.string().nullable().default(null),
|
||||
occupation: z.string().nullable().default(null),
|
||||
height: z.string().nullable().default(null),
|
||||
eyeColor: z.string().nullable().default(null),
|
||||
hairColor: z.string().nullable().default(null),
|
||||
distinguishingMarks: z.string().default(''),
|
||||
aliases: z.array(z.string()).default([]),
|
||||
licenses: z.array(licenseSchema).default([]),
|
||||
notes: z.string().default(''),
|
||||
flags: z.array(z.string()).default([]),
|
||||
createdByName: z.string().nullable().default(null),
|
||||
createdAt: isoDate.nullable().default(null),
|
||||
updatedAt: isoDate.nullable().default(null),
|
||||
updatedBy: z.string().nullable().default(null),
|
||||
vehicles: z.array(ownedVehicleSchema).default([]),
|
||||
// Zähler; Detaildaten kommen aus /warrants bzw. /cases
|
||||
openWarrants: z.number().int().default(0),
|
||||
totalCases: z.number().int().default(0),
|
||||
});
|
||||
export type Person = z.infer<typeof personSchema>;
|
||||
|
||||
export const personSearchQuerySchema = z.object({
|
||||
query: z.string().trim().min(1),
|
||||
});
|
||||
|
||||
/** Bearbeitbare Felder einer Bürgerakte (Create verlangt Name, Update macht alles optional). */
|
||||
export const createCitizenSchema = z.object({
|
||||
firstname: z.string().trim().min(1),
|
||||
lastname: z.string().trim().min(1),
|
||||
dob: z.string().trim().nullable().optional(),
|
||||
gender: genderSchema.optional(),
|
||||
phone: z.string().trim().nullable().optional(),
|
||||
nationality: z.string().trim().nullable().optional(),
|
||||
legalStatus: legalStatusSchema.optional(),
|
||||
address: z.string().trim().nullable().optional(),
|
||||
occupation: z.string().trim().nullable().optional(),
|
||||
height: z.string().trim().nullable().optional(),
|
||||
eyeColor: z.string().trim().nullable().optional(),
|
||||
hairColor: z.string().trim().nullable().optional(),
|
||||
distinguishingMarks: z.string().optional(),
|
||||
aliases: z.array(z.string()).optional(),
|
||||
licenses: z.array(licenseSchema).optional(),
|
||||
mugshotUrl: z.string().trim().nullable().optional(),
|
||||
notes: z.string().optional(),
|
||||
flags: z.array(z.string()).optional(),
|
||||
isWanted: z.boolean().optional(),
|
||||
public: z.boolean().optional(),
|
||||
acl: z.array(aclEntrySchema).optional(),
|
||||
});
|
||||
export type CreateCitizenInput = z.infer<typeof createCitizenSchema>;
|
||||
|
||||
export const updateCitizenSchema = createCitizenSchema.partial();
|
||||
export type UpdateCitizenInput = z.infer<typeof updateCitizenSchema>;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { z } from 'zod';
|
||||
import { PERMISSIONS, DEPARTMENTS, type Permission } from '../rbac';
|
||||
|
||||
export const permissionSchema = z.enum(PERMISSIONS as unknown as [Permission, ...Permission[]]);
|
||||
|
||||
/** Ein Rang einer Behörde (Department + QBox-Grade) mit seinem Rechte-Set. */
|
||||
export const rankSchema = z.object({
|
||||
department: z.enum(DEPARTMENTS),
|
||||
grade: z.number().int().min(0),
|
||||
label: z.string(),
|
||||
permissions: z.array(permissionSchema),
|
||||
});
|
||||
export type Rank = z.infer<typeof rankSchema>;
|
||||
|
||||
/** Update aus der Admin-Rechte-Matrix. */
|
||||
export const updateRankSchema = z.object({
|
||||
label: z.string().min(1).optional(),
|
||||
permissions: z.array(permissionSchema),
|
||||
});
|
||||
export type UpdateRankInput = z.infer<typeof updateRankSchema>;
|
||||
|
||||
/** Mitarbeiter-Eintrag (mdt_users) für das Verwaltungs-Modul. */
|
||||
export const mdtUserSchema = z.object({
|
||||
id: z.number().int(),
|
||||
citizenid: z.string().nullable(),
|
||||
discordId: z.string().nullable(),
|
||||
name: z.string(),
|
||||
callsign: z.string().nullable(),
|
||||
department: z.enum(DEPARTMENTS).nullable(),
|
||||
grade: z.number().int().nullable(),
|
||||
roles: z.array(z.string()),
|
||||
});
|
||||
export type MdtUser = z.infer<typeof mdtUserSchema>;
|
||||
|
||||
/** Override-Rollen eines Mitarbeiters setzen (admin/dispatch). */
|
||||
export const updateUserRolesSchema = z.object({
|
||||
roles: z.array(z.string()),
|
||||
});
|
||||
export type UpdateUserRolesInput = z.infer<typeof updateUserRolesSchema>;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { z } from 'zod';
|
||||
import { citizenId, isoDate } from './common';
|
||||
|
||||
/** Status einer Behandlungsakte (EMS-Pendant zu den Strafakten). */
|
||||
export const treatmentStatus = z.enum(['open', 'closed']);
|
||||
export type TreatmentStatus = z.infer<typeof treatmentStatus>;
|
||||
|
||||
/** Behandlungsakte (Tabelle mdt_treatment_records). */
|
||||
export const treatmentRecordSchema = z.object({
|
||||
id: z.number().int(),
|
||||
reference: z.string(), // TR-JJJJ-MM-TT-###
|
||||
patientCitizenid: citizenId,
|
||||
patientName: z.string().nullable(),
|
||||
title: z.string(),
|
||||
diagnosis: z.string().default(''),
|
||||
treatment: z.string().default(''), // HTML aus Tiptap
|
||||
status: treatmentStatus.default('open'),
|
||||
authorCitizenid: citizenId.nullable(),
|
||||
authorName: z.string().nullable(),
|
||||
createdAt: isoDate,
|
||||
updatedAt: isoDate.nullable(),
|
||||
});
|
||||
export type TreatmentRecord = z.infer<typeof treatmentRecordSchema>;
|
||||
|
||||
export const createTreatmentSchema = z.object({
|
||||
patientCitizenid: citizenId,
|
||||
title: z.string().min(1),
|
||||
diagnosis: z.string().default(''),
|
||||
treatment: z.string().default(''),
|
||||
});
|
||||
export type CreateTreatmentInput = z.infer<typeof createTreatmentSchema>;
|
||||
|
||||
export const updateTreatmentSchema = z.object({
|
||||
title: z.string().min(1).optional(),
|
||||
diagnosis: z.string().optional(),
|
||||
treatment: z.string().optional(),
|
||||
status: treatmentStatus.optional(),
|
||||
});
|
||||
export type UpdateTreatmentInput = z.infer<typeof updateTreatmentSchema>;
|
||||
@@ -0,0 +1,44 @@
|
||||
import { z } from 'zod';
|
||||
import { citizenId, plate } from './common';
|
||||
import { plateStatusSchema } from './person';
|
||||
|
||||
/** MDT-geführtes Fahrzeug (eigene Registry, unabhängig von QBox — inkl. gefälschter Kennzeichen). */
|
||||
export const vehicleSchema = z.object({
|
||||
id: z.number().int(),
|
||||
plate: z.string(),
|
||||
model: z.string().nullable(),
|
||||
color: z.string().nullable(),
|
||||
ownerCitizenid: citizenId.nullable(),
|
||||
ownerName: z.string().nullable(),
|
||||
plateStatus: plateStatusSchema.default('registered'),
|
||||
isStolen: z.boolean().default(false),
|
||||
hasBolo: z.boolean().default(false),
|
||||
notes: z.string().default(''),
|
||||
});
|
||||
export type Vehicle = z.infer<typeof vehicleSchema>;
|
||||
|
||||
export const vehicleLookupParamsSchema = z.object({
|
||||
plate,
|
||||
});
|
||||
|
||||
export const createVehicleSchema = z.object({
|
||||
plate: z.string().trim().min(1),
|
||||
model: z.string().trim().nullable().optional(),
|
||||
color: z.string().trim().nullable().optional(),
|
||||
ownerCitizenid: z.string().trim().nullable().optional(),
|
||||
plateStatus: plateStatusSchema.optional(),
|
||||
isStolen: z.boolean().optional(),
|
||||
hasBolo: z.boolean().optional(),
|
||||
notes: z.string().optional(),
|
||||
});
|
||||
export type CreateVehicleInput = z.infer<typeof createVehicleSchema>;
|
||||
|
||||
export const updateVehicleSchema = createVehicleSchema.partial();
|
||||
export type UpdateVehicleInput = z.infer<typeof updateVehicleSchema>;
|
||||
|
||||
/** Schneller „als gestohlen"-Umschalter (aus der Kennzeichen-Abfrage). */
|
||||
export const flagVehicleSchema = z.object({
|
||||
isStolen: z.boolean().optional(),
|
||||
reason: z.string().optional(),
|
||||
});
|
||||
export type FlagVehicleInput = z.infer<typeof flagVehicleSchema>;
|
||||
@@ -0,0 +1,34 @@
|
||||
import { z } from 'zod';
|
||||
import { citizenId, isoDate } from './common';
|
||||
|
||||
export const warrantStatus = z.enum(['active', 'served', 'revoked', 'expired']);
|
||||
export type WarrantStatus = z.infer<typeof warrantStatus>;
|
||||
|
||||
/** Haftbefehl (Tabelle mdt_warrants). */
|
||||
export const warrantSchema = z.object({
|
||||
id: z.number().int(),
|
||||
citizenid: citizenId,
|
||||
subjectName: z.string().nullable(),
|
||||
reason: z.string(),
|
||||
status: warrantStatus.default('active'),
|
||||
caseId: z.number().int().nullable(),
|
||||
issuedByCitizenid: citizenId.nullable(),
|
||||
issuedByName: z.string().nullable(),
|
||||
issuedAt: isoDate,
|
||||
expiresAt: isoDate.nullable(),
|
||||
});
|
||||
export type Warrant = z.infer<typeof warrantSchema>;
|
||||
|
||||
export const createWarrantSchema = z.object({
|
||||
citizenid: citizenId,
|
||||
reason: z.string().min(1),
|
||||
caseId: z.number().int().nullable().default(null),
|
||||
expiresAt: isoDate.nullable().default(null),
|
||||
});
|
||||
export type CreateWarrantInput = z.infer<typeof createWarrantSchema>;
|
||||
|
||||
export const updateWarrantSchema = z.object({
|
||||
status: warrantStatus.optional(),
|
||||
reason: z.string().min(1).optional(),
|
||||
});
|
||||
export type UpdateWarrantInput = z.infer<typeof updateWarrantSchema>;
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { DispatchCall } from '../dto/dispatch';
|
||||
import type { Officer, OfficerPositionUpdate, OfficerDutyUpdate } from '../dto/officer';
|
||||
import type { Warrant } from '../dto/warrant';
|
||||
import type { Coords } from '../dto/common';
|
||||
|
||||
/**
|
||||
* Socket.io Event-Namen — zentral, damit Server & Client nie divergieren.
|
||||
*/
|
||||
export const SOCKET_EVENTS = {
|
||||
// Server -> Client
|
||||
DISPATCH_CREATED: 'dispatch.created',
|
||||
DISPATCH_UPDATED: 'dispatch.updated',
|
||||
OFFICER_MOVED: 'officer.moved',
|
||||
OFFICER_DUTY: 'officer.duty',
|
||||
OFFICER_SNAPSHOT: 'officer.snapshot',
|
||||
BOLO_CREATED: 'bolo.created',
|
||||
WARRANT_UPDATED: 'warrant.updated',
|
||||
|
||||
// Client -> Server
|
||||
DISPATCH_ASSIGN: 'dispatch.assign',
|
||||
OFFICER_SET_WAYPOINT: 'officer.setWaypoint',
|
||||
} as const;
|
||||
|
||||
export type SocketEventName = (typeof SOCKET_EVENTS)[keyof typeof SOCKET_EVENTS];
|
||||
|
||||
/** Server -> Client Payloads. */
|
||||
export interface ServerToClientEvents {
|
||||
[SOCKET_EVENTS.DISPATCH_CREATED]: (call: DispatchCall) => void;
|
||||
[SOCKET_EVENTS.DISPATCH_UPDATED]: (call: DispatchCall) => void;
|
||||
[SOCKET_EVENTS.OFFICER_MOVED]: (update: OfficerPositionUpdate) => void;
|
||||
[SOCKET_EVENTS.OFFICER_DUTY]: (update: OfficerDutyUpdate) => void;
|
||||
[SOCKET_EVENTS.OFFICER_SNAPSHOT]: (officers: Officer[]) => void;
|
||||
[SOCKET_EVENTS.BOLO_CREATED]: (payload: { id: number; kind: 'person' | 'vehicle'; label: string }) => void;
|
||||
[SOCKET_EVENTS.WARRANT_UPDATED]: (warrant: Warrant) => void;
|
||||
}
|
||||
|
||||
/** Client -> Server Payloads. */
|
||||
export interface ClientToServerEvents {
|
||||
[SOCKET_EVENTS.DISPATCH_ASSIGN]: (payload: { callId: number; officers: string[] }) => void;
|
||||
[SOCKET_EVENTS.OFFICER_SET_WAYPOINT]: (payload: { targetCitizenid: string; coords: Coords }) => void;
|
||||
}
|
||||
|
||||
/** Socket-Rooms je Department (z. B. "dept:police"). */
|
||||
export function departmentRoom(dept: string): string {
|
||||
return `dept:${dept}`;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './rbac';
|
||||
export * from './dto';
|
||||
export * from './events';
|
||||
@@ -0,0 +1,234 @@
|
||||
/**
|
||||
* RBAC — granulare Permission-Nodes (Hybrid-Rang-System).
|
||||
* Nodes sind hierarchisch benannt: `<bereich>.<ressource>.<aktion>`.
|
||||
* Serverseitig erzwungen; die effektiven Rechte werden bei Login berechnet
|
||||
* (Rang-Matrix ∪ Override-Rollen) und ins JWT gebacken.
|
||||
*/
|
||||
|
||||
export const ROLES = ['police', 'ems', 'fire', 'dispatch', 'admin'] as const;
|
||||
export type Role = (typeof ROLES)[number];
|
||||
|
||||
export const DEPARTMENTS = ['police', 'ems', 'fire'] as const;
|
||||
export type Department = (typeof DEPARTMENTS)[number];
|
||||
|
||||
// ── Permission-Nodes (granular, an den echten Endpoints ausgerichtet) ──
|
||||
export const PERMISSIONS = [
|
||||
// MDT · Schwarzes Brett
|
||||
'mdt.board.view',
|
||||
'mdt.board.manage',
|
||||
// MDT · Personen (Bürgerakten)
|
||||
'mdt.persons.view',
|
||||
'mdt.persons.create',
|
||||
'mdt.persons.edit',
|
||||
'mdt.persons.delete',
|
||||
// MDT · Fahrzeuge
|
||||
'mdt.vehicles.view',
|
||||
'mdt.vehicles.create',
|
||||
'mdt.vehicles.flag',
|
||||
'mdt.vehicles.delete',
|
||||
// MDT · Strafakten
|
||||
'mdt.cases.view',
|
||||
'mdt.cases.create',
|
||||
'mdt.cases.edit',
|
||||
'mdt.charges.manage',
|
||||
// MDT · Gesetze
|
||||
'mdt.laws.view',
|
||||
'mdt.laws.manage',
|
||||
// MDT · Kalender
|
||||
'mdt.calendar.view',
|
||||
'mdt.calendar.manage',
|
||||
// MDT · Behandlungsakten (EMS)
|
||||
'mdt.treatment.view',
|
||||
'mdt.treatment.create',
|
||||
'mdt.treatment.edit',
|
||||
// MDT · Haftbefehle
|
||||
'mdt.warrants.view',
|
||||
'mdt.warrants.create',
|
||||
'mdt.warrants.revoke',
|
||||
// MDT · Dokumente
|
||||
'mdt.documents.view',
|
||||
'mdt.documents.manage',
|
||||
// CAD · Dispatch
|
||||
'cad.dispatch.view',
|
||||
'cad.dispatch.manage',
|
||||
// Administration
|
||||
'admin.ranks.manage',
|
||||
'admin.users.manage',
|
||||
'admin.audit.view',
|
||||
] as const;
|
||||
export type Permission = (typeof PERMISSIONS)[number];
|
||||
|
||||
// ── Baum-Struktur für den visuellen Rechte-Explorer ──
|
||||
export interface PermissionLeaf {
|
||||
id: Permission;
|
||||
label: string;
|
||||
}
|
||||
export interface PermissionGroup {
|
||||
id: string;
|
||||
label: string;
|
||||
children: PermissionLeaf[];
|
||||
}
|
||||
export interface PermissionArea {
|
||||
id: string;
|
||||
label: string;
|
||||
groups: PermissionGroup[];
|
||||
}
|
||||
|
||||
export const PERMISSION_TREE: PermissionArea[] = [
|
||||
{
|
||||
id: 'mdt',
|
||||
label: 'MDT',
|
||||
groups: [
|
||||
{
|
||||
id: 'board',
|
||||
label: 'Schwarzes Brett',
|
||||
children: [
|
||||
{ id: 'mdt.board.view', label: 'Ansehen' },
|
||||
{ id: 'mdt.board.manage', label: 'Beiträge verwalten' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'persons',
|
||||
label: 'Bürgerakten',
|
||||
children: [
|
||||
{ id: 'mdt.persons.view', label: 'Anzeigen & Suchen' },
|
||||
{ id: 'mdt.persons.create', label: 'Bürger anlegen' },
|
||||
{ id: 'mdt.persons.edit', label: 'Akte bearbeiten' },
|
||||
{ id: 'mdt.persons.delete', label: 'Bürger löschen' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'vehicles',
|
||||
label: 'Fahrzeuge',
|
||||
children: [
|
||||
{ id: 'mdt.vehicles.view', label: 'Anzeigen' },
|
||||
{ id: 'mdt.vehicles.create', label: 'Fahrzeug anlegen' },
|
||||
{ id: 'mdt.vehicles.flag', label: 'Als gestohlen markieren' },
|
||||
{ id: 'mdt.vehicles.delete', label: 'Fahrzeug löschen' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'cases',
|
||||
label: 'Strafakten',
|
||||
children: [
|
||||
{ id: 'mdt.cases.view', label: 'Anzeigen' },
|
||||
{ id: 'mdt.cases.create', label: 'Anlegen' },
|
||||
{ id: 'mdt.cases.edit', label: 'Bearbeiten' },
|
||||
{ id: 'mdt.charges.manage', label: 'Strafenkatalog pflegen' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'laws',
|
||||
label: 'Gesetze',
|
||||
children: [
|
||||
{ id: 'mdt.laws.view', label: 'Anzeigen' },
|
||||
{ id: 'mdt.laws.manage', label: 'Gesetze pflegen' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'calendar',
|
||||
label: 'Kalender',
|
||||
children: [
|
||||
{ id: 'mdt.calendar.view', label: 'Anzeigen & selbst eintragen' },
|
||||
{ id: 'mdt.calendar.manage', label: 'Termine verwalten' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'treatment',
|
||||
label: 'Behandlungsakten (EMS)',
|
||||
children: [
|
||||
{ id: 'mdt.treatment.view', label: 'Anzeigen' },
|
||||
{ id: 'mdt.treatment.create', label: 'Anlegen' },
|
||||
{ id: 'mdt.treatment.edit', label: 'Bearbeiten' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'warrants',
|
||||
label: 'Haftbefehle',
|
||||
children: [
|
||||
{ id: 'mdt.warrants.view', label: 'Anzeigen' },
|
||||
{ id: 'mdt.warrants.create', label: 'Ausstellen' },
|
||||
{ id: 'mdt.warrants.revoke', label: 'Aufheben' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'documents',
|
||||
label: 'Dokumente',
|
||||
children: [
|
||||
{ id: 'mdt.documents.view', label: 'Anzeigen' },
|
||||
{ id: 'mdt.documents.manage', label: 'Erstellen/Bearbeiten' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'cad',
|
||||
label: 'CAD / Dispatch',
|
||||
groups: [
|
||||
{
|
||||
id: 'dispatch',
|
||||
label: 'Dispatch',
|
||||
children: [
|
||||
{ id: 'cad.dispatch.view', label: 'Einsätze & Karte ansehen' },
|
||||
{ id: 'cad.dispatch.manage', label: 'Einsätze verwalten/zuweisen' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'admin',
|
||||
label: 'Administration',
|
||||
groups: [
|
||||
{
|
||||
id: 'admin',
|
||||
label: 'Verwaltung',
|
||||
children: [
|
||||
{ id: 'admin.ranks.manage', label: 'Rechte-Matrix / Ränge' },
|
||||
{ id: 'admin.users.manage', label: 'Mitarbeiter verwalten' },
|
||||
{ id: 'admin.audit.view', label: 'Audit-Log einsehen' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// ── Rollen-Templates (im Explorer anwendbar; auch Basis der Rang-Defaults) ──
|
||||
export const ROLE_TEMPLATES: Record<string, Permission[]> = {
|
||||
Officer: [
|
||||
'mdt.board.view',
|
||||
'mdt.persons.view', 'mdt.persons.create', 'mdt.persons.edit',
|
||||
'mdt.vehicles.view', 'mdt.vehicles.create', 'mdt.vehicles.flag',
|
||||
'mdt.cases.view', 'mdt.cases.create', 'mdt.cases.edit',
|
||||
'mdt.laws.view',
|
||||
'mdt.calendar.view',
|
||||
'mdt.warrants.view',
|
||||
'mdt.documents.view',
|
||||
'cad.dispatch.view',
|
||||
],
|
||||
Supervisor: [
|
||||
'mdt.board.view', 'mdt.board.manage',
|
||||
'mdt.persons.view', 'mdt.persons.create', 'mdt.persons.edit', 'mdt.persons.delete',
|
||||
'mdt.vehicles.view', 'mdt.vehicles.create', 'mdt.vehicles.flag', 'mdt.vehicles.delete',
|
||||
'mdt.cases.view', 'mdt.cases.create', 'mdt.cases.edit',
|
||||
'mdt.laws.view', 'mdt.laws.manage',
|
||||
'mdt.calendar.view', 'mdt.calendar.manage',
|
||||
'mdt.warrants.view', 'mdt.warrants.create', 'mdt.warrants.revoke',
|
||||
'mdt.documents.view', 'mdt.documents.manage',
|
||||
'cad.dispatch.view', 'cad.dispatch.manage',
|
||||
],
|
||||
Chief: [...([] as Permission[])], // unten gefüllt
|
||||
Dispatcher: ['mdt.board.view', 'mdt.persons.view', 'mdt.vehicles.view', 'mdt.calendar.view', 'mdt.documents.view', 'cad.dispatch.view', 'cad.dispatch.manage'],
|
||||
'EMS/Fire': ['mdt.board.view', 'mdt.persons.view', 'mdt.vehicles.view', 'mdt.laws.view', 'mdt.calendar.view', 'mdt.treatment.view', 'mdt.treatment.create', 'mdt.treatment.edit', 'mdt.documents.view', 'cad.dispatch.view', 'cad.dispatch.manage'],
|
||||
};
|
||||
ROLE_TEMPLATES.Chief = [...PERMISSIONS]; // Chief = alles
|
||||
|
||||
// ── Override-Rollen (nicht aus dem Job): admin = alles, dispatch = CAD ──
|
||||
export const ROLE_PERMISSIONS: Record<'admin' | 'dispatch', Permission[]> = {
|
||||
admin: [...PERMISSIONS],
|
||||
dispatch: ['mdt.persons.view', 'mdt.vehicles.view', 'cad.dispatch.view', 'cad.dispatch.manage'],
|
||||
};
|
||||
|
||||
/** Prüft eine berechnete Permission-Liste. */
|
||||
export function can(permissions: readonly Permission[], perm: Permission): boolean {
|
||||
return permissions.includes(perm);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user