Init Eslogad

This commit is contained in:
Aditya Siregar
2025-08-09 15:09:43 +07:00
commit 9e95e8ee5e
96 changed files with 7108 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
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
@@ -0,0 +1,146 @@
-- 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)
);
+132
View File
@@ -0,0 +1,132 @@
BEGIN;
-- =========================
-- Departments (as requested)
-- =========================
-- Root org namespace is "eslogad" in ltree path
INSERT INTO departments (name, code, path) VALUES
('RENBINMINLOG', 'renbinminlog', 'eslogad.renbinminlog'),
('FASKON BMN', 'faskon_bmn', 'eslogad.faskon_bmn'),
('BEKPALKES', 'bekpalkes', 'eslogad.bekpalkes')
ON CONFLICT (code) DO UPDATE
SET name = EXCLUDED.name,
path = EXCLUDED.path,
updated_at = CURRENT_TIMESTAMP;
-- =========================
-- 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 positions (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;
@@ -0,0 +1,30 @@
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
@@ -0,0 +1 @@
DROP TABLE IF EXISTS user_profiles;
+30
View File
@@ -0,0 +1,30 @@
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
@@ -0,0 +1,60 @@
-- =======================
-- 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');