CREATE TABLE IF NOT EXISTS roles ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(80) NOT NULL UNIQUE, description VARCHAR(255) NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; CREATE TABLE IF NOT EXISTS permissions ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(120) NOT NULL UNIQUE, description VARCHAR(255) NULL ) ENGINE=InnoDB; CREATE TABLE IF NOT EXISTS role_permissions ( role_id BIGINT UNSIGNED NOT NULL, permission_id BIGINT UNSIGNED NOT NULL, PRIMARY KEY (role_id, permission_id), FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE ) ENGINE=InnoDB; CREATE TABLE IF NOT EXISTS users ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, role_id BIGINT UNSIGNED NOT NULL, email VARCHAR(190) NOT NULL UNIQUE, name VARCHAR(120) NOT NULL, password_hash VARCHAR(255) NOT NULL, is_active BOOLEAN NOT NULL DEFAULT TRUE, last_login_at TIMESTAMP NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (role_id) REFERENCES roles(id) ) ENGINE=InnoDB; CREATE TABLE IF NOT EXISTS clients ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(190) NOT NULL, registration_number VARCHAR(120) NULL, status ENUM('active', 'inactive') NOT NULL DEFAULT 'active', support_email VARCHAR(190) NULL, support_phone VARCHAR(60) NULL, preferred_contact_method VARCHAR(40) NULL, physical_address TEXT NULL, postal_address TEXT NULL, general_notes TEXT NULL, created_by BIGINT UNSIGNED NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX clients_name_idx (name), INDEX clients_status_idx (status), FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL ) ENGINE=InnoDB; CREATE TABLE IF NOT EXISTS client_contacts ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, client_id BIGINT UNSIGNED NOT NULL, name VARCHAR(120) NOT NULL, email VARCHAR(190) NULL, phone VARCHAR(60) NULL, is_primary BOOLEAN NOT NULL DEFAULT FALSE, primary_client_id BIGINT UNSIGNED AS (IF(is_primary, client_id, NULL)) STORED, notes TEXT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE CASCADE, INDEX contacts_client_idx (client_id), UNIQUE KEY one_primary_contact (primary_client_id) ) ENGINE=InnoDB; CREATE TABLE IF NOT EXISTS jobcard_sequences ( sequence_year SMALLINT UNSIGNED PRIMARY KEY, next_sequence INT UNSIGNED NOT NULL ) ENGINE=InnoDB; CREATE TABLE IF NOT EXISTS sla_agreements ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, client_id BIGINT UNSIGNED NOT NULL, enabled BOOLEAN NOT NULL DEFAULT TRUE, agreement_type VARCHAR(120) NULL, allocated_hours DECIMAL(10,2) NOT NULL DEFAULT 0, period_type ENUM('monthly', 'annual', 'custom') NOT NULL DEFAULT 'monthly', start_date DATE NULL, end_date DATE NULL, rollover_enabled BOOLEAN NOT NULL DEFAULT FALSE, notes TEXT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE CASCADE, INDEX sla_client_idx (client_id) ) ENGINE=InnoDB; CREATE TABLE IF NOT EXISTS jobcards ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, reference_no VARCHAR(40) NOT NULL UNIQUE, client_id BIGINT UNSIGNED NOT NULL, created_by BIGINT UNSIGNED NULL, priority VARCHAR(40) NOT NULL DEFAULT 'normal', status VARCHAR(60) NOT NULL DEFAULT 'new', work_requested TEXT NOT NULL, technician_notes TEXT NULL, internal_notes TEXT NULL, completed_at DATETIME NULL, closed_at DATETIME NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (client_id) REFERENCES clients(id), FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL, INDEX jobcards_client_idx (client_id), INDEX jobcards_status_idx (status), INDEX jobcards_created_idx (created_at) ) ENGINE=InnoDB; CREATE TABLE IF NOT EXISTS jobcard_assignments ( jobcard_id BIGINT UNSIGNED NOT NULL, user_id BIGINT UNSIGNED NOT NULL, assigned_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, assigned_by BIGINT UNSIGNED NULL, PRIMARY KEY (jobcard_id, user_id), FOREIGN KEY (jobcard_id) REFERENCES jobcards(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (assigned_by) REFERENCES users(id) ON DELETE SET NULL ) ENGINE=InnoDB; CREATE TABLE IF NOT EXISTS time_entries ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, jobcard_id BIGINT UNSIGNED NOT NULL, technician_id BIGINT UNSIGNED NOT NULL, work_date DATE NOT NULL, start_time TIME NULL, end_time TIME NULL, hours DECIMAL(10,2) NOT NULL, notes TEXT NULL, counts_toward_sla BOOLEAN NOT NULL DEFAULT TRUE, created_by BIGINT UNSIGNED NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (jobcard_id) REFERENCES jobcards(id) ON DELETE CASCADE, FOREIGN KEY (technician_id) REFERENCES users(id), FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL, INDEX time_jobcard_idx (jobcard_id), INDEX time_date_idx (work_date) ) ENGINE=InnoDB; CREATE TABLE IF NOT EXISTS audit_events ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id BIGINT UNSIGNED NULL, action VARCHAR(120) NOT NULL, entity_type VARCHAR(80) NOT NULL, entity_id BIGINT UNSIGNED NULL, metadata JSON NULL, ip_address VARCHAR(45) NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX audit_entity_idx (entity_type, entity_id), INDEX audit_created_idx (created_at), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL ) ENGINE=InnoDB; INSERT IGNORE INTO roles (name, description) VALUES ('Administrator', 'Full system access'), ('Accounts', 'Client, jobcard and reporting access'), ('Technician', 'Assigned support work access'); INSERT IGNORE INTO permissions (name, description) VALUES ('dashboard.view', 'View the operational dashboard'), ('clients.view', 'View client records'), ('clients.manage', 'Create and edit client records'), ('jobcards.view', 'View jobcards'), ('jobcards.manage', 'Create and update jobcards'), ('reports.view', 'View reports'), ('reports.export', 'Export reports'), ('users.manage', 'Manage users'), ('roles.manage', 'Manage roles and permissions'), ('audit.view', 'View audit events'), ('credentials.view', 'View protected credentials'); INSERT IGNORE INTO role_permissions (role_id, permission_id) SELECT r.id, p.id FROM roles r CROSS JOIN permissions p WHERE r.name = 'Administrator'; INSERT IGNORE INTO role_permissions (role_id, permission_id) SELECT r.id, p.id FROM roles r JOIN permissions p ON p.name IN ('dashboard.view','clients.view','jobcards.view','reports.view','reports.export') WHERE r.name = 'Accounts'; INSERT IGNORE INTO role_permissions (role_id, permission_id) SELECT r.id, p.id FROM roles r JOIN permissions p ON p.name IN ('dashboard.view','clients.view','jobcards.view','jobcards.manage') WHERE r.name = 'Technician';