Slim API deployment - composer install + full CRUD endpoints

This commit is contained in:
Hermes Agent
2026-05-20 14:36:05 +02:00
parent 48bf9c7088
commit 8ab4e532fd
1727 changed files with 7746 additions and 7 deletions

View File

@@ -0,0 +1,22 @@
-- Migration: 015_lesson_type_allows_student_and_notes_private.sql
-- Adds allows_student column to lesson_types and notes_private to appointments
-- 1. lesson_types.allows_student: which lesson types can be assigned a student
ALTER TABLE lesson_types ADD COLUMN IF NOT EXISTS allows_student integer NOT NULL DEFAULT 0;
COMMENT ON COLUMN lesson_types.allows_student IS '1 = lesson type can have a student_id (e.g. driving lessons), 0 = no student (theory, work, private)';
-- 2. appointments.notes_private: private note only visible to the instructor (not chef/office)
ALTER TABLE appointments ADD COLUMN IF NOT EXISTS notes_private text DEFAULT '';
-- Default: only 'student' category allows student_id, all other categories = 0
UPDATE lesson_types SET allows_student = 1 WHERE category = 'student';
UPDATE lesson_types SET allows_student = 0 WHERE category != 'student';
-- For existing appointments, clear student_id if the lesson_type doesn't allow it
-- (data cleanup - not normally needed but defensive)
UPDATE appointments
SET student_id = NULL
WHERE student_id IS NOT NULL
AND lesson_type_id IN (
SELECT id FROM lesson_types WHERE allows_student = 0
);

View File

@@ -0,0 +1,39 @@
-- Migration: 016_consolidate_lesson_types.sql
-- Reihen: tenant 1 und tenant 2 bereinigen, konsistente Einstellungen
-- ══════════════════════════════════════════════════════
-- TENANT 1
-- ══════════════════════════════════════════════════════
-- Tenant 1: Doppelte Einweisung (id=1, id=14 → id=14 renamed to avoid conflict via rename approach)
-- Already unique (no action needed)
-- Tenant 1: Doppelte Übungsstunde (id=2, id=15 → id=15 is in tenant 1 but was already unique)
-- Actually tenant 1 has only id=2 (Uebungsstunde) - no duplicate
-- Tenant 1 work type fixes:
-- Sonstige Arbeiten (id=8): is_rest_relevant=1 (zählt für Ruhezeit)
UPDATE lesson_types SET is_rest_relevant = 1, fixed_duration = 0 WHERE id = 8 AND tenant_id = 1;
-- Theorieunterricht (id=7): is_rest_relevant=1, is_billable=1, fixed_duration=0
UPDATE lesson_types SET is_rest_relevant = 1, fixed_duration = 0, is_billable = 1 WHERE id = 7 AND tenant_id = 1;
-- ══════════════════════════════════════════════════════
-- TENANT 2
-- ══════════════════════════════════════════════════════
-- Tenant 2: Doppelte Namen konsolidieren
-- Autobahnfahrt → Autobahn (id=13 → name='Autobahn')
UPDATE lesson_types SET name = 'Autobahn' WHERE id = 13 AND tenant_id = 2;
-- Übungsstunde (id=15 already named correctly)
-- Verify tenant 2 student types
-- Note: id=11 Nachtfahrten stays (different from tenant 1 which has no Nachtfahrten)
-- id=16 Praktische Prüfung stays
-- id=12 Überlandfahrt stays
-- ══════════════════════════════════════════════════════
-- ALL TENANTS: allows_student defaults
-- ══════════════════════════════════════════════════════
-- already done in migration 015
-- student=1, all others=0

View File

@@ -0,0 +1,202 @@
-- Migration: 017_student_booking_schema.sql
-- Fahrschüler-Buchungssystem + Multi-Tenant Instructor + Break-Rules
BEGIN;
-- ══════════════════════════════════════════════════════
-- 1. users.role ist bereits text (kein ENUM), keine Änderung nötig
-- ══════════════════════════════════════════════════════
-- ══════════════════════════════════════════════════════
-- 2. instructors erweitern: UE-Limits + Booking-Settings
-- ══════════════════════════════════════════════════════
ALTER TABLE instructors ADD COLUMN IF NOT EXISTS max_daily_units integer DEFAULT NULL;
ALTER TABLE instructors ADD COLUMN IF NOT EXISTS max_daily_override integer DEFAULT 0;
ALTER TABLE instructors ADD COLUMN IF NOT EXISTS max_weekly_units integer DEFAULT NULL;
ALTER TABLE instructors ADD COLUMN IF NOT EXISTS max_weekly_override integer DEFAULT 0;
ALTER TABLE instructors ADD COLUMN IF NOT EXISTS booking_enabled integer DEFAULT 1;
-- ══════════════════════════════════════════════════════
-- 3. students erweitern: user_id FK
-- ══════════════════════════════════════════════════════
ALTER TABLE students ADD COLUMN IF NOT EXISTS user_id bigint REFERENCES users(id) NULL;
-- ══════════════════════════════════════════════════════
-- 4. tenants erweitern: free_cancellation_hours
-- ══════════════════════════════════════════════════════
ALTER TABLE tenants ADD COLUMN IF NOT EXISTS free_cancellation_hours integer DEFAULT 24;
-- ══════════════════════════════════════════════════════
-- 5. tenant_registration_codes
-- ══════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS tenant_registration_codes (
id bigserial PRIMARY KEY,
tenant_id bigint NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
code text NOT NULL UNIQUE,
is_active integer DEFAULT 1,
used_count integer DEFAULT 0,
created_at timestamptz DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_tenant_reg_codes_code ON tenant_registration_codes(code);
CREATE INDEX IF NOT EXISTS idx_tenant_reg_codes_tenant ON tenant_registration_codes(tenant_id);
-- ══════════════════════════════════════════════════════
-- 6. student_tenants
-- ══════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS student_tenants (
id bigserial PRIMARY KEY,
student_user_id bigint NOT NULL REFERENCES users(id) ON DELETE CASCADE,
tenant_id bigint NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'active', 'suspended')),
registered_at timestamptz DEFAULT NOW(),
confirmed_at timestamptz NULL,
confirmed_by bigint NULL REFERENCES users(id),
UNIQUE(student_user_id, tenant_id)
);
CREATE INDEX IF NOT EXISTS idx_student_tenants_user ON student_tenants(student_user_id);
CREATE INDEX IF NOT EXISTS idx_student_tenants_tenant ON student_tenants(tenant_id);
-- ══════════════════════════════════════════════════════
-- 7. instructor_availability
-- ══════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS instructor_availability (
id bigserial PRIMARY KEY,
instructor_id bigint NOT NULL REFERENCES instructors(id) ON DELETE CASCADE,
weekday integer NOT NULL CHECK (weekday BETWEEN 0 AND 6),
time_from text NOT NULL,
time_to text NOT NULL,
is_active integer DEFAULT 1,
UNIQUE(instructor_id, weekday, time_from)
);
CREATE INDEX IF NOT EXISTS idx_instructor_avail_instructor ON instructor_availability(instructor_id);
-- ══════════════════════════════════════════════════════
-- 8. appointment_requests
-- ══════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS appointment_requests (
id bigserial PRIMARY KEY,
student_user_id bigint NOT NULL REFERENCES users(id) ON DELETE CASCADE,
instructor_id bigint NOT NULL REFERENCES instructors(id) ON DELETE CASCADE,
lesson_type_id bigint NOT NULL REFERENCES lesson_types(id),
requested_start timestamptz NOT NULL,
requested_end timestamptz NOT NULL,
status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'confirmed', 'rejected', 'cancelled')),
requested_at timestamptz DEFAULT NOW(),
responded_at timestamptz NULL,
response_by bigint NULL REFERENCES users(id),
notes text DEFAULT '',
confirmed_appointment_id bigint NULL REFERENCES appointments(id)
);
CREATE INDEX IF NOT EXISTS idx_appt_req_student ON appointment_requests(student_user_id);
CREATE INDEX IF NOT EXISTS idx_appt_req_instructor ON appointment_requests(instructor_id);
CREATE INDEX IF NOT EXISTS idx_appt_req_status ON appointment_requests(status);
-- ══════════════════════════════════════════════════════
-- 9. instructor_vacations
-- ══════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS instructor_vacations (
id bigserial PRIMARY KEY,
instructor_id bigint NOT NULL REFERENCES instructors(id) ON DELETE CASCADE,
date_from date NOT NULL,
date_to date NOT NULL,
notes text DEFAULT '',
created_at timestamptz DEFAULT NOW(),
UNIQUE(instructor_id, date_from, date_to)
);
CREATE INDEX IF NOT EXISTS idx_instructor_vacations_instructor ON instructor_vacations(instructor_id);
CREATE INDEX IF NOT EXISTS idx_instructor_vacations_dates ON instructor_vacations(date_from, date_to);
-- ══════════════════════════════════════════════════════
-- 10. private_appointments
-- ══════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS private_appointments (
id bigserial PRIMARY KEY,
instructor_id bigint NOT NULL REFERENCES instructors(id) ON DELETE CASCADE,
title text NOT NULL DEFAULT 'Privat',
date date NOT NULL,
time_from text NOT NULL,
time_to text NOT NULL,
color text DEFAULT '#9ca3af',
created_at timestamptz DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_private_appts_instructor ON private_appointments(instructor_id);
CREATE INDEX IF NOT EXISTS idx_private_appts_date ON private_appointments(date);
-- ══════════════════════════════════════════════════════
-- 11. instructor_tenants
-- ══════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS instructor_tenants (
id bigserial PRIMARY KEY,
instructor_id bigint NOT NULL REFERENCES instructors(id) ON DELETE CASCADE,
tenant_id bigint NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
is_primary integer DEFAULT 0,
created_at timestamptz DEFAULT NOW(),
UNIQUE(instructor_id, tenant_id)
);
CREATE INDEX IF NOT EXISTS idx_instructor_tenants_instructor ON instructor_tenants(instructor_id);
CREATE INDEX IF NOT EXISTS idx_instructor_tenants_tenant ON instructor_tenants(tenant_id);
-- ══════════════════════════════════════════════════════
-- 12. instructor_travel_times
-- ══════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS instructor_travel_times (
id bigserial PRIMARY KEY,
instructor_id bigint NOT NULL REFERENCES instructors(id) ON DELETE CASCADE,
from_tenant_id bigint NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
to_tenant_id bigint NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
minutes integer NOT NULL,
UNIQUE(instructor_id, from_tenant_id, to_tenant_id)
);
CREATE INDEX IF NOT EXISTS idx_travel_times_instructor ON instructor_travel_times(instructor_id);
-- ══════════════════════════════════════════════════════
-- 13. instructor_home_to_tenant
-- ══════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS instructor_home_to_tenant (
id bigserial PRIMARY KEY,
instructor_id bigint NOT NULL REFERENCES instructors(id) ON DELETE CASCADE,
tenant_id bigint NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
minutes integer NOT NULL,
UNIQUE(instructor_id, tenant_id)
);
CREATE INDEX IF NOT EXISTS idx_home_to_tenant_instructor ON instructor_home_to_tenant(instructor_id);
-- ══════════════════════════════════════════════════════
-- 14. instructor_break_rules
-- ══════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS instructor_break_rules (
id bigserial PRIMARY KEY,
instructor_id bigint NOT NULL REFERENCES instructors(id) ON DELETE CASCADE,
condition_type text NOT NULL CHECK (condition_type IN ('daily_units', 'weekly_units', 'hours_daily', 'between_appointments', 'continuous_minutes')),
operator text NOT NULL CHECK (operator IN ('greater_than', 'equals', 'at_least')),
threshold numeric NOT NULL,
break_minutes integer NOT NULL,
is_active integer DEFAULT 1,
created_at timestamptz DEFAULT NOW(),
UNIQUE(instructor_id, condition_type, operator, threshold)
);
CREATE INDEX IF NOT EXISTS idx_break_rules_instructor ON instructor_break_rules(instructor_id);
-- ══════════════════════════════════════════════════════
-- 15. Default Registration-Codes für bestehende Tenants
-- ══════════════════════════════════════════════════════
INSERT INTO tenant_registration_codes (tenant_id, code, is_active)
SELECT id, LPAD(id::text, 8, '0') || '-' || UPPER(SUBSTRING(MD5(id::text || 'salt') FROM 1 FOR 4)), 1
FROM tenants
WHERE id NOT IN (SELECT tenant_id FROM tenant_registration_codes)
ON CONFLICT (code) DO NOTHING;
-- ══════════════════════════════════════════════════════
-- 16. Default Availability für alle Instructors (Mo-So 08:00-19:00)
-- ══════════════════════════════════════════════════════
INSERT INTO instructor_availability (instructor_id, weekday, time_from, time_to, is_active)
SELECT i.id, w.day, '08:00', '19:00', 1
FROM instructors i
CROSS JOIN (VALUES (0),(1),(2),(3),(4),(5),(6)) AS w(day)
WHERE i.is_active = 1
AND NOT EXISTS (
SELECT 1 FROM instructor_availability ia
WHERE ia.instructor_id = i.id AND ia.weekday = w.day
);
COMMIT;