135 lines
6.0 KiB
MySQL
135 lines
6.0 KiB
MySQL
|
|
-- 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;
|