add dukcapil

This commit is contained in:
Aditya Siregar
2026-05-07 04:01:32 +07:00
parent 9a975d146e
commit bc64eb20ea
235 changed files with 1197 additions and 28187 deletions
@@ -0,0 +1,2 @@
-- Rollback: Drop users table
DROP TABLE IF EXISTS users;
@@ -0,0 +1,14 @@
-- Example migration: Create users table
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username VARCHAR(255) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
full_name VARCHAR(255),
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_username ON users(username);
-14
View File
@@ -1,14 +0,0 @@
DROP TABLE IF EXISTS position_roles;
DROP TABLE IF EXISTS user_position;
DROP TABLE IF EXISTS user_department;
DROP TABLE IF EXISTS positions;
DROP TABLE IF EXISTS departments;
DROP TABLE IF EXISTS role_permissions;
DROP TABLE IF EXISTS user_role;
DROP TABLE IF EXISTS permissions;
DROP TABLE IF EXISTS roles;
DROP TABLE IF EXISTS users;
DROP FUNCTION IF EXISTS set_updated_at() CASCADE;
-146
View File
@@ -1,146 +0,0 @@
-- ESLOGAD Core Init (Users, Roles, Permissions, Departments, Positions)
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
CREATE EXTENSION IF NOT EXISTS ltree;
-- Helper to auto-update updated_at
CREATE OR REPLACE FUNCTION set_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- =======================
-- USERS
-- =======================
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username VARCHAR(100) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
name VARCHAR(255),
email VARCHAR(255) UNIQUE,
status VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (status IN ('active','inactive')),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
last_login_at TIMESTAMP WITHOUT TIME ZONE,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE TRIGGER trg_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- =======================
-- ROLES & PERMISSIONS
-- =======================
CREATE TABLE IF NOT EXISTS roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL, -- e.g., SUPERADMIN
code TEXT UNIQUE NOT NULL, -- e.g., superadmin
description TEXT,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TRIGGER trg_roles_updated_at
BEFORE UPDATE ON roles
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
CREATE TABLE IF NOT EXISTS permissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code TEXT UNIQUE NOT NULL, -- e.g., letter.view
description TEXT,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TRIGGER trg_permissions_updated_at
BEFORE UPDATE ON permissions
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
CREATE TABLE IF NOT EXISTS user_role (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
assigned_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
removed_at TIMESTAMP WITHOUT TIME ZONE
)
;
CREATE UNIQUE INDEX IF NOT EXISTS uq_user_role_active
ON user_role(user_id, role_id) WHERE removed_at IS NULL;
CREATE TABLE IF NOT EXISTS role_permissions (
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
permission_id UUID NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,
PRIMARY KEY (role_id, permission_id)
);
CREATE TABLE IF NOT EXISTS departments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
code TEXT UNIQUE,
path LTREE UNIQUE NOT NULL, -- e.g., eslogad.aslog.waaslog_faskon_bmn
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_departments_path_gist
ON departments USING GIST (path);
CREATE TRIGGER trg_departments_updated_at
BEFORE UPDATE ON departments
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
CREATE TABLE IF NOT EXISTS positions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL, -- e.g., PABAN III/FASKON
code TEXT UNIQUE, -- e.g., paban-III-faskon
path LTREE UNIQUE NOT NULL, -- hierarchy within org chart
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_positions_path_gist
ON positions USING GIST (path);
CREATE TRIGGER trg_positions_updated_at
BEFORE UPDATE ON positions
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
CREATE TABLE IF NOT EXISTS user_department (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
department_id UUID NOT NULL REFERENCES departments(id) ON DELETE CASCADE,
is_primary BOOLEAN NOT NULL DEFAULT FALSE,
assigned_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
removed_at TIMESTAMP WITHOUT TIME ZONE
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_user_department_active
ON user_department(user_id, department_id) WHERE removed_at IS NULL;
CREATE TRIGGER trg_user_department_updated_at
BEFORE UPDATE ON user_department
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
CREATE TABLE IF NOT EXISTS user_position (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
position_id UUID NOT NULL REFERENCES positions(id) ON DELETE CASCADE,
assigned_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
removed_at TIMESTAMP WITHOUT TIME ZONE
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_user_position_active
ON user_position(user_id, position_id) WHERE removed_at IS NULL;
CREATE TRIGGER trg_user_position_updated_at
BEFORE UPDATE ON user_position
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
CREATE TABLE IF NOT EXISTS position_roles (
position_id UUID NOT NULL REFERENCES positions(id) ON DELETE CASCADE,
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
PRIMARY KEY (position_id, role_id)
);
@@ -0,0 +1,2 @@
-- Rollback: Drop user profiles table
DROP TABLE IF EXISTS user_profiles;
@@ -0,0 +1,13 @@
-- Example migration: Create user profiles table
CREATE TABLE IF NOT EXISTS user_profiles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
avatar_url VARCHAR(500),
bio TEXT,
phone VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id)
);
CREATE INDEX idx_user_profiles_user_id ON user_profiles(user_id);
-119
View File
@@ -1,119 +0,0 @@
BEGIN;
-- =========================
-- Positions (hierarchy)
-- =========================
-- Conventions:
-- - superadmin is a separate root
-- - eslogad.aslog is head; waaslog_* under aslog
-- - paban_* under each waaslog_*; pabandya_* under its paban_*
INSERT INTO departments (name, code, path) VALUES
-- ROOTS
('SUPERADMIN', 'superadmin', 'superadmin'),
('ASLOG', 'aslog', 'eslogad.aslog'),
-- WAASLOG under ASLOG
('WAASLOG RENBINMINLOG', 'waaslogrenbinminlog', 'eslogad.aslog.waaslog_renbinminlog'),
('WAASLOG FASKON BMN', 'waaslogfaskonbmn', 'eslogad.aslog.waaslog_faskon_bmn'),
('WAASLOG BEKPALKES', 'waaslogbekpalkes', 'eslogad.aslog.waaslog_bekpalkes'),
-- Other posts directly under ASLOG
('KADISADAAD', 'kadisadaad', 'eslogad.aslog.kadisadaad'),
('KATUUD', 'katuud', 'eslogad.aslog.katuud'),
('SPRI', 'spri', 'eslogad.aslog.spri'),
-- PABAN under WAASLOG RENBINMINLOG
('PABAN I/REN', 'paban-I-ren', 'eslogad.aslog.waaslog_renbinminlog.paban_I_ren'),
('PABAN II/BINMINLOG', 'paban-II-binminlog', 'eslogad.aslog.waaslog_renbinminlog.paban_II_binminlog'),
-- PABAN under WAASLOG FASKON BMN
('PABAN III/FASKON', 'paban-III-faskon', 'eslogad.aslog.waaslog_faskon_bmn.paban_III_faskon'),
('PABAN IV/BMN', 'paban-iv-bmn', 'eslogad.aslog.waaslog_faskon_bmn.paban_IV_bmn'),
-- PABAN under WAASLOG BEKPALKES
('PABAN V/BEK', 'paban-v-bek', 'eslogad.aslog.waaslog_bekpalkes.paban_V_bek'),
('PABAN VI/ALPAL', 'paban-vi-alpal', 'eslogad.aslog.waaslog_bekpalkes.paban_VI_alpal'),
('PABAN VII/KES', 'paban-vii-kes', 'eslogad.aslog.waaslog_bekpalkes.paban_VII_kes'),
-- PABANDYA under PABAN I/REN
('PABANDYA 1 / RENPROGGAR', 'pabandya-1-renproggar', 'eslogad.aslog.waaslog_renbinminlog.paban_I_ren.pabandya_1_renproggar'),
('PABANDYA 2 / DALWASGAR', 'pabandya-2-dalwasgar', 'eslogad.aslog.waaslog_renbinminlog.paban_I_ren.pabandya_2_dalwasgar'),
('PABANDYA 3 / ANEVDATA', 'pabandya-3-anevdata', 'eslogad.aslog.waaslog_renbinminlog.paban_I_ren.pabandya_3_anevdata'),
-- PABANDYA under PABAN II/BINMINLOG
('PABANDYA 1 / MINLOG', 'pabandya-1-minlog', 'eslogad.aslog.waaslog_renbinminlog.paban_II_binminlog.pabandya_1_minlog'),
('PABANDYA 2 / HIBAHKOD', 'pabandya-2-hibahkod', 'eslogad.aslog.waaslog_renbinminlog.paban_II_binminlog.pabandya_2_hibahkod'),
('PABANDYA 3 / PUSMAT', 'pabandya-3-pusmat', 'eslogad.aslog.waaslog_renbinminlog.paban_II_binminlog.pabandya_3_pusmat'),
-- PABANDYA under PABAN IV/BMN
('PABANDYA 1 / TANAH', 'pabandya-1-tanah', 'eslogad.aslog.waaslog_faskon_bmn.paban_IV_bmn.pabandya_1_tanah'),
('PABANDYA 2 / PANGKALAN KONSTRUKSI','pabandya-2-pangkalankonstruksi','eslogad.aslog.waaslog_faskon_bmn.paban_IV_bmn.pabandya_2_pangkalan_konstruksi'),
('PABANDYA 3 / FASMATZI', 'pabandya-3-fasmatzi', 'eslogad.aslog.waaslog_faskon_bmn.paban_IV_bmn.pabandya_3_fasmatzi'),
-- PABANDYA under PABAN IV/BMN (AKUN group)
('PABANDYA 1 / AKUN BB', 'pabandya-1-akunbb', 'eslogad.aslog.waaslog_faskon_bmn.paban_IV_bmn.pabandya_1_akun_bb'),
('PABANDYA 2 / AKUN BTB', 'pabandya-2-akunbtb', 'eslogad.aslog.waaslog_faskon_bmn.paban_IV_bmn.pabandya_2_akun_btb'),
('PABANDYA 3 / SISFO BMN DAN UAKPB-KP','pabandya-3-sisfo-bmn-uakpbkp','eslogad.aslog.waaslog_faskon_bmn.paban_IV_bmn.pabandya_3_sisfo_bmn_uakpb_kp'),
-- PABANDYA under PABAN III/FASKON
('PABANDYA 1 / JATOPTIKMU', 'pabandya-1-jatoptikmu', 'eslogad.aslog.waaslog_faskon_bmn.paban_III_faskon.pabandya_1_jatoptikmu'),
('PABANDYA 2 / RANTEKMEK', 'pabandya-2-rantekmek', 'eslogad.aslog.waaslog_faskon_bmn.paban_III_faskon.pabandya_2_rantekmek'),
('PABANDYA 3 / ALHUBTOPPALSUS', 'pabandya-3-alhubtoppalsus', 'eslogad.aslog.waaslog_faskon_bmn.paban_III_faskon.pabandya_3_alhubtoppalsus'),
('PABANDYA 4 / PESUD', 'pabandya-4-pesud', 'eslogad.aslog.waaslog_faskon_bmn.paban_III_faskon.pabandya_4_pesud'),
-- PABANDYA under PABAN VII/KES
('PABANDYA 1 / BEKKES', 'pabandya-1-bekkes', 'eslogad.aslog.waaslog_bekpalkes.paban_VII_kes.pabandya_1_bekkes'),
('PABANDYA 2 / ALKES', 'pabandya-2-alkes', 'eslogad.aslog.waaslog_bekpalkes.paban_VII_kes.pabandya_2_alkes')
ON CONFLICT (code) DO UPDATE
SET name = EXCLUDED.name,
path = EXCLUDED.path,
updated_at = CURRENT_TIMESTAMP;
-- =========================
-- SUPERADMIN role (minimal)
-- =========================
INSERT INTO roles (name, code, description) VALUES
('SUPERADMIN', 'superadmin', 'Full system access and management'),
('ADMIN', 'admin', 'Manage users, letters, and settings within their department'),
('HEAD', 'head', 'Approve outgoing letters and manage dispositions in their department'),
('STAFF', 'staff', 'Create letters, process assigned dispositions')
ON CONFLICT (code) DO UPDATE
SET name = EXCLUDED.name,
description = EXCLUDED.description,
updated_at = CURRENT_TIMESTAMP;
-- =========================
-- Users (seed 1 superadmin)
-- =========================
-- Replace the plaintext password as needed; pgcrypto hashes it with bcrypt.
INSERT INTO users (username, password_hash, name, email, status, is_active)
VALUES ('superadmin',
crypt('ChangeMe!Super#123', gen_salt('bf')),
'Super Admin',
'superadmin@example.com',
'active',
TRUE)
ON CONFLICT (username) DO UPDATE
SET name = EXCLUDED.name,
email = EXCLUDED.email,
status = EXCLUDED.status,
is_active = EXCLUDED.is_active,
updated_at = CURRENT_TIMESTAMP;
-- =========================
-- Link: SUPERADMIN user ↔ role ↔ position
-- =========================
WITH u AS (SELECT id FROM users WHERE username = 'superadmin'),
r AS (SELECT id FROM roles WHERE code = 'superadmin'),
p AS (SELECT id FROM positions WHERE code = 'superadmin')
INSERT INTO user_role (user_id, role_id)
SELECT u.id, r.id FROM u, r
ON CONFLICT (user_id, role_id) WHERE removed_at IS NULL DO NOTHING;
WITH u AS (SELECT id FROM users WHERE username = 'superadmin'),
p AS (SELECT id FROM positions WHERE code = 'superadmin')
INSERT INTO user_position (user_id, position_id)
SELECT u.id, p.id FROM u, p
ON CONFLICT (user_id, position_id) WHERE removed_at IS NULL DO NOTHING;
COMMIT;
@@ -1,30 +0,0 @@
INSERT INTO permissions (id, code, description, created_at, updated_at) VALUES
-- Users
(gen_random_uuid(), 'user.read', 'View user list and details', now(), now()),
(gen_random_uuid(), 'user.create', 'Create new users', now(), now()),
(gen_random_uuid(), 'user.update', 'Edit existing users', now(), now()),
(gen_random_uuid(), 'user.delete', 'Delete users', now(), now()),
-- Roles
(gen_random_uuid(), 'role.read', 'View roles', now(), now()),
(gen_random_uuid(), 'role.create', 'Create new roles', now(), now()),
(gen_random_uuid(), 'role.update', 'Edit existing roles', now(), now()),
(gen_random_uuid(), 'role.delete', 'Delete roles', now(), now()),
-- Permissions
(gen_random_uuid(), 'permission.read', 'View permissions', now(), now()),
(gen_random_uuid(), 'permission.create', 'Create new permissions', now(), now()),
(gen_random_uuid(), 'permission.update', 'Edit existing permissions', now(), now()),
(gen_random_uuid(), 'permission.delete', 'Delete permissions', now(), now()),
-- Departments
(gen_random_uuid(), 'department.read', 'View departments', now(), now()),
(gen_random_uuid(), 'department.create', 'Create new departments', now(), now()),
(gen_random_uuid(), 'department.update', 'Edit existing departments', now(), now()),
(gen_random_uuid(), 'department.delete', 'Delete departments', now(), now()),
-- Positions
(gen_random_uuid(), 'position.read', 'View positions', now(), now()),
(gen_random_uuid(), 'position.create', 'Create new positions', now(), now()),
(gen_random_uuid(), 'position.update', 'Edit existing positions', now(), now()),
(gen_random_uuid(), 'position.delete', 'Delete positions', now(), now());
-1
View File
@@ -1 +0,0 @@
DROP TABLE IF EXISTS user_profiles;
-30
View File
@@ -1,30 +0,0 @@
BEGIN;
CREATE TABLE IF NOT EXISTS user_profiles (
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
full_name VARCHAR(150) NOT NULL,
display_name VARCHAR(100),
phone VARCHAR(50),
avatar_url TEXT,
job_title VARCHAR(120),
employee_no VARCHAR(60),
bio TEXT,
timezone VARCHAR(64) DEFAULT 'Asia/Jakarta',
locale VARCHAR(16) DEFAULT 'id-ID',
preferences JSONB NOT NULL DEFAULT '{}'::jsonb,
notification_prefs JSONB NOT NULL DEFAULT '{}'::jsonb,
last_seen_at TIMESTAMP WITHOUT TIME ZONE,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_user_profiles_phone ON user_profiles(phone);
CREATE INDEX IF NOT EXISTS idx_user_profiles_employee_no ON user_profiles(employee_no);
CREATE INDEX IF NOT EXISTS idx_user_profiles_prefs_gin ON user_profiles USING GIN (preferences);
CREATE INDEX IF NOT EXISTS idx_user_profiles_notif_gin ON user_profiles USING GIN (notification_prefs);
CREATE TRIGGER trg_user_profiles_updated_at
BEFORE UPDATE ON user_profiles
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
COMMIT;
-60
View File
@@ -1,60 +0,0 @@
-- =======================
-- TITLES
-- =======================
CREATE TABLE IF NOT EXISTS titles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL, -- e.g., "Senior Software Engineer"
code TEXT UNIQUE, -- e.g., "senior-software-engineer"
description TEXT, -- optional: extra details
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Trigger for updated_at
CREATE TRIGGER trg_titles_updated_at
BEFORE UPDATE ON titles
FOR EACH ROW
EXECUTE FUNCTION set_updated_at();
-- Perwira Tinggi (High-ranking Officers)
INSERT INTO titles (name, code, description) VALUES
('Jenderal', 'jenderal', 'Pangkat tertinggi di TNI AD'),
('Letnan Jenderal', 'letnan-jenderal', 'Pangkat tinggi di bawah Jenderal'),
('Mayor Jenderal', 'mayor-jenderal', 'Pangkat tinggi di bawah Letnan Jenderal'),
('Brigadir Jenderal', 'brigadir-jenderal', 'Pangkat tinggi di bawah Mayor Jenderal');
-- Perwira Menengah (Middle-ranking Officers)
INSERT INTO titles (name, code, description) VALUES
('Kolonel', 'kolonel', 'Pangkat perwira menengah tertinggi'),
('Letnan Kolonel', 'letnan-kolonel', 'Pangkat perwira menengah di bawah Kolonel'),
('Mayor', 'mayor', 'Pangkat perwira menengah di bawah Letnan Kolonel');
-- Perwira Pertama (Junior Officers)
INSERT INTO titles (name, code, description) VALUES
('Kapten', 'kapten', 'Pangkat perwira pertama tertinggi'),
('Letnan Satu', 'letnan-satu', 'Pangkat perwira pertama di bawah Kapten'),
('Letnan Dua', 'letnan-dua', 'Pangkat perwira pertama di bawah Letnan Satu');
-- Bintara Tinggi (Senior NCOs)
INSERT INTO titles (name, code, description) VALUES
('Pembantu Letnan Satu', 'pembantu-letnan-satu', 'Pangkat bintara tinggi tertinggi'),
('Pembantu Letnan Dua', 'pembantu-letnan-dua', 'Pangkat bintara tinggi di bawah Pelda');
-- Bintara (NCOs)
INSERT INTO titles (name, code, description) VALUES
('Sersan Mayor', 'sersan-mayor', 'Pangkat bintara di bawah Pelda'),
('Sersan Kepala', 'sersan-kepala', 'Pangkat bintara di bawah Serma'),
('Sersan Satu', 'sersan-satu', 'Pangkat bintara di bawah Serka'),
('Sersan Dua', 'sersan-dua', 'Pangkat bintara di bawah Sertu');
-- Tamtama Tinggi (Senior Enlisted)
INSERT INTO titles (name, code, description) VALUES
('Kopral Kepala', 'kopral-kepala', 'Pangkat tamtama tinggi tertinggi'),
('Kopral Satu', 'kopral-satu', 'Pangkat tamtama tinggi di bawah Kopka'),
('Kopral Dua', 'kopral-dua', 'Pangkat tamtama tinggi di bawah Koptu');
-- Tamtama (Enlisted)
INSERT INTO titles (name, code, description) VALUES
('Prajurit Kepala', 'prajurit-kepala', 'Pangkat tamtama di bawah Kopda'),
('Prajurit Satu', 'prajurit-satu', 'Pangkat tamtama di bawah Prada'),
('Prajurit Dua', 'prajurit-dua', 'Pangkat tamtama terendah');
@@ -1,7 +0,0 @@
BEGIN;
DROP TABLE IF EXISTS institutions;
DROP TABLE IF EXISTS priorities;
DROP TABLE IF EXISTS labels;
COMMIT;
@@ -1,52 +0,0 @@
BEGIN;
-- =======================
-- LABELS
-- =======================
CREATE TABLE IF NOT EXISTS labels (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
color VARCHAR(16), -- HEX color code (e.g., #FF0000)
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TRIGGER trg_labels_updated_at
BEFORE UPDATE ON labels
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- =======================
-- PRIORITIES
-- =======================
CREATE TABLE IF NOT EXISTS priorities (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
level INT NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TRIGGER trg_priorities_updated_at
BEFORE UPDATE ON priorities
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- =======================
-- INSTITUTIONS
-- =======================
CREATE TABLE IF NOT EXISTS institutions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
type TEXT NOT NULL CHECK (type IN ('government','private','ngo','individual')),
address TEXT,
contact_person VARCHAR(255),
phone VARCHAR(50),
email VARCHAR(255),
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TRIGGER trg_institutions_updated_at
BEFORE UPDATE ON institutions
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
COMMIT;
@@ -1,5 +0,0 @@
BEGIN;
DROP TABLE IF EXISTS disposition_actions;
COMMIT;
@@ -1,23 +0,0 @@
BEGIN;
-- =======================
-- DISPOSITION ACTIONS
-- =======================
CREATE TABLE IF NOT EXISTS disposition_actions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code TEXT UNIQUE NOT NULL,
label TEXT NOT NULL,
description TEXT,
requires_note BOOLEAN NOT NULL DEFAULT FALSE,
group_name TEXT,
sort_order INT,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TRIGGER trg_disposition_actions_updated_at
BEFORE UPDATE ON disposition_actions
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
COMMIT;
@@ -1,17 +0,0 @@
BEGIN;
DROP TABLE IF EXISTS letter_incoming_activity_logs;
DROP TABLE IF EXISTS letter_incoming_discussion_attachments;
DROP TABLE IF EXISTS letter_incoming_discussions;
DROP TABLE IF EXISTS letter_disposition_actions;
DROP TABLE IF EXISTS disposition_notes;
DROP TABLE IF EXISTS letter_incoming_dispositions_department;
DROP TABLE IF EXISTS letter_incoming_dispositions;
DROP TABLE IF EXISTS letter_incoming_attachments;
DROP TABLE IF EXISTS letter_incoming_labels;
DROP TABLE IF EXISTS letter_incoming_recipients;
DROP TABLE IF EXISTS letters_incoming;
DROP SEQUENCE IF EXISTS letters_incoming_seq;
COMMIT;
@@ -1,189 +0,0 @@
BEGIN;
-- =======================
-- SEQUENCE FOR LETTER NUMBER
-- =======================
CREATE SEQUENCE IF NOT EXISTS letters_incoming_seq;
-- =======================
-- LETTERS INCOMING
-- =======================
CREATE TABLE IF NOT EXISTS letters_incoming (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
letter_number TEXT NOT NULL UNIQUE DEFAULT ('IN-' || lpad(nextval('letters_incoming_seq')::text, 8, '0')),
reference_number TEXT,
subject TEXT NOT NULL,
description TEXT,
priority_id UUID REFERENCES priorities(id) ON DELETE SET NULL,
sender_institution_id UUID REFERENCES institutions(id) ON DELETE SET NULL,
received_date DATE NOT NULL,
due_date DATE,
status TEXT NOT NULL DEFAULT 'new' CHECK (status IN ('new','in_progress','completed')),
created_by UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP WITHOUT TIME ZONE
);
CREATE INDEX IF NOT EXISTS idx_letters_incoming_status ON letters_incoming(status);
CREATE INDEX IF NOT EXISTS idx_letters_incoming_received_date ON letters_incoming(received_date);
CREATE TRIGGER trg_letters_incoming_updated_at
BEFORE UPDATE ON letters_incoming
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- =======================
-- LETTER INCOMING RECIPIENTS
-- =======================
CREATE TABLE IF NOT EXISTS letter_incoming_recipients (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
letter_id UUID NOT NULL REFERENCES letters_incoming(id) ON DELETE CASCADE,
recipient_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
recipient_department_id UUID REFERENCES departments(id) ON DELETE SET NULL,
status TEXT NOT NULL DEFAULT 'new' CHECK (status IN ('new','read','completed')),
read_at TIMESTAMP WITHOUT TIME ZONE,
completed_at TIMESTAMP WITHOUT TIME ZONE,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);I
CREATE INDEX IF NOT EXISTS idx_letter_incoming_recipients_letter ON letter_incoming_recipients(letter_id);
-- =======================
-- LETTER INCOMING LABELS (M:N)
-- =======================
CREATE TABLE IF NOT EXISTS letter_incoming_labels (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
letter_id UUID NOT NULL REFERENCES letters_incoming(id) ON DELETE CASCADE,
label_id UUID NOT NULL REFERENCES labels(id) ON DELETE CASCADE,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE (letter_id, label_id)
);
CREATE INDEX IF NOT EXISTS idx_letter_incoming_labels_letter ON letter_incoming_labels(letter_id);
-- =======================
-- LETTER INCOMING ATTACHMENTS
-- =======================
CREATE TABLE IF NOT EXISTS letter_incoming_attachments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
letter_id UUID NOT NULL REFERENCES letters_incoming(id) ON DELETE CASCADE,
file_url TEXT NOT NULL,
file_name TEXT NOT NULL,
file_type TEXT NOT NULL,
uploaded_by UUID REFERENCES users(id) ON DELETE SET NULL,
uploaded_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_letter_incoming_attachments_letter ON letter_incoming_attachments(letter_id);
-- =======================
-- LETTER DISPOSITIONS
-- =======================
CREATE TABLE IF NOT EXISTS letter_dispositions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
letter_id UUID NOT NULL REFERENCES letters_incoming(id) ON DELETE CASCADE,
from_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
from_department_id UUID REFERENCES departments(id) ON DELETE SET NULL,
to_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
to_department_id UUID REFERENCES departments(id) ON DELETE SET NULL,
notes TEXT,
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','read','rejected','completed')),
created_by UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
read_at TIMESTAMP WITHOUT TIME ZONE,
completed_at TIMESTAMP WITHOUT TIME ZONE,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_letter_dispositions_letter ON letter_dispositions(letter_id);
CREATE TRIGGER trg_letter_dispositions_updated_at
BEFORE UPDATE ON letter_dispositions
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- =======================
-- DISPOSITION NOTES
-- =======================
CREATE TABLE IF NOT EXISTS disposition_notes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
disposition_id UUID NOT NULL REFERENCES letter_incoming_dispositions(id) ON DELETE CASCADE,
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
note TEXT NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_disposition_notes_disposition ON disposition_notes(disposition_id);
-- =======================
-- LETTER DISPOSITION ACTIONS (Selections)
-- =======================
CREATE TABLE IF NOT EXISTS letter_disposition_actions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
disposition_id UUID NOT NULL REFERENCES letter_incoming_dispositions(id) ON DELETE CASCADE,
action_id UUID NOT NULL REFERENCES disposition_actions(id) ON DELETE RESTRICT,
note TEXT,
created_by UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE (disposition_id, action_id)
);
CREATE INDEX IF NOT EXISTS idx_letter_disposition_actions_disposition ON letter_disposition_actions(disposition_id);
-- =======================
-- LETTER INCOMING DISCUSSIONS (Threaded)
-- =======================
CREATE TABLE IF NOT EXISTS letter_incoming_discussions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
letter_id UUID NOT NULL REFERENCES letters_incoming(id) ON DELETE CASCADE,
parent_id UUID REFERENCES letter_incoming_discussions(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
message TEXT NOT NULL,
mentions JSONB,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
edited_at TIMESTAMP WITHOUT TIME ZONE
);
CREATE INDEX IF NOT EXISTS idx_letter_incoming_discussions_letter ON letter_incoming_discussions(letter_id);
CREATE INDEX IF NOT EXISTS idx_letter_incoming_discussions_parent ON letter_incoming_discussions(parent_id);
CREATE TRIGGER trg_letter_incoming_discussions_updated_at
BEFORE UPDATE ON letter_incoming_discussions
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- =======================
-- LETTER INCOMING DISCUSSION ATTACHMENTS
-- =======================
CREATE TABLE IF NOT EXISTS letter_incoming_discussion_attachments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
discussion_id UUID NOT NULL REFERENCES letter_incoming_discussions(id) ON DELETE CASCADE,
file_url TEXT NOT NULL,
file_name TEXT NOT NULL,
file_type TEXT NOT NULL,
uploaded_by UUID REFERENCES users(id) ON DELETE SET NULL,
uploaded_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_letter_incoming_discussion_attachments_discussion ON letter_incoming_discussion_attachments(discussion_id);
-- =======================
-- LETTER INCOMING ACTIVITY LOGS (Immutable)
-- =======================
CREATE TABLE IF NOT EXISTS letter_incoming_activity_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
letter_id UUID NOT NULL REFERENCES letters_incoming(id) ON DELETE CASCADE,
action_type TEXT NOT NULL,
actor_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
actor_department_id UUID REFERENCES departments(id) ON DELETE SET NULL,
target_type TEXT,
target_id UUID,
from_status TEXT,
to_status TEXT,
context JSONB,
occurred_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_letter_incoming_activity_logs_letter ON letter_incoming_activity_logs(letter_id);
CREATE INDEX IF NOT EXISTS idx_letter_incoming_activity_logs_action ON letter_incoming_activity_logs(action_type);
COMMIT;
@@ -1,5 +0,0 @@
BEGIN;
DROP TABLE IF EXISTS disposition_routes;
COMMIT;
@@ -1,27 +0,0 @@
BEGIN;
-- =======================
-- DISPOSITION ROUTES
-- =======================
CREATE TABLE IF NOT EXISTS disposition_routes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
from_department_id UUID NOT NULL REFERENCES departments(id) ON DELETE CASCADE,
to_department_id UUID NOT NULL REFERENCES departments(id) ON DELETE CASCADE,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
allowed_actions JSONB,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_disposition_routes_from_dept ON disposition_routes(from_department_id);
-- Prevent duplicate active routes from -> to
CREATE UNIQUE INDEX IF NOT EXISTS uq_disposition_routes_active
ON disposition_routes(from_department_id, to_department_id)
WHERE is_active = TRUE;
CREATE TRIGGER trg_disposition_routes_updated_at
BEFORE UPDATE ON disposition_routes
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
COMMIT;
-4
View File
@@ -1,4 +0,0 @@
BEGIN;
DROP TRIGGER IF EXISTS trg_app_settings_updated_at ON app_settings;
DROP TABLE IF EXISTS app_settings;
COMMIT;
-21
View File
@@ -1,21 +0,0 @@
BEGIN;
CREATE TABLE IF NOT EXISTS app_settings (
key TEXT PRIMARY KEY,
value JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TRIGGER trg_app_settings_updated_at
BEFORE UPDATE ON app_settings
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
INSERT INTO app_settings(key, value)
VALUES
('INCOMING_LETTER_PREFIX', '{"value": "ESLI"}'::jsonb),
('INCOMING_LETTER_SEQUENCE', '{"value": 0}'::jsonb),
('INCOMING_LETTER_RECIPIENTS', '{"department_codes": ["aslog"]}'::jsonb)
ON CONFLICT (key) DO NOTHING;
COMMIT;
@@ -1,41 +0,0 @@
BEGIN;
-- =======================
-- DROP NEW ASSOCIATION TABLE
-- =======================
DROP TABLE IF EXISTS letter_incoming_dispositions_department;
-- =======================
-- RESTORE LETTER DISPOSITIONS TABLE STRUCTURE
-- =======================
-- Add back the columns that were removed
ALTER TABLE letter_incoming_dispositions ADD COLUMN IF NOT EXISTS from_user_id UUID REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE letter_incoming_dispositions ADD COLUMN IF NOT EXISTS to_user_id UUID REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE letter_incoming_dispositions ADD COLUMN IF NOT EXISTS to_department_id UUID REFERENCES departments(id) ON DELETE SET NULL;
ALTER TABLE letter_incoming_dispositions ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','read','rejected','completed'));
ALTER TABLE letter_incoming_dispositions ADD COLUMN IF NOT EXISTS completed_at TIMESTAMP WITHOUT TIME ZONE;
-- Rename department_id back to from_department_id
ALTER TABLE letter_incoming_dispositions RENAME COLUMN department_id TO from_department_id;
-- =======================
-- RESTORE TRIGGERS AND INDEXES
-- =======================
-- Drop new trigger
DROP TRIGGER IF EXISTS trg_letter_incoming_dispositions_updated_at ON letter_incoming_dispositions;
-- Restore old trigger
CREATE TRIGGER trg_letter_dispositions_updated_at
BEFORE UPDATE ON letter_incoming_dispositions
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- Restore index names
DROP INDEX IF EXISTS idx_letter_incoming_dispositions_letter;
CREATE INDEX IF NOT EXISTS idx_letter_dispositions_letter ON letter_incoming_dispositions(letter_id);
-- =======================
-- RENAME TABLE BACK
-- =======================
ALTER TABLE letter_incoming_dispositions RENAME TO letter_dispositions;
COMMIT;
@@ -1,54 +0,0 @@
BEGIN;
-- =======================
-- RENAME LETTER DISPOSITIONS TABLE
-- =======================
ALTER TABLE letter_dispositions RENAME TO letter_incoming_dispositions;
-- =======================
-- MODIFY LETTER INCOMING DISPOSITIONS TABLE STRUCTURE
-- =======================
-- Drop existing columns that are not needed
ALTER TABLE letter_incoming_dispositions DROP COLUMN IF EXISTS from_user_id;
ALTER TABLE letter_incoming_dispositions DROP COLUMN IF EXISTS to_user_id;
ALTER TABLE letter_incoming_dispositions DROP COLUMN IF EXISTS to_department_id;
ALTER TABLE letter_incoming_dispositions DROP COLUMN IF EXISTS status;
ALTER TABLE letter_incoming_dispositions DROP COLUMN IF EXISTS completed_at;
-- Rename from_department_id to department_id
ALTER TABLE letter_incoming_dispositions RENAME COLUMN from_department_id TO department_id;
-- Add missing columns if they don't exist
ALTER TABLE letter_incoming_dispositions ADD COLUMN IF NOT EXISTS read_at TIMESTAMP WITHOUT TIME ZONE;
ALTER TABLE letter_incoming_dispositions ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP;
-- =======================
-- CREATE LETTER INCOMING DISPOSITIONS DEPARTMENT ASSOCIATION TABLE
-- =======================
CREATE TABLE IF NOT EXISTS letter_incoming_dispositions_department (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
letter_incoming_disposition_id UUID NOT NULL REFERENCES letter_incoming_dispositions(id) ON DELETE CASCADE,
department_id UUID NOT NULL REFERENCES departments(id) ON DELETE CASCADE,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE (letter_incoming_disposition_id, department_id)
);
CREATE INDEX IF NOT EXISTS idx_letter_incoming_dispositions_department_disposition ON letter_incoming_dispositions_department(letter_incoming_disposition_id);
CREATE INDEX IF NOT EXISTS idx_letter_incoming_dispositions_department_dept ON letter_incoming_dispositions_department(department_id);
-- =======================
-- UPDATE TRIGGERS AND INDEXES
-- =======================
-- Drop old trigger
DROP TRIGGER IF EXISTS trg_letter_dispositions_updated_at ON letter_incoming_dispositions;
-- Create new trigger
CREATE TRIGGER trg_letter_incoming_dispositions_updated_at
BEFORE UPDATE ON letter_incoming_dispositions
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- Update index names
DROP INDEX IF EXISTS idx_letter_dispositions_letter;
CREATE INDEX IF NOT EXISTS idx_letter_incoming_dispositions_letter ON letter_incoming_dispositions(letter_id);
COMMIT;
@@ -1,24 +0,0 @@
BEGIN;
-- Drop triggers first
DROP TRIGGER IF EXISTS trg_letter_outgoing_discussions_updated_at ON letter_outgoing_discussions;
DROP TRIGGER IF EXISTS trg_approval_flow_steps_updated_at ON approval_flow_steps;
DROP TRIGGER IF EXISTS trg_approval_flows_updated_at ON approval_flows;
DROP TRIGGER IF EXISTS trg_letters_outgoing_updated_at ON letters_outgoing;
-- Drop tables in reverse order (due to foreign key constraints)
DROP TABLE IF EXISTS letter_outgoing_activity_logs;
DROP TABLE IF EXISTS letter_outgoing_discussion_attachments;
DROP TABLE IF EXISTS letter_outgoing_discussions;
DROP TABLE IF EXISTS letter_outgoing_approvals;
DROP TABLE IF EXISTS letter_outgoing_attachments;
DROP TABLE IF EXISTS letter_outgoing_labels;
DROP TABLE IF EXISTS letter_outgoing_recipients;
DROP TABLE IF EXISTS letters_outgoing;
DROP TABLE IF EXISTS approval_flow_steps;
DROP TABLE IF EXISTS approval_flows;
-- Drop sequence
DROP SEQUENCE IF EXISTS letters_outgoing_seq;
COMMIT;
@@ -1,199 +0,0 @@
BEGIN;
-- =======================
-- SEQUENCE FOR LETTER NUMBER
-- =======================
CREATE SEQUENCE IF NOT EXISTS letters_outgoing_seq;
-- =======================
-- APPROVAL FLOWS
-- =======================
CREATE TABLE IF NOT EXISTS approval_flows (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
department_id UUID NOT NULL REFERENCES departments(id) ON DELETE RESTRICT,
name TEXT NOT NULL,
description TEXT,
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_approval_flows_department ON approval_flows(department_id);
CREATE INDEX IF NOT EXISTS idx_approval_flows_active ON approval_flows(is_active);
CREATE TRIGGER trg_approval_flows_updated_at
BEFORE UPDATE ON approval_flows
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- =======================
-- APPROVAL FLOW STEPS
-- =======================
CREATE TABLE IF NOT EXISTS approval_flow_steps (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
flow_id UUID NOT NULL REFERENCES approval_flows(id) ON DELETE CASCADE,
step_order INT NOT NULL,
parallel_group INT DEFAULT 1,
approver_role_id UUID REFERENCES roles(id) ON DELETE SET NULL,
approver_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
required BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(flow_id, step_order, parallel_group, approver_role_id, approver_user_id),
CHECK ((approver_role_id IS NOT NULL) OR (approver_user_id IS NOT NULL))
);
CREATE INDEX IF NOT EXISTS idx_approval_flow_steps_flow ON approval_flow_steps(flow_id);
CREATE INDEX IF NOT EXISTS idx_approval_flow_steps_order ON approval_flow_steps(flow_id, step_order);
CREATE TRIGGER trg_approval_flow_steps_updated_at
BEFORE UPDATE ON approval_flow_steps
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- =======================
-- LETTERS OUTGOING
-- =======================
CREATE TABLE IF NOT EXISTS letters_outgoing (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
letter_number TEXT NOT NULL UNIQUE DEFAULT ('OUT-' || lpad(nextval('letters_outgoing_seq')::text, 8, '0')),
reference_number TEXT,
subject TEXT NOT NULL,
description TEXT,
priority_id UUID REFERENCES priorities(id) ON DELETE SET NULL,
receiver_institution_id UUID REFERENCES institutions(id) ON DELETE SET NULL,
issue_date DATE NOT NULL,
status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft','pending_approval','approved','sent','archived')),
approval_flow_id UUID REFERENCES approval_flows(id) ON DELETE SET NULL,
created_by UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP WITHOUT TIME ZONE
);
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_status ON letters_outgoing(status);
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_issue_date ON letters_outgoing(issue_date);
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_approval_flow ON letters_outgoing(approval_flow_id);
CREATE TRIGGER trg_letters_outgoing_updated_at
BEFORE UPDATE ON letters_outgoing
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- =======================
-- LETTER OUTGOING RECIPIENTS
-- =======================
CREATE TABLE IF NOT EXISTS letter_outgoing_recipients (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
letter_id UUID NOT NULL REFERENCES letters_outgoing(id) ON DELETE CASCADE,
recipient_name TEXT NOT NULL,
recipient_email TEXT,
recipient_position TEXT,
recipient_institution TEXT,
is_primary BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_recipients_letter ON letter_outgoing_recipients(letter_id);
-- =======================
-- LETTER OUTGOING LABELS (M:N)
-- =======================
CREATE TABLE IF NOT EXISTS letter_outgoing_labels (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
letter_id UUID NOT NULL REFERENCES letters_outgoing(id) ON DELETE CASCADE,
label_id UUID NOT NULL REFERENCES labels(id) ON DELETE CASCADE,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE (letter_id, label_id)
);
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_labels_letter ON letter_outgoing_labels(letter_id);
-- =======================
-- LETTER OUTGOING ATTACHMENTS
-- =======================
CREATE TABLE IF NOT EXISTS letter_outgoing_attachments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
letter_id UUID NOT NULL REFERENCES letters_outgoing(id) ON DELETE CASCADE,
file_url TEXT NOT NULL,
file_name TEXT NOT NULL,
file_type TEXT NOT NULL,
uploaded_by UUID REFERENCES users(id) ON DELETE SET NULL,
uploaded_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_attachments_letter ON letter_outgoing_attachments(letter_id);
-- =======================
-- LETTER OUTGOING APPROVALS
-- =======================
CREATE TABLE IF NOT EXISTS letter_outgoing_approvals (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
letter_id UUID NOT NULL REFERENCES letters_outgoing(id) ON DELETE CASCADE,
step_id UUID NOT NULL REFERENCES approval_flow_steps(id) ON DELETE CASCADE,
approver_id UUID REFERENCES users(id) ON DELETE SET NULL,
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','approved','rejected')),
remarks TEXT,
acted_at TIMESTAMP WITHOUT TIME ZONE,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_approvals_letter ON letter_outgoing_approvals(letter_id);
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_approvals_step ON letter_outgoing_approvals(step_id);
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_approvals_status ON letter_outgoing_approvals(status);
-- =======================
-- LETTER OUTGOING DISCUSSIONS (Threaded)
-- =======================
CREATE TABLE IF NOT EXISTS letter_outgoing_discussions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
letter_id UUID NOT NULL REFERENCES letters_outgoing(id) ON DELETE CASCADE,
parent_id UUID REFERENCES letter_outgoing_discussions(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
message TEXT NOT NULL,
mentions JSONB,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
edited_at TIMESTAMP WITHOUT TIME ZONE
);
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_discussions_letter ON letter_outgoing_discussions(letter_id);
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_discussions_parent ON letter_outgoing_discussions(parent_id);
CREATE TRIGGER trg_letter_outgoing_discussions_updated_at
BEFORE UPDATE ON letter_outgoing_discussions
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- =======================
-- LETTER OUTGOING DISCUSSION ATTACHMENTS
-- =======================
CREATE TABLE IF NOT EXISTS letter_outgoing_discussion_attachments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
discussion_id UUID NOT NULL REFERENCES letter_outgoing_discussions(id) ON DELETE CASCADE,
file_url TEXT NOT NULL,
file_name TEXT NOT NULL,
file_type TEXT NOT NULL,
uploaded_by UUID REFERENCES users(id) ON DELETE SET NULL,
uploaded_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_discussion_attachments_discussion ON letter_outgoing_discussion_attachments(discussion_id);
-- =======================
-- LETTER OUTGOING ACTIVITY LOGS (Immutable)
-- =======================
CREATE TABLE IF NOT EXISTS letter_outgoing_activity_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
letter_id UUID NOT NULL REFERENCES letters_outgoing(id) ON DELETE CASCADE,
action_type TEXT NOT NULL,
actor_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
actor_department_id UUID REFERENCES departments(id) ON DELETE SET NULL,
target_type TEXT,
target_id UUID,
from_status TEXT,
to_status TEXT,
context JSONB,
occurred_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_activity_logs_letter ON letter_outgoing_activity_logs(letter_id);
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_activity_logs_action ON letter_outgoing_activity_logs(action_type);
COMMIT;
@@ -1,15 +0,0 @@
-- Rollback modules and permissions update
-- Remove the new structured permissions
DELETE FROM permissions WHERE code LIKE '%_READ' OR code LIKE '%_WRITE' OR code LIKE '%_CREATE' OR code LIKE '%_DELETE';
-- Drop indexes
DROP INDEX IF EXISTS idx_permissions_module_id;
-- Remove columns from permissions table
ALTER TABLE permissions
DROP COLUMN IF EXISTS module_id,
DROP COLUMN IF EXISTS action;
-- Drop modules table
DROP TABLE IF EXISTS modules CASCADE;
@@ -1,65 +0,0 @@
-- Add modules table and update permissions structure
-- Create modules table
CREATE TABLE IF NOT EXISTS modules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
code TEXT UNIQUE NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TRIGGER trg_modules_updated_at
BEFORE UPDATE ON modules
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- Add module_id and action columns to permissions table
ALTER TABLE permissions
ADD COLUMN IF NOT EXISTS module_id UUID REFERENCES modules(id) ON DELETE CASCADE,
ADD COLUMN IF NOT EXISTS action TEXT;
-- Create index on module_id for better query performance
CREATE INDEX IF NOT EXISTS idx_permissions_module_id ON permissions(module_id);
-- Seed initial modules
INSERT INTO modules (name, code) VALUES
('User Management', 'USER_MANAGEMENT'),
('Content Management', 'CONTENT_MANAGEMENT'),
('Letter Management', 'LETTER_MANAGEMENT'),
('Disposition Management', 'DISPOSITION_MANAGEMENT'),
('Reporting', 'REPORTING'),
('Settings', 'SETTINGS')
ON CONFLICT (code) DO NOTHING;
-- Update existing permissions to include module_id and action
-- This is a sample mapping - adjust based on your existing permission codes
UPDATE permissions SET
module_id = (SELECT id FROM modules WHERE code = 'LETTER_MANAGEMENT'),
action = 'READ'
WHERE code LIKE 'letter.%' AND code LIKE '%.view';
UPDATE permissions SET
module_id = (SELECT id FROM modules WHERE code = 'LETTER_MANAGEMENT'),
action = 'WRITE'
WHERE code LIKE 'letter.%' AND code LIKE '%.edit';
UPDATE permissions SET
module_id = (SELECT id FROM modules WHERE code = 'LETTER_MANAGEMENT'),
action = 'CREATE'
WHERE code LIKE 'letter.%' AND code LIKE '%.create';
UPDATE permissions SET
module_id = (SELECT id FROM modules WHERE code = 'LETTER_MANAGEMENT'),
action = 'DELETE'
WHERE code LIKE 'letter.%' AND code LIKE '%.delete';
-- Insert new structured permissions for each module
INSERT INTO permissions (module_id, action, code, description)
SELECT
m.id,
a.action,
CONCAT(m.code, '_', a.action),
CONCAT('Can ', LOWER(a.action), ' ', LOWER(m.name))
FROM modules m
CROSS JOIN (VALUES ('READ'), ('WRITE'), ('CREATE'), ('DELETE')) AS a(action)
ON CONFLICT (code) DO NOTHING;
@@ -1,14 +0,0 @@
-- Revert changes to letter_outgoing_recipients table
ALTER TABLE letter_outgoing_recipients
DROP COLUMN IF EXISTS user_id,
DROP COLUMN IF EXISTS department_id,
DROP COLUMN IF EXISTS status,
DROP COLUMN IF EXISTS read_at,
DROP COLUMN IF EXISTS flag,
DROP COLUMN IF EXISTS is_archived;
-- Drop indexes
DROP INDEX IF EXISTS idx_letter_outgoing_recipients_user;
DROP INDEX IF EXISTS idx_letter_outgoing_recipients_department;
DROP INDEX IF EXISTS idx_letter_outgoing_recipients_status;
DROP INDEX IF EXISTS idx_letter_outgoing_recipients_archived;
@@ -1,14 +0,0 @@
-- Add missing fields to letter_outgoing_recipients table to match incoming letter structure
ALTER TABLE letter_outgoing_recipients
ADD COLUMN IF NOT EXISTS user_id UUID REFERENCES users(id),
ADD COLUMN IF NOT EXISTS department_id UUID REFERENCES departments(id),
ADD COLUMN IF NOT EXISTS status TEXT DEFAULT 'pending',
ADD COLUMN IF NOT EXISTS read_at TIMESTAMP WITHOUT TIME ZONE,
ADD COLUMN IF NOT EXISTS flag TEXT,
ADD COLUMN IF NOT EXISTS is_archived BOOLEAN DEFAULT false;
-- Add indexes for better query performance
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_recipients_user ON letter_outgoing_recipients(user_id);
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_recipients_department ON letter_outgoing_recipients(department_id);
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_recipients_status ON letter_outgoing_recipients(status);
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_recipients_archived ON letter_outgoing_recipients(is_archived);
@@ -1,4 +0,0 @@
ALTER TABLE letter_outgoing_recipients
ADD COLUMN IF NOT EXISTS recipient_name VARCHAR(255) NOT NULL,
ADD COLUMN IF NOT EXISTS recipient_email VARCHAR(255) NOT NULL,
ADD COLUMN IF NOT EXISTS recipient_position VARCHAR(255) NOT NULL;
@@ -1,4 +0,0 @@
ALTER TABLE letter_outgoing_recipients
DROP COLUMN IF EXISTS recipient_name,
DROP COLUMN IF EXISTS recipient_email,
DROP COLUMN IF EXISTS recipient_position;
@@ -1,12 +0,0 @@
-- Drop triggers
DROP TRIGGER IF EXISTS update_document_sessions_updated_at ON document_sessions;
DROP TRIGGER IF EXISTS update_document_metadata_updated_at ON document_metadata;
-- Drop function
DROP FUNCTION IF EXISTS update_updated_at_column();
-- Drop tables
DROP TABLE IF EXISTS document_errors;
DROP TABLE IF EXISTS document_metadata;
DROP TABLE IF EXISTS document_versions;
DROP TABLE IF EXISTS document_sessions;
@@ -1,91 +0,0 @@
-- Create document sessions table for OnlyOffice integration
CREATE TABLE IF NOT EXISTS document_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
document_id UUID NOT NULL,
document_key VARCHAR(255) UNIQUE NOT NULL,
user_id UUID NOT NULL REFERENCES users(id),
status INTEGER NOT NULL DEFAULT 0,
is_locked BOOLEAN DEFAULT false,
locked_by UUID REFERENCES users(id),
locked_at TIMESTAMP WITHOUT TIME ZONE,
last_saved_at TIMESTAMP WITHOUT TIME ZONE,
version INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Create indexes for document sessions
CREATE INDEX idx_document_sessions_document ON document_sessions(document_id);
CREATE INDEX idx_document_sessions_user ON document_sessions(user_id);
CREATE INDEX idx_document_sessions_status ON document_sessions(status);
CREATE INDEX idx_document_sessions_locked ON document_sessions(is_locked);
-- Create document versions table
CREATE TABLE IF NOT EXISTS document_versions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
document_id UUID NOT NULL,
version INTEGER NOT NULL,
file_url TEXT NOT NULL,
file_size BIGINT NOT NULL,
changes_url TEXT,
saved_by UUID NOT NULL REFERENCES users(id),
saved_at TIMESTAMP WITHOUT TIME ZONE NOT NULL,
is_active BOOLEAN DEFAULT false,
comments TEXT,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Create indexes for document versions
CREATE INDEX idx_document_versions_document ON document_versions(document_id);
CREATE INDEX idx_document_versions_active ON document_versions(is_active);
CREATE UNIQUE INDEX idx_document_versions_active_unique ON document_versions(document_id, is_active) WHERE is_active = true;
-- Create document metadata table
CREATE TABLE IF NOT EXISTS document_metadata (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
document_id UUID UNIQUE NOT NULL,
document_type VARCHAR(50) NOT NULL,
reference_id UUID NOT NULL,
file_name TEXT NOT NULL,
file_type VARCHAR(50) NOT NULL,
file_size BIGINT NOT NULL,
mime_type VARCHAR(255),
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Create indexes for document metadata
CREATE INDEX idx_document_metadata_document ON document_metadata(document_id);
CREATE INDEX idx_document_metadata_type ON document_metadata(document_type);
CREATE INDEX idx_document_metadata_reference ON document_metadata(reference_id);
-- Create document errors table for logging
CREATE TABLE IF NOT EXISTS document_errors (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
document_id UUID NOT NULL,
session_id UUID REFERENCES document_sessions(id),
error_type VARCHAR(100) NOT NULL,
error_msg TEXT NOT NULL,
details JSONB,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Create indexes for document errors
CREATE INDEX idx_document_errors_document ON document_errors(document_id);
CREATE INDEX idx_document_errors_session ON document_errors(session_id);
CREATE INDEX idx_document_errors_type ON document_errors(error_type);
-- Add trigger to update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE TRIGGER update_document_sessions_updated_at BEFORE UPDATE ON document_sessions
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_document_metadata_updated_at BEFORE UPDATE ON document_metadata
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
@@ -1,12 +0,0 @@
-- Drop performance indexes
DROP INDEX IF EXISTS idx_letters_outgoing_id_deleted;
DROP INDEX IF EXISTS idx_users_id;
DROP INDEX IF EXISTS idx_document_sessions_document_key;
DROP INDEX IF EXISTS idx_document_metadata_document_id;
DROP INDEX IF EXISTS idx_letter_outgoing_attachments_id;
DROP INDEX IF EXISTS idx_letter_outgoing_attachments_letter_id;
DROP INDEX IF EXISTS idx_letter_incoming_attachments_id;
DROP INDEX IF EXISTS idx_letter_incoming_attachments_letter_id;
DROP INDEX IF EXISTS idx_letter_outgoing_discussion_attachments_id;
DROP INDEX IF EXISTS idx_letter_outgoing_discussion_attachments_discussion_id;
@@ -1,22 +0,0 @@
-- Add indexes to improve query performance
-- Index for letters_outgoing
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_id_deleted ON letters_outgoing(id, deleted_at);
-- Index for users table
CREATE INDEX IF NOT EXISTS idx_users_id ON users(id);
-- Indexes for document sessions lookups
CREATE INDEX IF NOT EXISTS idx_document_sessions_document_key ON document_sessions(document_key);
CREATE INDEX IF NOT EXISTS idx_document_metadata_document_id ON document_metadata(document_id);
-- Additional indexes for letter attachments
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_attachments_id ON letter_outgoing_attachments(id);
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_attachments_letter_id ON letter_outgoing_attachments(letter_id);
CREATE INDEX IF NOT EXISTS idx_letter_incoming_attachments_id ON letter_incoming_attachments(id);
CREATE INDEX IF NOT EXISTS idx_letter_incoming_attachments_letter_id ON letter_incoming_attachments(letter_id);
-- Index for discussion attachments
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_discussion_attachments_id ON letter_outgoing_discussion_attachments(id);
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_discussion_attachments_discussion_id ON letter_outgoing_discussion_attachments(discussion_id);
@@ -1,9 +0,0 @@
-- Revert document metadata VARCHAR constraints
-- Note: This may fail if existing data exceeds 50 characters
ALTER TABLE document_metadata
ALTER COLUMN document_type TYPE VARCHAR(50),
ALTER COLUMN file_type TYPE VARCHAR(50);
ALTER TABLE document_metadata
ALTER COLUMN mime_type TYPE VARCHAR(255);
@@ -1,10 +0,0 @@
-- Fix document metadata VARCHAR constraints
-- Increase the size limit for document_type and file_type columns
ALTER TABLE document_metadata
ALTER COLUMN document_type TYPE VARCHAR(255),
ALTER COLUMN file_type TYPE VARCHAR(255);
-- Also ensure mime_type has sufficient length
ALTER TABLE document_metadata
ALTER COLUMN mime_type TYPE VARCHAR(255);
@@ -1,13 +0,0 @@
-- Revert to original status check constraint without 'not_started'
-- First update any 'not_started' statuses to 'pending'
UPDATE letter_outgoing_approvals
SET status = 'pending'
WHERE status = 'not_started';
-- Drop and recreate the constraint
ALTER TABLE letter_outgoing_approvals
DROP CONSTRAINT IF EXISTS letter_outgoing_approvals_status_check;
ALTER TABLE letter_outgoing_approvals
ADD CONSTRAINT letter_outgoing_approvals_status_check
CHECK (status IN ('pending', 'approved', 'rejected'));
@@ -1,10 +0,0 @@
-- Add 'not_started' status to letter_outgoing_approvals status check constraint
ALTER TABLE letter_outgoing_approvals
DROP CONSTRAINT IF EXISTS letter_outgoing_approvals_status_check;
ALTER TABLE letter_outgoing_approvals
ADD CONSTRAINT letter_outgoing_approvals_status_check
CHECK (status IN ('not_started', 'pending', 'approved', 'rejected'));
-- Update default value for new approvals (optional, keeping 'pending' as default)
-- ALTER TABLE letter_outgoing_approvals ALTER COLUMN status SET DEFAULT 'not_started';
@@ -1,9 +0,0 @@
-- Remove indexes
DROP INDEX IF EXISTS idx_letter_outgoing_approvals_step_order;
DROP INDEX IF EXISTS idx_letter_outgoing_approvals_parallel_group;
-- Remove columns
ALTER TABLE letter_outgoing_approvals
DROP COLUMN IF EXISTS step_order,
DROP COLUMN IF EXISTS parallel_group,
DROP COLUMN IF EXISTS is_required;
@@ -1,17 +0,0 @@
-- Add step details to letter_outgoing_approvals table
ALTER TABLE letter_outgoing_approvals
ADD COLUMN IF NOT EXISTS step_order INT NOT NULL DEFAULT 1,
ADD COLUMN IF NOT EXISTS parallel_group INT DEFAULT 1,
ADD COLUMN IF NOT EXISTS is_required BOOLEAN DEFAULT true;
-- Add indexes for better query performance
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_approvals_step_order
ON letter_outgoing_approvals(letter_id, step_order);
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_approvals_parallel_group
ON letter_outgoing_approvals(letter_id, parallel_group);
-- Add comments for documentation
COMMENT ON COLUMN letter_outgoing_approvals.step_order IS 'The order in which this approval step should be processed';
COMMENT ON COLUMN letter_outgoing_approvals.parallel_group IS 'Steps with the same parallel_group value can be processed simultaneously';
COMMENT ON COLUMN letter_outgoing_approvals.is_required IS 'Whether this approval step is required for the letter to proceed';
@@ -1,23 +0,0 @@
-- Drop triggers
DROP TRIGGER IF EXISTS update_letter_summary_on_incoming ON letters_incoming;
DROP TRIGGER IF EXISTS update_letter_summary_on_outgoing ON letters_outgoing;
-- Drop functions
DROP FUNCTION IF EXISTS update_letter_summary();
DROP FUNCTION IF EXISTS populate_letter_summary_history();
-- Drop indexes
DROP INDEX IF EXISTS idx_letter_summary_date;
DROP INDEX IF EXISTS idx_letter_summary_type_date;
DROP INDEX IF EXISTS idx_dept_letter_summary_dept_date;
DROP INDEX IF EXISTS idx_dept_letter_summary_date;
DROP INDEX IF EXISTS idx_inst_letter_summary_inst_date;
DROP INDEX IF EXISTS idx_inst_letter_summary_date;
DROP INDEX IF EXISTS idx_approval_sla_summary_date;
DROP INDEX IF EXISTS idx_approval_sla_summary_dept_date;
-- Drop tables
DROP TABLE IF EXISTS approval_sla_summary;
DROP TABLE IF EXISTS institution_letter_summary;
DROP TABLE IF EXISTS department_letter_summary;
DROP TABLE IF EXISTS letter_summary;
@@ -1,389 +0,0 @@
-- Create letter_summary table for daily aggregated statistics
CREATE TABLE IF NOT EXISTS letter_summary (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
summary_date DATE NOT NULL,
letter_type VARCHAR(20) NOT NULL, -- 'incoming' or 'outgoing'
total_count INTEGER DEFAULT 0,
pending_count INTEGER DEFAULT 0,
approved_count INTEGER DEFAULT 0,
rejected_count INTEGER DEFAULT 0,
archived_count INTEGER DEFAULT 0,
sent_count INTEGER DEFAULT 0,
avg_processing_hours DECIMAL(10,2),
min_processing_hours DECIMAL(10,2),
max_processing_hours DECIMAL(10,2),
median_processing_hours DECIMAL(10,2),
p95_processing_hours DECIMAL(10,2),
p99_processing_hours DECIMAL(10,2),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(summary_date, letter_type)
);
-- Create department_letter_summary table
CREATE TABLE IF NOT EXISTS department_letter_summary (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
department_id UUID NOT NULL REFERENCES departments(id),
summary_date DATE NOT NULL,
incoming_count INTEGER DEFAULT 0,
outgoing_count INTEGER DEFAULT 0,
pending_incoming INTEGER DEFAULT 0,
pending_outgoing INTEGER DEFAULT 0,
approved_outgoing INTEGER DEFAULT 0,
rejected_outgoing INTEGER DEFAULT 0,
avg_response_hours DECIMAL(10,2),
completion_rate DECIMAL(5,2),
total_recipients INTEGER DEFAULT 0,
unique_senders INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(department_id, summary_date)
);
-- Create institution_letter_summary table
CREATE TABLE IF NOT EXISTS institution_letter_summary (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
institution_id UUID NOT NULL REFERENCES institutions(id),
summary_date DATE NOT NULL,
incoming_sent INTEGER DEFAULT 0,
outgoing_received INTEGER DEFAULT 0,
total_correspondence INTEGER DEFAULT 0,
avg_turnaround_hours DECIMAL(10,2),
last_activity_at TIMESTAMP,
priority_high_count INTEGER DEFAULT 0,
priority_medium_count INTEGER DEFAULT 0,
priority_low_count INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(institution_id, summary_date)
);
-- Create approval_sla_summary table
CREATE TABLE IF NOT EXISTS approval_sla_summary (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
summary_date DATE NOT NULL,
department_id UUID REFERENCES departments(id),
total_approvals INTEGER DEFAULT 0,
approved_count INTEGER DEFAULT 0,
rejected_count INTEGER DEFAULT 0,
pending_count INTEGER DEFAULT 0,
avg_approval_hours DECIMAL(10,2),
min_approval_hours DECIMAL(10,2),
max_approval_hours DECIMAL(10,2),
median_approval_hours DECIMAL(10,2),
within_sla_count INTEGER DEFAULT 0,
exceeded_sla_count INTEGER DEFAULT 0,
sla_compliance_rate DECIMAL(5,2),
avg_approval_steps DECIMAL(5,2),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(summary_date, department_id)
);
-- Create indexes for better query performance
CREATE INDEX idx_letter_summary_date ON letter_summary(summary_date DESC);
CREATE INDEX idx_letter_summary_type_date ON letter_summary(letter_type, summary_date DESC);
CREATE INDEX idx_dept_letter_summary_dept_date ON department_letter_summary(department_id, summary_date DESC);
CREATE INDEX idx_dept_letter_summary_date ON department_letter_summary(summary_date DESC);
CREATE INDEX idx_inst_letter_summary_inst_date ON institution_letter_summary(institution_id, summary_date DESC);
CREATE INDEX idx_inst_letter_summary_date ON institution_letter_summary(summary_date DESC);
CREATE INDEX idx_approval_sla_summary_date ON approval_sla_summary(summary_date DESC);
CREATE INDEX idx_approval_sla_summary_dept_date ON approval_sla_summary(department_id, summary_date DESC);
-- Create function to update letter_summary
CREATE OR REPLACE FUNCTION update_letter_summary()
RETURNS TRIGGER AS $$
DECLARE
v_date DATE;
v_type VARCHAR(20);
BEGIN
-- Determine date and type
IF TG_TABLE_NAME = 'letters_incoming' THEN
v_type := 'incoming';
v_date := DATE(COALESCE(NEW.created_at, OLD.created_at));
ELSE
v_type := 'outgoing';
v_date := DATE(COALESCE(NEW.created_at, OLD.created_at));
END IF;
-- Update or insert summary
INSERT INTO letter_summary (
summary_date,
letter_type,
total_count,
pending_count,
approved_count,
rejected_count,
archived_count,
sent_count
)
SELECT
v_date,
v_type,
COUNT(*) as total_count,
COUNT(*) FILTER (WHERE status = 'pending' OR status = 'pending_approval') as pending_count,
COUNT(*) FILTER (WHERE status = 'approved') as approved_count,
COUNT(*) FILTER (WHERE status = 'rejected') as rejected_count,
COUNT(*) FILTER (WHERE status = 'archived') as archived_count,
COUNT(*) FILTER (WHERE status = 'sent') as sent_count
FROM (
SELECT status, created_at FROM letters_incoming WHERE DATE(created_at) = v_date AND v_type = 'incoming'
UNION ALL
SELECT status, created_at FROM letters_outgoing WHERE DATE(created_at) = v_date AND v_type = 'outgoing'
) t
ON CONFLICT (summary_date, letter_type)
DO UPDATE SET
total_count = EXCLUDED.total_count,
pending_count = EXCLUDED.pending_count,
approved_count = EXCLUDED.approved_count,
rejected_count = EXCLUDED.rejected_count,
archived_count = EXCLUDED.archived_count,
sent_count = EXCLUDED.sent_count,
updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Create triggers for automatic updates
CREATE TRIGGER update_letter_summary_on_incoming
AFTER INSERT OR UPDATE OR DELETE ON letters_incoming
FOR EACH ROW
EXECUTE FUNCTION update_letter_summary();
CREATE TRIGGER update_letter_summary_on_outgoing
AFTER INSERT OR UPDATE OR DELETE ON letters_outgoing
FOR EACH ROW
EXECUTE FUNCTION update_letter_summary();
-- Function to populate historical data
CREATE OR REPLACE FUNCTION populate_letter_summary_history()
RETURNS void AS $$
BEGIN
-- Populate letter_summary with historical data
INSERT INTO letter_summary (
summary_date,
letter_type,
total_count,
pending_count,
approved_count,
rejected_count,
archived_count,
sent_count,
avg_processing_hours
)
SELECT
DATE(created_at) as summary_date,
'incoming' as letter_type,
COUNT(*) as total_count,
COUNT(*) FILTER (WHERE status = 'pending') as pending_count,
COUNT(*) FILTER (WHERE status = 'approved') as approved_count,
COUNT(*) FILTER (WHERE status = 'rejected') as rejected_count,
COUNT(*) FILTER (WHERE status = 'archived') as archived_count,
0 as sent_count,
AVG(EXTRACT(EPOCH FROM (updated_at - created_at))/3600) as avg_processing_hours
FROM letters_incoming
WHERE deleted_at IS NULL
GROUP BY DATE(created_at)
UNION ALL
SELECT
DATE(created_at) as summary_date,
'outgoing' as letter_type,
COUNT(*) as total_count,
COUNT(*) FILTER (WHERE status = 'pending_approval') as pending_count,
COUNT(*) FILTER (WHERE status = 'approved') as approved_count,
COUNT(*) FILTER (WHERE status = 'rejected') as rejected_count,
COUNT(*) FILTER (WHERE status = 'archived') as archived_count,
COUNT(*) FILTER (WHERE status = 'sent') as sent_count,
AVG(EXTRACT(EPOCH FROM (updated_at - created_at))/3600) as avg_processing_hours
FROM letters_outgoing
WHERE deleted_at IS NULL
GROUP BY DATE(created_at)
ON CONFLICT (summary_date, letter_type) DO NOTHING;
-- Populate department_letter_summary
INSERT INTO department_letter_summary (
department_id,
summary_date,
incoming_count,
outgoing_count,
pending_incoming,
pending_outgoing,
approved_outgoing,
rejected_outgoing,
avg_response_hours,
completion_rate
)
SELECT
d.id as department_id,
dates.summary_date,
COALESCE(inc.count, 0) as incoming_count,
COALESCE(outg.count, 0) as outgoing_count,
COALESCE(inc.pending, 0) as pending_incoming,
COALESCE(outg.pending, 0) as pending_outgoing,
COALESCE(outg.approved, 0) as approved_outgoing,
COALESCE(outg.rejected, 0) as rejected_outgoing,
COALESCE(outg.avg_hours, 0) as avg_response_hours,
CASE
WHEN COALESCE(outg.count, 0) > 0
THEN (COALESCE(outg.completed, 0)::DECIMAL / outg.count::DECIMAL) * 100
ELSE 0
END as completion_rate
FROM departments d
CROSS JOIN (
SELECT DISTINCT DATE(created_at) as summary_date
FROM letters_incoming
UNION
SELECT DISTINCT DATE(created_at)
FROM letters_outgoing
) dates
LEFT JOIN (
SELECT
lir.recipient_department_id as dept_id,
DATE(li.created_at) as date,
COUNT(DISTINCT li.id) as count,
COUNT(DISTINCT li.id) FILTER (WHERE li.status = 'pending') as pending
FROM letters_incoming li
JOIN letter_incoming_recipients lir ON lir.letter_id = li.id
WHERE li.deleted_at IS NULL
GROUP BY lir.recipient_department_id, DATE(li.created_at)
) inc ON inc.dept_id = d.id AND inc.date = dates.summary_date
LEFT JOIN (
SELECT
lor.department_id as dept_id,
DATE(lo.created_at) as date,
COUNT(DISTINCT lo.id) as count,
COUNT(DISTINCT lo.id) FILTER (WHERE lo.status = 'pending_approval') as pending,
COUNT(DISTINCT lo.id) FILTER (WHERE lo.status = 'approved') as approved,
COUNT(DISTINCT lo.id) FILTER (WHERE lo.status = 'rejected') as rejected,
COUNT(DISTINCT lo.id) FILTER (WHERE lo.status IN ('sent', 'archived')) as completed,
AVG(EXTRACT(EPOCH FROM (lo.updated_at - lo.created_at))/3600) as avg_hours
FROM letters_outgoing lo
JOIN letter_outgoing_recipients lor ON lor.letter_id = lo.id
WHERE lo.deleted_at IS NULL
GROUP BY lor.department_id, DATE(lo.created_at)
) outg ON outg.dept_id = d.id AND outg.date = dates.summary_date
WHERE inc.count > 0 OR outg.count > 0
ON CONFLICT (department_id, summary_date) DO NOTHING;
-- Populate institution_letter_summary
INSERT INTO institution_letter_summary (
institution_id,
summary_date,
incoming_sent,
outgoing_received,
total_correspondence,
avg_turnaround_hours,
last_activity_at,
priority_high_count,
priority_medium_count,
priority_low_count
)
SELECT
i.id as institution_id,
dates.summary_date,
COALESCE(inc.count, 0) as incoming_sent,
COALESCE(outg.count, 0) as outgoing_received,
COALESCE(inc.count, 0) + COALESCE(outg.count, 0) as total_correspondence,
COALESCE((inc.avg_hours + outg.avg_hours) / 2, 0) as avg_turnaround_hours,
GREATEST(inc.last_activity, outg.last_activity) as last_activity_at,
COALESCE(inc.high_priority, 0) + COALESCE(outg.high_priority, 0) as priority_high_count,
COALESCE(inc.medium_priority, 0) + COALESCE(outg.medium_priority, 0) as priority_medium_count,
COALESCE(inc.low_priority, 0) + COALESCE(outg.low_priority, 0) as priority_low_count
FROM institutions i
CROSS JOIN (
SELECT DISTINCT DATE(created_at) as summary_date
FROM letters_incoming
UNION
SELECT DISTINCT DATE(created_at)
FROM letters_outgoing
) dates
LEFT JOIN (
SELECT
sender_institution_id as inst_id,
DATE(created_at) as date,
COUNT(*) as count,
AVG(EXTRACT(EPOCH FROM (updated_at - created_at))/3600) as avg_hours,
MAX(created_at) as last_activity,
COUNT(*) FILTER (WHERE priority_id IN (SELECT id FROM priorities WHERE level = 1)) as high_priority,
COUNT(*) FILTER (WHERE priority_id IN (SELECT id FROM priorities WHERE level = 2)) as medium_priority,
COUNT(*) FILTER (WHERE priority_id IN (SELECT id FROM priorities WHERE level = 3)) as low_priority
FROM letters_incoming
WHERE deleted_at IS NULL AND sender_institution_id IS NOT NULL
GROUP BY sender_institution_id, DATE(created_at)
) inc ON inc.inst_id = i.id AND inc.date = dates.summary_date
LEFT JOIN (
SELECT
receiver_institution_id as inst_id,
DATE(created_at) as date,
COUNT(*) as count,
AVG(EXTRACT(EPOCH FROM (updated_at - created_at))/3600) as avg_hours,
MAX(created_at) as last_activity,
COUNT(*) FILTER (WHERE priority_id IN (SELECT id FROM priorities WHERE level = 1)) as high_priority,
COUNT(*) FILTER (WHERE priority_id IN (SELECT id FROM priorities WHERE level = 2)) as medium_priority,
COUNT(*) FILTER (WHERE priority_id IN (SELECT id FROM priorities WHERE level = 3)) as low_priority
FROM letters_outgoing
WHERE deleted_at IS NULL AND receiver_institution_id IS NOT NULL
GROUP BY receiver_institution_id, DATE(created_at)
) outg ON outg.inst_id = i.id AND outg.date = dates.summary_date
WHERE inc.count > 0 OR outg.count > 0
ON CONFLICT (institution_id, summary_date) DO NOTHING;
-- Populate approval_sla_summary
INSERT INTO approval_sla_summary (
summary_date,
department_id,
total_approvals,
approved_count,
rejected_count,
pending_count,
avg_approval_hours,
min_approval_hours,
max_approval_hours,
median_approval_hours,
within_sla_count,
exceeded_sla_count,
sla_compliance_rate,
avg_approval_steps
)
SELECT
DATE(loa.created_at) as summary_date,
d.id as department_id,
COUNT(loa.id) as total_approvals,
COUNT(loa.id) FILTER (WHERE loa.status = 'approved') as approved_count,
COUNT(loa.id) FILTER (WHERE loa.status = 'rejected') as rejected_count,
COUNT(loa.id) FILTER (WHERE loa.status = 'pending') as pending_count,
AVG(EXTRACT(EPOCH FROM (loa.acted_at - loa.created_at))/3600) FILTER (WHERE loa.acted_at IS NOT NULL) as avg_approval_hours,
MIN(EXTRACT(EPOCH FROM (loa.acted_at - loa.created_at))/3600) FILTER (WHERE loa.acted_at IS NOT NULL) as min_approval_hours,
MAX(EXTRACT(EPOCH FROM (loa.acted_at - loa.created_at))/3600) FILTER (WHERE loa.acted_at IS NOT NULL) as max_approval_hours,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY EXTRACT(EPOCH FROM (loa.acted_at - loa.created_at))/3600) FILTER (WHERE loa.acted_at IS NOT NULL) as median_approval_hours,
COUNT(loa.id) FILTER (WHERE EXTRACT(EPOCH FROM (loa.acted_at - loa.created_at))/3600 <= 24 AND loa.acted_at IS NOT NULL) as within_sla_count,
COUNT(loa.id) FILTER (WHERE EXTRACT(EPOCH FROM (loa.acted_at - loa.created_at))/3600 > 24 AND loa.acted_at IS NOT NULL) as exceeded_sla_count,
CASE
WHEN COUNT(loa.id) FILTER (WHERE loa.acted_at IS NOT NULL) > 0
THEN (COUNT(loa.id) FILTER (WHERE EXTRACT(EPOCH FROM (loa.acted_at - loa.created_at))/3600 <= 24 AND loa.acted_at IS NOT NULL)::DECIMAL /
COUNT(loa.id) FILTER (WHERE loa.acted_at IS NOT NULL)::DECIMAL) * 100
ELSE 0
END as sla_compliance_rate,
AVG(step_counts.step_count) as avg_approval_steps
FROM letter_outgoing_approvals loa
JOIN letters_outgoing lo ON lo.id = loa.letter_id
JOIN letter_outgoing_recipients lor ON lor.letter_id = lo.id
JOIN departments d ON d.id = lor.department_id
LEFT JOIN (
SELECT letter_id, COUNT(*) as step_count
FROM letter_outgoing_approvals
GROUP BY letter_id
) step_counts ON step_counts.letter_id = loa.letter_id
WHERE lo.deleted_at IS NULL
GROUP BY DATE(loa.created_at), d.id
ON CONFLICT (summary_date, department_id) DO NOTHING;
END;
$$ LANGUAGE plpgsql;
-- Execute the function to populate historical data
SELECT populate_letter_summary_history();
@@ -1,25 +0,0 @@
-- Rollback: Remove status and letter_incoming_id columns from letter_incoming_dispositions_department table
-- Drop the constraint first
ALTER TABLE letter_incoming_dispositions_department
DROP CONSTRAINT IF EXISTS check_disposition_department_status;
-- Drop indexes
DROP INDEX IF EXISTS idx_letter_incoming_dispositions_department_status;
DROP INDEX IF EXISTS idx_letter_incoming_dispositions_department_letter_id;
-- Drop the columns
ALTER TABLE letter_incoming_dispositions_department
DROP COLUMN IF EXISTS status;
ALTER TABLE letter_incoming_dispositions_department
DROP COLUMN IF EXISTS letter_incoming_id;
ALTER TABLE letter_incoming_dispositions_department
DROP COLUMN IF EXISTS read_at;
ALTER TABLE letter_incoming_dispositions_department
DROP COLUMN IF EXISTS completed_at;
ALTER TABLE letter_incoming_dispositions_department
DROP COLUMN IF EXISTS updated_at;
@@ -1,38 +0,0 @@
-- Add status and letter_incoming_id columns to letter_incoming_dispositions_department table
-- Add letter_incoming_id column
ALTER TABLE letter_incoming_dispositions_department
ADD COLUMN IF NOT EXISTS letter_incoming_id UUID NOT NULL REFERENCES letters_incoming(id);
-- Add status column with default value
ALTER TABLE letter_incoming_dispositions_department
ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'pending';
-- Add read_at column
ALTER TABLE letter_incoming_dispositions_department
ADD COLUMN IF NOT EXISTS read_at TIMESTAMP;
-- Add completed_at column
ALTER TABLE letter_incoming_dispositions_department
ADD COLUMN IF NOT EXISTS completed_at TIMESTAMP;
-- Add updated_at column
ALTER TABLE letter_incoming_dispositions_department
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
-- Create index for faster queries
CREATE INDEX IF NOT EXISTS idx_letter_incoming_dispositions_department_status
ON letter_incoming_dispositions_department(department_id, status);
CREATE INDEX IF NOT EXISTS idx_letter_incoming_dispositions_department_letter_id
ON letter_incoming_dispositions_department(letter_incoming_id);
-- Update existing records to have status 'dispositioned' (assuming existing records are already dispositioned)
UPDATE letter_incoming_dispositions_department
SET status = 'dispositioned'
WHERE status = 'pending';
-- Add constraint to ensure valid status values
ALTER TABLE letter_incoming_dispositions_department
ADD CONSTRAINT check_disposition_department_status
CHECK (status IN ('pending', 'dispositioned', 'read', 'completed'));
@@ -1,12 +0,0 @@
BEGIN;
-- Delete new setting
DELETE FROM app_settings WHERE key = 'INCOMING_LETTER_DEPARTMENT_RECIPIENTS';
-- Restore old setting
INSERT INTO app_settings(key, value)
VALUES
('INCOMING_LETTER_RECIPIENTS', '{"department_codes": ["aslog"]}'::jsonb)
ON CONFLICT (key) DO NOTHING;
COMMIT;
@@ -1,18 +0,0 @@
BEGIN;
-- Delete old setting if exists
DELETE FROM app_settings WHERE key = 'INCOMING_LETTER_RECIPIENTS';
-- Insert new setting with department IDs as a direct array
-- Using empty array as placeholder that should be replaced with actual department IDs
INSERT INTO app_settings(key, value)
VALUES
('INCOMING_LETTER_DEPARTMENT_RECIPIENTS', '[]'::jsonb)
ON CONFLICT (key) DO NOTHING;
-- Example to set actual department IDs (uncomment and modify as needed):
-- UPDATE app_settings
-- SET value = '["ae66af10-bcb7-4939-8110-940d2a387e19"]'::jsonb
-- WHERE key = 'INCOMING_LETTER_DEPARTMENT_RECIPIENTS';
COMMIT;
@@ -1,16 +0,0 @@
BEGIN;
-- Drop indexes
DROP INDEX IF EXISTS idx_users_email;
DROP INDEX IF EXISTS idx_user_department_user_id;
DROP INDEX IF EXISTS idx_user_department_department_id;
DROP INDEX IF EXISTS idx_letter_outgoing_recipients_user_id;
DROP INDEX IF EXISTS idx_letter_outgoing_recipients_letter_id;
DROP INDEX IF EXISTS idx_letters_outgoing_status;
DROP INDEX IF EXISTS idx_letters_outgoing_deleted_at;
DROP INDEX IF EXISTS idx_letters_incoming_status;
DROP INDEX IF EXISTS idx_letters_incoming_deleted_at;
DROP INDEX IF EXISTS idx_letter_incoming_recipients_dept_id;
DROP INDEX IF EXISTS idx_letter_incoming_recipients_user_id;
COMMIT;
@@ -1,26 +0,0 @@
BEGIN;
-- Add index for users.email for faster email lookups
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
-- Add indexes for user_department relationship (singular table name)
CREATE INDEX IF NOT EXISTS idx_user_department_user_id ON user_department(user_id);
CREATE INDEX IF NOT EXISTS idx_user_department_department_id ON user_department(department_id);
-- Add indexes for letter_outgoing_recipients for faster joins
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_recipients_user_id ON letter_outgoing_recipients(user_id);
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_recipients_letter_id ON letter_outgoing_recipients(letter_id);
-- Add index for letters_outgoing status queries
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_status ON letters_outgoing(status) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_deleted_at ON letters_outgoing(deleted_at);
-- Add index for letters_incoming status queries
CREATE INDEX IF NOT EXISTS idx_letters_incoming_status ON letters_incoming(status) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_letters_incoming_deleted_at ON letters_incoming(deleted_at);
-- Add index for letter_incoming_recipients for department lookups
CREATE INDEX IF NOT EXISTS idx_letter_incoming_recipients_dept_id ON letter_incoming_recipients(recipient_department_id);
CREATE INDEX IF NOT EXISTS idx_letter_incoming_recipients_user_id ON letter_incoming_recipients(recipient_user_id);
COMMIT;
@@ -1,9 +0,0 @@
BEGIN;
-- Drop indexes
DROP INDEX IF EXISTS idx_user_profiles_user_id;
DROP INDEX IF EXISTS idx_departments_id;
DROP INDEX IF EXISTS idx_user_department_composite;
DROP INDEX IF EXISTS idx_users_id_not_deleted;
COMMIT;
@@ -1,11 +0,0 @@
BEGIN;
CREATE INDEX IF NOT EXISTS idx_user_profiles_user_id ON user_profiles(user_id);
CREATE INDEX IF NOT EXISTS idx_departments_id ON departments(id);
CREATE INDEX IF NOT EXISTS idx_user_department_composite ON user_department(user_id, department_id);
CREATE INDEX IF NOT EXISTS idx_users_id_not_deleted ON users(id);
COMMIT;
@@ -1,20 +0,0 @@
BEGIN;
-- Remove system user from department (cascade should handle this, but being explicit)
DELETE FROM user_department
WHERE user_id = '11111111-2222-3333-4444-555555555555'::uuid
AND department_id = '11111111-2222-3333-4444-555555555555'::uuid;
-- Remove system user profile
DELETE FROM user_profiles
WHERE user_id = '11111111-2222-3333-4444-555555555555'::uuid;
-- Remove system user
DELETE FROM users
WHERE id = '11111111-2222-3333-4444-555555555555'::uuid;
-- Remove system department
DELETE FROM departments
WHERE id = '11111111-2222-3333-4444-555555555555'::uuid;
COMMIT;
@@ -1,88 +0,0 @@
BEGIN;
INSERT INTO departments (id, name, code, path, created_at, updated_at)
VALUES (
'11111111-2222-3333-4444-555555555555'::uuid,
'System',
'SYSTEM',
'system'::ltree,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
) ON CONFLICT (id) DO NOTHING;
INSERT INTO users (
id,
username,
email,
name,
password_hash,
status,
is_active,
created_at,
updated_at
)
VALUES (
'11111111-2222-3333-4444-555555555555'::uuid,
'system',
'system@eslogad.internal',
'System User',
'$2a$10$SYSTEM.USER.SHOULD.NEVER.LOGIN.WITH.PASSWORD.HASH',
'active',
true,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
) ON CONFLICT (id) DO NOTHING;
-- Create user profile for system user
INSERT INTO user_profiles (
user_id,
full_name,
display_name,
job_title,
preferences,
notification_prefs,
created_at,
updated_at
)
VALUES (
'11111111-2222-3333-4444-555555555555'::uuid,
'System User',
'System',
'Automated Process',
'{}'::jsonb,
'{}'::jsonb,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
) ON CONFLICT (user_id) DO NOTHING;
-- Link system user to system department
INSERT INTO user_department (
id,
user_id,
department_id,
is_primary,
assigned_at,
removed_at
)
VALUES (
gen_random_uuid(),
'11111111-2222-3333-4444-555555555555'::uuid,
'11111111-2222-3333-4444-555555555555'::uuid,
true,
CURRENT_TIMESTAMP,
NULL
) ON CONFLICT (user_id, department_id) WHERE removed_at IS NULL DO NOTHING;
-- Optionally, create a system role if your application uses roles
-- Uncomment and modify if needed:
-- INSERT INTO roles (id, code, name, description, created_at, updated_at)
-- VALUES (
-- gen_random_uuid(),
-- 'SYSTEM',
-- 'System Role',
-- 'Role for system automated processes',
-- CURRENT_TIMESTAMP,
-- CURRENT_TIMESTAMP
-- ) ON CONFLICT (code) DO NOTHING;
COMMIT;
@@ -1,10 +0,0 @@
BEGIN;
-- Drop the index first
DROP INDEX IF EXISTS idx_letter_incoming_disposition_dept_status_notes;
-- Remove notes column from letter_incoming_disposition_department table
ALTER TABLE letter_incoming_disposition_department
DROP COLUMN IF EXISTS notes;
COMMIT;
@@ -1,12 +0,0 @@
BEGIN;
-- Add notes column to letter_incoming_dispositions_department table
ALTER TABLE letter_incoming_dispositions_department
ADD COLUMN IF NOT EXISTS notes TEXT;
-- Add index for better performance when querying by status with notes
CREATE INDEX IF NOT EXISTS idx_letter_incoming_disposition_dept_status_notes
ON letter_incoming_dispositions_department(status)
WHERE notes IS NOT NULL;
COMMIT;
@@ -1,17 +0,0 @@
-- Drop indexes for letter_incoming_recipients
DROP INDEX IF EXISTS idx_letter_incoming_recipients_archived;
DROP INDEX IF EXISTS idx_letter_incoming_recipients_user_archived;
DROP INDEX IF EXISTS idx_letter_incoming_recipients_letter_archived;
DROP INDEX IF EXISTS idx_letter_incoming_recipients_dept_archived;
-- Drop indexes for letter_outgoing_recipients
DROP INDEX IF EXISTS idx_letter_outgoing_recipients_archived;
DROP INDEX IF EXISTS idx_letter_outgoing_recipients_user_archived;
DROP INDEX IF EXISTS idx_letter_outgoing_recipients_letter_archived;
-- Drop the is_archived columns
ALTER TABLE letter_incoming_recipients
DROP COLUMN IF EXISTS is_archived;
ALTER TABLE letter_outgoing_recipients
DROP COLUMN IF EXISTS is_archived;
@@ -1,36 +0,0 @@
-- Add is_archived column to letter_incoming_recipients if it doesn't exist
ALTER TABLE letter_incoming_recipients
ADD COLUMN IF NOT EXISTS is_archived BOOLEAN DEFAULT FALSE;
-- Add is_archived column to letter_outgoing_recipients if it doesn't exist
ALTER TABLE letter_outgoing_recipients
ADD COLUMN IF NOT EXISTS is_archived BOOLEAN DEFAULT FALSE;
-- Add indexes for efficient archiving queries on letter_incoming_recipients
-- Note: letter_incoming_recipients uses recipient_user_id and recipient_department_id
CREATE INDEX IF NOT EXISTS idx_letter_incoming_recipients_archived
ON letter_incoming_recipients(is_archived, recipient_user_id, letter_id)
WHERE is_archived = false;
CREATE INDEX IF NOT EXISTS idx_letter_incoming_recipients_user_archived
ON letter_incoming_recipients(recipient_user_id, is_archived, letter_id);
CREATE INDEX IF NOT EXISTS idx_letter_incoming_recipients_letter_archived
ON letter_incoming_recipients(letter_id, is_archived);
-- Add indexes for efficient archiving queries on letter_outgoing_recipients
-- Note: letter_outgoing_recipients uses user_id and department_id
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_recipients_archived
ON letter_outgoing_recipients(is_archived, user_id, letter_id)
WHERE is_archived = false;
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_recipients_user_archived
ON letter_outgoing_recipients(user_id, is_archived, letter_id);
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_recipients_letter_archived
ON letter_outgoing_recipients(letter_id, is_archived);
-- Add composite index for filtering non-archived letters by department
CREATE INDEX IF NOT EXISTS idx_letter_incoming_recipients_dept_archived
ON letter_incoming_recipients(recipient_department_id, is_archived, letter_id)
WHERE is_archived = false;
@@ -1,15 +0,0 @@
-- Drop the view
DROP VIEW IF EXISTS department_hierarchy;
-- Drop the functions
DROP FUNCTION IF EXISTS get_department_hierarchy_path(UUID);
DROP FUNCTION IF EXISTS check_department_hierarchy();
-- Drop the trigger
DROP TRIGGER IF EXISTS check_department_hierarchy_trigger ON departments;
-- Drop the index
DROP INDEX IF EXISTS idx_departments_parent_id;
-- Remove the parent_department_id column
ALTER TABLE departments DROP COLUMN IF EXISTS parent_department_id;
@@ -1,128 +0,0 @@
-- 1) Schema changes
ALTER TABLE departments
ADD COLUMN parent_department_id UUID REFERENCES departments(id) ON DELETE CASCADE;
CREATE INDEX idx_departments_parent_id ON departments(parent_department_id);
-- (Optional but recommended for ltree lookups)
-- CREATE EXTENSION IF NOT EXISTS ltree;
-- CREATE INDEX idx_departments_path_gist ON departments USING GIST (path);
-- CREATE INDEX idx_departments_path_nlevel ON departments ((nlevel(path)));
-- 2) Migrate parent ids from existing ltree path
-- Use ltree helpers: nlevel() and subpath()
UPDATE departments d1
SET parent_department_id = d2.id
FROM departments d2
WHERE nlevel(d1.path) > 1
AND d2.path = subpath(d1.path, 0, nlevel(d1.path) - 1);
-- 3) Guard against cycles/self-parent via trigger
CREATE OR REPLACE FUNCTION check_department_hierarchy()
RETURNS TRIGGER AS $$
DECLARE
current_id UUID;
max_depth INT := 100;
depth INT := 0;
BEGIN
IF NEW.parent_department_id IS NULL THEN
RETURN NEW;
END IF;
-- self-reference
IF NEW.parent_department_id = NEW.id THEN
RAISE EXCEPTION 'Department cannot be its own parent';
END IF;
-- walk up the tree
current_id := NEW.parent_department_id;
WHILE current_id IS NOT NULL AND depth < max_depth LOOP
IF current_id = NEW.id THEN
RAISE EXCEPTION 'Circular reference detected in department hierarchy';
END IF;
SELECT parent_department_id INTO current_id
FROM departments
WHERE id = current_id;
depth := depth + 1;
END LOOP;
IF depth >= max_depth THEN
RAISE EXCEPTION 'Department hierarchy too deep (max: %)', max_depth;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS check_department_hierarchy_trigger ON departments;
CREATE TRIGGER check_department_hierarchy_trigger
BEFORE INSERT OR UPDATE OF parent_department_id ON departments
FOR EACH ROW
EXECUTE FUNCTION check_department_hierarchy();
-- 4) Helper to rebuild dotted text path from codes (top.down.leaf)
CREATE OR REPLACE FUNCTION get_department_hierarchy_path(dept_id UUID)
RETURNS TEXT AS $$
DECLARE
path_text TEXT := '';
current_dept RECORD;
current_id UUID := dept_id;
BEGIN
WHILE current_id IS NOT NULL LOOP
SELECT id, name, parent_department_id, code
INTO current_dept
FROM departments
WHERE id = current_id;
IF current_dept IS NULL THEN
EXIT;
END IF;
IF path_text = '' THEN
path_text := current_dept.code;
ELSE
path_text := current_dept.code || '.' || path_text;
END IF;
current_id := current_dept.parent_department_id;
END LOOP;
RETURN path_text;
END;
$$ LANGUAGE plpgsql;
-- 5) View for easy hierarchy queries
CREATE OR REPLACE VIEW department_hierarchy AS
WITH RECURSIVE dept_tree AS (
-- roots
SELECT
id,
name,
code,
parent_department_id,
path,
0 AS level,
ARRAY[id] AS hierarchy_ids,
ARRAY[name] AS hierarchy_names
FROM departments
WHERE parent_department_id IS NULL
UNION ALL
-- children
SELECT
d.id,
d.name,
d.code,
d.parent_department_id,
d.path,
dt.level + 1,
dt.hierarchy_ids || d.id,
dt.hierarchy_names || d.name
FROM departments d
JOIN dept_tree dt ON d.parent_department_id = dt.id
)
SELECT * FROM dept_tree;
@@ -1,31 +0,0 @@
-- Drop functions
DROP FUNCTION IF EXISTS search_outgoing_letters(text);
DROP FUNCTION IF EXISTS search_incoming_letters(text);
-- Drop indexes for outgoing letters
DROP INDEX IF EXISTS idx_letters_outgoing_letter_number_text;
DROP INDEX IF EXISTS idx_letters_outgoing_subject_text;
DROP INDEX IF EXISTS idx_letters_outgoing_description_text;
DROP INDEX IF EXISTS idx_letters_outgoing_reference_number_text;
DROP INDEX IF EXISTS idx_letters_outgoing_status_created;
DROP INDEX IF EXISTS idx_letters_outgoing_priority_created;
DROP INDEX IF EXISTS idx_letters_outgoing_institution_created;
DROP INDEX IF EXISTS idx_letters_outgoing_created_by;
DROP INDEX IF EXISTS idx_letters_outgoing_issue_date;
-- Drop indexes for incoming letters
DROP INDEX IF EXISTS idx_letters_incoming_letter_number_text;
DROP INDEX IF EXISTS idx_letters_incoming_subject_text;
DROP INDEX IF EXISTS idx_letters_incoming_description_text;
DROP INDEX IF EXISTS idx_letters_incoming_reference_number_text;
DROP INDEX IF EXISTS idx_letters_incoming_status_created;
DROP INDEX IF EXISTS idx_letters_incoming_priority_created;
DROP INDEX IF EXISTS idx_letters_incoming_institution_created;
DROP INDEX IF EXISTS idx_letters_incoming_created_by;
DROP INDEX IF EXISTS idx_letters_incoming_received_date;
-- Drop recipient indexes
DROP INDEX IF EXISTS idx_letter_outgoing_recipients_user;
DROP INDEX IF EXISTS idx_letter_outgoing_recipients_dept;
DROP INDEX IF EXISTS idx_letter_incoming_recipients_user;
DROP INDEX IF EXISTS idx_letter_incoming_recipients_dept;
@@ -1,63 +0,0 @@
-- Add indexes for optimized search on outgoing letters
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_letter_number_text ON letters_outgoing USING gin(to_tsvector('simple', letter_number));
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_subject_text ON letters_outgoing USING gin(to_tsvector('simple', subject));
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_description_text ON letters_outgoing USING gin(to_tsvector('simple', COALESCE(description, '')));
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_reference_number_text ON letters_outgoing USING gin(to_tsvector('simple', COALESCE(reference_number, '')));
-- Composite indexes for common query patterns on outgoing letters
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_status_created ON letters_outgoing(status, created_at DESC) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_priority_created ON letters_outgoing(priority_id, created_at DESC) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_institution_created ON letters_outgoing(receiver_institution_id, created_at DESC) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_created_by ON letters_outgoing(created_by, created_at DESC) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_issue_date ON letters_outgoing(issue_date DESC) WHERE deleted_at IS NULL;
-- Add indexes for optimized search on incoming letters
CREATE INDEX IF NOT EXISTS idx_letters_incoming_letter_number_text ON letters_incoming USING gin(to_tsvector('simple', letter_number));
CREATE INDEX IF NOT EXISTS idx_letters_incoming_subject_text ON letters_incoming USING gin(to_tsvector('simple', subject));
CREATE INDEX IF NOT EXISTS idx_letters_incoming_description_text ON letters_incoming USING gin(to_tsvector('simple', COALESCE(description, '')));
CREATE INDEX IF NOT EXISTS idx_letters_incoming_reference_number_text ON letters_incoming USING gin(to_tsvector('simple', COALESCE(reference_number, '')));
-- Composite indexes for common query patterns on incoming letters
CREATE INDEX IF NOT EXISTS idx_letters_incoming_status_created ON letters_incoming(status, created_at DESC) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_letters_incoming_priority_created ON letters_incoming(priority_id, created_at DESC) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_letters_incoming_institution_created ON letters_incoming(sender_institution_id, created_at DESC) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_letters_incoming_created_by ON letters_incoming(created_by, created_at DESC) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_letters_incoming_received_date ON letters_incoming(received_date DESC) WHERE deleted_at IS NULL;
-- Indexes for recipient lookups
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_recipients_user ON letter_outgoing_recipients(user_id, letter_id);
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_recipients_dept ON letter_outgoing_recipients(department_id, letter_id);
CREATE INDEX IF NOT EXISTS idx_letter_incoming_recipients_user ON letter_incoming_recipients(recipient_user_id, letter_id);
CREATE INDEX IF NOT EXISTS idx_letter_incoming_recipients_dept ON letter_incoming_recipients(recipient_department_id, letter_id);
-- Create a function for full-text search on outgoing letters
CREATE OR REPLACE FUNCTION search_outgoing_letters(search_query text)
RETURNS SETOF letters_outgoing AS $$
BEGIN
RETURN QUERY
SELECT * FROM letters_outgoing
WHERE deleted_at IS NULL
AND (
to_tsvector('simple', letter_number) @@ plainto_tsquery('simple', search_query)
OR to_tsvector('simple', subject) @@ plainto_tsquery('simple', search_query)
OR to_tsvector('simple', COALESCE(description, '')) @@ plainto_tsquery('simple', search_query)
OR to_tsvector('simple', COALESCE(reference_number, '')) @@ plainto_tsquery('simple', search_query)
);
END;
$$ LANGUAGE plpgsql;
-- Create a function for full-text search on incoming letters
CREATE OR REPLACE FUNCTION search_incoming_letters(search_query text)
RETURNS SETOF letters_incoming AS $$
BEGIN
RETURN QUERY
SELECT * FROM letters_incoming
WHERE deleted_at IS NULL
AND (
to_tsvector('simple', letter_number) @@ plainto_tsquery('simple', search_query)
OR to_tsvector('simple', subject) @@ plainto_tsquery('simple', search_query)
OR to_tsvector('simple', COALESCE(description, '')) @@ plainto_tsquery('simple', search_query)
OR to_tsvector('simple', COALESCE(reference_number, '')) @@ plainto_tsquery('simple', search_query)
);
END;
$$ LANGUAGE plpgsql;
@@ -1,12 +0,0 @@
-- Drop indexes
DROP INDEX IF EXISTS idx_letters_incoming_sender_name_text;
DROP INDEX IF EXISTS idx_letters_outgoing_receiver_name_text;
DROP INDEX IF EXISTS idx_letters_incoming_sender_name;
DROP INDEX IF EXISTS idx_letters_outgoing_receiver_name;
-- Remove columns
ALTER TABLE letters_incoming
DROP COLUMN IF EXISTS sender_name;
ALTER TABLE letters_outgoing
DROP COLUMN IF EXISTS receiver_name;
@@ -1,15 +0,0 @@
-- Add sender_name to incoming letters
ALTER TABLE letters_incoming
ADD COLUMN IF NOT EXISTS sender_name VARCHAR(255);
-- Add receiver_name to outgoing letters
ALTER TABLE letters_outgoing
ADD COLUMN IF NOT EXISTS receiver_name VARCHAR(255);
-- Add indexes for the new fields to support searching
CREATE INDEX IF NOT EXISTS idx_letters_incoming_sender_name ON letters_incoming(sender_name) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_receiver_name ON letters_outgoing(receiver_name) WHERE deleted_at IS NULL;
-- Add GIN indexes for full-text search
CREATE INDEX IF NOT EXISTS idx_letters_incoming_sender_name_text ON letters_incoming USING gin(to_tsvector('simple', COALESCE(sender_name, '')));
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_receiver_name_text ON letters_outgoing USING gin(to_tsvector('simple', COALESCE(receiver_name, '')));
@@ -1,9 +0,0 @@
BEGIN;
-- Drop index
DROP INDEX IF EXISTS idx_letters_incoming_type;
-- Remove type column from letters_incoming table
ALTER TABLE letters_incoming DROP COLUMN IF EXISTS type;
COMMIT;
@@ -1,11 +0,0 @@
BEGIN;
-- Add type column to letters_incoming table
ALTER TABLE letters_incoming
ADD COLUMN IF NOT EXISTS type TEXT DEFAULT 'UTAMA'
CHECK (type IN ('UTAMA', 'TEMBUSAN'));
-- Add index for type column for better query performance
CREATE INDEX IF NOT EXISTS idx_letters_incoming_type ON letters_incoming(type);
COMMIT;
@@ -1,9 +0,0 @@
BEGIN;
-- Drop index
DROP INDEX IF EXISTS idx_letters_incoming_addressee;
-- Remove addressee column from letters_incoming table
ALTER TABLE letters_incoming DROP COLUMN IF EXISTS addressee;
COMMIT;
@@ -1,10 +0,0 @@
BEGIN;
-- Add addressee column to letters_incoming table
ALTER TABLE letters_incoming
ADD COLUMN IF NOT EXISTS addressee TEXT;
-- Add index for addressee column for better search performance
CREATE INDEX IF NOT EXISTS idx_letters_incoming_addressee ON letters_incoming(addressee);
COMMIT;
@@ -1,11 +0,0 @@
BEGIN;
DROP INDEX IF EXISTS idx_letters_outgoing_is_archived;
DROP INDEX IF EXISTS idx_letters_outgoing_archived_at;
DROP INDEX IF EXISTS idx_letters_outgoing_archived_status;
ALTER TABLE letters_outgoing
DROP COLUMN IF EXISTS is_archived,
DROP COLUMN IF EXISTS archived_at;
COMMIT;
@@ -1,12 +0,0 @@
BEGIN;
ALTER TABLE letters_outgoing
ADD COLUMN IF NOT EXISTS is_archived BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS archived_at TIMESTAMP WITHOUT TIME ZONE;
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_is_archived ON letters_outgoing(is_archived);
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_archived_at ON letters_outgoing(archived_at);
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_archived_status ON letters_outgoing(is_archived, status);
COMMIT;
@@ -1,11 +0,0 @@
BEGIN;
DROP INDEX IF EXISTS idx_letters_incoming_is_archived;
DROP INDEX IF EXISTS idx_letters_incoming_archived_at;
DROP INDEX IF EXISTS idx_letters_incoming_archived_status;
ALTER TABLE letters_incoming
DROP COLUMN IF EXISTS is_archived,
DROP COLUMN IF EXISTS archived_at;
COMMIT;
@@ -1,12 +0,0 @@
BEGIN;
ALTER TABLE letters_incoming
ADD COLUMN IF NOT EXISTS is_archived BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS archived_at TIMESTAMP WITHOUT TIME ZONE;
CREATE INDEX IF NOT EXISTS idx_letters_incoming_is_archived ON letters_incoming(is_archived);
CREATE INDEX IF NOT EXISTS idx_letters_incoming_archived_at ON letters_incoming(archived_at);
CREATE INDEX IF NOT EXISTS idx_letters_incoming_archived_status ON letters_incoming(is_archived, status);
COMMIT;
@@ -1,8 +0,0 @@
BEGIN;
DROP INDEX IF EXISTS idx_letters_outgoing_revision_number;
ALTER TABLE letters_outgoing
DROP COLUMN IF EXISTS revision_number;
COMMIT;
@@ -1,8 +0,0 @@
BEGIN;
ALTER TABLE letters_outgoing
ADD COLUMN IF NOT EXISTS revision_number INTEGER NOT NULL DEFAULT 0;
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_revision_number ON letters_outgoing(revision_number);
COMMIT;
@@ -1,14 +0,0 @@
BEGIN;
-- Drop indexes
DROP INDEX IF EXISTS idx_letter_outgoing_attachments_revision;
DROP INDEX IF EXISTS idx_letter_outgoing_approvals_revision;
-- Remove revision_number from tables
ALTER TABLE letter_outgoing_attachments
DROP COLUMN IF EXISTS revision_number;
ALTER TABLE letter_outgoing_approvals
DROP COLUMN IF EXISTS revision_number;
COMMIT;
@@ -1,15 +0,0 @@
BEGIN;
-- Add revision_number to letter_outgoing_attachments
ALTER TABLE letter_outgoing_attachments
ADD COLUMN IF NOT EXISTS revision_number INTEGER NOT NULL DEFAULT 0;
-- Add revision_number to letter_outgoing_approvals
ALTER TABLE letter_outgoing_approvals
ADD COLUMN IF NOT EXISTS revision_number INTEGER NOT NULL DEFAULT 0;
-- Create indexes for better query performance
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_attachments_revision ON letter_outgoing_attachments(letter_id, revision_number);
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_approvals_revision ON letter_outgoing_approvals(letter_id, revision_number);
COMMIT;
@@ -1,13 +0,0 @@
BEGIN;
CREATE TABLE IF NOT EXISTS repository_attachments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
file_url TEXT NOT NULL,
file_name TEXT NOT NULL,
file_type TEXT NOT NULL,
category TEXT NOT NULL,
uploaded_by UUID REFERENCES users(id) ON DELETE SET NULL,
uploaded_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
COMMIT;
@@ -1,2 +0,0 @@
ALTER TABLE letter_outgoing_attachments
ADD COLUMN IF NOT EXISTS is_final BOOLEAN NOT NULL DEFAULT FALSE;
@@ -1,12 +0,0 @@
ALTER TABLE letter_outgoing_attachments
DROP COLUMN IF EXISTS is_final;
CREATE TABLE IF NOT EXISTS letter_outgoing_final_attachments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
letter_id UUID NOT NULL REFERENCES letters_outgoing(id) ON DELETE CASCADE,
file_url TEXT NOT NULL,
file_name TEXT NOT NULL,
file_type TEXT NOT NULL,
uploaded_by UUID REFERENCES users(id) ON DELETE SET NULL,
uploaded_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);