bmc_hub/migrations/240_planner.sql

135 lines
6.0 KiB
SQL

-- Planner: independent case allocations, capacity sources and permissions.
ALTER TABLE sag_sager
ADD COLUMN IF NOT EXISTS estimated_minutes INTEGER;
ALTER TABLE sag_sager
ADD COLUMN IF NOT EXISTS type VARCHAR(50) NOT NULL DEFAULT 'ticket';
CREATE TABLE IF NOT EXISTS planner_settings (
id SMALLINT PRIMARY KEY DEFAULT 1 CHECK (id = 1),
warning_percent INTEGER NOT NULL DEFAULT 80 CHECK (warning_percent BETWEEN 1 AND 99),
full_percent INTEGER NOT NULL DEFAULT 100 CHECK (full_percent >= 100),
default_allocation_minutes INTEGER NOT NULL DEFAULT 60 CHECK (default_allocation_minutes > 0),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_by INTEGER REFERENCES users(user_id)
);
INSERT INTO planner_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
CREATE TABLE IF NOT EXISTS planner_work_schedules (
id BIGSERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
weekday SMALLINT NOT NULL CHECK (weekday BETWEEN 0 AND 6),
start_time TIME NOT NULL DEFAULT '08:00',
end_time TIME NOT NULL DEFAULT '16:00',
break_minutes INTEGER NOT NULL DEFAULT 30 CHECK (break_minutes >= 0),
valid_from DATE,
valid_to DATE,
active BOOLEAN NOT NULL DEFAULT TRUE,
CHECK (end_time > start_time)
);
CREATE INDEX IF NOT EXISTS idx_planner_work_user ON planner_work_schedules(user_id, weekday);
INSERT INTO planner_work_schedules (user_id, weekday, start_time, end_time, break_minutes)
SELECT u.user_id, d.weekday, '08:00', '16:00', 30
FROM users u CROSS JOIN (VALUES (0),(1),(2),(3),(4)) AS d(weekday)
WHERE u.is_active AND NOT EXISTS (
SELECT 1 FROM planner_work_schedules w WHERE w.user_id=u.user_id AND w.weekday=d.weekday
);
CREATE TABLE IF NOT EXISTS planner_absences (
id BIGSERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
starts_at TIMESTAMPTZ NOT NULL,
ends_at TIMESTAMPTZ NOT NULL,
kind VARCHAR(40) NOT NULL DEFAULT 'absence',
note TEXT,
CHECK (ends_at > starts_at)
);
CREATE INDEX IF NOT EXISTS idx_planner_absence_period ON planner_absences(user_id, starts_at, ends_at);
CREATE TABLE IF NOT EXISTS planner_external_busy (
id BIGSERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
provider VARCHAR(40) NOT NULL,
external_id VARCHAR(255) NOT NULL,
starts_at TIMESTAMPTZ NOT NULL,
ends_at TIMESTAMPTZ NOT NULL,
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CHECK (ends_at > starts_at),
UNIQUE(provider, external_id)
);
CREATE INDEX IF NOT EXISTS idx_planner_external_period ON planner_external_busy(user_id, starts_at, ends_at);
CREATE TABLE IF NOT EXISTS planner_integrations (
user_id INTEGER PRIMARY KEY REFERENCES users(user_id) ON DELETE CASCADE,
provider VARCHAR(40),
status VARCHAR(30) NOT NULL DEFAULT 'not_connected',
last_success_at TIMESTAMPTZ,
last_error_at TIMESTAMPTZ,
last_error TEXT,
CHECK (status IN ('connected', 'degraded', 'not_connected'))
);
CREATE TABLE IF NOT EXISTS planner_team_scope (
editor_user_id INTEGER NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
target_user_id INTEGER NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
PRIMARY KEY (editor_user_id, target_user_id)
);
CREATE TABLE IF NOT EXISTS planner_allocations (
id BIGSERIAL PRIMARY KEY,
sag_id INTEGER NOT NULL REFERENCES sag_sager(id),
user_id INTEGER NOT NULL REFERENCES users(user_id),
starts_at TIMESTAMPTZ NOT NULL,
ends_at TIMESTAMPTZ NOT NULL,
note TEXT,
source VARCHAR(40) NOT NULL DEFAULT 'manual',
conflict_status VARCHAR(30) NOT NULL DEFAULT 'none',
locked BOOLEAN NOT NULL DEFAULT FALSE,
version INTEGER NOT NULL DEFAULT 1,
created_by INTEGER NOT NULL REFERENCES users(user_id),
updated_by INTEGER NOT NULL 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),
CHECK (conflict_status IN ('none', 'overlap', 'overbooked', 'overlap_and_overbooked'))
);
ALTER TABLE planner_allocations ADD COLUMN IF NOT EXISTS locked BOOLEAN NOT NULL DEFAULT FALSE;
CREATE INDEX IF NOT EXISTS idx_planner_alloc_period ON planner_allocations(user_id, starts_at, ends_at) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_planner_alloc_case ON planner_allocations(sag_id) WHERE deleted_at IS NULL;
CREATE TABLE IF NOT EXISTS planner_allocation_history (
id BIGSERIAL PRIMARY KEY,
allocation_id BIGINT NOT NULL REFERENCES planner_allocations(id),
sag_id INTEGER NOT NULL REFERENCES sag_sager(id),
action VARCHAR(20) NOT NULL,
changed_by INTEGER NOT NULL REFERENCES users(user_id),
before_value JSONB,
after_value JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_planner_history_case ON planner_allocation_history(sag_id, created_at DESC);
INSERT INTO permissions (code, description, category) VALUES
('planner.view', 'Se relevante planer', 'planner'),
('planner.edit_own', 'Redigér egen plan', 'planner'),
('planner.edit_team', 'Redigér tilladte medarbejderes planer', 'planner'),
('planner.admin', 'Administrér kapacitet og grænser', 'planner')
ON CONFLICT (code) 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', 'planner.edit_own', 'planner.edit_team')
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 = 'Technicians' AND p.code IN ('planner.view', 'planner.edit_own')
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 = 'Viewers' AND p.code = 'planner.view'
ON CONFLICT DO NOTHING;