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() })