Initial commit: Lageplan v1.0 - Next.js 15.5, React 19
This commit is contained in:
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()
|
||||
})
|
||||
Reference in New Issue
Block a user