chore: Projektbasis und PostgreSQL-Migrationen einrichten

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Paul SchoeneckandCopilot committed 2026-11-03 08:01:24 +01:00
commit 10af6159b4
14 files changed
+2214

No files matched your search

+97
View File
@@ -0,0 +1,97 @@
CREATE TABLE pickup_contacts (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
child_id uuid NOT NULL REFERENCES children(id) ON DELETE CASCADE,
name text NOT NULL,
relationship text NOT NULL,
phone text NOT NULL DEFAULT '',
authorized boolean NOT NULL DEFAULT true,
notes text NOT NULL DEFAULT ''
);
CREATE INDEX pickup_contacts_child ON pickup_contacts(child_id);
CREATE TABLE alone_rules (
child_id uuid NOT NULL REFERENCES children(id) ON DELETE CASCADE,
weekday integer NOT NULL CHECK (weekday BETWEEN 1 AND 7),
earliest time NOT NULL,
PRIMARY KEY(child_id, weekday)
);
ALTER TABLE attendance DROP CONSTRAINT attendance_pickup_kind_check;
ALTER TABLE attendance ADD CONSTRAINT attendance_pickup_kind_check
CHECK (pickup_kind IN ('planned', 'parents', 'alone'));
ALTER TABLE attendance ADD CONSTRAINT attendance_chronology
CHECK (picked_up_at IS NULL OR picked_up_at >= arrived_at);
CREATE SEQUENCE attendance_revision;
ALTER TABLE attendance ADD COLUMN version bigint NOT NULL DEFAULT nextval('attendance_revision');
CREATE FUNCTION bump_attendance_revision() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
NEW.version := nextval('attendance_revision');
RETURN NEW;
END;
$$;
CREATE TRIGGER attendance_revision_trigger BEFORE UPDATE ON attendance
FOR EACH ROW EXECUTE FUNCTION bump_attendance_revision();
CREATE TABLE audit_log (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
child_id uuid NOT NULL,
entity text NOT NULL,
entity_id text,
day date,
operation text NOT NULL CHECK (operation IN ('INSERT', 'UPDATE', 'DELETE')),
actor_id uuid,
actor_name text NOT NULL,
reason text NOT NULL,
before_data jsonb,
after_data jsonb,
created_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
CREATE INDEX audit_log_child ON audit_log(child_id, id DESC);
CREATE FUNCTION record_child_audit() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE
old_data jsonb;
new_data jsonb;
row_data jsonb;
actor uuid;
actor_label text;
BEGIN
IF TG_OP <> 'INSERT' THEN old_data := to_jsonb(OLD); END IF;
IF TG_OP <> 'DELETE' THEN new_data := to_jsonb(NEW); END IF;
IF TG_OP = 'UPDATE' AND old_data - 'version' = new_data - 'version' THEN RETURN NEW; END IF;
row_data := COALESCE(new_data, old_data);
actor := NULLIF(current_setting('kibu.actor_id', true), '')::uuid;
SELECT name || ' (' || email || ')' INTO actor_label FROM staff WHERE id = actor;
INSERT INTO audit_log(child_id, entity, entity_id, day, operation, actor_id, actor_name,
reason, before_data, after_data)
VALUES (
CASE WHEN TG_TABLE_NAME = 'children' THEN (row_data->>'id')::uuid ELSE (row_data->>'child_id')::uuid END,
TG_TABLE_NAME, row_data->>'id', (row_data->>'day')::date, TG_OP, actor,
COALESCE(actor_label, 'Datenbank / Einrichtung'),
COALESCE(NULLIF(current_setting('kibu.reason', true), ''), 'Direkte Datenbankänderung'),
old_data, new_data
);
RETURN COALESCE(NEW, OLD);
END;
$$;
CREATE TRIGGER children_audit AFTER INSERT OR UPDATE OR DELETE ON children FOR EACH ROW EXECUTE FUNCTION record_child_audit();
CREATE TRIGGER weekly_audit AFTER INSERT OR UPDATE OR DELETE ON weekly_schedules FOR EACH ROW EXECUTE FUNCTION record_child_audit();
CREATE TRIGGER override_audit AFTER INSERT OR UPDATE OR DELETE ON day_overrides FOR EACH ROW EXECUTE FUNCTION record_child_audit();
CREATE TRIGGER attendance_audit AFTER INSERT OR UPDATE OR DELETE ON attendance FOR EACH ROW EXECUTE FUNCTION record_child_audit();
CREATE TRIGGER contacts_audit AFTER INSERT OR UPDATE OR DELETE ON pickup_contacts FOR EACH ROW EXECUTE FUNCTION record_child_audit();
CREATE TRIGGER alone_audit AFTER INSERT OR UPDATE OR DELETE ON alone_rules FOR EACH ROW EXECUTE FUNCTION record_child_audit();
CREATE FUNCTION protect_audit_log() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
RAISE EXCEPTION 'Das Audit-Log ist unveraenderlich.';
END;
$$;
CREATE TRIGGER audit_immutable BEFORE UPDATE OR DELETE OR TRUNCATE ON audit_log
FOR EACH STATEMENT EXECUTE FUNCTION protect_audit_log();
INSERT INTO audit_log(child_id, entity, day, operation, actor_name, reason, after_data)
SELECT child_id, 'attendance', day, 'INSERT', 'Datenbank / Einrichtung',
'Bestandsübernahme bei Einführung des Audit-Logs; ursprünglicher Änderungsverlauf nicht verfügbar.',
to_jsonb(a) FROM attendance a;
@@ -0,0 +1,57 @@
CREATE TABLE pickup_slots (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
starts_at time NOT NULL,
ends_at time NOT NULL,
active boolean NOT NULL DEFAULT true,
CHECK (starts_at < ends_at),
UNIQUE(starts_at, ends_at)
);
INSERT INTO pickup_slots(starts_at, ends_at) VALUES
('14:00', '14:15'), ('15:00', '15:15'), ('16:00', '16:15'), ('17:00', '17:15');
ALTER TABLE weekly_schedules ADD COLUMN pickup_end time;
ALTER TABLE weekly_schedules ADD COLUMN pickup_slot_id uuid REFERENCES pickup_slots(id);
ALTER TABLE weekly_schedules ADD CONSTRAINT weekly_pickup_window CHECK(pickup_end IS NULL OR pickup_end > pickup);
ALTER TABLE day_overrides ADD COLUMN pickup_end time;
ALTER TABLE day_overrides ADD COLUMN pickup_slot_id uuid REFERENCES pickup_slots(id);
ALTER TABLE day_overrides ADD CONSTRAINT override_pickup_window CHECK(pickup_end IS NULL OR (attending AND pickup_end > pickup));
UPDATE weekly_schedules w SET pickup_slot_id = s.id, pickup_end = s.ends_at FROM pickup_slots s WHERE w.pickup = s.starts_at;
UPDATE day_overrides o SET pickup_slot_id = s.id, pickup_end = s.ends_at FROM pickup_slots s WHERE o.pickup = s.starts_at;
CREATE TABLE admission_rounds (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL,
code_hash text NOT NULL UNIQUE,
expires_on date NOT NULL,
status text NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'closed', 'drawn')),
created_by uuid NOT NULL REFERENCES staff(id),
created_at timestamptz NOT NULL DEFAULT now(),
drawn_by uuid REFERENCES staff(id),
drawn_at timestamptz,
places integer CHECK(places > 0),
CHECK ((status = 'drawn' AND drawn_at IS NOT NULL AND drawn_by IS NOT NULL AND places IS NOT NULL)
OR (status <> 'drawn' AND drawn_at IS NULL AND drawn_by IS NULL AND places IS NULL))
);
CREATE TABLE applications (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
round_id uuid NOT NULL REFERENCES admission_rounds(id),
first_name text NOT NULL,
last_name text NOT NULL,
class_name text NOT NULL,
parent_name text NOT NULL,
email text NOT NULL,
notes text NOT NULL DEFAULT '',
status text NOT NULL DEFAULT 'waiting' CHECK(status IN ('waiting', 'selected', 'waitlisted', 'accepted')),
child_id uuid UNIQUE REFERENCES children(id),
submitted_at timestamptz NOT NULL DEFAULT now(),
accepted_at timestamptz,
accepted_by uuid REFERENCES staff(id),
CHECK ((status = 'accepted' AND child_id IS NOT NULL AND accepted_at IS NOT NULL AND accepted_by IS NOT NULL)
OR (status <> 'accepted' AND child_id IS NULL AND accepted_at IS NULL AND accepted_by IS NULL))
);
CREATE UNIQUE INDEX application_unique_child ON applications(round_id, lower(first_name), lower(last_name), lower(email));
CREATE INDEX applications_round ON applications(round_id, submitted_at, id);
CREATE TABLE application_limits (
key_hash text PRIMARY KEY,
attempts integer NOT NULL,
window_start timestamptz NOT NULL
);