-- JOBcard additive upgrade for installations created before the current schema. -- Take a database backup first. Run with the target database selected: -- mysql --default-character-set=utf8mb4 -u USER -p DATABASE < database/upgrade.sql -- Resolve duplicate SLA rows before adding the unique client constraint. CREATE TABLE IF NOT EXISTS technical_information ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, client_id BIGINT UNSIGNED NOT NULL, category VARCHAR(80) NOT NULL, data_json JSON NOT NULL, updated_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 (client_id) REFERENCES clients(id) ON DELETE CASCADE, FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL, UNIQUE KEY technical_client_category (client_id, category) ) ENGINE=InnoDB; CREATE TABLE IF NOT EXISTS credentials ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, client_id BIGINT UNSIGNED NOT NULL, category VARCHAR(80) NOT NULL, label VARCHAR(120) NOT NULL, username VARCHAR(190) NULL, secret_ciphertext TEXT NOT NULL, notes TEXT NULL, is_active BOOLEAN NOT NULL DEFAULT TRUE, created_by BIGINT UNSIGNED NULL, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE CASCADE, FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL, INDEX credentials_client_idx (client_id), INDEX credentials_category_idx (category) ) 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 jobcard_status_history ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, jobcard_id BIGINT UNSIGNED NOT NULL, from_status VARCHAR(60) NOT NULL, to_status VARCHAR(60) NOT NULL, changed_by BIGINT UNSIGNED NULL, changed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (jobcard_id) REFERENCES jobcards(id) ON DELETE CASCADE, FOREIGN KEY (changed_by) REFERENCES users(id) ON DELETE SET NULL, INDEX status_history_jobcard_idx (jobcard_id, changed_at) ) ENGINE=InnoDB; CREATE TABLE IF NOT EXISTS attachments ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, jobcard_id BIGINT UNSIGNED NOT NULL, original_name VARCHAR(255) NOT NULL, stored_name VARCHAR(255) NOT NULL UNIQUE, mime_type VARCHAR(120) NOT NULL, file_size BIGINT UNSIGNED NOT NULL, client_visible BOOLEAN NOT NULL DEFAULT FALSE, uploaded_by BIGINT UNSIGNED NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (jobcard_id) REFERENCES jobcards(id) ON DELETE CASCADE, FOREIGN KEY (uploaded_by) REFERENCES users(id) ON DELETE SET NULL, INDEX attachments_jobcard_idx (jobcard_id) ) ENGINE=InnoDB; CREATE TABLE IF NOT EXISTS notifications ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id BIGINT UNSIGNED NOT NULL, type VARCHAR(80) NOT NULL, title VARCHAR(190) NOT NULL, body TEXT NULL, deduplication_key VARCHAR(190) NULL, read_at TIMESTAMP NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, UNIQUE KEY notification_dedup_idx (user_id, deduplication_key), INDEX notification_unread_idx (user_id, read_at, created_at) ) ENGINE=InnoDB; INSERT IGNORE INTO permissions (name, description) VALUES ('technical.view', 'View client technical information'), ('technical.manage', 'Manage client technical information'), ('jobcards.close', 'Close jobcards'), ('credentials.view', 'View protected credentials'), ('credentials.manage', 'Manage protected credentials'), ('attachments.view', 'View jobcard attachments'), ('attachments.manage', 'Manage jobcard attachments'), ('notifications.view', 'View notifications'); 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 ('technical.view') WHERE r.name = 'Technician'; -- Enforce one SLA agreement per client after duplicate remediation. SET @sla_unique_exists := ( SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'sla_agreements' AND index_name = 'sla_client_unique' ); SET @sla_duplicate_count := ( SELECT COUNT(*) FROM (SELECT client_id FROM sla_agreements GROUP BY client_id HAVING COUNT(*) > 1) duplicates_check ); SET @sla_add_unique_sql := IF(@sla_duplicate_count = 0 AND @sla_unique_exists = 0, 'ALTER TABLE sla_agreements ADD UNIQUE KEY sla_client_unique (client_id)', IF(@sla_unique_exists > 0, 'SELECT 1', 'SELECT 1 FROM jobcard_upgrade_duplicate_sla_rows')); PREPARE sla_add_unique_stmt FROM @sla_add_unique_sql; EXECUTE sla_add_unique_stmt; DEALLOCATE PREPARE sla_add_unique_stmt;