Files
Lageplan/prisma/seed.js
Pepe Ziberi 5adadd246e
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 12m23s
fix(seed): prevent cascade-deletion of tenant_symbols on container restart
2026-05-20 11:44:12 +02:00

282 lines
10 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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'] },
]
// NOTE: We intentionally do NOT delete old system icons here.
// TenantSymbol rows reference IconAsset.id via foreign key.
// Deleting would either break references (tenant symbols become 404s)
// or cascade-delete tenant symbols. Instead we upsert by fileKey.
// 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
let updated = 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.update({
where: { id: existing.id },
data: {
name,
categoryId: category.id,
mimeType: 'image/svg+xml',
isSystem: true,
isActive: true,
width: 48,
height: 48,
},
})
updated++
} else {
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} created, ${updated} updated (${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) ─────────────────────
// No default SOMA templates — each tenant defines their own via Admin → SOMA tab
console.log(' SOMA templates: keine Standard-Vorgaben (Tenants konfigurieren eigene)')
console.log('🎉 Seed completed successfully!')
}
main()
.catch((e) => {
console.error('❌ Seed failed:', e)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})