#!/usr/bin/env npx tsx /** * ShiftCraft PocketBase Setup Script * * Creates all required collections and seeds legal templates. * Run after starting PocketBase: * pnpm pocketbase:start * npx tsx scripts/setup-pb.ts */ import PocketBase from 'pocketbase' const PB_URL = process.env.NUXT_PUBLIC_PB_URL || process.env.NUXT_PUBLIC_POCKETBASE_URL || 'http://localhost:8090' const PB_ADMIN_EMAIL = process.env.PB_ADMIN_EMAIL || process.env.SUPERUSER_EMAIL || 'admin@shiftcraft.app' const PB_ADMIN_PASSWORD = process.env.PB_ADMIN_PASSWORD || process.env.SUPERUSER_PW || 'changeme123' async function main() { const pb = new PocketBase(PB_URL) console.log(`Connecting to PocketBase at ${PB_URL}...`) await pb.admins.authWithPassword(PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD) console.log('Connected to PocketBase admin') const collections = [ { name: 'organizations', type: 'base', schema: [ { name: 'name', type: 'text', required: true }, { name: 'slug', type: 'text', required: true }, { name: 'timezone', type: 'text' }, { name: 'industry', type: 'text' }, { name: 'owner', type: 'text' }, { name: 'plan', type: 'select', options: { values: ['free', 'pro', 'business'], maxSelect: 1 } }, { name: 'plan_employee_limit', type: 'number' }, { name: 'plan_history_months', type: 'number' }, { name: 'stripe_customer_id', type: 'text' }, { name: 'stripe_subscription_id', type: 'text' }, { name: 'stripe_subscription_status', type: 'text' }, { name: 'trial_ends_at', type: 'date' }, ], }, { name: 'employees', type: 'base', schema: [ { name: 'org_id', type: 'text', required: true }, { name: 'name', type: 'text', required: true }, { name: 'email', type: 'email' }, { name: 'employee_number', type: 'text' }, { name: 'roles', type: 'json' }, { name: 'skills', type: 'json' }, { name: 'employment_type', type: 'select', options: { values: ['full_time', 'part_time', 'mini_job'], maxSelect: 1 } }, { name: 'weekly_hours_target', type: 'number' }, { name: 'max_weekly_hours', type: 'number' }, { name: 'available_periods', type: 'json' }, { name: 'unavailable_dates', type: 'json' }, { name: 'notes', type: 'text' }, { name: 'active', type: 'bool' }, ], }, { name: 'shift_frameworks', type: 'base', schema: [ { name: 'org_id', type: 'text', required: true }, { name: 'periods', type: 'json' }, { name: 'shifts', type: 'json' }, { name: 'scheduling_horizon_days', type: 'number' }, ], }, { name: 'constraints', type: 'base', schema: [ { name: 'org_id', type: 'text', required: true }, { name: 'label', type: 'text', required: true }, { name: 'source_text', type: 'text' }, { name: 'constraint_json', type: 'json' }, { name: 'scope', type: 'text' }, { name: 'scope_ref', type: 'text' }, { name: 'category', type: 'select', options: { values: ['legal', 'preference', 'operational', 'other'], maxSelect: 1 } }, { name: 'hard', type: 'bool' }, { name: 'weight', type: 'number' }, { name: 'active', type: 'bool' }, { name: 'source', type: 'select', options: { values: ['ai', 'legal', 'manual'], maxSelect: 1 } }, { name: 'template_id', type: 'text' }, ], }, { name: 'legal_templates', type: 'base', schema: [ { name: 'region', type: 'text' }, { name: 'law_name', type: 'text' }, { name: 'label', type: 'text', required: true }, { name: 'description', type: 'text' }, { name: 'constraint_json', type: 'json' }, { name: 'category', type: 'text' }, { name: 'mandatory', type: 'bool' }, { name: 'sort_order', type: 'number' }, ], }, { name: 'schedule_runs', type: 'base', schema: [ { name: 'org_id', type: 'text', required: true }, { name: 'name', type: 'text', required: true }, { name: 'period_start', type: 'date', required: true }, { name: 'period_end', type: 'date', required: true }, { name: 'framework_snapshot', type: 'json' }, { name: 'constraints_snapshot', type: 'json' }, { name: 'employees_snapshot', type: 'json' }, { name: 'status', type: 'select', options: { values: ['pending', 'solving', 'solved', 'infeasible', 'error'], maxSelect: 1 } }, { name: 'solver_duration_ms', type: 'number' }, { name: 'objective_value', type: 'number' }, { name: 'infeasibility_hints', type: 'json' }, { name: 'result', type: 'json' }, { name: 'created_by', type: 'text' }, ], }, ] for (const col of collections) { try { await pb.collections.getOne(col.name) console.log(` Collection '${col.name}' already exists — skipping`) } catch { try { await pb.collections.create(col) console.log(` Created collection '${col.name}'`) } catch (err) { console.error(` Failed to create '${col.name}':`, err) } } } // Seed legal templates console.log('\nSeeding legal templates...') const templates = [ { region: 'Deutschland', law_name: 'ArbZG §3', label: 'Maximale Arbeitszeit 10 Stunden/Tag', description: 'Die werktägliche Arbeitszeit darf 8 Stunden nicht überschreiten. Sie kann auf bis zu 10 Stunden verlängert werden, wenn innerhalb von 6 Kalendermonaten im Durchschnitt 8 Stunden/Tag nicht überschritten werden.', constraint_json: { type: 'max_hours_per_day', scope: { type: 'global' }, params: { max_hours: 10 }, hard: true, natural_language_summary: 'Maximal 10 Stunden Arbeitszeit pro Tag (ArbZG §3)', }, category: 'legal', mandatory: true, sort_order: 1, }, { region: 'Deutschland', law_name: 'ArbZG §5', label: 'Mindestruhezeit 11 Stunden', description: 'Nach Beendigung der täglichen Arbeitszeit ist den Arbeitnehmern eine ununterbrochene Ruhezeit von mindestens elf Stunden zu gewähren.', constraint_json: { type: 'min_rest_between_shifts', scope: { type: 'global' }, params: { min_hours: 11 }, hard: true, natural_language_summary: 'Mindestens 11 Stunden Ruhezeit zwischen Schichten (ArbZG §5)', }, category: 'legal', mandatory: true, sort_order: 2, }, { region: 'Deutschland', law_name: 'ArbZG §9', label: 'Sonntagsruhe', description: 'Arbeitnehmer dürfen an Sonn- und gesetzlichen Feiertagen von 0 bis 24 Uhr nicht beschäftigt werden (mit branchenspezifischen Ausnahmen).', constraint_json: { type: 'max_weekend_shifts_per_month', scope: { type: 'global' }, params: { max_count: 4 }, hard: false, weight: 80, natural_language_summary: 'Maximale Sonntagsschichten nach ArbZG §9 beachten', }, category: 'legal', mandatory: false, sort_order: 3, }, { region: 'Deutschland', law_name: 'Empfehlung', label: 'Maximal 6 Schichten am Stück', description: 'Empfohlene Begrenzung aufeinanderfolgender Arbeitstage.', constraint_json: { type: 'max_consecutive_shifts', scope: { type: 'global' }, params: { max_count: 6 }, hard: false, weight: 70, natural_language_summary: 'Nicht mehr als 6 Schichten in Folge', }, category: 'legal', mandatory: false, sort_order: 4, }, { region: 'Deutschland', law_name: 'Empfehlung', label: 'Faire Schichtverteilung', description: 'Nachtschichten und Wochenendschichten werden gleichmäßig auf alle Mitarbeiter verteilt.', constraint_json: { type: 'fair_distribution', scope: { type: 'global' }, params: { metric: 'night_shifts', max_deviation_percent: 20 }, hard: false, weight: 60, natural_language_summary: 'Faire Verteilung der Nachtschichten (max. 20% Abweichung)', }, category: 'preference', mandatory: false, sort_order: 5, }, ] for (const tmpl of templates) { try { await pb.collection('legal_templates').getFirstListItem(`label = "${tmpl.label}"`) console.log(` Template '${tmpl.label}' already exists — skipping`) } catch { try { await pb.collection('legal_templates').create(tmpl) console.log(` Created template '${tmpl.label}'`) } catch (err) { console.error(` Failed to create template '${tmpl.label}':`, err) } } } console.log('\nSetup complete!') console.log('Next steps:') console.log(' 1. Open PocketBase admin at http://localhost:8090/_/') console.log(' 2. Configure the users collection to include org_id and role fields') console.log(' 3. Run: pnpm dev') } main().catch(console.error)