Initial commit: Lageplan v1.0 - Next.js 15.5, React 19
This commit is contained in:
226
prisma/migrate.js
Normal file
226
prisma/migrate.js
Normal file
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* Database migration script using PrismaClient raw SQL.
|
||||
* Does NOT require the Prisma CLI (npx prisma) — only the runtime client.
|
||||
* Safe to run multiple times (all statements are idempotent).
|
||||
*/
|
||||
const { PrismaClient } = require('@prisma/client')
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
async function migrate() {
|
||||
console.log('🔧 Running database migrations...')
|
||||
|
||||
// ─── Step 1: Ensure enum values exist ───
|
||||
console.log(' [1/7] Ensuring enum values...')
|
||||
const enumMigrations = [
|
||||
// Role enum
|
||||
`DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_enum WHERE enumlabel = 'SERVER_ADMIN' AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'Role')) THEN ALTER TYPE "Role" ADD VALUE 'SERVER_ADMIN'; END IF; END$$;`,
|
||||
`DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_enum WHERE enumlabel = 'TENANT_ADMIN' AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'Role')) THEN ALTER TYPE "Role" ADD VALUE 'TENANT_ADMIN'; END IF; END$$;`,
|
||||
`DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_enum WHERE enumlabel = 'OPERATOR' AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'Role')) THEN ALTER TYPE "Role" ADD VALUE 'OPERATOR'; END IF; END$$;`,
|
||||
`DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_enum WHERE enumlabel = 'VIEWER' AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'Role')) THEN ALTER TYPE "Role" ADD VALUE 'VIEWER'; END IF; END$$;`,
|
||||
// DictionaryScope enum
|
||||
`DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'DictionaryScope') THEN CREATE TYPE "DictionaryScope" AS ENUM ('GLOBAL', 'TENANT'); END IF; END$$;`,
|
||||
]
|
||||
for (const sql of enumMigrations) {
|
||||
try { await prisma.$executeRawUnsafe(sql) } catch (e) { /* enum might already exist */ }
|
||||
}
|
||||
|
||||
// ─── Step 2: Migrate old enum data ───
|
||||
console.log(' [2/7] Migrating old data...')
|
||||
const dataMigrations = [
|
||||
`UPDATE users SET role = 'SERVER_ADMIN' WHERE role = 'ADMIN'`,
|
||||
`UPDATE users SET role = 'OPERATOR' WHERE role = 'EDITOR'`,
|
||||
`UPDATE tenants SET "subscriptionStatus" = 'ACTIVE' WHERE "subscriptionStatus" = 'TRIAL'`,
|
||||
]
|
||||
for (const sql of dataMigrations) {
|
||||
try { await prisma.$executeRawUnsafe(sql) } catch (e) { /* table might not exist yet */ }
|
||||
}
|
||||
|
||||
// ─── Step 3: Add missing columns (idempotent) ───
|
||||
console.log(' [3/7] Adding missing columns...')
|
||||
const columnMigrations = [
|
||||
// Tenants
|
||||
`ALTER TABLE tenants ADD COLUMN IF NOT EXISTS "logoUrl" TEXT`,
|
||||
`ALTER TABLE tenants ADD COLUMN IF NOT EXISTS "logoFileKey" TEXT`,
|
||||
`ALTER TABLE tenants ADD COLUMN IF NOT EXISTS "hiddenIconIds" TEXT[] DEFAULT '{}'`,
|
||||
`ALTER TABLE tenants ADD COLUMN IF NOT EXISTS "journalSuggestions" TEXT[] DEFAULT '{}'`,
|
||||
`ALTER TABLE tenants ADD COLUMN IF NOT EXISTS "contactEmail" TEXT`,
|
||||
`ALTER TABLE tenants ADD COLUMN IF NOT EXISTS "contactPhone" TEXT`,
|
||||
`ALTER TABLE tenants ADD COLUMN IF NOT EXISTS "address" TEXT`,
|
||||
`ALTER TABLE tenants ADD COLUMN IF NOT EXISTS "notes" TEXT`,
|
||||
`ALTER TABLE tenants ADD COLUMN IF NOT EXISTS "maxUsers" INTEGER DEFAULT 5`,
|
||||
`ALTER TABLE tenants ADD COLUMN IF NOT EXISTS "maxProjects" INTEGER DEFAULT 10`,
|
||||
// Users
|
||||
`ALTER TABLE users ADD COLUMN IF NOT EXISTS "lastLoginAt" TIMESTAMP`,
|
||||
`ALTER TABLE users ADD COLUMN IF NOT EXISTS "emailVerified" BOOLEAN DEFAULT true`,
|
||||
`ALTER TABLE users ADD COLUMN IF NOT EXISTS "emailVerificationToken" TEXT`,
|
||||
`ALTER TABLE users ADD COLUMN IF NOT EXISTS "resetToken" TEXT`,
|
||||
`ALTER TABLE users ADD COLUMN IF NOT EXISTS "resetTokenExpiry" TIMESTAMP`,
|
||||
// Icon categories
|
||||
`ALTER TABLE icon_categories ADD COLUMN IF NOT EXISTS "isGlobal" BOOLEAN DEFAULT false`,
|
||||
`ALTER TABLE icon_categories ADD COLUMN IF NOT EXISTS "tenantId" TEXT`,
|
||||
// Projects
|
||||
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS "planImageKey" TEXT`,
|
||||
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS "planBounds" JSONB`,
|
||||
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS "einsatzleiter" TEXT`,
|
||||
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS "journalfuehrer" TEXT`,
|
||||
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS "editingById" TEXT`,
|
||||
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS "editingUserName" TEXT`,
|
||||
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS "editingSessionId" TEXT`,
|
||||
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS "editingStartedAt" TIMESTAMP`,
|
||||
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS "editingHeartbeat" TIMESTAMP`,
|
||||
// Journal
|
||||
`ALTER TABLE journal_entries ADD COLUMN IF NOT EXISTS "isCorrected" BOOLEAN DEFAULT false`,
|
||||
`ALTER TABLE journal_entries ADD COLUMN IF NOT EXISTS "correctionOfId" TEXT`,
|
||||
]
|
||||
let added = 0
|
||||
for (const sql of columnMigrations) {
|
||||
try {
|
||||
await prisma.$executeRawUnsafe(sql)
|
||||
added++
|
||||
} catch (e) {
|
||||
// Table might not exist yet — that's OK, prisma db push or first seed will create it
|
||||
}
|
||||
}
|
||||
console.log(` ${added}/${columnMigrations.length} column migrations executed`)
|
||||
|
||||
// ─── Step 4: Create new tables ───
|
||||
console.log(' [4/7] Creating new tables...')
|
||||
const tableMigrations = [
|
||||
// Dictionary entries table
|
||||
`CREATE TABLE IF NOT EXISTS dictionary_entries (
|
||||
id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
word TEXT NOT NULL,
|
||||
scope "DictionaryScope" NOT NULL DEFAULT 'GLOBAL',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"tenantId" TEXT REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
UNIQUE(word, "tenantId")
|
||||
)`,
|
||||
// Rapports table
|
||||
`CREATE TABLE IF NOT EXISTS rapports (
|
||||
id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"reportNumber" TEXT NOT NULL,
|
||||
token TEXT NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
data JSONB NOT NULL DEFAULT '{}',
|
||||
"generatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"projectId" TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
"tenantId" TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
"createdById" TEXT REFERENCES users(id) ON DELETE SET NULL,
|
||||
UNIQUE("tenantId", "reportNumber")
|
||||
)`,
|
||||
]
|
||||
for (const sql of tableMigrations) {
|
||||
try { await prisma.$executeRawUnsafe(sql) } catch (e) { /* table might already exist */ }
|
||||
}
|
||||
|
||||
// ─── Step 5: Set safe defaults ───
|
||||
console.log(' [5/7] Setting defaults...')
|
||||
try {
|
||||
await prisma.$executeRawUnsafe(`UPDATE tenants SET "subscriptionStatus" = 'ACTIVE' WHERE "subscriptionStatus" = 'TRIAL'`)
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
// ─── Step 6: Clean up orphan users ───
|
||||
console.log(' [6/7] Cleaning up orphan users...')
|
||||
try {
|
||||
// Find users who are NOT SERVER_ADMIN and have NO tenant membership
|
||||
const orphans = await prisma.user.findMany({
|
||||
where: {
|
||||
role: { not: 'SERVER_ADMIN' },
|
||||
memberships: { none: {} },
|
||||
},
|
||||
select: { id: true, email: true, name: true },
|
||||
})
|
||||
if (orphans.length > 0) {
|
||||
console.log(` Found ${orphans.length} orphan user(s):`)
|
||||
for (const o of orphans) {
|
||||
console.log(` - ${o.email} (${o.name})`)
|
||||
}
|
||||
// Delete orphan users and their related data
|
||||
await prisma.user.deleteMany({
|
||||
where: {
|
||||
id: { in: orphans.map(o => o.id) },
|
||||
},
|
||||
})
|
||||
console.log(` 🗑️ ${orphans.length} orphan user(s) removed`)
|
||||
} else {
|
||||
console.log(' No orphan users found')
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(' Orphan cleanup skipped:', e.message)
|
||||
}
|
||||
|
||||
// ─── Step 7: Backfill logoFileKey from logoUrl ───
|
||||
console.log(' [7/7] Backfilling logoFileKey...')
|
||||
try {
|
||||
// Extract fileKey from MinIO URLs: the path after the bucket name
|
||||
// e.g. http://localhost:9002/lageplan-icons/logos/tenant-xxx.png → logos/tenant-xxx.png
|
||||
const tenants = await prisma.tenant.findMany({
|
||||
where: { logoUrl: { not: null }, logoFileKey: null },
|
||||
select: { id: true, logoUrl: true },
|
||||
})
|
||||
for (const t of tenants) {
|
||||
if (!t.logoUrl) continue
|
||||
// Try to extract the fileKey from the URL
|
||||
const match = t.logoUrl.match(/logos\/[^?]+/)
|
||||
if (match) {
|
||||
await prisma.tenant.update({
|
||||
where: { id: t.id },
|
||||
data: { logoFileKey: match[0] },
|
||||
})
|
||||
}
|
||||
}
|
||||
if (tenants.length > 0) console.log(` Backfilled ${tenants.length} logo fileKey(s)`)
|
||||
} catch (e) {
|
||||
console.log(' Logo backfill skipped:', e.message)
|
||||
}
|
||||
|
||||
// ─── Step 8: Drop unique constraint on rapports(tenantId, reportNumber) ───
|
||||
console.log(' [8] Dropping rapports unique constraint on (tenantId, reportNumber)...')
|
||||
try {
|
||||
await prisma.$executeRawUnsafe(`ALTER TABLE "rapports" DROP CONSTRAINT IF EXISTS "rapports_tenantId_reportNumber_key"`)
|
||||
console.log(' Constraint dropped (or did not exist)')
|
||||
} catch (e) {
|
||||
console.log(' Constraint drop skipped:', e.message)
|
||||
}
|
||||
|
||||
// ─── Step 9: Add einsatzNr column to projects ───
|
||||
console.log(' [9] Adding einsatzNr column to projects...')
|
||||
try {
|
||||
await prisma.$executeRawUnsafe(`ALTER TABLE "projects" ADD COLUMN IF NOT EXISTS "einsatzNr" TEXT`)
|
||||
console.log(' einsatzNr column added (or already exists)')
|
||||
} catch (e) {
|
||||
console.log(' einsatzNr column skipped:', e.message)
|
||||
}
|
||||
|
||||
// ─── Step 10: Make rapports.tenantId nullable ───
|
||||
console.log(' [10] Making rapports.tenantId nullable...')
|
||||
try {
|
||||
await prisma.$executeRawUnsafe(`ALTER TABLE "rapports" ALTER COLUMN "tenantId" DROP NOT NULL`)
|
||||
console.log(' rapports.tenantId is now nullable')
|
||||
} catch (e) {
|
||||
console.log(' rapports.tenantId nullable skipped:', e.message)
|
||||
}
|
||||
|
||||
// ─── Step 11: Add privacy consent columns to tenants ───
|
||||
console.log(' [11] Adding privacy consent columns to tenants...')
|
||||
try {
|
||||
await prisma.$executeRawUnsafe(`ALTER TABLE "tenants" ADD COLUMN IF NOT EXISTS "privacyAccepted" BOOLEAN DEFAULT false`)
|
||||
await prisma.$executeRawUnsafe(`ALTER TABLE "tenants" ADD COLUMN IF NOT EXISTS "privacyAcceptedAt" TIMESTAMP`)
|
||||
await prisma.$executeRawUnsafe(`ALTER TABLE "tenants" ADD COLUMN IF NOT EXISTS "adminAccessAccepted" BOOLEAN DEFAULT false`)
|
||||
console.log(' Privacy consent columns added')
|
||||
} catch (e) {
|
||||
console.log(' Privacy consent columns skipped:', e.message)
|
||||
}
|
||||
|
||||
console.log('✅ Database migrations complete')
|
||||
}
|
||||
|
||||
migrate()
|
||||
.then(async () => { await prisma.$disconnect() })
|
||||
.catch(async (e) => {
|
||||
console.error('Migration error:', e.message)
|
||||
await prisma.$disconnect()
|
||||
process.exit(1)
|
||||
})
|
||||
414
prisma/schema.prisma
Normal file
414
prisma/schema.prisma
Normal file
@@ -0,0 +1,414 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
enum Role {
|
||||
SERVER_ADMIN
|
||||
TENANT_ADMIN
|
||||
OPERATOR
|
||||
VIEWER
|
||||
}
|
||||
|
||||
enum IconType {
|
||||
STANDARD
|
||||
RETTUNG
|
||||
GEFAHRSTOFF
|
||||
FEUER
|
||||
WASSER
|
||||
FAHRZEUG
|
||||
}
|
||||
|
||||
enum ItemKind {
|
||||
SYMBOL
|
||||
LINE
|
||||
POLYGON
|
||||
RECTANGLE
|
||||
CIRCLE
|
||||
ARROW
|
||||
TEXT
|
||||
}
|
||||
|
||||
enum SubscriptionPlan {
|
||||
FREE
|
||||
PRO
|
||||
}
|
||||
|
||||
enum DictionaryScope {
|
||||
GLOBAL
|
||||
TENANT
|
||||
}
|
||||
|
||||
enum SubscriptionStatus {
|
||||
ACTIVE
|
||||
TRIAL
|
||||
SUSPENDED
|
||||
EXPIRED
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
model Tenant {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
slug String @unique
|
||||
description String?
|
||||
isActive Boolean @default(true)
|
||||
contactEmail String?
|
||||
contactPhone String?
|
||||
address String?
|
||||
|
||||
logoUrl String?
|
||||
logoFileKey String?
|
||||
|
||||
// Subscription
|
||||
plan SubscriptionPlan @default(FREE)
|
||||
subscriptionStatus SubscriptionStatus @default(ACTIVE)
|
||||
trialEndsAt DateTime?
|
||||
subscriptionEndsAt DateTime?
|
||||
maxUsers Int @default(5)
|
||||
maxProjects Int @default(10)
|
||||
notes String?
|
||||
hiddenIconIds String[] @default([])
|
||||
journalSuggestions String[] @default([])
|
||||
|
||||
// Privacy consent
|
||||
privacyAccepted Boolean @default(false)
|
||||
privacyAcceptedAt DateTime?
|
||||
adminAccessAccepted Boolean @default(false)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
memberships TenantMembership[]
|
||||
projects Project[]
|
||||
hoseTypes HoseType[]
|
||||
checkTemplates JournalCheckTemplate[]
|
||||
iconCategories IconCategory[]
|
||||
iconAssets IconAsset[]
|
||||
upgradeRequests UpgradeRequest[]
|
||||
dictionaryEntries DictionaryEntry[]
|
||||
rapports Rapport[]
|
||||
|
||||
@@map("tenants")
|
||||
}
|
||||
|
||||
// ─── System Settings (Key-Value, encrypted for secrets) ──────
|
||||
|
||||
model SystemSetting {
|
||||
id String @id @default(uuid())
|
||||
key String @unique
|
||||
value String
|
||||
isSecret Boolean @default(false)
|
||||
category String @default("general")
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("system_settings")
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
email String @unique
|
||||
password String
|
||||
name String
|
||||
role Role @default(OPERATOR)
|
||||
emailVerified Boolean @default(true)
|
||||
emailVerificationToken String? @unique
|
||||
resetToken String? @unique
|
||||
resetTokenExpiry DateTime?
|
||||
lastLoginAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
memberships TenantMembership[]
|
||||
projects Project[]
|
||||
iconAssets IconAsset[]
|
||||
upgradeRequests UpgradeRequest[]
|
||||
rapports Rapport[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model TenantMembership {
|
||||
id String @id @default(uuid())
|
||||
role Role @default(OPERATOR)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
tenantId String
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([userId, tenantId])
|
||||
@@map("tenant_memberships")
|
||||
}
|
||||
|
||||
model Project {
|
||||
id String @id @default(uuid())
|
||||
einsatzNr String?
|
||||
title String
|
||||
location String?
|
||||
description String?
|
||||
einsatzleiter String?
|
||||
journalfuehrer String?
|
||||
mapCenter Json @default("{\"lng\": 8.5417, \"lat\": 47.3769}")
|
||||
mapZoom Float @default(15)
|
||||
isLocked Boolean @default(false)
|
||||
|
||||
// Live editing lock (session-based for same-account multi-device)
|
||||
editingById String?
|
||||
editingUserName String?
|
||||
editingSessionId String?
|
||||
editingStartedAt DateTime?
|
||||
editingHeartbeat DateTime?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
ownerId String?
|
||||
owner User? @relation(fields: [ownerId], references: [id], onDelete: SetNull)
|
||||
tenantId String?
|
||||
tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: SetNull)
|
||||
|
||||
features Feature[]
|
||||
items Item[]
|
||||
journalEntries JournalEntry[]
|
||||
journalCheckItems JournalCheckItem[]
|
||||
journalPendenzen JournalPendenz[]
|
||||
rapports Rapport[]
|
||||
|
||||
@@map("projects")
|
||||
}
|
||||
|
||||
model Feature {
|
||||
id String @id @default(uuid())
|
||||
type String
|
||||
geometry Json
|
||||
properties Json @default("{}")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
projectId String
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("features")
|
||||
}
|
||||
|
||||
model IconCategory {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
description String?
|
||||
sortOrder Int @default(0)
|
||||
isGlobal Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
tenantId String?
|
||||
tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: SetNull)
|
||||
|
||||
icons IconAsset[]
|
||||
|
||||
@@map("icon_categories")
|
||||
}
|
||||
|
||||
model IconAsset {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
fileKey String
|
||||
mimeType String
|
||||
width Int?
|
||||
height Int?
|
||||
isSystem Boolean @default(false)
|
||||
isActive Boolean @default(true)
|
||||
iconType IconType @default(STANDARD)
|
||||
tags String[] @default([])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
categoryId String?
|
||||
category IconCategory? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
|
||||
|
||||
ownerId String?
|
||||
owner User? @relation(fields: [ownerId], references: [id], onDelete: SetNull)
|
||||
|
||||
tenantId String?
|
||||
tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@map("icon_assets")
|
||||
}
|
||||
|
||||
model HoseType {
|
||||
id String @id @default(uuid())
|
||||
name String @unique
|
||||
diameterMm Int
|
||||
lengthPerPieceM Int @default(10)
|
||||
flowRateLpm Float
|
||||
frictionCoeff Float
|
||||
description String?
|
||||
isDefault Boolean @default(false)
|
||||
isActive Boolean @default(true)
|
||||
sortOrder Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
tenantId String?
|
||||
tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@map("hose_types")
|
||||
}
|
||||
|
||||
// ─── Journal ───────────────────────────────────────────────
|
||||
|
||||
model JournalEntry {
|
||||
id String @id @default(uuid())
|
||||
time DateTime @default(now())
|
||||
what String
|
||||
who String?
|
||||
done Boolean @default(false)
|
||||
doneAt DateTime?
|
||||
sortOrder Int @default(0)
|
||||
isCorrected Boolean @default(false)
|
||||
correctionOfId String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
projectId String
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("journal_entries")
|
||||
}
|
||||
|
||||
model JournalCheckItem {
|
||||
id String @id @default(uuid())
|
||||
label String
|
||||
confirmed Boolean @default(false)
|
||||
confirmedAt DateTime?
|
||||
ok Boolean @default(false)
|
||||
okAt DateTime?
|
||||
sortOrder Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
projectId String
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("journal_check_items")
|
||||
}
|
||||
|
||||
model JournalPendenz {
|
||||
id String @id @default(uuid())
|
||||
what String
|
||||
who String?
|
||||
whenHow String?
|
||||
done Boolean @default(false)
|
||||
doneAt DateTime?
|
||||
sortOrder Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
projectId String
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("journal_pendenzen")
|
||||
}
|
||||
|
||||
model JournalCheckTemplate {
|
||||
id String @id @default(uuid())
|
||||
label String
|
||||
sortOrder Int @default(0)
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
tenantId String?
|
||||
tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@map("journal_check_templates")
|
||||
}
|
||||
|
||||
model Item {
|
||||
id String @id @default(uuid())
|
||||
kind ItemKind
|
||||
geometry Json
|
||||
style Json @default("{}")
|
||||
properties Json @default("{}")
|
||||
isVisible Boolean @default(true)
|
||||
sortOrder Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
projectId String
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
|
||||
iconId String?
|
||||
|
||||
@@map("items")
|
||||
}
|
||||
|
||||
// ─── Upgrade Requests ─────────────────────────────────────
|
||||
|
||||
enum UpgradeRequestStatus {
|
||||
PENDING
|
||||
APPROVED
|
||||
REJECTED
|
||||
}
|
||||
|
||||
model UpgradeRequest {
|
||||
id String @id @default(uuid())
|
||||
requestedPlan SubscriptionPlan
|
||||
currentPlan SubscriptionPlan
|
||||
message String?
|
||||
status UpgradeRequestStatus @default(PENDING)
|
||||
adminNote String?
|
||||
processedAt DateTime?
|
||||
processedById String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
tenantId String
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
|
||||
requestedById String
|
||||
requestedBy User @relation(fields: [requestedById], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("upgrade_requests")
|
||||
}
|
||||
|
||||
// ─── Dictionary (Global + Tenant word library) ────────────
|
||||
|
||||
model DictionaryEntry {
|
||||
id String @id @default(uuid())
|
||||
word String
|
||||
scope DictionaryScope @default(GLOBAL)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
tenantId String?
|
||||
tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([word, tenantId])
|
||||
@@map("dictionary_entries")
|
||||
}
|
||||
|
||||
// ─── Rapport (PDF reports with public token access) ───────
|
||||
|
||||
model Rapport {
|
||||
id String @id @default(uuid())
|
||||
reportNumber String
|
||||
token String @unique @default(uuid())
|
||||
data Json
|
||||
generatedAt DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
projectId String
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
|
||||
tenantId String?
|
||||
tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
|
||||
createdById String?
|
||||
createdBy User? @relation(fields: [createdById], references: [id], onDelete: SetNull)
|
||||
|
||||
@@map("rapports")
|
||||
}
|
||||
127
prisma/seed-icons-only.js
Normal file
127
prisma/seed-icons-only.js
Normal file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Seed SVG icons and categories ONLY — does NOT touch tenants, users, or projects.
|
||||
* Used when upgrading an existing database to new SVG icons.
|
||||
*/
|
||||
const { PrismaClient } = require('@prisma/client')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
async function main() {
|
||||
console.log('🎨 Seeding SVG icons only...')
|
||||
|
||||
const catDefs = [
|
||||
{ name: 'Feuer / Brand', description: 'Feuer, Brand, Brandschutz', sortOrder: 1,
|
||||
patterns: ['Feuer', 'Brand', 'Explosion', 'Entrauchung', 'Rauch', 'Kamin', 'Luefter', 'Sprinkler'] },
|
||||
{ name: 'Wasser', description: 'Wasser, Hydranten, Leitungen', sortOrder: 2,
|
||||
patterns: ['Wasser', 'Hydrant', 'Reservoir', 'Druckleitung', 'Druckleistung', 'Transportleitung',
|
||||
'Schieber', 'Wasserloeschposten', 'Wasserversorgung', 'Wasserleitung', 'Ueberschwemmung',
|
||||
'Offener_Wasserverlauf', 'Moeglicher_Wasserbezugsort', 'Stehendes_Gewaesser',
|
||||
'Kleinloeschgeraet', 'Gefahr_durch_Loeschen_mit_Wasser'] },
|
||||
{ name: 'Gefahren / Stoffe', description: 'Gefahrstoffe, Chemie, ABC', sortOrder: 3,
|
||||
patterns: ['Gefaehrlich', 'Chemie', 'Chemikalien', 'Biologisch', 'Radioaktiv', 'Gas', 'Elektri',
|
||||
'Elektrotableau', 'Achtung', 'Leitungsdaehte'] },
|
||||
{ name: 'Rettung / Personen', description: 'Rettung, Sanität, Personen', sortOrder: 4,
|
||||
patterns: ['Rettung', 'Retten', 'Sanitaet', 'Patienten', 'Sammelplatz', 'Sammelstelle',
|
||||
'Totensammelstelle', 'Sprungretter', 'Helikopterlandeplatz'] },
|
||||
{ name: 'Leitern / Geräte', description: 'Leitern, Fahrzeuge, Geräte', sortOrder: 5,
|
||||
patterns: ['Leiter', 'Anhaengeleiter', 'Strebenleiter', 'ADL', 'TLF', 'HRF', 'SWHP'] },
|
||||
{ name: 'Gebäude / Schäden', description: 'Gebäude, Zerstörung, Schäden', sortOrder: 6,
|
||||
patterns: ['Beschaedigung', 'Teilzerstoerung', 'Totalzerstoerung', 'Decke_eingestuerzt',
|
||||
'Umfassungswaende', 'AnzahlGeschosse', 'Eingang', 'Treppen', 'Lift', 'Bruecke',
|
||||
'Rutschgebiet', 'Strasse', 'Bahnlin'] },
|
||||
{ name: 'Einsatzführung', description: 'Führung, Organisation, Kommunikation', sortOrder: 7,
|
||||
patterns: ['Einsatzleiter', 'KP_Font', 'Offizier', 'Beobachtungsposten', 'Kontrollstelle',
|
||||
'Funk', 'Informationszentrum', 'Medienkontaktstelle', 'Fernsignaltableau',
|
||||
'Materialdepot', 'Schluesseldepot', 'Abschnitt', 'Absperrung'] },
|
||||
{ name: 'Organisationen', description: 'Feuerwehr, Polizei, Armee, Zivilschutz', sortOrder: 8,
|
||||
patterns: ['Feuerwehr', 'Polizei', 'Armee', 'Zivilschutz', 'Chemiewehr', 'MS_de', 'Anmarsch'] },
|
||||
{ name: 'Entwicklung / Taktik', description: 'Entwicklungsgrenzen, Ausbreitung', sortOrder: 9,
|
||||
patterns: ['Entwicklungsgrenze', 'Horizontale_Entwicklung', 'Vertikale_Entwicklung',
|
||||
'VertikaleEntwicklung', 'Unfall'] },
|
||||
{ name: 'Verschiedenes', description: 'Weitere Signaturen', sortOrder: 10,
|
||||
patterns: ['Massstab', 'Nordrichtung', 'Windrichtung'] },
|
||||
]
|
||||
|
||||
// Delete ALL old system icons (regardless of fileKey pattern)
|
||||
const deleted = await prisma.iconAsset.deleteMany({
|
||||
where: { isSystem: true },
|
||||
})
|
||||
console.log(`🗑️ ${deleted.count} old system icons removed`)
|
||||
|
||||
// Upsert global categories (preserves tenant categories)
|
||||
const catMap = {}
|
||||
for (const def of catDefs) {
|
||||
const catId = def.name.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase()
|
||||
const cat = await prisma.iconCategory.upsert({
|
||||
where: { id: catId },
|
||||
update: { name: def.name, description: def.description, sortOrder: def.sortOrder, isGlobal: true },
|
||||
create: {
|
||||
id: catId,
|
||||
name: def.name,
|
||||
description: def.description,
|
||||
sortOrder: def.sortOrder,
|
||||
isGlobal: true,
|
||||
tenantId: null,
|
||||
},
|
||||
})
|
||||
catMap[def.name] = cat
|
||||
}
|
||||
console.log(`✅ ${Object.keys(catMap).length} icon categories upserted`)
|
||||
|
||||
function findCategory(filename) {
|
||||
for (const def of catDefs) {
|
||||
for (const pattern of def.patterns) {
|
||||
if (filename.includes(pattern)) return catMap[def.name]
|
||||
}
|
||||
}
|
||||
return catMap['Verschiedenes']
|
||||
}
|
||||
|
||||
// Load SVG icons from public/signaturen/
|
||||
const signaturenDir = path.join(process.cwd(), 'public', 'signaturen')
|
||||
let svgFiles = []
|
||||
try {
|
||||
svgFiles = fs.readdirSync(signaturenDir).filter(f => f.endsWith('.svg')).sort()
|
||||
} catch (e) {
|
||||
console.warn('⚠️ public/signaturen/ not found')
|
||||
return
|
||||
}
|
||||
|
||||
let created = 0
|
||||
for (const file of svgFiles) {
|
||||
let name = file.replace('.svg', '')
|
||||
name = name.replace(/_de$/i, '').replace(/_DE$/i, '').replace(/-de$/i, '')
|
||||
name = name.replace(/[_-]/g, ' ').replace(/\s+/g, ' ').trim()
|
||||
|
||||
const fileKey = `signaturen/${file}`
|
||||
const category = findCategory(file)
|
||||
|
||||
const existing = await prisma.iconAsset.findFirst({ where: { fileKey } })
|
||||
if (!existing) {
|
||||
await prisma.iconAsset.create({
|
||||
data: {
|
||||
name,
|
||||
categoryId: category.id,
|
||||
fileKey,
|
||||
mimeType: 'image/svg+xml',
|
||||
isSystem: true,
|
||||
width: 48,
|
||||
height: 48,
|
||||
},
|
||||
})
|
||||
created++
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ FKS Signaturen: ${created} new SVG icons created (${svgFiles.length} total)`)
|
||||
}
|
||||
|
||||
main()
|
||||
.then(async () => { await prisma.$disconnect() })
|
||||
.catch(async (e) => {
|
||||
console.error(e)
|
||||
await prisma.$disconnect()
|
||||
process.exit(1)
|
||||
})
|
||||
291
prisma/seed.js
Normal file
291
prisma/seed.js
Normal file
@@ -0,0 +1,291 @@
|
||||
const { PrismaClient } = require('@prisma/client')
|
||||
const bcrypt = require('bcryptjs')
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
async function main() {
|
||||
console.log('🌱 Starting seed...')
|
||||
|
||||
// Create default tenant
|
||||
const defaultTenant = await prisma.tenant.upsert({
|
||||
where: { slug: 'demo-feuerwehr' },
|
||||
update: {},
|
||||
create: {
|
||||
name: 'Demo Feuerwehr',
|
||||
slug: 'demo-feuerwehr',
|
||||
description: 'Standard-Mandant für Demonstrationszwecke',
|
||||
},
|
||||
})
|
||||
console.log('✅ Default tenant created:', defaultTenant.name)
|
||||
|
||||
// Create default users
|
||||
const adminPassword = await bcrypt.hash('admin123', 12)
|
||||
const editorPassword = await bcrypt.hash('editor123', 12)
|
||||
const viewerPassword = await bcrypt.hash('viewer123', 12)
|
||||
|
||||
const admin = await prisma.user.upsert({
|
||||
where: { email: 'admin@lageplan.local' },
|
||||
update: { role: 'SERVER_ADMIN' },
|
||||
create: {
|
||||
email: 'admin@lageplan.local',
|
||||
name: 'Server Admin',
|
||||
password: adminPassword,
|
||||
role: 'SERVER_ADMIN',
|
||||
},
|
||||
})
|
||||
|
||||
const editor = await prisma.user.upsert({
|
||||
where: { email: 'editor@lageplan.local' },
|
||||
update: { role: 'TENANT_ADMIN' },
|
||||
create: {
|
||||
email: 'editor@lageplan.local',
|
||||
name: 'Einsatzleiter',
|
||||
password: editorPassword,
|
||||
role: 'TENANT_ADMIN',
|
||||
},
|
||||
})
|
||||
|
||||
const viewer = await prisma.user.upsert({
|
||||
where: { email: 'viewer@lageplan.local' },
|
||||
update: { role: 'OPERATOR' },
|
||||
create: {
|
||||
email: 'viewer@lageplan.local',
|
||||
name: 'Bediener',
|
||||
password: viewerPassword,
|
||||
role: 'OPERATOR',
|
||||
},
|
||||
})
|
||||
|
||||
console.log('✅ Users created:', { admin: admin.email, editor: editor.email, viewer: viewer.email })
|
||||
|
||||
// Create tenant memberships
|
||||
for (const u of [editor, viewer]) {
|
||||
await prisma.tenantMembership.upsert({
|
||||
where: { userId_tenantId: { userId: u.id, tenantId: defaultTenant.id } },
|
||||
update: {},
|
||||
create: {
|
||||
userId: u.id,
|
||||
tenantId: defaultTenant.id,
|
||||
role: u.id === editor.id ? 'TENANT_ADMIN' : 'OPERATOR',
|
||||
},
|
||||
})
|
||||
}
|
||||
console.log('✅ Tenant memberships created')
|
||||
|
||||
// ─── FKS Icon Categories & Symbols (Bildquelle: Feuerwehr Koordination Schweiz FKS) ───
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
// Category definitions with file-name patterns for auto-assignment
|
||||
const catDefs = [
|
||||
{ name: 'Feuer / Brand', description: 'Feuer, Brand, Brandschutz', sortOrder: 1,
|
||||
patterns: ['Feuer', 'Brand', 'Explosion', 'Entrauchung', 'Rauch', 'Kamin', 'Luefter', 'Sprinkler'] },
|
||||
{ name: 'Wasser', description: 'Wasser, Hydranten, Leitungen', sortOrder: 2,
|
||||
patterns: ['Wasser', 'Hydrant', 'Reservoir', 'Druckleitung', 'Druckleistung', 'Transportleitung',
|
||||
'Schieber', 'Wasserloeschposten', 'Wasserversorgung', 'Wasserleitung', 'Ueberschwemmung',
|
||||
'Offener_Wasserverlauf', 'Moeglicher_Wasserbezugsort', 'Stehendes_Gewaesser',
|
||||
'Kleinloeschgeraet', 'Gefahr_durch_Loeschen_mit_Wasser'] },
|
||||
{ name: 'Gefahren / Stoffe', description: 'Gefahrstoffe, Chemie, ABC', sortOrder: 3,
|
||||
patterns: ['Gefaehrlich', 'Chemie', 'Chemikalien', 'Biologisch', 'Radioaktiv', 'Gas', 'Elektri',
|
||||
'Elektrotableau', 'Achtung', 'Leitungsdaehte'] },
|
||||
{ name: 'Rettung / Personen', description: 'Rettung, Sanität, Personen', sortOrder: 4,
|
||||
patterns: ['Rettung', 'Retten', 'Sanitaet', 'Patienten', 'Sammelplatz', 'Sammelstelle',
|
||||
'Totensammelstelle', 'Sprungretter', 'Helikopterlandeplatz'] },
|
||||
{ name: 'Leitern / Geräte', description: 'Leitern, Fahrzeuge, Geräte', sortOrder: 5,
|
||||
patterns: ['Leiter', 'Anhaengeleiter', 'Strebenleiter', 'ADL', 'TLF', 'HRF', 'SWHP'] },
|
||||
{ name: 'Gebäude / Schäden', description: 'Gebäude, Zerstörung, Schäden', sortOrder: 6,
|
||||
patterns: ['Beschaedigung', 'Teilzerstoerung', 'Totalzerstoerung', 'Decke_eingestuerzt',
|
||||
'Umfassungswaende', 'AnzahlGeschosse', 'Eingang', 'Treppen', 'Lift', 'Bruecke',
|
||||
'Rutschgebiet', 'Strasse', 'Bahnlin'] },
|
||||
{ name: 'Einsatzführung', description: 'Führung, Organisation, Kommunikation', sortOrder: 7,
|
||||
patterns: ['Einsatzleiter', 'KP_Font', 'Offizier', 'Beobachtungsposten', 'Kontrollstelle',
|
||||
'Funk', 'Informationszentrum', 'Medienkontaktstelle', 'Fernsignaltableau',
|
||||
'Materialdepot', 'Schluesseldepot', 'Abschnitt', 'Absperrung'] },
|
||||
{ name: 'Organisationen', description: 'Feuerwehr, Polizei, Armee, Zivilschutz', sortOrder: 8,
|
||||
patterns: ['Feuerwehr', 'Polizei', 'Armee', 'Zivilschutz', 'Chemiewehr', 'MS_de', 'Anmarsch'] },
|
||||
{ name: 'Entwicklung / Taktik', description: 'Entwicklungsgrenzen, Ausbreitung', sortOrder: 9,
|
||||
patterns: ['Entwicklungsgrenze', 'Horizontale_Entwicklung', 'Vertikale_Entwicklung',
|
||||
'VertikaleEntwicklung', 'Unfall'] },
|
||||
{ name: 'Verschiedenes', description: 'Weitere Signaturen', sortOrder: 10,
|
||||
patterns: ['Massstab', 'Nordrichtung', 'Windrichtung'] },
|
||||
]
|
||||
|
||||
// Delete ALL old system icons (regardless of fileKey pattern)
|
||||
const deletedIcons = await prisma.iconAsset.deleteMany({
|
||||
where: { isSystem: true },
|
||||
})
|
||||
console.log(`🗑️ ${deletedIcons.count} old system icons removed`)
|
||||
|
||||
// Clean up empty global categories
|
||||
const oldGlobalCats = await prisma.iconCategory.findMany({ where: { tenantId: null } })
|
||||
for (const oldCat of oldGlobalCats) {
|
||||
const remaining = await prisma.iconAsset.count({ where: { categoryId: oldCat.id } })
|
||||
if (remaining === 0) {
|
||||
await prisma.iconCategory.delete({ where: { id: oldCat.id } }).catch(() => {})
|
||||
}
|
||||
}
|
||||
// Create new global categories
|
||||
const catMap = {}
|
||||
for (const def of catDefs) {
|
||||
const cat = await prisma.iconCategory.upsert({
|
||||
where: { id: def.name.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase() },
|
||||
update: { name: def.name, description: def.description, sortOrder: def.sortOrder, isGlobal: true },
|
||||
create: {
|
||||
id: def.name.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase(),
|
||||
name: def.name,
|
||||
description: def.description,
|
||||
sortOrder: def.sortOrder,
|
||||
isGlobal: true,
|
||||
tenantId: null,
|
||||
},
|
||||
})
|
||||
catMap[def.name] = cat
|
||||
}
|
||||
console.log(`✅ ${Object.keys(catMap).length} icon categories created`)
|
||||
|
||||
// Assign icon to category based on filename pattern matching
|
||||
function findCategory(filename) {
|
||||
for (const def of catDefs) {
|
||||
for (const pattern of def.patterns) {
|
||||
if (filename.includes(pattern)) return catMap[def.name]
|
||||
}
|
||||
}
|
||||
return catMap['Verschiedenes']
|
||||
}
|
||||
|
||||
// Load SVG icons from public/signaturen/
|
||||
const signaturenDir = path.join(process.cwd(), 'public', 'signaturen')
|
||||
let svgFiles = []
|
||||
try {
|
||||
svgFiles = fs.readdirSync(signaturenDir).filter(f => f.endsWith('.svg')).sort()
|
||||
} catch (e) {
|
||||
console.warn('⚠️ public/signaturen/ not found, skipping icon seed')
|
||||
}
|
||||
|
||||
let created = 0
|
||||
for (const file of svgFiles) {
|
||||
// Clean name: remove .svg, remove _de/_DE suffix
|
||||
let name = file.replace('.svg', '')
|
||||
name = name.replace(/_de$/i, '').replace(/_DE$/i, '').replace(/-de$/i, '')
|
||||
name = name.replace(/[_-]/g, ' ').replace(/\s+/g, ' ').trim()
|
||||
|
||||
const fileKey = `signaturen/${file}`
|
||||
const category = findCategory(file)
|
||||
|
||||
const existing = await prisma.iconAsset.findFirst({ where: { fileKey } })
|
||||
if (!existing) {
|
||||
await prisma.iconAsset.create({
|
||||
data: {
|
||||
name,
|
||||
categoryId: category.id,
|
||||
fileKey,
|
||||
mimeType: 'image/svg+xml',
|
||||
isSystem: true,
|
||||
width: 48,
|
||||
height: 48,
|
||||
},
|
||||
})
|
||||
created++
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ FKS Signaturen: ${created} new icons created (${svgFiles.length} total SVGs)`)
|
||||
|
||||
// Create a demo project
|
||||
const demoProject = await prisma.project.upsert({
|
||||
where: { id: 'demo-project-001' },
|
||||
update: {},
|
||||
create: {
|
||||
id: 'demo-project-001',
|
||||
title: 'Demo Einsatz - Wohnungsbrand',
|
||||
location: 'Musterstrasse 42, 8000 Zürich',
|
||||
description: 'Beispiel-Einsatz zur Demonstration der Funktionen',
|
||||
ownerId: editor.id,
|
||||
tenantId: defaultTenant.id,
|
||||
mapCenter: { lng: 8.5417, lat: 47.3769 },
|
||||
mapZoom: 17,
|
||||
},
|
||||
})
|
||||
|
||||
console.log('✅ Demo project created:', demoProject.title)
|
||||
|
||||
// Create default hose types (Swiss fire department standards)
|
||||
const hoseTypes = [
|
||||
{
|
||||
name: '55mm Transportleitung',
|
||||
diameterMm: 55,
|
||||
lengthPerPieceM: 10,
|
||||
flowRateLpm: 500,
|
||||
frictionCoeff: 0.034,
|
||||
description: 'Standard Transportleitung, 3er Verteiler (1500/3 = 500 l/min)',
|
||||
isDefault: true,
|
||||
sortOrder: 1,
|
||||
},
|
||||
{
|
||||
name: '75mm Zubringerleitung',
|
||||
diameterMm: 75,
|
||||
lengthPerPieceM: 20,
|
||||
flowRateLpm: 1500,
|
||||
frictionCoeff: 0.012,
|
||||
description: 'Zubringerleitung vom Hydranten zum Verteiler',
|
||||
isDefault: false,
|
||||
sortOrder: 2,
|
||||
},
|
||||
]
|
||||
|
||||
for (const ht of hoseTypes) {
|
||||
await prisma.hoseType.upsert({
|
||||
where: { name: ht.name },
|
||||
update: {
|
||||
diameterMm: ht.diameterMm,
|
||||
lengthPerPieceM: ht.lengthPerPieceM,
|
||||
flowRateLpm: ht.flowRateLpm,
|
||||
frictionCoeff: ht.frictionCoeff,
|
||||
description: ht.description,
|
||||
isDefault: ht.isDefault,
|
||||
sortOrder: ht.sortOrder,
|
||||
},
|
||||
create: ht,
|
||||
})
|
||||
}
|
||||
|
||||
console.log('✅ Hose types created:', hoseTypes.length)
|
||||
|
||||
// ─── Journal Check Templates (SOMA) ─────────────────────
|
||||
const somaTemplates = [
|
||||
{ label: 'Stopp Zutritt / Ex-Gefahr', sortOrder: 1 },
|
||||
{ label: 'Alarmierung Gesamt-Fw', sortOrder: 2 },
|
||||
{ label: 'Alarmierung Ofw Wohlen', sortOrder: 3 },
|
||||
{ label: 'Alarmierung Stp Baden', sortOrder: 4 },
|
||||
{ label: 'Alarmierung Ambulanz', sortOrder: 5 },
|
||||
{ label: 'Alarmierung BWL', sortOrder: 6 },
|
||||
{ label: 'Alarmierung BL/Meister', sortOrder: 7 },
|
||||
{ label: 'Brunnenchef Villmergen', sortOrder: 8 },
|
||||
{ label: 'Berieselung Tank / Anl', sortOrder: 9 },
|
||||
{ label: 'Druckerhöhung', sortOrder: 10 },
|
||||
]
|
||||
|
||||
for (const tpl of somaTemplates) {
|
||||
await prisma.journalCheckTemplate.upsert({
|
||||
where: { id: tpl.label.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase() },
|
||||
update: { label: tpl.label, sortOrder: tpl.sortOrder },
|
||||
create: {
|
||||
id: tpl.label.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase(),
|
||||
label: tpl.label,
|
||||
sortOrder: tpl.sortOrder,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
console.log('✅ SOMA templates created:', somaTemplates.length)
|
||||
console.log('🎉 Seed completed successfully!')
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error('❌ Seed failed:', e)
|
||||
process.exit(1)
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect()
|
||||
})
|
||||
156
prisma/seed.ts
Normal file
156
prisma/seed.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { PrismaClient, Role } from '@prisma/client'
|
||||
import bcrypt from 'bcryptjs'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
async function main() {
|
||||
console.log('🌱 Starting seed...')
|
||||
|
||||
// Create default users
|
||||
const adminPassword = await bcrypt.hash('admin123', 12)
|
||||
const editorPassword = await bcrypt.hash('editor123', 12)
|
||||
const viewerPassword = await bcrypt.hash('viewer123', 12)
|
||||
|
||||
const admin = await prisma.user.upsert({
|
||||
where: { email: 'admin@lageplan.local' },
|
||||
update: {},
|
||||
create: {
|
||||
email: 'admin@lageplan.local',
|
||||
name: 'Administrator',
|
||||
password: adminPassword,
|
||||
role: Role.ADMIN,
|
||||
},
|
||||
})
|
||||
|
||||
const editor = await prisma.user.upsert({
|
||||
where: { email: 'editor@lageplan.local' },
|
||||
update: {},
|
||||
create: {
|
||||
email: 'editor@lageplan.local',
|
||||
name: 'Einsatzleiter',
|
||||
password: editorPassword,
|
||||
role: Role.EDITOR,
|
||||
},
|
||||
})
|
||||
|
||||
const viewer = await prisma.user.upsert({
|
||||
where: { email: 'viewer@lageplan.local' },
|
||||
update: {},
|
||||
create: {
|
||||
email: 'viewer@lageplan.local',
|
||||
name: 'Beobachter',
|
||||
password: viewerPassword,
|
||||
role: Role.VIEWER,
|
||||
},
|
||||
})
|
||||
|
||||
console.log('✅ Users created:', { admin: admin.email, editor: editor.email, viewer: viewer.email })
|
||||
|
||||
// Create icon categories
|
||||
const categories = [
|
||||
{ name: 'Feuer', description: 'Brand- und Feuersymbole', sortOrder: 1 },
|
||||
{ name: 'Wasser', description: 'Wasser- und Überflutungssymbole', sortOrder: 2 },
|
||||
{ name: 'Gefahrstoffe', description: 'Chemie- und Gefahrstoffsymbole', sortOrder: 3 },
|
||||
{ name: 'Verkehr', description: 'Verkehrs- und Unfallsymbole', sortOrder: 4 },
|
||||
{ name: 'Personen', description: 'Personen- und Rettungssymbole', sortOrder: 5 },
|
||||
{ name: 'Fahrzeuge', description: 'Einsatzfahrzeuge und Geräte', sortOrder: 6 },
|
||||
{ name: 'Infrastruktur', description: 'Gebäude und Infrastruktursymbole', sortOrder: 7 },
|
||||
{ name: 'Taktik', description: 'Taktische Zeichen und Symbole', sortOrder: 8 },
|
||||
{ name: 'Eigene', description: 'Benutzerdefinierte Symbole', sortOrder: 99 },
|
||||
]
|
||||
|
||||
for (const cat of categories) {
|
||||
await prisma.iconCategory.upsert({
|
||||
where: { name: cat.name },
|
||||
update: { description: cat.description, sortOrder: cat.sortOrder },
|
||||
create: cat,
|
||||
})
|
||||
}
|
||||
|
||||
console.log('✅ Icon categories created:', categories.length)
|
||||
|
||||
// Get category IDs for system icons
|
||||
const feuerCat = await prisma.iconCategory.findUnique({ where: { name: 'Feuer' } })
|
||||
const wasserCat = await prisma.iconCategory.findUnique({ where: { name: 'Wasser' } })
|
||||
const gefahrstoffeCat = await prisma.iconCategory.findUnique({ where: { name: 'Gefahrstoffe' } })
|
||||
const verkehrCat = await prisma.iconCategory.findUnique({ where: { name: 'Verkehr' } })
|
||||
const personenCat = await prisma.iconCategory.findUnique({ where: { name: 'Personen' } })
|
||||
const fahrzeugeCat = await prisma.iconCategory.findUnique({ where: { name: 'Fahrzeuge' } })
|
||||
const infrastrukturCat = await prisma.iconCategory.findUnique({ where: { name: 'Infrastruktur' } })
|
||||
const taktikCat = await prisma.iconCategory.findUnique({ where: { name: 'Taktik' } })
|
||||
|
||||
// Create system icons (these use inline SVG data URIs - in production, upload to MinIO)
|
||||
const systemIcons = [
|
||||
{ name: 'Brand', categoryId: feuerCat!.id, fileKey: 'system/fire.svg' },
|
||||
{ name: 'Rauch', categoryId: feuerCat!.id, fileKey: 'system/smoke.svg' },
|
||||
{ name: 'Explosion', categoryId: feuerCat!.id, fileKey: 'system/explosion.svg' },
|
||||
{ name: 'Brandstelle', categoryId: feuerCat!.id, fileKey: 'system/fire-location.svg' },
|
||||
{ name: 'Überflutung', categoryId: wasserCat!.id, fileKey: 'system/flood.svg' },
|
||||
{ name: 'Wasserschaden', categoryId: wasserCat!.id, fileKey: 'system/water-damage.svg' },
|
||||
{ name: 'Hydrant', categoryId: wasserCat!.id, fileKey: 'system/hydrant.svg' },
|
||||
{ name: 'Gefahrstoff', categoryId: gefahrstoffeCat!.id, fileKey: 'system/hazmat.svg' },
|
||||
{ name: 'Radioaktiv', categoryId: gefahrstoffeCat!.id, fileKey: 'system/radioactive.svg' },
|
||||
{ name: 'Giftig', categoryId: gefahrstoffeCat!.id, fileKey: 'system/toxic.svg' },
|
||||
{ name: 'Unfall', categoryId: verkehrCat!.id, fileKey: 'system/accident.svg' },
|
||||
{ name: 'Strassensperre', categoryId: verkehrCat!.id, fileKey: 'system/road-block.svg' },
|
||||
{ name: 'Verletzte Person', categoryId: personenCat!.id, fileKey: 'system/injured.svg' },
|
||||
{ name: 'Vermisste Person', categoryId: personenCat!.id, fileKey: 'system/missing.svg' },
|
||||
{ name: 'Sammelplatz', categoryId: personenCat!.id, fileKey: 'system/assembly-point.svg' },
|
||||
{ name: 'Löschfahrzeug', categoryId: fahrzeugeCat!.id, fileKey: 'system/fire-truck.svg' },
|
||||
{ name: 'Rettungswagen', categoryId: fahrzeugeCat!.id, fileKey: 'system/ambulance.svg' },
|
||||
{ name: 'Kommandoposten', categoryId: taktikCat!.id, fileKey: 'system/command-post.svg' },
|
||||
{ name: 'Einsatzleitung', categoryId: taktikCat!.id, fileKey: 'system/incident-command.svg' },
|
||||
{ name: 'Absperrung', categoryId: taktikCat!.id, fileKey: 'system/barrier.svg' },
|
||||
{ name: 'Gebäude', categoryId: infrastrukturCat!.id, fileKey: 'system/building.svg' },
|
||||
{ name: 'Eingang', categoryId: infrastrukturCat!.id, fileKey: 'system/entrance.svg' },
|
||||
]
|
||||
|
||||
for (const icon of systemIcons) {
|
||||
const existing = await prisma.iconAsset.findFirst({
|
||||
where: { fileKey: icon.fileKey },
|
||||
})
|
||||
if (!existing) {
|
||||
await prisma.iconAsset.create({
|
||||
data: {
|
||||
name: icon.name,
|
||||
categoryId: icon.categoryId,
|
||||
fileKey: icon.fileKey,
|
||||
mimeType: 'image/svg+xml',
|
||||
isSystem: true,
|
||||
width: 48,
|
||||
height: 48,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ System icons created:', systemIcons.length)
|
||||
|
||||
// Create a demo project
|
||||
const demoProject = await prisma.project.upsert({
|
||||
where: { id: 'demo-project-001' },
|
||||
update: {},
|
||||
create: {
|
||||
id: 'demo-project-001',
|
||||
title: 'Demo Einsatz - Wohnungsbrand',
|
||||
location: 'Musterstrasse 42, 8000 Zürich',
|
||||
description: 'Beispiel-Einsatz zur Demonstration der Funktionen',
|
||||
ownerId: editor.id,
|
||||
mapCenter: { lng: 8.5417, lat: 47.3769 },
|
||||
mapZoom: 17,
|
||||
},
|
||||
})
|
||||
|
||||
console.log('✅ Demo project created:', demoProject.title)
|
||||
|
||||
console.log('🎉 Seed completed successfully!')
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error('❌ Seed failed:', e)
|
||||
process.exit(1)
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect()
|
||||
})
|
||||
Reference in New Issue
Block a user