167 lines
11 KiB
PL/PgSQL
167 lines
11 KiB
PL/PgSQL
-- Additive planning extension, after 1069. Existing case, booking and time IDs are preserved.
|
|
ALTER TABLE sag_case_assignees
|
|
ADD COLUMN IF NOT EXISTS estimated_minutes INTEGER CHECK (estimated_minutes >= 0),
|
|
ADD COLUMN IF NOT EXISTS remaining_minutes INTEGER CHECK (remaining_minutes >= 0),
|
|
ADD COLUMN IF NOT EXISTS queue_position NUMERIC NOT NULL DEFAULT 0,
|
|
ADD COLUMN IF NOT EXISTS pinned BOOLEAN NOT NULL DEFAULT FALSE,
|
|
ADD COLUMN IF NOT EXISTS work_status TEXT NOT NULL DEFAULT 'ready'
|
|
CHECK (work_status IN ('ready','working','paused','waiting','done')),
|
|
ADD COLUMN IF NOT EXISTS snoozed_until TIMESTAMPTZ,
|
|
ADD COLUMN IF NOT EXISTS version INTEGER NOT NULL DEFAULT 1;
|
|
ALTER TABLE sag_sager ADD COLUMN IF NOT EXISTS start_time TIME;
|
|
ALTER TABLE planner_settings ADD COLUMN IF NOT EXISTS appointment_warning_minutes INTEGER NOT NULL DEFAULT 30;
|
|
ALTER TABLE planner_settings ADD COLUMN IF NOT EXISTS deadline_warning_days INTEGER NOT NULL DEFAULT 1;
|
|
ALTER TABLE planner_settings ADD COLUMN IF NOT EXISTS queue_initialized BOOLEAN NOT NULL DEFAULT FALSE;
|
|
CREATE TABLE IF NOT EXISTS planner_queue_versions (
|
|
user_id INTEGER PRIMARY KEY REFERENCES users(user_id), version INTEGER NOT NULL DEFAULT 1
|
|
);
|
|
DO $$ BEGIN
|
|
IF NOT COALESCE((SELECT queue_initialized FROM planner_settings WHERE id=1),FALSE) THEN
|
|
INSERT INTO sag_case_assignees(sag_id,user_id)
|
|
SELECT s.id,s.ansvarlig_bruger_id FROM sag_sager s
|
|
JOIN users u ON u.user_id=s.ansvarlig_bruger_id
|
|
WHERE s.deleted_at IS NULL
|
|
AND LOWER(s.status) NOT IN ('afsluttet','lukket','løst','resolved','closed','done','arkiveret','archived','annulleret','cancelled')
|
|
ON CONFLICT DO NOTHING;
|
|
INSERT INTO sag_case_assignees(sag_id,user_id)
|
|
SELECT DISTINCT a.sag_id,a.user_id FROM planner_allocations a
|
|
JOIN sag_sager s ON s.id=a.sag_id
|
|
WHERE a.deleted_at IS NULL AND s.deleted_at IS NULL
|
|
AND LOWER(s.status) NOT IN ('afsluttet','lukket','løst','resolved','closed','done','arkiveret','archived','annulleret','cancelled')
|
|
ON CONFLICT DO NOTHING;
|
|
WITH ranked AS (
|
|
SELECT a.sag_id,a.user_id,ROW_NUMBER() OVER (PARTITION BY a.user_id ORDER BY
|
|
CASE s.priority::text WHEN 'urgent' THEN 0 WHEN 'high' THEN 1 WHEN 'normal' THEN 2 ELSE 3 END,
|
|
s.deadline NULLS LAST,s.id)*1024 AS position
|
|
FROM sag_case_assignees a JOIN sag_sager s ON s.id=a.sag_id
|
|
)
|
|
UPDATE sag_case_assignees a SET queue_position=r.position FROM ranked r
|
|
WHERE a.sag_id=r.sag_id AND a.user_id=r.user_id AND a.queue_position=0;
|
|
INSERT INTO planner_settings(id,queue_initialized) VALUES(1,TRUE)
|
|
ON CONFLICT(id) DO UPDATE SET queue_initialized=TRUE;
|
|
END IF;
|
|
END $$;
|
|
CREATE INDEX IF NOT EXISTS planner_queue_order ON sag_case_assignees(user_id,pinned DESC,queue_position,sag_id);
|
|
-- Legacy assignment routes also invalidate queue snapshots and append new members.
|
|
CREATE OR REPLACE FUNCTION planner_assignee_membership() RETURNS trigger AS $$
|
|
DECLARE uid INTEGER;
|
|
BEGIN
|
|
uid=CASE WHEN TG_OP='DELETE' THEN OLD.user_id ELSE NEW.user_id END;
|
|
PERFORM id FROM sag_sager WHERE id=CASE WHEN TG_OP='DELETE' THEN OLD.sag_id ELSE NEW.sag_id END FOR KEY SHARE;
|
|
PERFORM pg_advisory_xact_lock(252,uid);
|
|
INSERT INTO planner_queue_versions(user_id,version) VALUES(uid,2)
|
|
ON CONFLICT(user_id) DO UPDATE SET version=planner_queue_versions.version+1;
|
|
IF TG_OP='INSERT' AND NEW.queue_position=0 THEN
|
|
SELECT COALESCE(MAX(queue_position),0)+1024 INTO NEW.queue_position FROM sag_case_assignees WHERE user_id=uid;
|
|
END IF;
|
|
IF TG_OP='DELETE' THEN RETURN OLD; END IF;
|
|
RETURN NEW;
|
|
END $$ LANGUAGE plpgsql;
|
|
DROP TRIGGER IF EXISTS planner_assignee_membership ON sag_case_assignees;
|
|
CREATE TRIGGER planner_assignee_membership BEFORE INSERT OR DELETE ON sag_case_assignees
|
|
FOR EACH ROW EXECUTE FUNCTION planner_assignee_membership();
|
|
CREATE TABLE IF NOT EXISTS planner_events (
|
|
id BIGSERIAL PRIMARY KEY, sag_id INTEGER REFERENCES sag_sager(id),
|
|
title TEXT NOT NULL, description TEXT, kind TEXT NOT NULL DEFAULT 'work',
|
|
starts_at TIMESTAMPTZ NOT NULL, ends_at TIMESTAMPTZ NOT NULL,
|
|
version INTEGER NOT NULL DEFAULT 1, created_by INTEGER REFERENCES users(user_id),
|
|
updated_by INTEGER REFERENCES users(user_id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), deleted_at TIMESTAMPTZ,
|
|
CHECK(ends_at>starts_at)
|
|
);
|
|
ALTER TABLE planner_allocations ALTER COLUMN sag_id DROP NOT NULL;
|
|
ALTER TABLE planner_allocations ADD COLUMN IF NOT EXISTS event_id BIGINT REFERENCES planner_events(id);
|
|
-- One event per legacy booking, deliberately no inference of shared meetings.
|
|
DO $$ DECLARE a RECORD; eid BIGINT; BEGIN
|
|
FOR a IN SELECT p.*,s.titel FROM planner_allocations p JOIN sag_sager s ON s.id=p.sag_id WHERE event_id IS NULL LOOP
|
|
INSERT INTO planner_events(sag_id,title,description,starts_at,ends_at,created_by,updated_by,deleted_at)
|
|
VALUES(a.sag_id,a.titel,a.note,a.starts_at,a.ends_at,a.created_by,a.updated_by,a.deleted_at) RETURNING id INTO eid;
|
|
UPDATE planner_allocations SET event_id=eid WHERE id=a.id;
|
|
END LOOP;
|
|
END $$;
|
|
CREATE UNIQUE INDEX IF NOT EXISTS planner_event_participant ON planner_allocations(event_id,user_id) WHERE deleted_at IS NULL;
|
|
-- Legacy booking writes stay in sync with the shared event model.
|
|
CREATE OR REPLACE FUNCTION planner_booking_event() RETURNS trigger AS $$
|
|
DECLARE eid BIGINT; parent planner_events%ROWTYPE;
|
|
BEGIN
|
|
IF NEW.deleted_at IS NULL AND NEW.sag_id IS NOT NULL AND (TG_OP='INSERT' OR NEW.ends_at>NOW()) THEN
|
|
INSERT INTO sag_case_assignees(sag_id,user_id,assigned_by_user_id)
|
|
VALUES(NEW.sag_id,NEW.user_id,NEW.updated_by) ON CONFLICT DO NOTHING;
|
|
END IF;
|
|
IF NEW.event_id IS NULL THEN
|
|
INSERT INTO planner_events(sag_id,title,description,starts_at,ends_at,created_by,updated_by,deleted_at)
|
|
VALUES(NEW.sag_id,COALESCE((SELECT titel FROM sag_sager WHERE id=NEW.sag_id),'Aftale'),
|
|
NEW.note,NEW.starts_at,NEW.ends_at,NEW.created_by,NEW.updated_by,NEW.deleted_at) RETURNING id INTO eid;
|
|
UPDATE planner_allocations SET event_id=eid WHERE id=NEW.id;
|
|
ELSE
|
|
SELECT * INTO parent FROM planner_events WHERE id=NEW.event_id FOR UPDATE;
|
|
IF NEW.deleted_at IS NOT NULL THEN
|
|
IF parent.deleted_at IS NULL AND NOT EXISTS(SELECT 1 FROM planner_allocations WHERE event_id=NEW.event_id AND deleted_at IS NULL) THEN
|
|
UPDATE planner_events SET deleted_at=NOW(),version=version+1 WHERE id=NEW.event_id;
|
|
END IF;
|
|
RETURN NEW;
|
|
END IF;
|
|
IF parent.starts_at IS DISTINCT FROM NEW.starts_at OR parent.ends_at IS DISTINCT FROM NEW.ends_at
|
|
OR parent.sag_id IS DISTINCT FROM NEW.sag_id OR parent.description IS DISTINCT FROM NEW.note THEN
|
|
IF (SELECT COUNT(*) FROM planner_allocations WHERE event_id=NEW.event_id AND deleted_at IS NULL)>1 THEN
|
|
RAISE EXCEPTION 'Shared appointments must be moved through the event API';
|
|
END IF;
|
|
UPDATE planner_events SET starts_at=NEW.starts_at,ends_at=NEW.ends_at,sag_id=NEW.sag_id,description=NEW.note,
|
|
version=version+1,updated_by=NEW.updated_by,updated_at=NOW() WHERE id=NEW.event_id;
|
|
END IF;
|
|
IF parent.deleted_at IS NOT NULL THEN
|
|
UPDATE planner_events SET deleted_at=NULL,version=version+1 WHERE id=NEW.event_id;
|
|
END IF;
|
|
END IF;
|
|
RETURN NEW;
|
|
END $$ LANGUAGE plpgsql;
|
|
DROP TRIGGER IF EXISTS planner_booking_event ON planner_allocations;
|
|
CREATE TRIGGER planner_booking_event AFTER INSERT OR UPDATE ON planner_allocations
|
|
FOR EACH ROW EXECUTE FUNCTION planner_booking_event();
|
|
ALTER TABLE planner_allocation_history ALTER COLUMN allocation_id DROP NOT NULL;
|
|
ALTER TABLE planner_allocation_history ALTER COLUMN sag_id DROP NOT NULL;
|
|
ALTER TABLE planner_allocation_history ADD COLUMN IF NOT EXISTS entity_type TEXT DEFAULT 'allocation';
|
|
ALTER TABLE planner_allocation_history ADD COLUMN IF NOT EXISTS entity_id BIGINT;
|
|
CREATE TABLE IF NOT EXISTS planner_activity_types (
|
|
code TEXT PRIMARY KEY, label TEXT NOT NULL, active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
is_absence BOOLEAN NOT NULL DEFAULT TRUE
|
|
);
|
|
INSERT INTO planner_activity_types(code,label) VALUES
|
|
('vacation','Ferie'),('sickness','Sygdom'),('child_sickness','Barns sygedag'),
|
|
('time_off','Afspadsering'),('course','Kursus'),('absence','Andet fravær') ON CONFLICT DO NOTHING;
|
|
INSERT INTO planner_activity_types(code,label,is_absence) VALUES
|
|
('customer_work','Kundearbejde',FALSE),('internal_work','Internt arbejde',FALSE),
|
|
('internal_meeting','Internt møde',FALSE),('customer_meeting','Kundemøde',FALSE) ON CONFLICT DO NOTHING;
|
|
INSERT INTO planner_activity_types(code,label)
|
|
SELECT DISTINCT kind,kind FROM planner_absences WHERE kind IS NOT NULL ON CONFLICT DO NOTHING;
|
|
ALTER TABLE planner_absences ADD COLUMN IF NOT EXISTS version INTEGER NOT NULL DEFAULT 1;
|
|
ALTER TABLE planner_absences ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
|
ALTER TABLE planner_absences ADD COLUMN IF NOT EXISTS created_by INTEGER REFERENCES users(user_id);
|
|
ALTER TABLE tmodule_times ADD COLUMN IF NOT EXISTS planner_absence_id BIGINT REFERENCES planner_absences(id);
|
|
ALTER TABLE tmodule_times ADD COLUMN IF NOT EXISTS planner_activity_code TEXT REFERENCES planner_activity_types(code);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS planner_actual_absence_day ON tmodule_times(planner_absence_id,worked_date) WHERE planner_absence_id IS NOT NULL;
|
|
DROP INDEX IF EXISTS planner_actual_absence;
|
|
DO $$ BEGIN
|
|
IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname='planner_absence_nonbillable' AND conrelid='tmodule_times'::regclass) THEN
|
|
ALTER TABLE tmodule_times ADD CONSTRAINT planner_absence_nonbillable
|
|
CHECK(planner_absence_id IS NULL OR (billable IS FALSE AND COALESCE(fakturerbar_tid_min,0)=0));
|
|
END IF;
|
|
END $$;
|
|
CREATE INDEX IF NOT EXISTS planner_time_employee_case ON tmodule_times(sag_id,medarbejder_id) WHERE status<>'rejected';
|
|
CREATE INDEX IF NOT EXISTS planner_live_absence_period ON planner_absences(user_id,starts_at,ends_at) WHERE deleted_at IS NULL;
|
|
CREATE UNIQUE INDEX IF NOT EXISTS planner_reminder_key ON sag_reminders((trigger_config->>'planner_key'))
|
|
WHERE trigger_config ? 'planner_key';
|
|
INSERT INTO permissions(code,description,category) VALUES
|
|
('planner.view_team','Se tilladte kollegers planer','planner'),
|
|
('planner.allocate','Allokere medarbejdere','planner'),
|
|
('planner.absence_own','Registrere eget fravær','planner'),
|
|
('planner.absence_team','Registrere andres fravær','planner'),
|
|
('planner.absence_reason','Se fraværsårsager','planner') ON CONFLICT DO NOTHING;
|
|
INSERT INTO group_permissions(group_id,permission_id)
|
|
SELECT g.id,p.id FROM groups g CROSS JOIN permissions p
|
|
WHERE g.name='Administrators' AND p.category='planner' ON CONFLICT DO NOTHING;
|
|
INSERT INTO group_permissions(group_id,permission_id)
|
|
SELECT g.id,p.id FROM groups g CROSS JOIN permissions p
|
|
WHERE (g.name='Managers' AND p.code IN ('planner.view_team','planner.allocate','planner.absence_own','planner.absence_team'))
|
|
OR (g.name='Technicians' AND p.code='planner.absence_own') ON CONFLICT DO NOTHING;
|