Browse Source

committing to insanit

UMTS at Teleco 1 month ago
commit
b51d33cb37

+ 26 - 0
.gitignore

@@ -0,0 +1,26 @@
+# Ignore Rust build artifacts
+/target/
+Cargo.lock
+
+# Ignore vscodes bs files
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# Ignore macOS files
+.DS_Store
+
+# Ignore session data
+session.json
+
+# Ignore logs
+*.log
+
+# Ignore Sources
+backend/seckelapi/sources
+frontend/desktop-client/sources
+
+# Ignore Sensitive setup .env
+.env

+ 75 - 0
README.md

@@ -0,0 +1,75 @@
+# BeepZone Inverory Setup Helper Scripts and Database Files
+Huh so you want to actually try beepzone? (Oh god)
+
+Well then since I have no setup guides tbh I'll provide a bash script here to build and get you started, do not use this in Prod like at all I dont recommend my own software for such things in general tbh.
+
+## Whats in this repository?
+- Database Schema and Dumps from Dev Environement
+- Helper Script to get you started and do some basic user management as no client can do so yet lol
+
+what does the script do ?
+
+helps you get started by priividing an assistive TUI for :
+- running a podman container with mariadb and
+    - letting you configure access details
+    - importing the schema
+    - managing users and roles in the db (create/delete both with bcrypt password hashing)
+    - optionally import some seeding data
+- compiling and setting up SeckelAPI to be able to talk with mariadb
+    - either as a podman container (recommended, uses proper port mapping 5777:5777 and host gateway for db access)
+    - or natively on the system (for dev work)
+    - auto-configures database connection settings
+- compiling the desktop client so you can start beepin
+
+someday once i consider em release worthy it will also obviously be able to compile the terminal/kiosk/embedded client aswell as the mobile app 
+
+a real installer that is not just a setup bash script is planned but thats far into the future idk when 
+
+Setup Scripts currently do not support windows
+
+requirements for script:
+- mac
+    - podman installed and configured (podman-desktop recommended for dev work)
+    - brew installed
+    - mysql-client installed (brew install mysql-client -> script auto detects /opt/homebrew/opt/mysql-client/bin/)
+    - rust and tools installed via rustup
+    - dialog for TUI (brew install dialog)
+- linux
+    - any debian based distro (if not, install dependencies yourself)
+    - mariadb-client installed (apt-get install mariadb-client)
+    - rust and tools installed via rustup
+    - podman installed and configured (podman-desktop recommended for dev work)
+    - dialog for TUI (apt-get install dialog)
+
+internet access too obviously to pull em sources :
+https://git.teleco.ch/crt/beepzone-client-egui-emo.git
+https://git.teleco.ch/crt/seckelapi.git
+
+## how to use
+
+just run the helper script:
+```bash
+./beepzone-helper.sh
+```
+
+it will guide you through:
+1. configuring and running mariadb in podman
+2. importing the database schema (dumped version with all updates)
+3. very basics of managing users and roles (create/delete users/roles with power level 1-100)
+4. setting up SeckelAPI (container deployment recommended)
+5. building the desktop client
+
+the script stores config in `.env` file and auto-detects installed tools in common locations
+
+## container stuff
+
+- mariadb container: exposes port 3306, persists data
+- seckelapi container: uses rust 1.92 + debian trixie, port 5777:5777, connects to mariadb via host.containers.internal
+- both use latest stable base images
+
+## notes
+
+- passwords are hashed with bcrypt cost 12 using htpasswd or python bcrypt fallback
+- database config auto-updates when building containers (uses host.containers.internal for db host)
+- native builds use localhost for db access
+- run scripts provided: ./run-seckelapi.sh and ./run-client.sh (execute from sources/ dir)

File diff suppressed because it is too large
+ 34 - 0
backend/database/dev/beepzone-full-dump.sql


+ 1882 - 0
backend/database/dev/beepzone-schema-consolidated-backup.sql

@@ -0,0 +1,1882 @@
+-- BeepZone Database Schema v0.0.8 (Consolidated)
+-- MariaDB/MySQL Compatible
+-- Created: 2025-12-13
+-- Includes: Complete schema with triggers, asset change logging, printing, templates, zones
+--
+-- AUTO-POPULATION TRIGGERS:
+-- The following fields are auto-populated from @current_user_id if not provided:
+--   • assets.created_by (on INSERT)
+--   • assets.last_modified_by (on UPDATE)
+--   • borrowers.added_by (on INSERT)
+--   • borrowers.last_unban_by (on UPDATE when unbanning)
+--   • lending_history.checked_out_by (on INSERT)
+--   • lending_history.checked_in_by (on UPDATE when returning)
+--   • issue_tracker.reported_by (on INSERT)
+--   • physical_audit_logs.audited_by (on INSERT)
+-- Your API proxy should set @current_user_id before executing queries.
+
+-- Drop tables if they exist (in reverse order of dependencies)
+DROP TABLE IF EXISTS print_history;
+DROP TABLE IF EXISTS issue_tracker_change_log;
+DROP TABLE IF EXISTS asset_change_log;
+DROP TABLE IF EXISTS issue_tracker;
+DROP TABLE IF EXISTS physical_audit_logs;
+DROP TABLE IF EXISTS physical_audits;
+DROP TABLE IF EXISTS lending_history;
+DROP TABLE IF EXISTS templates;
+DROP TABLE IF EXISTS assets;
+DROP TABLE IF EXISTS borrowers;
+DROP TABLE IF EXISTS audit_tasks;
+DROP TABLE IF EXISTS suppliers;
+DROP TABLE IF EXISTS zones;
+DROP TABLE IF EXISTS categories;
+DROP TABLE IF EXISTS label_templates;
+DROP TABLE IF EXISTS printer_settings;
+DROP TABLE IF EXISTS users;
+DROP TABLE IF EXISTS roles;
+
+-- ============================================
+-- Roles Table
+-- ============================================
+CREATE TABLE roles (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    name VARCHAR(100) NOT NULL UNIQUE,
+    power INT NOT NULL CHECK (power >= 1 AND power <= 100),
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+
+-- ============================================
+-- Users Table
+-- ============================================
+CREATE TABLE users (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    name VARCHAR(200) NOT NULL,
+    username VARCHAR(100) NOT NULL UNIQUE,
+    password VARCHAR(255) NOT NULL,
+    pin_code VARCHAR(8) NULL,
+    login_string VARCHAR(255) NULL,
+    role_id INT NOT NULL,
+    email VARCHAR(255) NULL,
+    phone VARCHAR(50) NULL,
+    notes TEXT NULL,
+    active BOOLEAN DEFAULT TRUE,
+    last_login_date DATETIME NULL,
+    created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    password_reset_token VARCHAR(255) NULL,
+    password_reset_expiry DATETIME NULL,
+    FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE RESTRICT,
+    INDEX idx_username (username),
+    INDEX idx_login_string (login_string)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+
+-- ============================================
+-- Categories Table
+-- ============================================
+CREATE TABLE categories (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    category_name VARCHAR(200) NOT NULL,
+    category_description TEXT NULL,
+    parent_id INT NULL,
+    category_code VARCHAR(50) NULL,
+    FOREIGN KEY (parent_id) REFERENCES categories(id) ON DELETE RESTRICT,
+    INDEX idx_parent (parent_id),
+    INDEX idx_code (category_code)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+
+-- ============================================
+-- Zones Table
+-- ============================================
+CREATE TABLE zones (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    zone_name VARCHAR(200) NOT NULL,
+    zone_notes TEXT NULL,
+    zone_type ENUM('Building', 'Floor', 'Room', 'Storage Area') NOT NULL,
+    zone_code VARCHAR(50) NOT NULL COMMENT 'Full hierarchical code (e.g., PS52-1-108)',
+    mini_code VARCHAR(50) NOT NULL COMMENT 'Local short code for this node (e.g., PS52, 1, 108)',
+    parent_id INT NULL,
+    include_in_parent BOOLEAN DEFAULT TRUE,
+    audit_timeout_minutes INT DEFAULT 60 COMMENT 'Audit timeout in minutes for this zone',
+    FOREIGN KEY (parent_id) REFERENCES zones(id) ON DELETE RESTRICT,
+    INDEX idx_parent (parent_id),
+    INDEX idx_type (zone_type),
+    UNIQUE INDEX uq_zone_code (zone_code),
+    INDEX idx_parent_type_mini (parent_id, zone_type, mini_code)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+
+-- ============================================
+-- Suppliers Table
+-- ============================================
+CREATE TABLE suppliers (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    name VARCHAR(200) NOT NULL,
+    contact VARCHAR(200) NULL,
+    email VARCHAR(255) NULL,
+    phone VARCHAR(50) NULL,
+    website VARCHAR(255) NULL,
+    notes TEXT NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+
+-- ============================================
+-- Audit Tasks Table
+-- ============================================
+CREATE TABLE audit_tasks (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    task_name VARCHAR(200) NOT NULL,
+    json_sequence JSON NOT NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+
+-- ============================================
+-- Borrowers Table
+-- ============================================
+CREATE TABLE borrowers (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    name VARCHAR(200) NOT NULL,
+    email VARCHAR(255) NULL,
+    phone_number VARCHAR(50) NULL,
+    class_name VARCHAR(100) NULL,
+    role VARCHAR(100) NULL,
+    notes TEXT NULL,
+    added_by INT NOT NULL,
+    added_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    banned BOOLEAN DEFAULT FALSE,
+    unban_fine DECIMAL(10, 2) DEFAULT 0.00,
+    last_unban_by INT NULL,
+    last_unban_date DATE NULL,
+    FOREIGN KEY (added_by) REFERENCES users(id) ON DELETE RESTRICT,
+    FOREIGN KEY (last_unban_by) REFERENCES users(id) ON DELETE SET NULL,
+    INDEX idx_name (name),
+    INDEX idx_banned (banned)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+
+-- ============================================
+-- Assets Table (Sexy Edition v2)
+-- ============================================
+CREATE TABLE assets (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    asset_tag VARCHAR(200) NULL UNIQUE,
+    asset_numeric_id INT NOT NULL UNIQUE CHECK (asset_numeric_id BETWEEN 10000000 AND 99999999),
+    asset_type ENUM('N', 'B', 'L', 'C') NOT NULL,
+    name VARCHAR(255) NULL,
+    category_id INT NULL,
+    manufacturer VARCHAR(200) NULL,
+    model VARCHAR(200) NULL,
+    serial_number VARCHAR(200) NULL,
+    zone_id INT NULL,
+    zone_plus ENUM('Floating Local', 'Floating Global', 'Clarify') NULL,
+    zone_note TEXT NULL,
+    status ENUM('Good', 'Attention', 'Faulty', 'Missing', 'Retired', 'In Repair', 'In Transit', 'Expired', 'Unmanaged') DEFAULT 'Good',
+    last_audit DATE NULL,
+    last_audit_status VARCHAR(100) NULL,
+    price DECIMAL(12, 2) NULL CHECK (price IS NULL OR price >= 0),
+    purchase_date DATE NULL,
+    warranty_until DATE NULL,
+    expiry_date DATE NULL,
+    quantity_available INT NULL,
+    quantity_total INT NULL,
+    quantity_used INT DEFAULT 0,
+    supplier_id INT NULL,
+    lendable BOOLEAN DEFAULT FALSE,
+    minimum_role_for_lending INT DEFAULT 1 CHECK (minimum_role_for_lending >= 1 AND minimum_role_for_lending <= 100),
+    lending_status ENUM('Available', 'Deployed', 'Borrowed', 'Overdue', 'Illegally Handed Out', 'Stolen') NULL,
+    current_borrower_id INT NULL,
+    due_date DATE NULL,
+    previous_borrower_id INT NULL,
+    audit_task_id INT NULL,
+    label_template_id INT NULL COMMENT 'Label template to use for this asset',
+    no_scan ENUM('Yes', 'Ask', 'No') DEFAULT 'No',
+    notes TEXT NULL,
+    additional_fields JSON NULL,
+    file_attachment MEDIUMBLOB NULL,
+    created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    created_by INT NULL,
+    last_modified_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    last_modified_by INT NULL,
+    FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE RESTRICT,
+    FOREIGN KEY (zone_id) REFERENCES zones(id) ON DELETE RESTRICT,
+    FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE SET NULL,
+    FOREIGN KEY (current_borrower_id) REFERENCES borrowers(id) ON DELETE SET NULL,
+    FOREIGN KEY (previous_borrower_id) REFERENCES borrowers(id) ON DELETE SET NULL,
+    FOREIGN KEY (audit_task_id) REFERENCES audit_tasks(id) ON DELETE SET NULL,
+    FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
+    FOREIGN KEY (last_modified_by) REFERENCES users(id) ON DELETE SET NULL,
+    INDEX idx_asset_tag (asset_tag),
+    INDEX idx_asset_numeric (asset_numeric_id),
+    INDEX idx_type (asset_type),
+    INDEX idx_status (status),
+    INDEX idx_zone (zone_id),
+    INDEX idx_category (category_id),
+    INDEX idx_lendable (lendable),
+    INDEX idx_lending_status (lending_status)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+
+-- ============================================
+-- Templates Table (with new sexy columns)
+-- ============================================
+CREATE TABLE templates (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    template_code VARCHAR(50) NULL UNIQUE,
+    asset_tag_generation_string VARCHAR(500) NULL,
+    description TEXT NULL,
+    active BOOLEAN DEFAULT TRUE,
+    asset_type ENUM('N', 'B', 'L', 'C') NULL,
+    name VARCHAR(255) NULL,
+    category_id INT NULL,
+    manufacturer VARCHAR(200) NULL,
+    model VARCHAR(200) NULL,
+    zone_id INT NULL,
+    zone_plus ENUM('Floating Local', 'Floating Global', 'Clarify') NULL,
+    zone_note TEXT NULL,
+    status ENUM('Good', 'Attention', 'Faulty', 'Missing', 'Retired', 'In Repair', 'In Transit', 'Expired', 'Unmanaged') NULL,
+    price DECIMAL(12, 2) NULL CHECK (price IS NULL OR price >= 0),
+    purchase_date DATE NULL COMMENT 'Default purchase date for assets created from this template',
+    purchase_date_now BOOLEAN DEFAULT FALSE COMMENT 'Auto-set purchase date to current date when creating assets',
+    warranty_until DATE NULL,
+    warranty_auto BOOLEAN DEFAULT FALSE COMMENT 'Auto-calculate warranty_until from purchase_date',
+    warranty_auto_amount INT NULL COMMENT 'Number of days/years for warranty calculation',
+    warranty_auto_unit ENUM('days', 'years') DEFAULT 'years' COMMENT 'Unit for warranty auto-calculation',
+    expiry_date DATE NULL,
+    expiry_auto BOOLEAN DEFAULT FALSE COMMENT 'Auto-calculate expiry_date from purchase_date',
+    expiry_auto_amount INT NULL COMMENT 'Number of days/years for expiry calculation',
+    expiry_auto_unit ENUM('days', 'years') DEFAULT 'years' COMMENT 'Unit for expiry auto-calculation',
+    quantity_total INT NULL,
+    quantity_used INT NULL,
+    supplier_id INT NULL,
+    lendable BOOLEAN NULL,
+    lending_status ENUM('Available', 'Borrowed', 'Overdue', 'Deployed', 'Illegally Handed Out', 'Stolen') DEFAULT 'Available' COMMENT 'Default lending status for assets created from this template',
+    minimum_role_for_lending INT NULL,
+    audit_task_id INT NULL,
+    label_template_id INT NULL COMMENT 'Default label template for assets created from this template',
+    no_scan ENUM('Yes', 'Ask', 'No') NULL,
+    notes TEXT NULL,
+    additional_fields JSON NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL,
+    FOREIGN KEY (zone_id) REFERENCES zones(id) ON DELETE SET NULL,
+    FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE SET NULL,
+    FOREIGN KEY (audit_task_id) REFERENCES audit_tasks(id) ON DELETE SET NULL,
+    INDEX idx_template_code (template_code)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+
+-- ============================================
+-- Lending History Table
+-- ============================================
+CREATE TABLE lending_history (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    asset_id INT NOT NULL,
+    borrower_id INT NOT NULL,
+    checkout_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    due_date DATE NULL,
+    return_date DATETIME NULL,
+    checked_out_by INT NULL,
+    checked_in_by INT NULL,
+    notes TEXT NULL,
+    FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE CASCADE,
+    FOREIGN KEY (borrower_id) REFERENCES borrowers(id) ON DELETE RESTRICT,
+    FOREIGN KEY (checked_out_by) REFERENCES users(id) ON DELETE RESTRICT,
+    FOREIGN KEY (checked_in_by) REFERENCES users(id) ON DELETE SET NULL,
+    INDEX idx_asset (asset_id),
+    INDEX idx_borrower (borrower_id),
+    INDEX idx_checkout_date (checkout_date),
+    INDEX idx_return_date (return_date)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+
+-- ============================================
+-- Physical Audits Table
+-- ============================================
+CREATE TABLE physical_audits (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    audit_type ENUM('full-zone', 'spot-check') NOT NULL,
+    zone_id INT NULL COMMENT 'Zone being audited (NULL for spot-check audits)',
+    audit_name VARCHAR(255) NULL COMMENT 'Custom name for the audit session',
+    started_by INT NOT NULL,
+    started_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    completed_at DATETIME NULL,
+    status ENUM('in-progress', 'all-good', 'timeout', 'attention', 'cancelled') DEFAULT 'in-progress',
+    timeout_minutes INT NULL COMMENT 'Timeout setting used for this audit',
+    issues_found JSON NULL COMMENT 'Array of issues: missing_assets, moved_assets, damaged_assets, etc.',
+    assets_expected INT NULL COMMENT 'Total assets expected to be found in zone',
+    assets_found INT DEFAULT 0 COMMENT 'Total assets actually found and scanned',
+    notes TEXT NULL,
+    cancelled_reason TEXT NULL,
+    FOREIGN KEY (zone_id) REFERENCES zones(id) ON DELETE RESTRICT,
+    FOREIGN KEY (started_by) REFERENCES users(id) ON DELETE RESTRICT,
+    INDEX idx_audit_type (audit_type),
+    INDEX idx_zone (zone_id),
+    INDEX idx_status (status),
+    INDEX idx_started_at (started_at),
+    INDEX idx_started_by (started_by)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+
+-- ============================================
+-- Physical Audit Logs Table
+-- ============================================
+CREATE TABLE physical_audit_logs (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    physical_audit_id INT NOT NULL COMMENT 'Reference to the audit session',
+    asset_id INT NOT NULL,
+    audit_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    audited_by INT NOT NULL,
+    status_found ENUM('Good', 'Attention', 'Faulty', 'Missing', 'Retired', 'In Repair', 'In Transit', 'Expired', 'Unmanaged') DEFAULT 'Good',
+    audit_task_id INT NULL COMMENT 'Which audit task was run on this asset',
+    audit_task_responses JSON NULL COMMENT 'User responses to the JSON sequence questions',
+    exception_type ENUM('wrong-zone', 'unexpected-asset', 'damaged', 'missing-label', 'other') NULL,
+    exception_details TEXT NULL COMMENT 'Details about the exception found',
+    found_in_zone_id INT NULL COMMENT 'Which zone the asset was actually found in (if different from expected)',
+    auditor_action ENUM('physical-move', 'virtual-update', 'no-action') NULL COMMENT 'What the auditor chose to do about wrong-zone assets',
+    notes TEXT NULL,
+    FOREIGN KEY (physical_audit_id) REFERENCES physical_audits(id) ON DELETE CASCADE,
+    FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE CASCADE,
+    FOREIGN KEY (audited_by) REFERENCES users(id) ON DELETE RESTRICT,
+    FOREIGN KEY (audit_task_id) REFERENCES audit_tasks(id) ON DELETE SET NULL,
+    FOREIGN KEY (found_in_zone_id) REFERENCES zones(id) ON DELETE SET NULL,
+    INDEX idx_physical_audit (physical_audit_id),
+    INDEX idx_asset (asset_id),
+    INDEX idx_audit_date (audit_date),
+    INDEX idx_audited_by (audited_by),
+    INDEX idx_status_found (status_found),
+    INDEX idx_exception_type (exception_type)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+
+-- ============================================
+-- Issue Tracker Table
+-- ============================================
+CREATE TABLE issue_tracker (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    issue_type ENUM('Asset Issue', 'Borrower Issue', 'System Issue', 'Maintenance', 'Other') NOT NULL,
+    asset_id INT NULL,
+    borrower_id INT NULL,
+    title VARCHAR(255) NOT NULL,
+    description TEXT NOT NULL,
+    severity ENUM('Critical', 'High', 'Medium', 'Low') NULL,
+    priority ENUM('Urgent', 'High', 'Normal', 'Low') DEFAULT 'Normal',
+    status ENUM('Open', 'In Progress', 'Resolved', 'Closed', 'On Hold') DEFAULT 'Open',
+    solution ENUM('Fixed', 'Replaced', 'Clarify', 'No Action Needed', 'Deferred', 'Items Returned', 'Automatically Fixed') NULL,
+    solution_plus TEXT NULL,
+    replacement_asset_id INT NULL,
+    reported_by INT NOT NULL,
+    assigned_to INT NULL,
+    resolved_by INT NULL,
+    cost DECIMAL(10, 2) NULL,
+    created_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    updated_date DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    resolved_date DATETIME NULL,
+    notes TEXT NULL,
+    auto_detected BOOLEAN DEFAULT FALSE,
+    detection_trigger VARCHAR(100) NULL,
+    FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE CASCADE,
+    FOREIGN KEY (borrower_id) REFERENCES borrowers(id) ON DELETE CASCADE,
+    FOREIGN KEY (replacement_asset_id) REFERENCES assets(id) ON DELETE SET NULL,
+    FOREIGN KEY (reported_by) REFERENCES users(id) ON DELETE RESTRICT,
+    FOREIGN KEY (assigned_to) REFERENCES users(id) ON DELETE SET NULL,
+    FOREIGN KEY (resolved_by) REFERENCES users(id) ON DELETE SET NULL,
+    INDEX idx_issue_type (issue_type),
+    INDEX idx_asset (asset_id),
+    INDEX idx_borrower (borrower_id),
+    INDEX idx_severity (severity),
+    INDEX idx_status (status),
+    INDEX idx_created_date (created_date),
+    INDEX idx_auto_detected (auto_detected)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+
+-- ============================================
+-- Issue Tracker Change Log Table
+-- ============================================
+CREATE TABLE issue_tracker_change_log (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    issue_id INT NOT NULL,
+    change_type ENUM('INSERT', 'UPDATE', 'DELETE') NOT NULL,
+    changed_fields JSON NULL,
+    old_values JSON NULL,
+    new_values JSON NULL,
+    changed_by INT NULL,
+    change_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    FOREIGN KEY (issue_id) REFERENCES issue_tracker(id) ON DELETE CASCADE,
+    FOREIGN KEY (changed_by) REFERENCES users(id) ON DELETE SET NULL,
+    INDEX idx_issue (issue_id),
+    INDEX idx_change_type (change_type),
+    INDEX idx_change_date (change_date)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+
+-- ============================================
+-- Asset Change Log Table
+-- ============================================
+CREATE TABLE asset_change_log (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    table_name VARCHAR(50) NOT NULL,
+    action ENUM('INSERT', 'UPDATE', 'DELETE') NOT NULL,
+    record_id INT NOT NULL,
+    changed_fields JSON NULL COMMENT 'Only fields that actually changed',
+    old_values JSON NULL,
+    new_values JSON NULL,
+    changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    changed_by_id INT NULL,
+    changed_by_username VARCHAR(100) NULL,
+    FOREIGN KEY (changed_by_id) REFERENCES users(id) ON DELETE SET NULL,
+    INDEX idx_table_action (table_name, action),
+    INDEX idx_timestamp (changed_at),
+    INDEX idx_record (record_id),
+    INDEX idx_user (changed_by_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+
+-- ============================================
+-- Label Templates Table
+-- ============================================
+CREATE TABLE label_templates (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    template_code VARCHAR(100) NOT NULL UNIQUE COMMENT 'Unique code like "CABLE"',
+    template_name VARCHAR(200) NOT NULL COMMENT 'Human readable name',
+    layout_json JSON NOT NULL COMMENT 'Universal label design: graphics, auto-populated field placeholders, styling with space dimensions',
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    created_by INT NULL,
+    last_modified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    last_modified_by INT NULL,
+    FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
+    FOREIGN KEY (last_modified_by) REFERENCES users(id) ON DELETE SET NULL,
+    INDEX idx_template_code (template_code),
+    INDEX idx_template_name (template_name)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+
+-- ============================================
+-- Printer Settings Table
+-- ============================================
+CREATE TABLE printer_settings (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    printer_name VARCHAR(200) NOT NULL,
+    description TEXT NULL,
+    log BOOLEAN DEFAULT TRUE COMMENT 'Log all print jobs to this printer',
+    can_be_used_for_reports BOOLEAN DEFAULT FALSE COMMENT 'Can this printer be used for printing reports',
+    min_powerlevel_to_use INT NOT NULL DEFAULT 75 COMMENT 'Minimum role power level required to use this printer',
+    printer_plugin ENUM('Ptouch', 'Brother', 'Zebra', 'System', 'PDF', 'Network', 'Custom') NOT NULL COMMENT 'Which printer plugin the client should send printer_settings to',
+    printer_settings JSON NOT NULL COMMENT 'Printer-specific settings: connection, paper size, DPI, margins, etc.',
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    created_by INT NULL,
+    last_modified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    last_modified_by INT NULL,
+    FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
+    FOREIGN KEY (last_modified_by) REFERENCES users(id) ON DELETE SET NULL,
+    INDEX idx_printer_name (printer_name),
+    INDEX idx_printer_plugin (printer_plugin),
+    INDEX idx_min_powerlevel (min_powerlevel_to_use),
+    INDEX idx_can_reports (can_be_used_for_reports),
+    CHECK (min_powerlevel_to_use >= 1 AND min_powerlevel_to_use <= 100)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+
+-- ============================================
+-- Print History Table (Labels & Reports)
+-- ============================================
+CREATE TABLE print_history (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    entity_type ENUM('Asset', 'Template', 'Borrower', 'Zone', 'Report', 'Custom') NOT NULL,
+    entity_id INT NULL COMMENT 'ID of the asset/template/borrower/zone (NULL for reports)',
+    label_template_id INT NULL,
+    printer_id INT NULL,
+    quantity INT DEFAULT 1,
+    print_status ENUM('Success', 'Failed', 'Cancelled', 'Queued') NOT NULL,
+    error_message TEXT NULL,
+    rendered_data JSON NULL COMMENT 'The actual data that was sent to printer (for debugging)',
+    printed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    printed_by INT NULL,
+    FOREIGN KEY (label_template_id) REFERENCES label_templates(id) ON DELETE SET NULL,
+    FOREIGN KEY (printer_id) REFERENCES printer_settings(id) ON DELETE SET NULL,
+    FOREIGN KEY (printed_by) REFERENCES users(id) ON DELETE SET NULL,
+    INDEX idx_entity (entity_type, entity_id),
+    INDEX idx_printed_at (printed_at),
+    INDEX idx_printed_by (printed_by),
+    INDEX idx_printer (printer_id),
+    INDEX idx_status (print_status)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+
+-- ============================================
+-- TRIGGERS FOR ASSETS TABLE
+-- ============================================
+
+DELIMITER //
+
+-- Trigger: Auto-populate created_by on INSERT
+DROP TRIGGER IF EXISTS assets_before_insert_meta//
+CREATE TRIGGER assets_before_insert_meta
+BEFORE INSERT ON assets
+FOR EACH ROW
+BEGIN
+    IF NEW.created_by IS NULL AND @current_user_id IS NOT NULL THEN
+        SET NEW.created_by = @current_user_id;
+    END IF;
+END//
+
+-- Trigger: Auto-update last_modified_date and last_modified_by
+DROP TRIGGER IF EXISTS assets_before_update_meta//
+CREATE TRIGGER assets_before_update_meta
+BEFORE UPDATE ON assets
+FOR EACH ROW
+BEGIN
+    SET NEW.last_modified_date = NOW();
+    IF @current_user_id IS NOT NULL THEN
+        SET NEW.last_modified_by = @current_user_id;
+    END IF;
+END//
+
+-- Trigger: Log INSERT operations (only non-NULL fields for efficiency)
+DROP TRIGGER IF EXISTS assets_after_insert_log//
+CREATE TRIGGER assets_after_insert_log
+AFTER INSERT ON assets
+FOR EACH ROW
+BEGIN
+    DECLARE username VARCHAR(100);
+    DECLARE set_fields_array JSON;
+    DECLARE new_vals JSON;
+    
+    IF @current_user_id IS NOT NULL THEN
+        SELECT users.username INTO username FROM users WHERE id = @current_user_id;
+    END IF;
+    
+    -- Build JSON objects only with non-NULL fields
+    SET set_fields_array = JSON_ARRAY();
+    SET new_vals = JSON_OBJECT();
+    
+    -- Always log these core fields
+    IF NEW.asset_tag IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'asset_tag');
+        SET new_vals = JSON_SET(new_vals, '$.asset_tag', NEW.asset_tag);
+    END IF;
+    
+    IF NEW.asset_numeric_id IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'asset_numeric_id');
+        SET new_vals = JSON_SET(new_vals, '$.asset_numeric_id', NEW.asset_numeric_id);
+    END IF;
+    
+    IF NEW.asset_type IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'asset_type');
+        SET new_vals = JSON_SET(new_vals, '$.asset_type', NEW.asset_type);
+    END IF;
+    
+    IF NEW.name IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'name');
+        SET new_vals = JSON_SET(new_vals, '$.name', NEW.name);
+    END IF;
+    
+    IF NEW.category_id IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'category_id');
+        SET new_vals = JSON_SET(new_vals, '$.category_id', NEW.category_id);
+    END IF;
+    
+    IF NEW.manufacturer IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'manufacturer');
+        SET new_vals = JSON_SET(new_vals, '$.manufacturer', NEW.manufacturer);
+    END IF;
+    
+    IF NEW.model IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'model');
+        SET new_vals = JSON_SET(new_vals, '$.model', NEW.model);
+    END IF;
+    
+    IF NEW.serial_number IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'serial_number');
+        SET new_vals = JSON_SET(new_vals, '$.serial_number', NEW.serial_number);
+    END IF;
+    
+    IF NEW.zone_id IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'zone_id');
+        SET new_vals = JSON_SET(new_vals, '$.zone_id', NEW.zone_id);
+    END IF;
+    
+    IF NEW.zone_plus IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'zone_plus');
+        SET new_vals = JSON_SET(new_vals, '$.zone_plus', NEW.zone_plus);
+    END IF;
+    
+    IF NEW.zone_note IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'zone_note');
+        SET new_vals = JSON_SET(new_vals, '$.zone_note', NEW.zone_note);
+    END IF;
+    
+    IF NEW.status IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'status');
+        SET new_vals = JSON_SET(new_vals, '$.status', NEW.status);
+    END IF;
+    
+    IF NEW.last_audit IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'last_audit');
+        SET new_vals = JSON_SET(new_vals, '$.last_audit', NEW.last_audit);
+    END IF;
+    
+    IF NEW.last_audit_status IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'last_audit_status');
+        SET new_vals = JSON_SET(new_vals, '$.last_audit_status', NEW.last_audit_status);
+    END IF;
+    
+    IF NEW.price IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'price');
+        SET new_vals = JSON_SET(new_vals, '$.price', NEW.price);
+    END IF;
+    
+    IF NEW.purchase_date IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'purchase_date');
+        SET new_vals = JSON_SET(new_vals, '$.purchase_date', NEW.purchase_date);
+    END IF;
+    
+    IF NEW.warranty_until IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'warranty_until');
+        SET new_vals = JSON_SET(new_vals, '$.warranty_until', NEW.warranty_until);
+    END IF;
+    
+    IF NEW.expiry_date IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'expiry_date');
+        SET new_vals = JSON_SET(new_vals, '$.expiry_date', NEW.expiry_date);
+    END IF;
+    
+    IF NEW.quantity_available IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'quantity_available');
+        SET new_vals = JSON_SET(new_vals, '$.quantity_available', NEW.quantity_available);
+    END IF;
+    
+    IF NEW.quantity_total IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'quantity_total');
+        SET new_vals = JSON_SET(new_vals, '$.quantity_total', NEW.quantity_total);
+    END IF;
+    
+    IF NEW.quantity_used IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'quantity_used');
+        SET new_vals = JSON_SET(new_vals, '$.quantity_used', NEW.quantity_used);
+    END IF;
+    
+    IF NEW.supplier_id IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'supplier_id');
+        SET new_vals = JSON_SET(new_vals, '$.supplier_id', NEW.supplier_id);
+    END IF;
+    
+    IF NEW.lendable IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'lendable');
+        SET new_vals = JSON_SET(new_vals, '$.lendable', NEW.lendable);
+    END IF;
+    
+    IF NEW.minimum_role_for_lending IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'minimum_role_for_lending');
+        SET new_vals = JSON_SET(new_vals, '$.minimum_role_for_lending', NEW.minimum_role_for_lending);
+    END IF;
+    
+    IF NEW.lending_status IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'lending_status');
+        SET new_vals = JSON_SET(new_vals, '$.lending_status', NEW.lending_status);
+    END IF;
+    
+    IF NEW.current_borrower_id IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'current_borrower_id');
+        SET new_vals = JSON_SET(new_vals, '$.current_borrower_id', NEW.current_borrower_id);
+    END IF;
+    
+    IF NEW.due_date IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'due_date');
+        SET new_vals = JSON_SET(new_vals, '$.due_date', NEW.due_date);
+    END IF;
+    
+    IF NEW.previous_borrower_id IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'previous_borrower_id');
+        SET new_vals = JSON_SET(new_vals, '$.previous_borrower_id', NEW.previous_borrower_id);
+    END IF;
+    
+    IF NEW.audit_task_id IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'audit_task_id');
+        SET new_vals = JSON_SET(new_vals, '$.audit_task_id', NEW.audit_task_id);
+    END IF;
+    
+    IF NEW.no_scan IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'no_scan');
+        SET new_vals = JSON_SET(new_vals, '$.no_scan', NEW.no_scan);
+    END IF;
+    
+    IF NEW.notes IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'notes');
+        SET new_vals = JSON_SET(new_vals, '$.notes', NEW.notes);
+    END IF;
+    
+    IF NEW.additional_fields IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'additional_fields');
+        SET new_vals = JSON_SET(new_vals, '$.additional_fields', NEW.additional_fields);
+    END IF;
+    
+    IF NEW.created_by IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'created_by');
+        SET new_vals = JSON_SET(new_vals, '$.created_by', NEW.created_by);
+    END IF;
+    
+    -- Log the INSERT with only the fields that were set
+    INSERT INTO asset_change_log (
+        table_name, action, record_id, changed_fields, new_values,
+        changed_by_id, changed_by_username
+    )
+    VALUES (
+        'assets',
+        'INSERT',
+        NEW.id,
+        set_fields_array,
+        new_vals,
+        @current_user_id,
+        username
+    );
+END//
+
+-- Trigger: Log UPDATE operations (only changed fields)
+DROP TRIGGER IF EXISTS assets_after_update_log//
+CREATE TRIGGER assets_after_update_log
+AFTER UPDATE ON assets
+FOR EACH ROW
+BEGIN
+    DECLARE username VARCHAR(100);
+    DECLARE changed_fields_array JSON;
+    DECLARE old_vals JSON;
+    DECLARE new_vals JSON;
+    
+    IF @current_user_id IS NOT NULL THEN
+        SELECT users.username INTO username FROM users WHERE id = @current_user_id;
+    END IF;
+    
+    -- Build JSON objects only with changed fields
+    SET changed_fields_array = JSON_ARRAY();
+    SET old_vals = JSON_OBJECT();
+    SET new_vals = JSON_OBJECT();
+    
+    IF OLD.asset_tag <=> NEW.asset_tag IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'asset_tag');
+        SET old_vals = JSON_SET(old_vals, '$.asset_tag', OLD.asset_tag);
+        SET new_vals = JSON_SET(new_vals, '$.asset_tag', NEW.asset_tag);
+    END IF;
+    
+    IF OLD.asset_numeric_id <=> NEW.asset_numeric_id IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'asset_numeric_id');
+        SET old_vals = JSON_SET(old_vals, '$.asset_numeric_id', OLD.asset_numeric_id);
+        SET new_vals = JSON_SET(new_vals, '$.asset_numeric_id', NEW.asset_numeric_id);
+    END IF;
+    
+    IF OLD.asset_type <=> NEW.asset_type IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'asset_type');
+        SET old_vals = JSON_SET(old_vals, '$.asset_type', OLD.asset_type);
+        SET new_vals = JSON_SET(new_vals, '$.asset_type', NEW.asset_type);
+    END IF;
+    
+    IF OLD.name <=> NEW.name IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'name');
+        SET old_vals = JSON_SET(old_vals, '$.name', OLD.name);
+        SET new_vals = JSON_SET(new_vals, '$.name', NEW.name);
+    END IF;
+    
+    IF OLD.category_id <=> NEW.category_id IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'category_id');
+        SET old_vals = JSON_SET(old_vals, '$.category_id', OLD.category_id);
+        SET new_vals = JSON_SET(new_vals, '$.category_id', NEW.category_id);
+    END IF;
+    
+    IF OLD.manufacturer <=> NEW.manufacturer IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'manufacturer');
+        SET old_vals = JSON_SET(old_vals, '$.manufacturer', OLD.manufacturer);
+        SET new_vals = JSON_SET(new_vals, '$.manufacturer', NEW.manufacturer);
+    END IF;
+    
+    IF OLD.model <=> NEW.model IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'model');
+        SET old_vals = JSON_SET(old_vals, '$.model', OLD.model);
+        SET new_vals = JSON_SET(new_vals, '$.model', NEW.model);
+    END IF;
+    
+    IF OLD.serial_number <=> NEW.serial_number IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'serial_number');
+        SET old_vals = JSON_SET(old_vals, '$.serial_number', OLD.serial_number);
+        SET new_vals = JSON_SET(new_vals, '$.serial_number', NEW.serial_number);
+    END IF;
+    
+    IF OLD.zone_id <=> NEW.zone_id IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'zone_id');
+        SET old_vals = JSON_SET(old_vals, '$.zone_id', OLD.zone_id);
+        SET new_vals = JSON_SET(new_vals, '$.zone_id', NEW.zone_id);
+    END IF;
+    
+    IF OLD.zone_plus <=> NEW.zone_plus IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'zone_plus');
+        SET old_vals = JSON_SET(old_vals, '$.zone_plus', OLD.zone_plus);
+        SET new_vals = JSON_SET(new_vals, '$.zone_plus', NEW.zone_plus);
+    END IF;
+    
+    IF OLD.zone_note <=> NEW.zone_note IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'zone_note');
+        SET old_vals = JSON_SET(old_vals, '$.zone_note', OLD.zone_note);
+        SET new_vals = JSON_SET(new_vals, '$.zone_note', NEW.zone_note);
+    END IF;
+    
+    IF OLD.status <=> NEW.status IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'status');
+        SET old_vals = JSON_SET(old_vals, '$.status', OLD.status);
+        SET new_vals = JSON_SET(new_vals, '$.status', NEW.status);
+    END IF;
+    
+    IF OLD.last_audit <=> NEW.last_audit IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'last_audit');
+        SET old_vals = JSON_SET(old_vals, '$.last_audit', OLD.last_audit);
+        SET new_vals = JSON_SET(new_vals, '$.last_audit', NEW.last_audit);
+    END IF;
+    
+    IF OLD.last_audit_status <=> NEW.last_audit_status IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'last_audit_status');
+        SET old_vals = JSON_SET(old_vals, '$.last_audit_status', OLD.last_audit_status);
+        SET new_vals = JSON_SET(new_vals, '$.last_audit_status', NEW.last_audit_status);
+    END IF;
+    
+    IF OLD.price <=> NEW.price IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'price');
+        SET old_vals = JSON_SET(old_vals, '$.price', OLD.price);
+        SET new_vals = JSON_SET(new_vals, '$.price', NEW.price);
+    END IF;
+    
+    IF OLD.purchase_date <=> NEW.purchase_date IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'purchase_date');
+        SET old_vals = JSON_SET(old_vals, '$.purchase_date', OLD.purchase_date);
+        SET new_vals = JSON_SET(new_vals, '$.purchase_date', NEW.purchase_date);
+    END IF;
+    
+    IF OLD.warranty_until <=> NEW.warranty_until IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'warranty_until');
+        SET old_vals = JSON_SET(old_vals, '$.warranty_until', OLD.warranty_until);
+        SET new_vals = JSON_SET(new_vals, '$.warranty_until', NEW.warranty_until);
+    END IF;
+    
+    IF OLD.expiry_date <=> NEW.expiry_date IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'expiry_date');
+        SET old_vals = JSON_SET(old_vals, '$.expiry_date', OLD.expiry_date);
+        SET new_vals = JSON_SET(new_vals, '$.expiry_date', NEW.expiry_date);
+    END IF;
+    
+    IF OLD.quantity_available <=> NEW.quantity_available IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'quantity_available');
+        SET old_vals = JSON_SET(old_vals, '$.quantity_available', OLD.quantity_available);
+        SET new_vals = JSON_SET(new_vals, '$.quantity_available', NEW.quantity_available);
+    END IF;
+    
+    IF OLD.quantity_total <=> NEW.quantity_total IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'quantity_total');
+        SET old_vals = JSON_SET(old_vals, '$.quantity_total', OLD.quantity_total);
+        SET new_vals = JSON_SET(new_vals, '$.quantity_total', NEW.quantity_total);
+    END IF;
+    
+    IF OLD.quantity_used <=> NEW.quantity_used IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'quantity_used');
+        SET old_vals = JSON_SET(old_vals, '$.quantity_used', OLD.quantity_used);
+        SET new_vals = JSON_SET(new_vals, '$.quantity_used', NEW.quantity_used);
+    END IF;
+    
+    IF OLD.supplier_id <=> NEW.supplier_id IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'supplier_id');
+        SET old_vals = JSON_SET(old_vals, '$.supplier_id', OLD.supplier_id);
+        SET new_vals = JSON_SET(new_vals, '$.supplier_id', NEW.supplier_id);
+    END IF;
+    
+    IF OLD.lendable <=> NEW.lendable IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'lendable');
+        SET old_vals = JSON_SET(old_vals, '$.lendable', OLD.lendable);
+        SET new_vals = JSON_SET(new_vals, '$.lendable', NEW.lendable);
+    END IF;
+    
+    IF OLD.minimum_role_for_lending <=> NEW.minimum_role_for_lending IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'minimum_role_for_lending');
+        SET old_vals = JSON_SET(old_vals, '$.minimum_role_for_lending', OLD.minimum_role_for_lending);
+        SET new_vals = JSON_SET(new_vals, '$.minimum_role_for_lending', NEW.minimum_role_for_lending);
+    END IF;
+    
+    IF OLD.lending_status <=> NEW.lending_status IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'lending_status');
+        SET old_vals = JSON_SET(old_vals, '$.lending_status', OLD.lending_status);
+        SET new_vals = JSON_SET(new_vals, '$.lending_status', NEW.lending_status);
+    END IF;
+    
+    IF OLD.current_borrower_id <=> NEW.current_borrower_id IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'current_borrower_id');
+        SET old_vals = JSON_SET(old_vals, '$.current_borrower_id', OLD.current_borrower_id);
+        SET new_vals = JSON_SET(new_vals, '$.current_borrower_id', NEW.current_borrower_id);
+    END IF;
+    
+    IF OLD.due_date <=> NEW.due_date IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'due_date');
+        SET old_vals = JSON_SET(old_vals, '$.due_date', OLD.due_date);
+        SET new_vals = JSON_SET(new_vals, '$.due_date', NEW.due_date);
+    END IF;
+    
+    IF OLD.previous_borrower_id <=> NEW.previous_borrower_id IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'previous_borrower_id');
+        SET old_vals = JSON_SET(old_vals, '$.previous_borrower_id', OLD.previous_borrower_id);
+        SET new_vals = JSON_SET(new_vals, '$.previous_borrower_id', NEW.previous_borrower_id);
+    END IF;
+    
+    IF OLD.audit_task_id <=> NEW.audit_task_id IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'audit_task_id');
+        SET old_vals = JSON_SET(old_vals, '$.audit_task_id', OLD.audit_task_id);
+        SET new_vals = JSON_SET(new_vals, '$.audit_task_id', NEW.audit_task_id);
+    END IF;
+    
+    IF OLD.no_scan <=> NEW.no_scan IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'no_scan');
+        SET old_vals = JSON_SET(old_vals, '$.no_scan', OLD.no_scan);
+        SET new_vals = JSON_SET(new_vals, '$.no_scan', NEW.no_scan);
+    END IF;
+    
+    IF OLD.notes <=> NEW.notes IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'notes');
+        SET old_vals = JSON_SET(old_vals, '$.notes', OLD.notes);
+        SET new_vals = JSON_SET(new_vals, '$.notes', NEW.notes);
+    END IF;
+    
+    IF OLD.additional_fields <=> NEW.additional_fields IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'additional_fields');
+        SET old_vals = JSON_SET(old_vals, '$.additional_fields', OLD.additional_fields);
+        SET new_vals = JSON_SET(new_vals, '$.additional_fields', NEW.additional_fields);
+    END IF;
+    
+    -- Only log if there were actual changes (excluding auto-updated fields)
+    IF JSON_LENGTH(changed_fields_array) > 0 THEN
+        INSERT INTO asset_change_log (
+            table_name, action, record_id, changed_fields, old_values, new_values,
+            changed_by_id, changed_by_username
+        )
+        VALUES (
+            'assets',
+            'UPDATE',
+            NEW.id,
+            changed_fields_array,
+            old_vals,
+            new_vals,
+            @current_user_id,
+            username
+        );
+    END IF;
+END//
+
+-- Trigger: Log DELETE operations (only non-NULL fields for efficiency, but preserve all data for restore)
+DROP TRIGGER IF EXISTS assets_after_delete_log//
+CREATE TRIGGER assets_after_delete_log
+AFTER DELETE ON assets
+FOR EACH ROW
+BEGIN
+    DECLARE username VARCHAR(100);
+    DECLARE deleted_fields_array JSON;
+    DECLARE old_vals JSON;
+    
+    IF @current_user_id IS NOT NULL THEN
+        SELECT users.username INTO username FROM users WHERE id = @current_user_id;
+    END IF;
+    
+    -- Build JSON objects only with non-NULL fields (for restore capability)
+    SET deleted_fields_array = JSON_ARRAY();
+    SET old_vals = JSON_OBJECT();
+    
+    IF OLD.asset_tag IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'asset_tag');
+        SET old_vals = JSON_SET(old_vals, '$.asset_tag', OLD.asset_tag);
+    END IF;
+    
+    IF OLD.asset_numeric_id IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'asset_numeric_id');
+        SET old_vals = JSON_SET(old_vals, '$.asset_numeric_id', OLD.asset_numeric_id);
+    END IF;
+    
+    IF OLD.asset_type IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'asset_type');
+        SET old_vals = JSON_SET(old_vals, '$.asset_type', OLD.asset_type);
+    END IF;
+    
+    IF OLD.name IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'name');
+        SET old_vals = JSON_SET(old_vals, '$.name', OLD.name);
+    END IF;
+    
+    IF OLD.category_id IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'category_id');
+        SET old_vals = JSON_SET(old_vals, '$.category_id', OLD.category_id);
+    END IF;
+    
+    IF OLD.manufacturer IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'manufacturer');
+        SET old_vals = JSON_SET(old_vals, '$.manufacturer', OLD.manufacturer);
+    END IF;
+    
+    IF OLD.model IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'model');
+        SET old_vals = JSON_SET(old_vals, '$.model', OLD.model);
+    END IF;
+    
+    IF OLD.serial_number IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'serial_number');
+        SET old_vals = JSON_SET(old_vals, '$.serial_number', OLD.serial_number);
+    END IF;
+    
+    IF OLD.zone_id IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'zone_id');
+        SET old_vals = JSON_SET(old_vals, '$.zone_id', OLD.zone_id);
+    END IF;
+    
+    IF OLD.zone_plus IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'zone_plus');
+        SET old_vals = JSON_SET(old_vals, '$.zone_plus', OLD.zone_plus);
+    END IF;
+    
+    IF OLD.zone_note IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'zone_note');
+        SET old_vals = JSON_SET(old_vals, '$.zone_note', OLD.zone_note);
+    END IF;
+    
+    IF OLD.status IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'status');
+        SET old_vals = JSON_SET(old_vals, '$.status', OLD.status);
+    END IF;
+    
+    IF OLD.last_audit IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'last_audit');
+        SET old_vals = JSON_SET(old_vals, '$.last_audit', OLD.last_audit);
+    END IF;
+    
+    IF OLD.last_audit_status IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'last_audit_status');
+        SET old_vals = JSON_SET(old_vals, '$.last_audit_status', OLD.last_audit_status);
+    END IF;
+    
+    IF OLD.price IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'price');
+        SET old_vals = JSON_SET(old_vals, '$.price', OLD.price);
+    END IF;
+    
+    IF OLD.purchase_date IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'purchase_date');
+        SET old_vals = JSON_SET(old_vals, '$.purchase_date', OLD.purchase_date);
+    END IF;
+    
+    IF OLD.warranty_until IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'warranty_until');
+        SET old_vals = JSON_SET(old_vals, '$.warranty_until', OLD.warranty_until);
+    END IF;
+    
+    IF OLD.expiry_date IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'expiry_date');
+        SET old_vals = JSON_SET(old_vals, '$.expiry_date', OLD.expiry_date);
+    END IF;
+    
+    IF OLD.quantity_available IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'quantity_available');
+        SET old_vals = JSON_SET(old_vals, '$.quantity_available', OLD.quantity_available);
+    END IF;
+    
+    IF OLD.quantity_total IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'quantity_total');
+        SET old_vals = JSON_SET(old_vals, '$.quantity_total', OLD.quantity_total);
+    END IF;
+    
+    IF OLD.quantity_used IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'quantity_used');
+        SET old_vals = JSON_SET(old_vals, '$.quantity_used', OLD.quantity_used);
+    END IF;
+    
+    IF OLD.supplier_id IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'supplier_id');
+        SET old_vals = JSON_SET(old_vals, '$.supplier_id', OLD.supplier_id);
+    END IF;
+    
+    IF OLD.lendable IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'lendable');
+        SET old_vals = JSON_SET(old_vals, '$.lendable', OLD.lendable);
+    END IF;
+    
+    IF OLD.minimum_role_for_lending IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'minimum_role_for_lending');
+        SET old_vals = JSON_SET(old_vals, '$.minimum_role_for_lending', OLD.minimum_role_for_lending);
+    END IF;
+    
+    IF OLD.lending_status IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'lending_status');
+        SET old_vals = JSON_SET(old_vals, '$.lending_status', OLD.lending_status);
+    END IF;
+    
+    IF OLD.current_borrower_id IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'current_borrower_id');
+        SET old_vals = JSON_SET(old_vals, '$.current_borrower_id', OLD.current_borrower_id);
+    END IF;
+    
+    IF OLD.due_date IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'due_date');
+        SET old_vals = JSON_SET(old_vals, '$.due_date', OLD.due_date);
+    END IF;
+    
+    IF OLD.previous_borrower_id IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'previous_borrower_id');
+        SET old_vals = JSON_SET(old_vals, '$.previous_borrower_id', OLD.previous_borrower_id);
+    END IF;
+    
+    IF OLD.audit_task_id IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'audit_task_id');
+        SET old_vals = JSON_SET(old_vals, '$.audit_task_id', OLD.audit_task_id);
+    END IF;
+    
+    IF OLD.no_scan IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'no_scan');
+        SET old_vals = JSON_SET(old_vals, '$.no_scan', OLD.no_scan);
+    END IF;
+    
+    IF OLD.notes IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'notes');
+        SET old_vals = JSON_SET(old_vals, '$.notes', OLD.notes);
+    END IF;
+    
+    IF OLD.additional_fields IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'additional_fields');
+        SET old_vals = JSON_SET(old_vals, '$.additional_fields', OLD.additional_fields);
+    END IF;
+    
+    -- Always capture metadata fields for restore
+    IF OLD.created_date IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'created_date');
+        SET old_vals = JSON_SET(old_vals, '$.created_date', OLD.created_date);
+    END IF;
+    
+    IF OLD.created_by IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'created_by');
+        SET old_vals = JSON_SET(old_vals, '$.created_by', OLD.created_by);
+    END IF;
+    
+    IF OLD.last_modified_date IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'last_modified_date');
+        SET old_vals = JSON_SET(old_vals, '$.last_modified_date', OLD.last_modified_date);
+    END IF;
+    
+    IF OLD.last_modified_by IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'last_modified_by');
+        SET old_vals = JSON_SET(old_vals, '$.last_modified_by', OLD.last_modified_by);
+    END IF;
+    
+    -- Log the DELETE with only non-NULL fields
+    INSERT INTO asset_change_log (
+        table_name, action, record_id, changed_fields, old_values,
+        changed_by_id, changed_by_username
+    )
+    VALUES (
+        'assets',
+        'DELETE',
+        OLD.id,
+        deleted_fields_array,
+        old_vals,
+        @current_user_id,
+        username
+    );
+END//
+
+-- ============================================
+-- BUSINESS LOGIC TRIGGERS
+-- ============================================
+
+-- Trigger: Prevent lending non-lendable assets
+DROP TRIGGER IF EXISTS prevent_lend_non_lendable_assets//
+CREATE TRIGGER prevent_lend_non_lendable_assets
+BEFORE UPDATE ON assets
+FOR EACH ROW
+BEGIN
+    -- Check if trying to set lending_status to any borrowed state on a non-lendable asset
+    IF (NEW.lendable = FALSE OR NEW.lendable IS NULL) AND 
+       NEW.lending_status IN ('Borrowed', 'Deployed', 'Overdue') AND
+       (OLD.lending_status NOT IN ('Borrowed', 'Deployed', 'Overdue') OR OLD.lending_status IS NULL) THEN
+        SIGNAL SQLSTATE '45000'
+        SET MESSAGE_TEXT = 'Cannot lend asset that is marked as non-lendable. Set lendable=TRUE first.';
+    END IF;
+END//
+
+-- Trigger: Prevent deleting borrowed items
+DROP TRIGGER IF EXISTS prevent_delete_borrowed_assets//
+CREATE TRIGGER prevent_delete_borrowed_assets
+BEFORE DELETE ON assets
+FOR EACH ROW
+BEGIN
+    IF OLD.lending_status IN ('Borrowed', 'Deployed', 'Overdue') THEN
+        SIGNAL SQLSTATE '45000'
+        SET MESSAGE_TEXT = 'Cannot delete asset that is currently borrowed or deployed, maybe update to retired or unmanaged before';
+    END IF;
+END//
+
+-- Trigger: Validate zone_plus requires zone_note for 'Clarify'
+DROP TRIGGER IF EXISTS validate_zone_plus_insert//
+CREATE TRIGGER validate_zone_plus_insert
+BEFORE INSERT ON assets
+FOR EACH ROW
+BEGIN
+    IF NEW.zone_plus = 'Clarify' AND (NEW.zone_note IS NULL OR NEW.zone_note = '') THEN
+        SIGNAL SQLSTATE '45000'
+        SET MESSAGE_TEXT = 'zone_note is required when zone_plus is set to Clarify';
+    END IF;
+END//
+
+DROP TRIGGER IF EXISTS validate_zone_plus_update//
+CREATE TRIGGER validate_zone_plus_update
+BEFORE UPDATE ON assets
+FOR EACH ROW
+BEGIN
+    IF NEW.zone_plus = 'Clarify' AND (NEW.zone_note IS NULL OR NEW.zone_note = '') THEN
+        SIGNAL SQLSTATE '45000'
+        SET MESSAGE_TEXT = 'zone_note is required when zone_plus is set to Clarify';
+    END IF;
+END//
+
+-- ============================================
+-- BORROWERS TABLE TRIGGERS
+-- ============================================
+
+-- Trigger: Auto-populate added_by on INSERT
+DROP TRIGGER IF EXISTS borrowers_before_insert_meta//
+CREATE TRIGGER borrowers_before_insert_meta
+BEFORE INSERT ON borrowers
+FOR EACH ROW
+BEGIN
+    IF NEW.added_by IS NULL AND @current_user_id IS NOT NULL THEN
+        SET NEW.added_by = @current_user_id;
+    END IF;
+END//
+
+-- Trigger: Auto-populate last_unban_by on UPDATE when unbanning
+DROP TRIGGER IF EXISTS borrowers_before_update_meta//
+CREATE TRIGGER borrowers_before_update_meta
+BEFORE UPDATE ON borrowers
+FOR EACH ROW
+BEGIN
+    IF OLD.banned = TRUE AND NEW.banned = FALSE THEN
+        IF NEW.last_unban_by IS NULL AND @current_user_id IS NOT NULL THEN
+            SET NEW.last_unban_by = @current_user_id;
+        END IF;
+        IF NEW.last_unban_date IS NULL THEN
+            SET NEW.last_unban_date = CURDATE();
+        END IF;
+    END IF;
+END//
+
+-- ============================================
+-- LENDING HISTORY TRIGGERS
+-- ============================================
+
+-- Trigger: Auto-populate checked_out_by on INSERT
+DROP TRIGGER IF EXISTS lending_history_before_insert_meta//
+CREATE TRIGGER lending_history_before_insert_meta
+BEFORE INSERT ON lending_history
+FOR EACH ROW
+BEGIN
+    IF NEW.checked_out_by IS NULL AND @current_user_id IS NOT NULL THEN
+        SET NEW.checked_out_by = @current_user_id;
+    END IF;
+END//
+
+-- Trigger: Auto-populate checked_in_by on UPDATE when returning
+DROP TRIGGER IF EXISTS lending_history_before_update_meta//
+CREATE TRIGGER lending_history_before_update_meta
+BEFORE UPDATE ON lending_history
+FOR EACH ROW
+BEGIN
+    IF OLD.return_date IS NULL AND NEW.return_date IS NOT NULL THEN
+        IF NEW.checked_in_by IS NULL AND @current_user_id IS NOT NULL THEN
+            SET NEW.checked_in_by = @current_user_id;
+        END IF;
+    END IF;
+END//
+
+-- ============================================
+-- ISSUE TRACKER TRIGGERS
+-- ============================================
+
+-- Trigger: Auto-populate reported_by on INSERT
+DROP TRIGGER IF EXISTS issue_tracker_before_insert_meta//
+CREATE TRIGGER issue_tracker_before_insert_meta
+BEFORE INSERT ON issue_tracker
+FOR EACH ROW
+BEGIN
+    IF NEW.reported_by IS NULL AND @current_user_id IS NOT NULL THEN
+        SET NEW.reported_by = @current_user_id;
+    END IF;
+END//
+
+-- Trigger: Validate issue_tracker business rules on INSERT
+DROP TRIGGER IF EXISTS validate_issue_tracker_insert//
+CREATE TRIGGER validate_issue_tracker_insert
+BEFORE INSERT ON issue_tracker
+FOR EACH ROW
+BEGIN
+    -- Clarify solution requires solution_plus
+    IF NEW.solution = 'Clarify' AND (NEW.solution_plus IS NULL OR NEW.solution_plus = '') THEN
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'solution_plus is required when solution is set to Clarify';
+    END IF;
+    
+    -- Replacement solution requires replacement_asset_id
+    IF NEW.solution = 'Replaced' AND NEW.replacement_asset_id IS NULL THEN
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'replacement_asset_id is required when solution is set to Replaced';
+    END IF;
+    
+    -- Asset Issue requires asset_id
+    IF NEW.issue_type = 'Asset Issue' AND NEW.asset_id IS NULL THEN
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'asset_id is required for Asset Issue type';
+    END IF;
+    
+    -- Borrower Issue requires borrower_id
+    IF NEW.issue_type = 'Borrower Issue' AND NEW.borrower_id IS NULL THEN
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'borrower_id is required for Borrower Issue type';
+    END IF;
+    
+    -- Auto-set resolved_date when status becomes Resolved or Closed
+    IF NEW.status IN ('Resolved', 'Closed') AND NEW.resolved_date IS NULL THEN
+        SET NEW.resolved_date = NOW();
+    END IF;
+    
+    -- Auto-set resolved_by when status becomes Resolved or Closed
+    IF NEW.status IN ('Resolved', 'Closed') AND NEW.resolved_by IS NULL AND @current_user_id IS NOT NULL THEN
+        SET NEW.resolved_by = @current_user_id;
+    END IF;
+END//
+
+-- Trigger: Validate issue_tracker business rules on UPDATE
+DROP TRIGGER IF EXISTS validate_issue_tracker_update//
+CREATE TRIGGER validate_issue_tracker_update
+BEFORE UPDATE ON issue_tracker
+FOR EACH ROW
+BEGIN
+    -- Clarify solution requires solution_plus
+    IF NEW.solution = 'Clarify' AND (NEW.solution_plus IS NULL OR NEW.solution_plus = '') THEN
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'solution_plus is required when solution is set to Clarify';
+    END IF;
+    
+    -- Replacement solution requires replacement_asset_id
+    IF NEW.solution = 'Replaced' AND NEW.replacement_asset_id IS NULL THEN
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'replacement_asset_id is required when solution is set to Replaced';
+    END IF;
+    
+    -- Auto-set resolved_date when status changes to Resolved or Closed
+    IF OLD.status NOT IN ('Resolved', 'Closed') AND NEW.status IN ('Resolved', 'Closed') THEN
+        SET NEW.resolved_date = NOW();
+        IF @current_user_id IS NOT NULL THEN
+            SET NEW.resolved_by = @current_user_id;
+        END IF;
+    END IF;
+    
+    -- Clear resolved_date when status changes away from Resolved/Closed
+    IF OLD.status IN ('Resolved', 'Closed') AND NEW.status NOT IN ('Resolved', 'Closed') THEN
+        SET NEW.resolved_date = NULL;
+        SET NEW.resolved_by = NULL;
+    END IF;
+END//
+
+-- Trigger: Auto-resolve issue before DELETE
+DROP TRIGGER IF EXISTS issue_tracker_before_delete//
+CREATE TRIGGER issue_tracker_before_delete
+BEFORE DELETE ON issue_tracker
+FOR EACH ROW
+BEGIN
+    -- If issue is not already resolved/closed, update it before deletion
+    IF OLD.status NOT IN ('Resolved', 'Closed') THEN
+        -- Can't UPDATE in a BEFORE DELETE trigger, so we just ensure it was marked resolved
+        -- This will prevent accidental deletion of open issues
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Cannot delete open issues. Please close or resolve the issue first.';
+    END IF;
+END//
+
+-- Trigger: Log issue_tracker INSERT operations
+DROP TRIGGER IF EXISTS issue_tracker_after_insert_log//
+CREATE TRIGGER issue_tracker_after_insert_log
+AFTER INSERT ON issue_tracker
+FOR EACH ROW
+BEGIN
+    DECLARE set_fields JSON DEFAULT JSON_ARRAY();
+    DECLARE new_vals JSON DEFAULT JSON_OBJECT();
+    
+    -- Build JSON of non-NULL inserted fields
+    IF NEW.issue_type IS NOT NULL THEN
+        SET set_fields = JSON_ARRAY_APPEND(set_fields, '$', 'issue_type');
+        SET new_vals = JSON_SET(new_vals, '$.issue_type', NEW.issue_type);
+    END IF;
+    IF NEW.asset_id IS NOT NULL THEN
+        SET set_fields = JSON_ARRAY_APPEND(set_fields, '$', 'asset_id');
+        SET new_vals = JSON_SET(new_vals, '$.asset_id', NEW.asset_id);
+    END IF;
+    IF NEW.borrower_id IS NOT NULL THEN
+        SET set_fields = JSON_ARRAY_APPEND(set_fields, '$', 'borrower_id');
+        SET new_vals = JSON_SET(new_vals, '$.borrower_id', NEW.borrower_id);
+    END IF;
+    IF NEW.title IS NOT NULL THEN
+        SET set_fields = JSON_ARRAY_APPEND(set_fields, '$', 'title');
+        SET new_vals = JSON_SET(new_vals, '$.title', NEW.title);
+    END IF;
+    IF NEW.severity IS NOT NULL THEN
+        SET set_fields = JSON_ARRAY_APPEND(set_fields, '$', 'severity');
+        SET new_vals = JSON_SET(new_vals, '$.severity', NEW.severity);
+    END IF;
+    IF NEW.status IS NOT NULL THEN
+        SET set_fields = JSON_ARRAY_APPEND(set_fields, '$', 'status');
+        SET new_vals = JSON_SET(new_vals, '$.status', NEW.status);
+    END IF;
+    
+    INSERT INTO issue_tracker_change_log (issue_id, change_type, changed_fields, new_values, changed_by)
+    VALUES (NEW.id, 'INSERT', set_fields, new_vals, COALESCE(@current_user_id, NEW.reported_by));
+END//
+
+-- Trigger: Log issue_tracker UPDATE operations
+DROP TRIGGER IF EXISTS issue_tracker_after_update_log//
+CREATE TRIGGER issue_tracker_after_update_log
+AFTER UPDATE ON issue_tracker
+FOR EACH ROW
+BEGIN
+    DECLARE changed_fields JSON DEFAULT JSON_ARRAY();
+    DECLARE old_vals JSON DEFAULT JSON_OBJECT();
+    DECLARE new_vals JSON DEFAULT JSON_OBJECT();
+    
+    -- Track all changed fields
+    IF OLD.status <=> NEW.status IS FALSE THEN
+        SET changed_fields = JSON_ARRAY_APPEND(changed_fields, '$', 'status');
+        SET old_vals = JSON_SET(old_vals, '$.status', OLD.status);
+        SET new_vals = JSON_SET(new_vals, '$.status', NEW.status);
+    END IF;
+    IF OLD.severity <=> NEW.severity IS FALSE THEN
+        SET changed_fields = JSON_ARRAY_APPEND(changed_fields, '$', 'severity');
+        SET old_vals = JSON_SET(old_vals, '$.severity', OLD.severity);
+        SET new_vals = JSON_SET(new_vals, '$.severity', NEW.severity);
+    END IF;
+    IF OLD.priority <=> NEW.priority IS FALSE THEN
+        SET changed_fields = JSON_ARRAY_APPEND(changed_fields, '$', 'priority');
+        SET old_vals = JSON_SET(old_vals, '$.priority', OLD.priority);
+        SET new_vals = JSON_SET(new_vals, '$.priority', NEW.priority);
+    END IF;
+    IF OLD.solution <=> NEW.solution IS FALSE THEN
+        SET changed_fields = JSON_ARRAY_APPEND(changed_fields, '$', 'solution');
+        SET old_vals = JSON_SET(old_vals, '$.solution', OLD.solution);
+        SET new_vals = JSON_SET(new_vals, '$.solution', NEW.solution);
+    END IF;
+    IF OLD.assigned_to <=> NEW.assigned_to IS FALSE THEN
+        SET changed_fields = JSON_ARRAY_APPEND(changed_fields, '$', 'assigned_to');
+        SET old_vals = JSON_SET(old_vals, '$.assigned_to', OLD.assigned_to);
+        SET new_vals = JSON_SET(new_vals, '$.assigned_to', NEW.assigned_to);
+    END IF;
+    IF OLD.resolved_by <=> NEW.resolved_by IS FALSE THEN
+        SET changed_fields = JSON_ARRAY_APPEND(changed_fields, '$', 'resolved_by');
+        SET old_vals = JSON_SET(old_vals, '$.resolved_by', OLD.resolved_by);
+        SET new_vals = JSON_SET(new_vals, '$.resolved_by', NEW.resolved_by);
+    END IF;
+    
+    -- Only log if something actually changed
+    IF JSON_LENGTH(changed_fields) > 0 THEN
+        INSERT INTO issue_tracker_change_log (issue_id, change_type, changed_fields, old_values, new_values, changed_by)
+        VALUES (NEW.id, 'UPDATE', changed_fields, old_vals, new_vals, COALESCE(@current_user_id, OLD.reported_by));
+    END IF;
+END//
+
+-- Trigger: Log issue_tracker DELETE operations
+DROP TRIGGER IF EXISTS issue_tracker_after_delete_log//
+CREATE TRIGGER issue_tracker_after_delete_log
+AFTER DELETE ON issue_tracker
+FOR EACH ROW
+BEGIN
+    DECLARE deleted_fields JSON DEFAULT JSON_ARRAY();
+    DECLARE old_vals JSON DEFAULT JSON_OBJECT();
+    
+    -- Log all fields from deleted issue
+    IF OLD.issue_type IS NOT NULL THEN
+        SET deleted_fields = JSON_ARRAY_APPEND(deleted_fields, '$', 'issue_type');
+        SET old_vals = JSON_SET(old_vals, '$.issue_type', OLD.issue_type);
+    END IF;
+    IF OLD.asset_id IS NOT NULL THEN
+        SET deleted_fields = JSON_ARRAY_APPEND(deleted_fields, '$', 'asset_id');
+        SET old_vals = JSON_SET(old_vals, '$.asset_id', OLD.asset_id);
+    END IF;
+    IF OLD.title IS NOT NULL THEN
+        SET deleted_fields = JSON_ARRAY_APPEND(deleted_fields, '$', 'title');
+        SET old_vals = JSON_SET(old_vals, '$.title', OLD.title);
+    END IF;
+    IF OLD.status IS NOT NULL THEN
+        SET deleted_fields = JSON_ARRAY_APPEND(deleted_fields, '$', 'status');
+        SET old_vals = JSON_SET(old_vals, '$.status', OLD.status);
+    END IF;
+    IF OLD.solution IS NOT NULL THEN
+        SET deleted_fields = JSON_ARRAY_APPEND(deleted_fields, '$', 'solution');
+        SET old_vals = JSON_SET(old_vals, '$.solution', OLD.solution);
+    END IF;
+    
+    INSERT INTO issue_tracker_change_log (issue_id, change_type, changed_fields, old_values, changed_by)
+    VALUES (OLD.id, 'DELETE', deleted_fields, old_vals, COALESCE(@current_user_id, OLD.reported_by));
+END//
+
+-- Trigger: Auto-detect asset issues when status becomes problematic
+DROP TRIGGER IF EXISTS auto_detect_asset_issues//
+CREATE TRIGGER auto_detect_asset_issues
+AFTER UPDATE ON assets
+FOR EACH ROW
+BEGIN
+    DECLARE issue_title VARCHAR(255);
+    DECLARE issue_description TEXT;
+    DECLARE issue_severity ENUM('Critical', 'High', 'Medium', 'Low');
+    DECLARE detection_trigger_name VARCHAR(100);
+    
+    -- Check for lending_status changes to problematic states
+    IF (OLD.lending_status IS NULL OR OLD.lending_status != NEW.lending_status) 
+       AND NEW.lending_status IN ('Overdue', 'Illegally Handed Out', 'Stolen') THEN
+        
+        -- Determine issue details based on lending_status
+        CASE NEW.lending_status
+            WHEN 'Overdue' THEN
+                SET issue_title = CONCAT('Asset Overdue: ', COALESCE(NEW.name, NEW.asset_tag, CAST(NEW.asset_numeric_id AS CHAR)));
+                SET issue_description = CONCAT('Asset lending status changed to Overdue. Asset: ', NEW.asset_tag, 
+                    CASE WHEN NEW.name IS NOT NULL THEN CONCAT(' (', NEW.name, ')') ELSE '' END);
+                SET issue_severity = 'High';
+                SET detection_trigger_name = 'LENDING_OVERDUE';
+                
+            WHEN 'Illegally Handed Out' THEN
+                SET issue_title = CONCAT('Asset Illegally Handed Out: ', COALESCE(NEW.name, NEW.asset_tag, CAST(NEW.asset_numeric_id AS CHAR)));
+                SET issue_description = CONCAT('Asset lending status changed to Illegally Handed Out. Asset: ', NEW.asset_tag,
+                    CASE WHEN NEW.name IS NOT NULL THEN CONCAT(' (', NEW.name, ')') ELSE '' END);
+                SET issue_severity = 'Critical';
+                SET detection_trigger_name = 'LENDING_ILLEGAL';
+                
+            WHEN 'Stolen' THEN
+                SET issue_title = CONCAT('Asset Stolen: ', COALESCE(NEW.name, NEW.asset_tag, CAST(NEW.asset_numeric_id AS CHAR)));
+                SET issue_description = CONCAT('Asset lending status changed to Stolen (14+ days overdue). Asset: ', NEW.asset_tag,
+                    CASE WHEN NEW.name IS NOT NULL THEN CONCAT(' (', NEW.name, ')') ELSE '' END);
+                SET issue_severity = 'Critical';
+                SET detection_trigger_name = 'LENDING_STOLEN';
+        END CASE;
+        
+        -- Insert the auto-detected issue
+        INSERT INTO issue_tracker (
+            issue_type, asset_id, title, description, severity, priority, status,
+            reported_by, auto_detected, detection_trigger, created_date
+        )
+        VALUES (
+            'Asset Issue', NEW.id, issue_title, issue_description, issue_severity, 'Urgent', 'Open',
+            COALESCE(@current_user_id, 1), TRUE, detection_trigger_name, NOW()
+        );
+    END IF;
+    
+    -- Check for status changes to problematic states
+    IF OLD.status != NEW.status AND NEW.status IN ('Attention', 'Faulty', 'Missing', 'Retired', 'In Repair', 'Expired') THEN
+        
+        -- Determine issue details based on status
+        CASE NEW.status
+            WHEN 'Attention' THEN
+                SET issue_title = CONCAT('Asset Needs Attention: ', COALESCE(NEW.name, NEW.asset_tag, NEW.asset_numeric_id));
+                SET issue_description = CONCAT('Asset status changed to Attention. Asset: ', NEW.asset_numeric_id, CASE WHEN NEW.name IS NOT NULL THEN CONCAT(' (', NEW.name, ')') ELSE '' END);
+                SET issue_severity = 'Medium';
+                SET detection_trigger_name = 'STATUS_ATTENTION';
+                
+            WHEN 'Faulty' THEN
+                SET issue_title = CONCAT('Asset Faulty: ', COALESCE(NEW.name, NEW.asset_tag, NEW.asset_numeric_id));
+                SET issue_description = CONCAT('Asset status changed to Faulty. Asset: ', NEW.asset_numeric_id, CASE WHEN NEW.name IS NOT NULL THEN CONCAT(' (', NEW.name, ')') ELSE '' END);
+                SET issue_severity = 'High';
+                SET detection_trigger_name = 'STATUS_FAULTY';
+                
+            WHEN 'Missing' THEN
+                SET issue_title = CONCAT('Asset Missing: ', COALESCE(NEW.name, NEW.asset_tag, NEW.asset_numeric_id));
+                SET issue_description = CONCAT('Asset status changed to Missing. Asset: ', NEW.asset_numeric_id, CASE WHEN NEW.name IS NOT NULL THEN CONCAT(' (', NEW.name, ')') ELSE '' END);
+                SET issue_severity = 'Critical';
+                SET detection_trigger_name = 'STATUS_MISSING';
+                
+            WHEN 'Retired' THEN
+                SET issue_title = CONCAT('Asset Retired: ', COALESCE(NEW.name, NEW.asset_tag, NEW.asset_numeric_id));
+                SET issue_description = CONCAT('Asset status changed to Retired. Asset: ', NEW.asset_numeric_id, CASE WHEN NEW.name IS NOT NULL THEN CONCAT(' (', NEW.name, ')') ELSE '' END);
+                SET issue_severity = 'Low';
+                SET detection_trigger_name = 'STATUS_RETIRED';
+                
+            WHEN 'In Repair' THEN
+                SET issue_title = CONCAT('Asset In Repair: ', COALESCE(NEW.name, NEW.asset_tag, NEW.asset_numeric_id));
+                SET issue_description = CONCAT('Asset status changed to In Repair. Asset: ', NEW.asset_numeric_id, CASE WHEN NEW.name IS NOT NULL THEN CONCAT(' (', NEW.name, ')') ELSE '' END);
+                SET issue_severity = 'Medium';
+                SET detection_trigger_name = 'STATUS_IN_REPAIR';
+                
+            WHEN 'Expired' THEN
+                SET issue_title = CONCAT('Asset Expired: ', COALESCE(NEW.name, NEW.asset_tag, NEW.asset_numeric_id));
+                SET issue_description = CONCAT('Asset status changed to Expired. Asset: ', NEW.asset_numeric_id, CASE WHEN NEW.name IS NOT NULL THEN CONCAT(' (', NEW.name, ')') ELSE '' END);
+                SET issue_severity = 'Medium';
+                SET detection_trigger_name = 'STATUS_EXPIRED';
+        END CASE;
+        
+        -- Insert the auto-detected issue
+        INSERT INTO issue_tracker (
+            issue_type, asset_id, title, description, severity, priority, status,
+            reported_by, auto_detected, detection_trigger, created_date
+        )
+        VALUES (
+            'Asset Issue', NEW.id, issue_title, issue_description, issue_severity, 'Normal', 'Open',
+            COALESCE(@current_user_id, 1), TRUE, detection_trigger_name, NOW()
+        );
+    END IF;
+    
+    -- Auto-resolve issues when status becomes Good again
+    IF OLD.status != NEW.status AND NEW.status = 'Good' AND OLD.status IN ('Faulty', 'Missing', 'In Repair', 'Expired') THEN
+        UPDATE issue_tracker 
+        SET status = 'Resolved',
+            solution = 'Automatically Fixed',
+            solution_plus = CONCAT('Asset status automatically changed from ', OLD.status, ' to Good'),
+            resolved_date = NOW(),
+            resolved_by = COALESCE(@current_user_id, 1)
+        WHERE asset_id = NEW.id 
+        AND status IN ('Open', 'In Progress')
+        AND auto_detected = TRUE
+        AND detection_trigger IN ('STATUS_FAULTY', 'STATUS_MISSING', 'STATUS_IN_REPAIR', 'STATUS_EXPIRED');
+    END IF;
+    
+    -- Auto-resolve overdue/stolen/illegal issues when item is returned (lending_status becomes Available)
+    IF (OLD.lending_status IS NULL OR OLD.lending_status != NEW.lending_status) 
+       AND NEW.lending_status = 'Available' 
+       AND OLD.lending_status IN ('Overdue', 'Illegally Handed Out', 'Stolen') THEN
+        UPDATE issue_tracker 
+        SET status = 'Resolved',
+            solution = 'Items Returned',
+            solution_plus = CONCAT('Asset was returned - lending status changed from ', OLD.lending_status, ' to Available'),
+            resolved_date = NOW(),
+            resolved_by = COALESCE(@current_user_id, 1)
+        WHERE asset_id = NEW.id 
+        AND status IN ('Open', 'In Progress')
+        AND auto_detected = TRUE
+        AND detection_trigger IN ('LENDING_OVERDUE', 'LENDING_ILLEGAL', 'LENDING_STOLEN');
+    END IF;
+END//
+
+-- Trigger: Auto-detect borrower issues when borrower is banned
+DROP TRIGGER IF EXISTS auto_detect_borrower_issues//
+CREATE TRIGGER auto_detect_borrower_issues
+AFTER UPDATE ON borrowers
+FOR EACH ROW
+BEGIN
+    DECLARE issue_title VARCHAR(255);
+    DECLARE issue_description TEXT;
+    
+    -- Auto-detect when borrower gets banned
+    IF OLD.banned = FALSE AND NEW.banned = TRUE THEN
+        SET issue_title = CONCAT('Borrower Banned: ', NEW.name);
+        SET issue_description = CONCAT('Borrower has been banned. Name: ', NEW.name, CASE WHEN NEW.unban_fine > 0 THEN CONCAT(', Unban Fine: $', NEW.unban_fine) ELSE '' END);
+        
+        INSERT INTO issue_tracker (
+            issue_type, borrower_id, title, description, severity, priority, status,
+            reported_by, auto_detected, detection_trigger, created_date
+        )
+        VALUES (
+            'Borrower Issue', NEW.id, issue_title, issue_description, 'High', 'Normal', 'Open',
+            COALESCE(@current_user_id, 1), TRUE, 'BORROWER_BANNED', NOW()
+        );
+    END IF;
+    
+    -- Auto-resolve when borrower gets unbanned
+    IF OLD.banned = TRUE AND NEW.banned = FALSE THEN
+        UPDATE issue_tracker 
+        SET status = 'Resolved',
+            solution = 'Items Returned',
+            solution_plus = CONCAT('Borrower unbanned on ', COALESCE(NEW.last_unban_date, CURDATE()), CASE WHEN NEW.last_unban_by IS NOT NULL THEN CONCAT(' by user ID ', NEW.last_unban_by) ELSE '' END),
+            resolved_date = NOW(),
+            resolved_by = COALESCE(@current_user_id, NEW.last_unban_by, 1)
+        WHERE borrower_id = NEW.id 
+        AND status IN ('Open', 'In Progress')
+        AND auto_detected = TRUE
+        AND detection_trigger = 'BORROWER_BANNED';
+    END IF;
+END//
+
+-- ============================================
+-- PHYSICAL AUDIT TRIGGERS (Simplified)
+-- ============================================
+
+-- Trigger: Auto-calculate assets_expected when starting full-zone audit
+DROP TRIGGER IF EXISTS calculate_assets_expected//
+CREATE TRIGGER calculate_assets_expected
+BEFORE INSERT ON physical_audits
+FOR EACH ROW
+BEGIN
+    DECLARE expected_count INT DEFAULT 0;
+    DECLARE v_timeout INT;
+    
+    -- For full-zone audits, calculate expected assets in the zone
+    IF NEW.audit_type = 'full-zone' AND NEW.zone_id IS NOT NULL THEN
+        SELECT COUNT(*) INTO expected_count
+        FROM assets 
+        WHERE zone_id = NEW.zone_id 
+        AND status NOT IN ('Missing', 'Retired');
+        
+        SET NEW.assets_expected = expected_count;
+    END IF;
+    
+    -- Set timeout from zone settings if not specified
+    IF NEW.timeout_minutes IS NULL AND NEW.zone_id IS NOT NULL THEN
+        SELECT audit_timeout_minutes INTO v_timeout
+        FROM zones 
+        WHERE id = NEW.zone_id
+        LIMIT 1;
+        
+        IF v_timeout IS NOT NULL THEN
+            SET NEW.timeout_minutes = v_timeout;
+        END IF;
+    END IF;
+END//
+
+-- Trigger: Auto-populate audited_by from session user
+DROP TRIGGER IF EXISTS physical_audit_logs_before_insert_meta//
+CREATE TRIGGER physical_audit_logs_before_insert_meta
+BEFORE INSERT ON physical_audit_logs
+FOR EACH ROW
+BEGIN
+    -- Auto-populate audited_by from session variable if not provided
+    IF NEW.audited_by IS NULL OR NEW.audited_by = 0 THEN
+        SET NEW.audited_by = COALESCE(@current_user_id, 1);
+    END IF;
+END//
+
+-- Trigger: Update assets_found counter when asset is audited
+DROP TRIGGER IF EXISTS update_assets_found//
+CREATE TRIGGER update_assets_found
+AFTER INSERT ON physical_audit_logs
+FOR EACH ROW
+BEGIN
+    UPDATE physical_audits 
+    SET assets_found = assets_found + 1
+    WHERE id = NEW.physical_audit_id;
+END//
+
+-- Trigger: Simple audit issue detection
+DROP TRIGGER IF EXISTS auto_detect_audit_issues//
+CREATE TRIGGER auto_detect_audit_issues
+AFTER UPDATE ON physical_audits
+FOR EACH ROW
+BEGIN
+    DECLARE missing_count INT DEFAULT 0;
+    DECLARE zone_name VARCHAR(200);
+    
+    -- Only process when audit status changes to completed states
+    IF OLD.status = 'in-progress' AND NEW.status IN ('all-good', 'attention', 'timeout') THEN
+        
+        -- Get zone name for reporting
+        IF NEW.zone_id IS NOT NULL THEN
+            SELECT zone_name INTO zone_name FROM zones WHERE id = NEW.zone_id;
+        END IF;
+        
+        -- For full-zone audits, check for missing assets
+        IF NEW.audit_type = 'full-zone' AND NEW.assets_expected IS NOT NULL THEN
+            SET missing_count = GREATEST(0, NEW.assets_expected - NEW.assets_found);
+        END IF;
+        
+        -- Create issue for missing assets
+        IF missing_count > 0 THEN
+            INSERT INTO issue_tracker (
+                issue_type, title, description, severity, priority, status,
+                reported_by, auto_detected, detection_trigger, created_date, notes
+            )
+            VALUES (
+                'System Issue', 
+                CONCAT('Audit: Missing Assets in ', COALESCE(zone_name, 'Unknown Zone')),
+                CONCAT('Full zone audit completed with ', missing_count, ' missing assets. Expected: ', NEW.assets_expected, ', Found: ', NEW.assets_found, '. Audit ID: ', NEW.id),
+                CASE WHEN missing_count >= 5 THEN 'Critical' WHEN missing_count >= 2 THEN 'High' ELSE 'Medium' END,
+                'High', 'Open',
+                NEW.started_by, TRUE, 'AUDIT_MISSING_ASSETS', NOW(),
+                CONCAT('Physical Audit ID: ', NEW.id, ' in zone: ', COALESCE(zone_name, NEW.zone_id))
+            );
+        END IF;
+    END IF;
+END//
+
+-- Trigger: Basic asset audit update
+DROP TRIGGER IF EXISTS update_asset_from_audit//
+CREATE TRIGGER update_asset_from_audit
+AFTER INSERT ON physical_audit_logs
+FOR EACH ROW
+BEGIN
+    DECLARE current_status VARCHAR(100);
+    
+    -- Update asset's last_audit date
+    UPDATE assets 
+    SET last_audit = DATE(NEW.audit_date),
+        last_audit_status = NEW.status_found
+    WHERE id = NEW.asset_id;
+    
+    -- Compare found status with current asset status
+    SELECT status INTO current_status FROM assets WHERE id = NEW.asset_id LIMIT 1;
+    
+    IF NEW.status_found != current_status THEN
+        UPDATE assets 
+        SET status = NEW.status_found
+        WHERE id = NEW.asset_id;
+    END IF;
+END//
+
+DELIMITER ;
+
+-- End of clean triggers file
+
+-- ============================================
+-- USAGE NOTES
+-- ============================================
+
+/*
+HOW TO USE FROM YOUR RUST PROXY:
+
+Before any INSERT/UPDATE/DELETE operation, set the user context:
+    SET @current_user_id = 123;
+
+Then execute your query normally. The triggers will automatically:
+1. Auto-populate user tracking fields (if not explicitly provided):
+   • assets.created_by (on INSERT)
+   • assets.last_modified_by (on UPDATE)
+   • borrowers.added_by (on INSERT)
+   • borrowers.last_unban_by (on UPDATE when unbanning)
+   • issue_tracker.reported_by (on INSERT)
+2. Log changes to asset_change_log EFFICIENTLY:
+   • INSERT: Only logs fields that were actually set (non-NULL)
+   • UPDATE: Only logs fields that actually changed
+   • DELETE: Only logs fields that had values (non-NULL) for restore capability
+3. Include user_id and username in logs
+4. Update last_modified_by and last_modified_date automatically
+5. Enforce business rules (prevent deleting borrowed items, validate zone_plus, etc.)
+6. Auto-calculate quantities for lendable items with quantity tracking
+7. Auto-detect and track issues in issue_tracker
+8. Manage physical audits with automatic issue detection
+
+NOTE: You can still explicitly provide user tracking fields in your queries if needed.
+      The triggers only set them if they are NULL and @current_user_id is available.
+
+EFFICIENCY: Change logging only captures non-NULL/changed fields, reducing storage by ~80%
+            for typical operations. The 'changed_fields' JSON array shows exactly what was
+            set/changed/deleted, making audit logs cleaner and more queryable.
+
+PHYSICAL AUDIT WORKFLOW:
+1. Start audit: INSERT INTO physical_audits (audit_type, zone_id, started_by) VALUES ('full-zone', 1, 123);
+2. Scan assets: INSERT INTO physical_audit_logs (physical_audit_id, asset_id, audited_by, status_found, audit_task_id, audit_task_responses, exception_type, found_in_zone_id, auditor_action) VALUES (...);
+3. Complete audit: UPDATE physical_audits SET status = 'all-good' (or 'attention') WHERE id = audit_id;
+4. System automatically creates issues for missing/moved/damaged assets
+
+WRONG-ZONE ASSET HANDLING:
+When asset found in wrong zone (exception_type = 'wrong-zone'), auditor has 3 options:
+- auditor_action = 'physical-move': Auditor will physically move item to correct zone (no issue created)
+- auditor_action = 'virtual-update': Update asset's zone in system to where found (auto-detects label reprinting needs)
+- auditor_action = NULL: Creates standard follow-up issue for later resolution
+
+LABEL REPRINTING DETECTION:
+System automatically detects if asset_tag contains location info when doing virtual-update:
+- Checks if asset_tag contains old zone name or room codes
+- Checks for common label patterns (e.g., "MB101-001", "RoomA-Device")  
+- Creates 'Maintenance' issue with 'Low' priority for label reprinting if needed
+
+CROSS-AUDIT RECONCILIATION:
+System automatically resolves "missing asset" issues from previous audits when assets are found:
+- When asset is scanned in any audit, checks if it was missing from previous completed audits
+- Auto-resolves related missing asset issues with solution 'Automatically Fixed'
+- Logs reconciliation activity in asset_change_log for audit trail
+- Prevents false "missing" reports when assets are just in different locations
+
+ISSUE TRACKER FEATURES:
+- Auto-creates issues for problematic asset status changes
+- Auto-resolves issues when assets return to Good status
+- Tracks borrower ban/unban cycles
+- Comprehensive audit issue detection and tracking
+- Intelligent cross-audit reconciliation to prevent false missing asset reports
+
+Example queries in asset_change_log:
+- changed_fields: ["status", "zone_id"] (array of field names that changed)
+- old_values: {"status": "Good", "zone_id": 5}
+- new_values: {"status": "Faulty", "zone_id": 7}
+
+Example audit_task_responses JSON:
+{"step_1_answer": "yes", "step_2_answer": "Good", "damage_notes": "Minor scratches on case"}
+
+Example issues_found JSON in physical_audits:
+{"missing_assets": 2, "moved_assets": 5, "damaged_assets": 1, "total_issues": 8}
+*/
+
+-- ============================================
+-- End of Schema
+-- ============================================

+ 2081 - 0
backend/database/dev/beepzone-schema-dump.sql

@@ -0,0 +1,2081 @@
+/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
+/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `asset_change_log` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `table_name` varchar(50) NOT NULL,
+  `action` enum('INSERT','UPDATE','DELETE') NOT NULL,
+  `record_id` int(11) NOT NULL,
+  `changed_fields` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'Only fields that actually changed' CHECK (json_valid(`changed_fields`)),
+  `old_values` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`old_values`)),
+  `new_values` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`new_values`)),
+  `changed_at` timestamp NULL DEFAULT current_timestamp(),
+  `changed_by_id` int(11) DEFAULT NULL,
+  `changed_by_username` varchar(100) DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `idx_table_action` (`table_name`,`action`),
+  KEY `idx_timestamp` (`changed_at`),
+  KEY `idx_record` (`record_id`),
+  KEY `idx_user` (`changed_by_id`),
+  CONSTRAINT `asset_change_log_ibfk_1` FOREIGN KEY (`changed_by_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `assets` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `asset_tag` varchar(200) DEFAULT NULL,
+  `asset_numeric_id` int(11) NOT NULL CHECK (`asset_numeric_id` between 10000000 and 99999999),
+  `asset_type` enum('N','B','L','C') NOT NULL,
+  `name` varchar(255) DEFAULT NULL,
+  `category_id` int(11) DEFAULT NULL,
+  `manufacturer` varchar(200) DEFAULT NULL,
+  `model` varchar(200) DEFAULT NULL,
+  `serial_number` varchar(200) DEFAULT NULL,
+  `zone_id` int(11) DEFAULT NULL,
+  `zone_plus` enum('Floating Local','Floating Global','Clarify') DEFAULT NULL,
+  `zone_note` text DEFAULT NULL,
+  `status` enum('Good','Attention','Faulty','Missing','Retired','In Repair','In Transit','Expired','Unmanaged') DEFAULT 'Good',
+  `last_audit` date DEFAULT NULL,
+  `last_audit_status` varchar(100) DEFAULT NULL,
+  `price` decimal(12,2) DEFAULT NULL CHECK (`price` is null or `price` >= 0),
+  `purchase_date` date DEFAULT NULL,
+  `warranty_until` date DEFAULT NULL,
+  `expiry_date` date DEFAULT NULL,
+  `quantity_available` int(11) DEFAULT NULL,
+  `quantity_total` int(11) DEFAULT NULL,
+  `quantity_used` int(11) DEFAULT 0,
+  `supplier_id` int(11) DEFAULT NULL,
+  `lendable` tinyint(1) DEFAULT 0,
+  `minimum_role_for_lending` int(11) DEFAULT 1 CHECK (`minimum_role_for_lending` >= 1 and `minimum_role_for_lending` <= 100),
+  `lending_status` enum('Available','Deployed','Borrowed','Overdue','Illegally Handed Out','Stolen') DEFAULT NULL,
+  `current_borrower_id` int(11) DEFAULT NULL,
+  `due_date` date DEFAULT NULL,
+  `previous_borrower_id` int(11) DEFAULT NULL,
+  `audit_task_id` int(11) DEFAULT NULL,
+  `label_template_id` int(11) DEFAULT NULL,
+  `no_scan` enum('Yes','Ask','No') DEFAULT 'No',
+  `notes` text DEFAULT NULL,
+  `additional_fields` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`additional_fields`)),
+  `file_attachment` mediumblob DEFAULT NULL,
+  `created_date` timestamp NULL DEFAULT current_timestamp(),
+  `created_by` int(11) DEFAULT NULL,
+  `last_modified_date` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
+  `last_modified_by` int(11) DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `asset_numeric_id` (`asset_numeric_id`),
+  UNIQUE KEY `asset_tag` (`asset_tag`),
+  KEY `supplier_id` (`supplier_id`),
+  KEY `current_borrower_id` (`current_borrower_id`),
+  KEY `previous_borrower_id` (`previous_borrower_id`),
+  KEY `audit_task_id` (`audit_task_id`),
+  KEY `created_by` (`created_by`),
+  KEY `last_modified_by` (`last_modified_by`),
+  KEY `idx_asset_tag` (`asset_tag`),
+  KEY `idx_asset_numeric` (`asset_numeric_id`),
+  KEY `idx_type` (`asset_type`),
+  KEY `idx_status` (`status`),
+  KEY `idx_zone` (`zone_id`),
+  KEY `idx_category` (`category_id`),
+  KEY `idx_lendable` (`lendable`),
+  KEY `idx_lending_status` (`lending_status`),
+  KEY `idx_label_template` (`label_template_id`),
+  CONSTRAINT `assets_ibfk_1` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`),
+  CONSTRAINT `assets_ibfk_2` FOREIGN KEY (`zone_id`) REFERENCES `zones` (`id`),
+  CONSTRAINT `assets_ibfk_3` FOREIGN KEY (`supplier_id`) REFERENCES `suppliers` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `assets_ibfk_4` FOREIGN KEY (`current_borrower_id`) REFERENCES `borrowers` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `assets_ibfk_5` FOREIGN KEY (`previous_borrower_id`) REFERENCES `borrowers` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `assets_ibfk_6` FOREIGN KEY (`audit_task_id`) REFERENCES `audit_tasks` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `assets_ibfk_7` FOREIGN KEY (`created_by`) REFERENCES `users` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `assets_ibfk_8` FOREIGN KEY (`last_modified_by`) REFERENCES `users` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `fk_asset_label_template` FOREIGN KEY (`label_template_id`) REFERENCES `label_templates` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER assets_before_insert_meta
+BEFORE INSERT ON assets
+FOR EACH ROW
+BEGIN
+    IF NEW.created_by IS NULL AND @current_user_id IS NOT NULL THEN
+        SET NEW.created_by = @current_user_id;
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER validate_zone_plus_insert
+BEFORE INSERT ON assets
+FOR EACH ROW
+BEGIN
+    IF NEW.zone_plus = 'Clarify' AND (NEW.zone_note IS NULL OR NEW.zone_note = '') THEN
+        SIGNAL SQLSTATE '45000'
+        SET MESSAGE_TEXT = 'zone_note is required when zone_plus is set to Clarify';
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER assets_after_insert_log
+AFTER INSERT ON assets
+FOR EACH ROW
+BEGIN
+    DECLARE username VARCHAR(100);
+    DECLARE set_fields_array JSON;
+    DECLARE new_vals JSON;
+    
+    IF @current_user_id IS NOT NULL THEN
+        SELECT users.username INTO username FROM users WHERE id = @current_user_id;
+    END IF;
+    
+    -- Build JSON objects only with non-NULL fields
+    SET set_fields_array = JSON_ARRAY();
+    SET new_vals = JSON_OBJECT();
+    
+    -- Always log these core fields
+    IF NEW.asset_tag IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'asset_tag');
+        SET new_vals = JSON_SET(new_vals, '$.asset_tag', NEW.asset_tag);
+    END IF;
+    
+    IF NEW.asset_numeric_id IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'asset_numeric_id');
+        SET new_vals = JSON_SET(new_vals, '$.asset_numeric_id', NEW.asset_numeric_id);
+    END IF;
+    
+    IF NEW.asset_type IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'asset_type');
+        SET new_vals = JSON_SET(new_vals, '$.asset_type', NEW.asset_type);
+    END IF;
+    
+    IF NEW.name IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'name');
+        SET new_vals = JSON_SET(new_vals, '$.name', NEW.name);
+    END IF;
+    
+    IF NEW.category_id IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'category_id');
+        SET new_vals = JSON_SET(new_vals, '$.category_id', NEW.category_id);
+    END IF;
+    
+    IF NEW.manufacturer IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'manufacturer');
+        SET new_vals = JSON_SET(new_vals, '$.manufacturer', NEW.manufacturer);
+    END IF;
+    
+    IF NEW.model IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'model');
+        SET new_vals = JSON_SET(new_vals, '$.model', NEW.model);
+    END IF;
+    
+    IF NEW.serial_number IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'serial_number');
+        SET new_vals = JSON_SET(new_vals, '$.serial_number', NEW.serial_number);
+    END IF;
+    
+    IF NEW.zone_id IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'zone_id');
+        SET new_vals = JSON_SET(new_vals, '$.zone_id', NEW.zone_id);
+    END IF;
+    
+    IF NEW.zone_plus IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'zone_plus');
+        SET new_vals = JSON_SET(new_vals, '$.zone_plus', NEW.zone_plus);
+    END IF;
+    
+    IF NEW.zone_note IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'zone_note');
+        SET new_vals = JSON_SET(new_vals, '$.zone_note', NEW.zone_note);
+    END IF;
+    
+    IF NEW.status IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'status');
+        SET new_vals = JSON_SET(new_vals, '$.status', NEW.status);
+    END IF;
+    
+    IF NEW.last_audit IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'last_audit');
+        SET new_vals = JSON_SET(new_vals, '$.last_audit', NEW.last_audit);
+    END IF;
+    
+    IF NEW.last_audit_status IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'last_audit_status');
+        SET new_vals = JSON_SET(new_vals, '$.last_audit_status', NEW.last_audit_status);
+    END IF;
+    
+    IF NEW.price IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'price');
+        SET new_vals = JSON_SET(new_vals, '$.price', NEW.price);
+    END IF;
+    
+    IF NEW.purchase_date IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'purchase_date');
+        SET new_vals = JSON_SET(new_vals, '$.purchase_date', NEW.purchase_date);
+    END IF;
+    
+    IF NEW.warranty_until IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'warranty_until');
+        SET new_vals = JSON_SET(new_vals, '$.warranty_until', NEW.warranty_until);
+    END IF;
+    
+    IF NEW.expiry_date IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'expiry_date');
+        SET new_vals = JSON_SET(new_vals, '$.expiry_date', NEW.expiry_date);
+    END IF;
+    
+    IF NEW.quantity_available IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'quantity_available');
+        SET new_vals = JSON_SET(new_vals, '$.quantity_available', NEW.quantity_available);
+    END IF;
+    
+    IF NEW.quantity_total IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'quantity_total');
+        SET new_vals = JSON_SET(new_vals, '$.quantity_total', NEW.quantity_total);
+    END IF;
+    
+    IF NEW.quantity_used IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'quantity_used');
+        SET new_vals = JSON_SET(new_vals, '$.quantity_used', NEW.quantity_used);
+    END IF;
+    
+    IF NEW.supplier_id IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'supplier_id');
+        SET new_vals = JSON_SET(new_vals, '$.supplier_id', NEW.supplier_id);
+    END IF;
+    
+    IF NEW.lendable IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'lendable');
+        SET new_vals = JSON_SET(new_vals, '$.lendable', NEW.lendable);
+    END IF;
+    
+    IF NEW.minimum_role_for_lending IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'minimum_role_for_lending');
+        SET new_vals = JSON_SET(new_vals, '$.minimum_role_for_lending', NEW.minimum_role_for_lending);
+    END IF;
+    
+    IF NEW.lending_status IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'lending_status');
+        SET new_vals = JSON_SET(new_vals, '$.lending_status', NEW.lending_status);
+    END IF;
+    
+    IF NEW.current_borrower_id IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'current_borrower_id');
+        SET new_vals = JSON_SET(new_vals, '$.current_borrower_id', NEW.current_borrower_id);
+    END IF;
+    
+    IF NEW.due_date IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'due_date');
+        SET new_vals = JSON_SET(new_vals, '$.due_date', NEW.due_date);
+    END IF;
+    
+    IF NEW.previous_borrower_id IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'previous_borrower_id');
+        SET new_vals = JSON_SET(new_vals, '$.previous_borrower_id', NEW.previous_borrower_id);
+    END IF;
+    
+    IF NEW.audit_task_id IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'audit_task_id');
+        SET new_vals = JSON_SET(new_vals, '$.audit_task_id', NEW.audit_task_id);
+    END IF;
+    
+    IF NEW.no_scan IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'no_scan');
+        SET new_vals = JSON_SET(new_vals, '$.no_scan', NEW.no_scan);
+    END IF;
+    
+    IF NEW.notes IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'notes');
+        SET new_vals = JSON_SET(new_vals, '$.notes', NEW.notes);
+    END IF;
+    
+    IF NEW.additional_fields IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'additional_fields');
+        SET new_vals = JSON_SET(new_vals, '$.additional_fields', NEW.additional_fields);
+    END IF;
+    
+    IF NEW.created_by IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'created_by');
+        SET new_vals = JSON_SET(new_vals, '$.created_by', NEW.created_by);
+    END IF;
+    
+    -- Log the INSERT with only the fields that were set
+    INSERT INTO asset_change_log (
+        table_name, action, record_id, changed_fields, new_values,
+        changed_by_id, changed_by_username
+    )
+    VALUES (
+        'assets',
+        'INSERT',
+        NEW.id,
+        set_fields_array,
+        new_vals,
+        @current_user_id,
+        username
+    );
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER assets_before_update_meta
+BEFORE UPDATE ON assets
+FOR EACH ROW
+BEGIN
+    SET NEW.last_modified_date = NOW();
+    IF @current_user_id IS NOT NULL THEN
+        SET NEW.last_modified_by = @current_user_id;
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER prevent_lend_non_lendable_assets
+BEFORE UPDATE ON assets
+FOR EACH ROW
+BEGIN
+    -- Check if trying to set lending_status to any borrowed state on a non-lendable asset
+    IF (NEW.lendable = FALSE OR NEW.lendable IS NULL) AND 
+       NEW.lending_status IN ('Borrowed', 'Deployed', 'Overdue') AND
+       (OLD.lending_status NOT IN ('Borrowed', 'Deployed', 'Overdue') OR OLD.lending_status IS NULL) THEN
+        SIGNAL SQLSTATE '45000'
+        SET MESSAGE_TEXT = 'Cannot lend asset that is marked as non-lendable. Set lendable=TRUE first.';
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER validate_zone_plus_update
+BEFORE UPDATE ON assets
+FOR EACH ROW
+BEGIN
+    IF NEW.zone_plus = 'Clarify' AND (NEW.zone_note IS NULL OR NEW.zone_note = '') THEN
+        SIGNAL SQLSTATE '45000'
+        SET MESSAGE_TEXT = 'zone_note is required when zone_plus is set to Clarify';
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER assets_after_update_log
+AFTER UPDATE ON assets
+FOR EACH ROW
+BEGIN
+    DECLARE username VARCHAR(100);
+    DECLARE changed_fields_array JSON;
+    DECLARE old_vals JSON;
+    DECLARE new_vals JSON;
+    
+    IF @current_user_id IS NOT NULL THEN
+        SELECT users.username INTO username FROM users WHERE id = @current_user_id;
+    END IF;
+    
+    -- Build JSON objects only with changed fields
+    SET changed_fields_array = JSON_ARRAY();
+    SET old_vals = JSON_OBJECT();
+    SET new_vals = JSON_OBJECT();
+    
+    IF OLD.asset_tag <=> NEW.asset_tag IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'asset_tag');
+        SET old_vals = JSON_SET(old_vals, '$.asset_tag', OLD.asset_tag);
+        SET new_vals = JSON_SET(new_vals, '$.asset_tag', NEW.asset_tag);
+    END IF;
+    
+    IF OLD.asset_numeric_id <=> NEW.asset_numeric_id IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'asset_numeric_id');
+        SET old_vals = JSON_SET(old_vals, '$.asset_numeric_id', OLD.asset_numeric_id);
+        SET new_vals = JSON_SET(new_vals, '$.asset_numeric_id', NEW.asset_numeric_id);
+    END IF;
+    
+    IF OLD.asset_type <=> NEW.asset_type IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'asset_type');
+        SET old_vals = JSON_SET(old_vals, '$.asset_type', OLD.asset_type);
+        SET new_vals = JSON_SET(new_vals, '$.asset_type', NEW.asset_type);
+    END IF;
+    
+    IF OLD.name <=> NEW.name IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'name');
+        SET old_vals = JSON_SET(old_vals, '$.name', OLD.name);
+        SET new_vals = JSON_SET(new_vals, '$.name', NEW.name);
+    END IF;
+    
+    IF OLD.category_id <=> NEW.category_id IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'category_id');
+        SET old_vals = JSON_SET(old_vals, '$.category_id', OLD.category_id);
+        SET new_vals = JSON_SET(new_vals, '$.category_id', NEW.category_id);
+    END IF;
+    
+    IF OLD.manufacturer <=> NEW.manufacturer IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'manufacturer');
+        SET old_vals = JSON_SET(old_vals, '$.manufacturer', OLD.manufacturer);
+        SET new_vals = JSON_SET(new_vals, '$.manufacturer', NEW.manufacturer);
+    END IF;
+    
+    IF OLD.model <=> NEW.model IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'model');
+        SET old_vals = JSON_SET(old_vals, '$.model', OLD.model);
+        SET new_vals = JSON_SET(new_vals, '$.model', NEW.model);
+    END IF;
+    
+    IF OLD.serial_number <=> NEW.serial_number IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'serial_number');
+        SET old_vals = JSON_SET(old_vals, '$.serial_number', OLD.serial_number);
+        SET new_vals = JSON_SET(new_vals, '$.serial_number', NEW.serial_number);
+    END IF;
+    
+    IF OLD.zone_id <=> NEW.zone_id IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'zone_id');
+        SET old_vals = JSON_SET(old_vals, '$.zone_id', OLD.zone_id);
+        SET new_vals = JSON_SET(new_vals, '$.zone_id', NEW.zone_id);
+    END IF;
+    
+    IF OLD.zone_plus <=> NEW.zone_plus IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'zone_plus');
+        SET old_vals = JSON_SET(old_vals, '$.zone_plus', OLD.zone_plus);
+        SET new_vals = JSON_SET(new_vals, '$.zone_plus', NEW.zone_plus);
+    END IF;
+    
+    IF OLD.zone_note <=> NEW.zone_note IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'zone_note');
+        SET old_vals = JSON_SET(old_vals, '$.zone_note', OLD.zone_note);
+        SET new_vals = JSON_SET(new_vals, '$.zone_note', NEW.zone_note);
+    END IF;
+    
+    IF OLD.status <=> NEW.status IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'status');
+        SET old_vals = JSON_SET(old_vals, '$.status', OLD.status);
+        SET new_vals = JSON_SET(new_vals, '$.status', NEW.status);
+    END IF;
+    
+    IF OLD.last_audit <=> NEW.last_audit IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'last_audit');
+        SET old_vals = JSON_SET(old_vals, '$.last_audit', OLD.last_audit);
+        SET new_vals = JSON_SET(new_vals, '$.last_audit', NEW.last_audit);
+    END IF;
+    
+    IF OLD.last_audit_status <=> NEW.last_audit_status IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'last_audit_status');
+        SET old_vals = JSON_SET(old_vals, '$.last_audit_status', OLD.last_audit_status);
+        SET new_vals = JSON_SET(new_vals, '$.last_audit_status', NEW.last_audit_status);
+    END IF;
+    
+    IF OLD.price <=> NEW.price IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'price');
+        SET old_vals = JSON_SET(old_vals, '$.price', OLD.price);
+        SET new_vals = JSON_SET(new_vals, '$.price', NEW.price);
+    END IF;
+    
+    IF OLD.purchase_date <=> NEW.purchase_date IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'purchase_date');
+        SET old_vals = JSON_SET(old_vals, '$.purchase_date', OLD.purchase_date);
+        SET new_vals = JSON_SET(new_vals, '$.purchase_date', NEW.purchase_date);
+    END IF;
+    
+    IF OLD.warranty_until <=> NEW.warranty_until IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'warranty_until');
+        SET old_vals = JSON_SET(old_vals, '$.warranty_until', OLD.warranty_until);
+        SET new_vals = JSON_SET(new_vals, '$.warranty_until', NEW.warranty_until);
+    END IF;
+    
+    IF OLD.expiry_date <=> NEW.expiry_date IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'expiry_date');
+        SET old_vals = JSON_SET(old_vals, '$.expiry_date', OLD.expiry_date);
+        SET new_vals = JSON_SET(new_vals, '$.expiry_date', NEW.expiry_date);
+    END IF;
+    
+    IF OLD.quantity_available <=> NEW.quantity_available IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'quantity_available');
+        SET old_vals = JSON_SET(old_vals, '$.quantity_available', OLD.quantity_available);
+        SET new_vals = JSON_SET(new_vals, '$.quantity_available', NEW.quantity_available);
+    END IF;
+    
+    IF OLD.quantity_total <=> NEW.quantity_total IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'quantity_total');
+        SET old_vals = JSON_SET(old_vals, '$.quantity_total', OLD.quantity_total);
+        SET new_vals = JSON_SET(new_vals, '$.quantity_total', NEW.quantity_total);
+    END IF;
+    
+    IF OLD.quantity_used <=> NEW.quantity_used IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'quantity_used');
+        SET old_vals = JSON_SET(old_vals, '$.quantity_used', OLD.quantity_used);
+        SET new_vals = JSON_SET(new_vals, '$.quantity_used', NEW.quantity_used);
+    END IF;
+    
+    IF OLD.supplier_id <=> NEW.supplier_id IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'supplier_id');
+        SET old_vals = JSON_SET(old_vals, '$.supplier_id', OLD.supplier_id);
+        SET new_vals = JSON_SET(new_vals, '$.supplier_id', NEW.supplier_id);
+    END IF;
+    
+    IF OLD.lendable <=> NEW.lendable IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'lendable');
+        SET old_vals = JSON_SET(old_vals, '$.lendable', OLD.lendable);
+        SET new_vals = JSON_SET(new_vals, '$.lendable', NEW.lendable);
+    END IF;
+    
+    IF OLD.minimum_role_for_lending <=> NEW.minimum_role_for_lending IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'minimum_role_for_lending');
+        SET old_vals = JSON_SET(old_vals, '$.minimum_role_for_lending', OLD.minimum_role_for_lending);
+        SET new_vals = JSON_SET(new_vals, '$.minimum_role_for_lending', NEW.minimum_role_for_lending);
+    END IF;
+    
+    IF OLD.lending_status <=> NEW.lending_status IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'lending_status');
+        SET old_vals = JSON_SET(old_vals, '$.lending_status', OLD.lending_status);
+        SET new_vals = JSON_SET(new_vals, '$.lending_status', NEW.lending_status);
+    END IF;
+    
+    IF OLD.current_borrower_id <=> NEW.current_borrower_id IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'current_borrower_id');
+        SET old_vals = JSON_SET(old_vals, '$.current_borrower_id', OLD.current_borrower_id);
+        SET new_vals = JSON_SET(new_vals, '$.current_borrower_id', NEW.current_borrower_id);
+    END IF;
+    
+    IF OLD.due_date <=> NEW.due_date IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'due_date');
+        SET old_vals = JSON_SET(old_vals, '$.due_date', OLD.due_date);
+        SET new_vals = JSON_SET(new_vals, '$.due_date', NEW.due_date);
+    END IF;
+    
+    IF OLD.previous_borrower_id <=> NEW.previous_borrower_id IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'previous_borrower_id');
+        SET old_vals = JSON_SET(old_vals, '$.previous_borrower_id', OLD.previous_borrower_id);
+        SET new_vals = JSON_SET(new_vals, '$.previous_borrower_id', NEW.previous_borrower_id);
+    END IF;
+    
+    IF OLD.audit_task_id <=> NEW.audit_task_id IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'audit_task_id');
+        SET old_vals = JSON_SET(old_vals, '$.audit_task_id', OLD.audit_task_id);
+        SET new_vals = JSON_SET(new_vals, '$.audit_task_id', NEW.audit_task_id);
+    END IF;
+    
+    IF OLD.no_scan <=> NEW.no_scan IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'no_scan');
+        SET old_vals = JSON_SET(old_vals, '$.no_scan', OLD.no_scan);
+        SET new_vals = JSON_SET(new_vals, '$.no_scan', NEW.no_scan);
+    END IF;
+    
+    IF OLD.notes <=> NEW.notes IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'notes');
+        SET old_vals = JSON_SET(old_vals, '$.notes', OLD.notes);
+        SET new_vals = JSON_SET(new_vals, '$.notes', NEW.notes);
+    END IF;
+    
+    IF OLD.additional_fields <=> NEW.additional_fields IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'additional_fields');
+        SET old_vals = JSON_SET(old_vals, '$.additional_fields', OLD.additional_fields);
+        SET new_vals = JSON_SET(new_vals, '$.additional_fields', NEW.additional_fields);
+    END IF;
+    
+    -- Only log if there were actual changes (excluding auto-updated fields)
+    IF JSON_LENGTH(changed_fields_array) > 0 THEN
+        INSERT INTO asset_change_log (
+            table_name, action, record_id, changed_fields, old_values, new_values,
+            changed_by_id, changed_by_username
+        )
+        VALUES (
+            'assets',
+            'UPDATE',
+            NEW.id,
+            changed_fields_array,
+            old_vals,
+            new_vals,
+            @current_user_id,
+            username
+        );
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER auto_detect_asset_issues
+AFTER UPDATE ON assets
+FOR EACH ROW
+BEGIN
+    DECLARE issue_title VARCHAR(255);
+    DECLARE issue_description TEXT;
+    DECLARE issue_severity ENUM('Critical', 'High', 'Medium', 'Low');
+    DECLARE detection_trigger_name VARCHAR(100);
+    
+    -- Check for lending_status changes to problematic states
+    IF (OLD.lending_status IS NULL OR OLD.lending_status != NEW.lending_status) 
+       AND NEW.lending_status IN ('Overdue', 'Illegally Handed Out', 'Stolen') THEN
+        
+        -- Determine issue details based on lending_status
+        CASE NEW.lending_status
+            WHEN 'Overdue' THEN
+                SET issue_title = CONCAT('Asset Overdue: ', COALESCE(NEW.name, NEW.asset_tag, CAST(NEW.asset_numeric_id AS CHAR)));
+                SET issue_description = CONCAT('Asset lending status changed to Overdue. Asset: ', NEW.asset_tag, 
+                    CASE WHEN NEW.name IS NOT NULL THEN CONCAT(' (', NEW.name, ')') ELSE '' END);
+                SET issue_severity = 'High';
+                SET detection_trigger_name = 'LENDING_OVERDUE';
+                
+            WHEN 'Illegally Handed Out' THEN
+                SET issue_title = CONCAT('Asset Illegally Handed Out: ', COALESCE(NEW.name, NEW.asset_tag, CAST(NEW.asset_numeric_id AS CHAR)));
+                SET issue_description = CONCAT('Asset lending status changed to Illegally Handed Out. Asset: ', NEW.asset_tag,
+                    CASE WHEN NEW.name IS NOT NULL THEN CONCAT(' (', NEW.name, ')') ELSE '' END);
+                SET issue_severity = 'Critical';
+                SET detection_trigger_name = 'LENDING_ILLEGAL';
+                
+            WHEN 'Stolen' THEN
+                SET issue_title = CONCAT('Asset Stolen: ', COALESCE(NEW.name, NEW.asset_tag, CAST(NEW.asset_numeric_id AS CHAR)));
+                SET issue_description = CONCAT('Asset lending status changed to Stolen (14+ days overdue). Asset: ', NEW.asset_tag,
+                    CASE WHEN NEW.name IS NOT NULL THEN CONCAT(' (', NEW.name, ')') ELSE '' END);
+                SET issue_severity = 'Critical';
+                SET detection_trigger_name = 'LENDING_STOLEN';
+        END CASE;
+        
+        -- Insert the auto-detected issue
+        INSERT INTO issue_tracker (
+            issue_type, asset_id, title, description, severity, priority, status,
+            reported_by, auto_detected, detection_trigger, created_date
+        )
+        VALUES (
+            'Asset Issue', NEW.id, issue_title, issue_description, issue_severity, 'Urgent', 'Open',
+            COALESCE(@current_user_id, 1), TRUE, detection_trigger_name, NOW()
+        );
+    END IF;
+    
+    -- Check for status changes to problematic states
+    IF OLD.status != NEW.status AND NEW.status IN ('Attention', 'Faulty', 'Missing', 'Retired', 'In Repair', 'Expired') THEN
+        
+        -- Determine issue details based on status
+        CASE NEW.status
+            WHEN 'Attention' THEN
+                SET issue_title = CONCAT('Asset Needs Attention: ', COALESCE(NEW.name, NEW.asset_tag, NEW.asset_numeric_id));
+                SET issue_description = CONCAT('Asset status changed to Attention. Asset: ', NEW.asset_numeric_id, CASE WHEN NEW.name IS NOT NULL THEN CONCAT(' (', NEW.name, ')') ELSE '' END);
+                SET issue_severity = 'Medium';
+                SET detection_trigger_name = 'STATUS_ATTENTION';
+                
+            WHEN 'Faulty' THEN
+                SET issue_title = CONCAT('Asset Faulty: ', COALESCE(NEW.name, NEW.asset_tag, NEW.asset_numeric_id));
+                SET issue_description = CONCAT('Asset status changed to Faulty. Asset: ', NEW.asset_numeric_id, CASE WHEN NEW.name IS NOT NULL THEN CONCAT(' (', NEW.name, ')') ELSE '' END);
+                SET issue_severity = 'High';
+                SET detection_trigger_name = 'STATUS_FAULTY';
+                
+            WHEN 'Missing' THEN
+                SET issue_title = CONCAT('Asset Missing: ', COALESCE(NEW.name, NEW.asset_tag, NEW.asset_numeric_id));
+                SET issue_description = CONCAT('Asset status changed to Missing. Asset: ', NEW.asset_numeric_id, CASE WHEN NEW.name IS NOT NULL THEN CONCAT(' (', NEW.name, ')') ELSE '' END);
+                SET issue_severity = 'Critical';
+                SET detection_trigger_name = 'STATUS_MISSING';
+                
+            WHEN 'Retired' THEN
+                SET issue_title = CONCAT('Asset Retired: ', COALESCE(NEW.name, NEW.asset_tag, NEW.asset_numeric_id));
+                SET issue_description = CONCAT('Asset status changed to Retired. Asset: ', NEW.asset_numeric_id, CASE WHEN NEW.name IS NOT NULL THEN CONCAT(' (', NEW.name, ')') ELSE '' END);
+                SET issue_severity = 'Low';
+                SET detection_trigger_name = 'STATUS_RETIRED';
+                
+            WHEN 'In Repair' THEN
+                SET issue_title = CONCAT('Asset In Repair: ', COALESCE(NEW.name, NEW.asset_tag, NEW.asset_numeric_id));
+                SET issue_description = CONCAT('Asset status changed to In Repair. Asset: ', NEW.asset_numeric_id, CASE WHEN NEW.name IS NOT NULL THEN CONCAT(' (', NEW.name, ')') ELSE '' END);
+                SET issue_severity = 'Medium';
+                SET detection_trigger_name = 'STATUS_IN_REPAIR';
+                
+            WHEN 'Expired' THEN
+                SET issue_title = CONCAT('Asset Expired: ', COALESCE(NEW.name, NEW.asset_tag, NEW.asset_numeric_id));
+                SET issue_description = CONCAT('Asset status changed to Expired. Asset: ', NEW.asset_numeric_id, CASE WHEN NEW.name IS NOT NULL THEN CONCAT(' (', NEW.name, ')') ELSE '' END);
+                SET issue_severity = 'Medium';
+                SET detection_trigger_name = 'STATUS_EXPIRED';
+        END CASE;
+        
+        -- Insert the auto-detected issue
+        INSERT INTO issue_tracker (
+            issue_type, asset_id, title, description, severity, priority, status,
+            reported_by, auto_detected, detection_trigger, created_date
+        )
+        VALUES (
+            'Asset Issue', NEW.id, issue_title, issue_description, issue_severity, 'Normal', 'Open',
+            COALESCE(@current_user_id, 1), TRUE, detection_trigger_name, NOW()
+        );
+    END IF;
+    
+    -- Auto-resolve issues when status becomes Good again
+    IF OLD.status != NEW.status AND NEW.status = 'Good' AND OLD.status IN ('Faulty', 'Missing', 'In Repair', 'Expired') THEN
+        UPDATE issue_tracker 
+        SET status = 'Resolved',
+            solution = 'Automatically Fixed',
+            solution_plus = CONCAT('Asset status automatically changed from ', OLD.status, ' to Good'),
+            resolved_date = NOW(),
+            resolved_by = COALESCE(@current_user_id, 1)
+        WHERE asset_id = NEW.id 
+        AND status IN ('Open', 'In Progress')
+        AND auto_detected = TRUE
+        AND detection_trigger IN ('STATUS_FAULTY', 'STATUS_MISSING', 'STATUS_IN_REPAIR', 'STATUS_EXPIRED');
+    END IF;
+    
+    -- Auto-resolve overdue/stolen/illegal issues when item is returned (lending_status becomes Available)
+    IF (OLD.lending_status IS NULL OR OLD.lending_status != NEW.lending_status) 
+       AND NEW.lending_status = 'Available' 
+       AND OLD.lending_status IN ('Overdue', 'Illegally Handed Out', 'Stolen') THEN
+        UPDATE issue_tracker 
+        SET status = 'Resolved',
+            solution = 'Items Returned',
+            solution_plus = CONCAT('Asset was returned - lending status changed from ', OLD.lending_status, ' to Available'),
+            resolved_date = NOW(),
+            resolved_by = COALESCE(@current_user_id, 1)
+        WHERE asset_id = NEW.id 
+        AND status IN ('Open', 'In Progress')
+        AND auto_detected = TRUE
+        AND detection_trigger IN ('LENDING_OVERDUE', 'LENDING_ILLEGAL', 'LENDING_STOLEN');
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER prevent_delete_borrowed_assets
+BEFORE DELETE ON assets
+FOR EACH ROW
+BEGIN
+    IF OLD.lending_status IN ('Borrowed', 'Deployed', 'Overdue') THEN
+        SIGNAL SQLSTATE '45000'
+        SET MESSAGE_TEXT = 'Cannot delete asset that is currently borrowed or deployed, maybe update to retired or unmanaged before';
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER assets_after_delete_log
+AFTER DELETE ON assets
+FOR EACH ROW
+BEGIN
+    DECLARE username VARCHAR(100);
+    DECLARE deleted_fields_array JSON;
+    DECLARE old_vals JSON;
+    
+    IF @current_user_id IS NOT NULL THEN
+        SELECT users.username INTO username FROM users WHERE id = @current_user_id;
+    END IF;
+    
+    -- Build JSON objects only with non-NULL fields (for restore capability)
+    SET deleted_fields_array = JSON_ARRAY();
+    SET old_vals = JSON_OBJECT();
+    
+    IF OLD.asset_tag IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'asset_tag');
+        SET old_vals = JSON_SET(old_vals, '$.asset_tag', OLD.asset_tag);
+    END IF;
+    
+    IF OLD.asset_numeric_id IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'asset_numeric_id');
+        SET old_vals = JSON_SET(old_vals, '$.asset_numeric_id', OLD.asset_numeric_id);
+    END IF;
+    
+    IF OLD.asset_type IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'asset_type');
+        SET old_vals = JSON_SET(old_vals, '$.asset_type', OLD.asset_type);
+    END IF;
+    
+    IF OLD.name IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'name');
+        SET old_vals = JSON_SET(old_vals, '$.name', OLD.name);
+    END IF;
+    
+    IF OLD.category_id IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'category_id');
+        SET old_vals = JSON_SET(old_vals, '$.category_id', OLD.category_id);
+    END IF;
+    
+    IF OLD.manufacturer IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'manufacturer');
+        SET old_vals = JSON_SET(old_vals, '$.manufacturer', OLD.manufacturer);
+    END IF;
+    
+    IF OLD.model IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'model');
+        SET old_vals = JSON_SET(old_vals, '$.model', OLD.model);
+    END IF;
+    
+    IF OLD.serial_number IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'serial_number');
+        SET old_vals = JSON_SET(old_vals, '$.serial_number', OLD.serial_number);
+    END IF;
+    
+    IF OLD.zone_id IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'zone_id');
+        SET old_vals = JSON_SET(old_vals, '$.zone_id', OLD.zone_id);
+    END IF;
+    
+    IF OLD.zone_plus IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'zone_plus');
+        SET old_vals = JSON_SET(old_vals, '$.zone_plus', OLD.zone_plus);
+    END IF;
+    
+    IF OLD.zone_note IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'zone_note');
+        SET old_vals = JSON_SET(old_vals, '$.zone_note', OLD.zone_note);
+    END IF;
+    
+    IF OLD.status IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'status');
+        SET old_vals = JSON_SET(old_vals, '$.status', OLD.status);
+    END IF;
+    
+    IF OLD.last_audit IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'last_audit');
+        SET old_vals = JSON_SET(old_vals, '$.last_audit', OLD.last_audit);
+    END IF;
+    
+    IF OLD.last_audit_status IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'last_audit_status');
+        SET old_vals = JSON_SET(old_vals, '$.last_audit_status', OLD.last_audit_status);
+    END IF;
+    
+    IF OLD.price IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'price');
+        SET old_vals = JSON_SET(old_vals, '$.price', OLD.price);
+    END IF;
+    
+    IF OLD.purchase_date IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'purchase_date');
+        SET old_vals = JSON_SET(old_vals, '$.purchase_date', OLD.purchase_date);
+    END IF;
+    
+    IF OLD.warranty_until IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'warranty_until');
+        SET old_vals = JSON_SET(old_vals, '$.warranty_until', OLD.warranty_until);
+    END IF;
+    
+    IF OLD.expiry_date IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'expiry_date');
+        SET old_vals = JSON_SET(old_vals, '$.expiry_date', OLD.expiry_date);
+    END IF;
+    
+    IF OLD.quantity_available IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'quantity_available');
+        SET old_vals = JSON_SET(old_vals, '$.quantity_available', OLD.quantity_available);
+    END IF;
+    
+    IF OLD.quantity_total IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'quantity_total');
+        SET old_vals = JSON_SET(old_vals, '$.quantity_total', OLD.quantity_total);
+    END IF;
+    
+    IF OLD.quantity_used IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'quantity_used');
+        SET old_vals = JSON_SET(old_vals, '$.quantity_used', OLD.quantity_used);
+    END IF;
+    
+    IF OLD.supplier_id IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'supplier_id');
+        SET old_vals = JSON_SET(old_vals, '$.supplier_id', OLD.supplier_id);
+    END IF;
+    
+    IF OLD.lendable IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'lendable');
+        SET old_vals = JSON_SET(old_vals, '$.lendable', OLD.lendable);
+    END IF;
+    
+    IF OLD.minimum_role_for_lending IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'minimum_role_for_lending');
+        SET old_vals = JSON_SET(old_vals, '$.minimum_role_for_lending', OLD.minimum_role_for_lending);
+    END IF;
+    
+    IF OLD.lending_status IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'lending_status');
+        SET old_vals = JSON_SET(old_vals, '$.lending_status', OLD.lending_status);
+    END IF;
+    
+    IF OLD.current_borrower_id IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'current_borrower_id');
+        SET old_vals = JSON_SET(old_vals, '$.current_borrower_id', OLD.current_borrower_id);
+    END IF;
+    
+    IF OLD.due_date IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'due_date');
+        SET old_vals = JSON_SET(old_vals, '$.due_date', OLD.due_date);
+    END IF;
+    
+    IF OLD.previous_borrower_id IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'previous_borrower_id');
+        SET old_vals = JSON_SET(old_vals, '$.previous_borrower_id', OLD.previous_borrower_id);
+    END IF;
+    
+    IF OLD.audit_task_id IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'audit_task_id');
+        SET old_vals = JSON_SET(old_vals, '$.audit_task_id', OLD.audit_task_id);
+    END IF;
+    
+    IF OLD.no_scan IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'no_scan');
+        SET old_vals = JSON_SET(old_vals, '$.no_scan', OLD.no_scan);
+    END IF;
+    
+    IF OLD.notes IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'notes');
+        SET old_vals = JSON_SET(old_vals, '$.notes', OLD.notes);
+    END IF;
+    
+    IF OLD.additional_fields IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'additional_fields');
+        SET old_vals = JSON_SET(old_vals, '$.additional_fields', OLD.additional_fields);
+    END IF;
+    
+    -- Always capture metadata fields for restore
+    IF OLD.created_date IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'created_date');
+        SET old_vals = JSON_SET(old_vals, '$.created_date', OLD.created_date);
+    END IF;
+    
+    IF OLD.created_by IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'created_by');
+        SET old_vals = JSON_SET(old_vals, '$.created_by', OLD.created_by);
+    END IF;
+    
+    IF OLD.last_modified_date IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'last_modified_date');
+        SET old_vals = JSON_SET(old_vals, '$.last_modified_date', OLD.last_modified_date);
+    END IF;
+    
+    IF OLD.last_modified_by IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'last_modified_by');
+        SET old_vals = JSON_SET(old_vals, '$.last_modified_by', OLD.last_modified_by);
+    END IF;
+    
+    -- Log the DELETE with only non-NULL fields
+    INSERT INTO asset_change_log (
+        table_name, action, record_id, changed_fields, old_values,
+        changed_by_id, changed_by_username
+    )
+    VALUES (
+        'assets',
+        'DELETE',
+        OLD.id,
+        deleted_fields_array,
+        old_vals,
+        @current_user_id,
+        username
+    );
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `audit_tasks` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `task_name` varchar(200) NOT NULL,
+  `json_sequence` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL CHECK (json_valid(`json_sequence`)),
+  `created_at` timestamp NULL DEFAULT current_timestamp(),
+  `updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `borrowers` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `name` varchar(200) NOT NULL,
+  `email` varchar(255) DEFAULT NULL,
+  `phone_number` varchar(50) DEFAULT NULL,
+  `class_name` varchar(100) DEFAULT NULL,
+  `role` varchar(100) DEFAULT NULL,
+  `notes` text DEFAULT NULL,
+  `added_by` int(11) NOT NULL,
+  `added_date` timestamp NULL DEFAULT current_timestamp(),
+  `banned` tinyint(1) DEFAULT 0,
+  `unban_fine` decimal(10,2) DEFAULT 0.00,
+  `last_unban_by` int(11) DEFAULT NULL,
+  `last_unban_date` date DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `added_by` (`added_by`),
+  KEY `last_unban_by` (`last_unban_by`),
+  KEY `idx_name` (`name`),
+  KEY `idx_banned` (`banned`),
+  CONSTRAINT `borrowers_ibfk_1` FOREIGN KEY (`added_by`) REFERENCES `users` (`id`),
+  CONSTRAINT `borrowers_ibfk_2` FOREIGN KEY (`last_unban_by`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER borrowers_before_insert_meta
+BEFORE INSERT ON borrowers
+FOR EACH ROW
+BEGIN
+    IF NEW.added_by IS NULL AND @current_user_id IS NOT NULL THEN
+        SET NEW.added_by = @current_user_id;
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER borrowers_before_update_meta
+BEFORE UPDATE ON borrowers
+FOR EACH ROW
+BEGIN
+    IF OLD.banned = TRUE AND NEW.banned = FALSE THEN
+        IF NEW.last_unban_by IS NULL AND @current_user_id IS NOT NULL THEN
+            SET NEW.last_unban_by = @current_user_id;
+        END IF;
+        IF NEW.last_unban_date IS NULL THEN
+            SET NEW.last_unban_date = CURDATE();
+        END IF;
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER auto_detect_borrower_issues
+AFTER UPDATE ON borrowers
+FOR EACH ROW
+BEGIN
+    DECLARE issue_title VARCHAR(255);
+    DECLARE issue_description TEXT;
+    
+    -- Auto-detect when borrower gets banned
+    IF OLD.banned = FALSE AND NEW.banned = TRUE THEN
+        SET issue_title = CONCAT('Borrower Banned: ', NEW.name);
+        SET issue_description = CONCAT('Borrower has been banned. Name: ', NEW.name, CASE WHEN NEW.unban_fine > 0 THEN CONCAT(', Unban Fine: $', NEW.unban_fine) ELSE '' END);
+        
+        INSERT INTO issue_tracker (
+            issue_type, borrower_id, title, description, severity, priority, status,
+            reported_by, auto_detected, detection_trigger, created_date
+        )
+        VALUES (
+            'Borrower Issue', NEW.id, issue_title, issue_description, 'High', 'Normal', 'Open',
+            COALESCE(@current_user_id, 1), TRUE, 'BORROWER_BANNED', NOW()
+        );
+    END IF;
+    
+    -- Auto-resolve when borrower gets unbanned
+    IF OLD.banned = TRUE AND NEW.banned = FALSE THEN
+        UPDATE issue_tracker 
+        SET status = 'Resolved',
+            solution = 'Items Returned',
+            solution_plus = CONCAT('Borrower unbanned on ', COALESCE(NEW.last_unban_date, CURDATE()), CASE WHEN NEW.last_unban_by IS NOT NULL THEN CONCAT(' by user ID ', NEW.last_unban_by) ELSE '' END),
+            resolved_date = NOW(),
+            resolved_by = COALESCE(@current_user_id, NEW.last_unban_by, 1)
+        WHERE borrower_id = NEW.id 
+        AND status IN ('Open', 'In Progress')
+        AND auto_detected = TRUE
+        AND detection_trigger = 'BORROWER_BANNED';
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `categories` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `category_name` varchar(200) NOT NULL,
+  `category_description` text DEFAULT NULL,
+  `parent_id` int(11) DEFAULT NULL,
+  `category_code` varchar(50) DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `idx_parent` (`parent_id`),
+  KEY `idx_code` (`category_code`),
+  CONSTRAINT `categories_ibfk_1` FOREIGN KEY (`parent_id`) REFERENCES `categories` (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `issue_tracker` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `issue_type` enum('Asset Issue','Borrower Issue','System Issue','Maintenance','Other') NOT NULL,
+  `asset_id` int(11) DEFAULT NULL,
+  `borrower_id` int(11) DEFAULT NULL,
+  `title` varchar(255) NOT NULL,
+  `description` text NOT NULL,
+  `severity` enum('Critical','High','Medium','Low') DEFAULT NULL,
+  `priority` enum('Urgent','High','Normal','Low') DEFAULT 'Normal',
+  `status` enum('Open','In Progress','Resolved','Closed','On Hold') DEFAULT 'Open',
+  `solution` enum('Fixed','Replaced','Clarify','No Action Needed','Deferred','Items Returned','Automatically Fixed') DEFAULT NULL,
+  `solution_plus` text DEFAULT NULL,
+  `replacement_asset_id` int(11) DEFAULT NULL,
+  `reported_by` int(11) NOT NULL,
+  `assigned_to` int(11) DEFAULT NULL,
+  `resolved_by` int(11) DEFAULT NULL,
+  `cost` decimal(10,2) DEFAULT NULL,
+  `created_date` datetime NOT NULL DEFAULT current_timestamp(),
+  `updated_date` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
+  `resolved_date` datetime DEFAULT NULL,
+  `notes` text DEFAULT NULL,
+  `auto_detected` tinyint(1) DEFAULT 0,
+  `detection_trigger` varchar(100) DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `replacement_asset_id` (`replacement_asset_id`),
+  KEY `reported_by` (`reported_by`),
+  KEY `assigned_to` (`assigned_to`),
+  KEY `resolved_by` (`resolved_by`),
+  KEY `idx_issue_type` (`issue_type`),
+  KEY `idx_asset` (`asset_id`),
+  KEY `idx_borrower` (`borrower_id`),
+  KEY `idx_severity` (`severity`),
+  KEY `idx_status` (`status`),
+  KEY `idx_created_date` (`created_date`),
+  KEY `idx_auto_detected` (`auto_detected`),
+  CONSTRAINT `issue_tracker_ibfk_1` FOREIGN KEY (`asset_id`) REFERENCES `assets` (`id`) ON DELETE CASCADE,
+  CONSTRAINT `issue_tracker_ibfk_2` FOREIGN KEY (`borrower_id`) REFERENCES `borrowers` (`id`) ON DELETE CASCADE,
+  CONSTRAINT `issue_tracker_ibfk_3` FOREIGN KEY (`replacement_asset_id`) REFERENCES `assets` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `issue_tracker_ibfk_4` FOREIGN KEY (`reported_by`) REFERENCES `users` (`id`),
+  CONSTRAINT `issue_tracker_ibfk_5` FOREIGN KEY (`assigned_to`) REFERENCES `users` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `issue_tracker_ibfk_6` FOREIGN KEY (`resolved_by`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER issue_tracker_before_insert_meta
+BEFORE INSERT ON issue_tracker
+FOR EACH ROW
+BEGIN
+    IF NEW.reported_by IS NULL AND @current_user_id IS NOT NULL THEN
+        SET NEW.reported_by = @current_user_id;
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER validate_issue_tracker_insert
+BEFORE INSERT ON issue_tracker
+FOR EACH ROW
+BEGIN
+    -- Clarify solution requires solution_plus
+    IF NEW.solution = 'Clarify' AND (NEW.solution_plus IS NULL OR NEW.solution_plus = '') THEN
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'solution_plus is required when solution is set to Clarify';
+    END IF;
+    
+    -- Replacement solution requires replacement_asset_id
+    IF NEW.solution = 'Replaced' AND NEW.replacement_asset_id IS NULL THEN
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'replacement_asset_id is required when solution is set to Replaced';
+    END IF;
+    
+    -- Asset Issue requires asset_id
+    IF NEW.issue_type = 'Asset Issue' AND NEW.asset_id IS NULL THEN
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'asset_id is required for Asset Issue type';
+    END IF;
+    
+    -- Borrower Issue requires borrower_id
+    IF NEW.issue_type = 'Borrower Issue' AND NEW.borrower_id IS NULL THEN
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'borrower_id is required for Borrower Issue type';
+    END IF;
+    
+    -- Auto-set resolved_date when status becomes Resolved or Closed
+    IF NEW.status IN ('Resolved', 'Closed') AND NEW.resolved_date IS NULL THEN
+        SET NEW.resolved_date = NOW();
+    END IF;
+    
+    -- Auto-set resolved_by when status becomes Resolved or Closed
+    IF NEW.status IN ('Resolved', 'Closed') AND NEW.resolved_by IS NULL AND @current_user_id IS NOT NULL THEN
+        SET NEW.resolved_by = @current_user_id;
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER issue_tracker_after_insert_log
+AFTER INSERT ON issue_tracker
+FOR EACH ROW
+BEGIN
+    DECLARE set_fields JSON DEFAULT JSON_ARRAY();
+    DECLARE new_vals JSON DEFAULT JSON_OBJECT();
+    
+    -- Build JSON of non-NULL inserted fields
+    IF NEW.issue_type IS NOT NULL THEN
+        SET set_fields = JSON_ARRAY_APPEND(set_fields, '$', 'issue_type');
+        SET new_vals = JSON_SET(new_vals, '$.issue_type', NEW.issue_type);
+    END IF;
+    IF NEW.asset_id IS NOT NULL THEN
+        SET set_fields = JSON_ARRAY_APPEND(set_fields, '$', 'asset_id');
+        SET new_vals = JSON_SET(new_vals, '$.asset_id', NEW.asset_id);
+    END IF;
+    IF NEW.borrower_id IS NOT NULL THEN
+        SET set_fields = JSON_ARRAY_APPEND(set_fields, '$', 'borrower_id');
+        SET new_vals = JSON_SET(new_vals, '$.borrower_id', NEW.borrower_id);
+    END IF;
+    IF NEW.title IS NOT NULL THEN
+        SET set_fields = JSON_ARRAY_APPEND(set_fields, '$', 'title');
+        SET new_vals = JSON_SET(new_vals, '$.title', NEW.title);
+    END IF;
+    IF NEW.severity IS NOT NULL THEN
+        SET set_fields = JSON_ARRAY_APPEND(set_fields, '$', 'severity');
+        SET new_vals = JSON_SET(new_vals, '$.severity', NEW.severity);
+    END IF;
+    IF NEW.status IS NOT NULL THEN
+        SET set_fields = JSON_ARRAY_APPEND(set_fields, '$', 'status');
+        SET new_vals = JSON_SET(new_vals, '$.status', NEW.status);
+    END IF;
+    
+    INSERT INTO issue_tracker_change_log (issue_id, change_type, changed_fields, new_values, changed_by)
+    VALUES (NEW.id, 'INSERT', set_fields, new_vals, COALESCE(@current_user_id, NEW.reported_by));
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER validate_issue_tracker_update
+BEFORE UPDATE ON issue_tracker
+FOR EACH ROW
+BEGIN
+    -- Clarify solution requires solution_plus
+    IF NEW.solution = 'Clarify' AND (NEW.solution_plus IS NULL OR NEW.solution_plus = '') THEN
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'solution_plus is required when solution is set to Clarify';
+    END IF;
+    
+    -- Replacement solution requires replacement_asset_id
+    IF NEW.solution = 'Replaced' AND NEW.replacement_asset_id IS NULL THEN
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'replacement_asset_id is required when solution is set to Replaced';
+    END IF;
+    
+    -- Auto-set resolved_date when status changes to Resolved or Closed
+    IF OLD.status NOT IN ('Resolved', 'Closed') AND NEW.status IN ('Resolved', 'Closed') THEN
+        SET NEW.resolved_date = NOW();
+        IF @current_user_id IS NOT NULL THEN
+            SET NEW.resolved_by = @current_user_id;
+        END IF;
+    END IF;
+    
+    -- Clear resolved_date when status changes away from Resolved/Closed
+    IF OLD.status IN ('Resolved', 'Closed') AND NEW.status NOT IN ('Resolved', 'Closed') THEN
+        SET NEW.resolved_date = NULL;
+        SET NEW.resolved_by = NULL;
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER issue_tracker_after_update_log
+AFTER UPDATE ON issue_tracker
+FOR EACH ROW
+BEGIN
+    DECLARE changed_fields JSON DEFAULT JSON_ARRAY();
+    DECLARE old_vals JSON DEFAULT JSON_OBJECT();
+    DECLARE new_vals JSON DEFAULT JSON_OBJECT();
+    
+    -- Track all changed fields
+    IF OLD.status <=> NEW.status IS FALSE THEN
+        SET changed_fields = JSON_ARRAY_APPEND(changed_fields, '$', 'status');
+        SET old_vals = JSON_SET(old_vals, '$.status', OLD.status);
+        SET new_vals = JSON_SET(new_vals, '$.status', NEW.status);
+    END IF;
+    IF OLD.severity <=> NEW.severity IS FALSE THEN
+        SET changed_fields = JSON_ARRAY_APPEND(changed_fields, '$', 'severity');
+        SET old_vals = JSON_SET(old_vals, '$.severity', OLD.severity);
+        SET new_vals = JSON_SET(new_vals, '$.severity', NEW.severity);
+    END IF;
+    IF OLD.priority <=> NEW.priority IS FALSE THEN
+        SET changed_fields = JSON_ARRAY_APPEND(changed_fields, '$', 'priority');
+        SET old_vals = JSON_SET(old_vals, '$.priority', OLD.priority);
+        SET new_vals = JSON_SET(new_vals, '$.priority', NEW.priority);
+    END IF;
+    IF OLD.solution <=> NEW.solution IS FALSE THEN
+        SET changed_fields = JSON_ARRAY_APPEND(changed_fields, '$', 'solution');
+        SET old_vals = JSON_SET(old_vals, '$.solution', OLD.solution);
+        SET new_vals = JSON_SET(new_vals, '$.solution', NEW.solution);
+    END IF;
+    IF OLD.assigned_to <=> NEW.assigned_to IS FALSE THEN
+        SET changed_fields = JSON_ARRAY_APPEND(changed_fields, '$', 'assigned_to');
+        SET old_vals = JSON_SET(old_vals, '$.assigned_to', OLD.assigned_to);
+        SET new_vals = JSON_SET(new_vals, '$.assigned_to', NEW.assigned_to);
+    END IF;
+    IF OLD.resolved_by <=> NEW.resolved_by IS FALSE THEN
+        SET changed_fields = JSON_ARRAY_APPEND(changed_fields, '$', 'resolved_by');
+        SET old_vals = JSON_SET(old_vals, '$.resolved_by', OLD.resolved_by);
+        SET new_vals = JSON_SET(new_vals, '$.resolved_by', NEW.resolved_by);
+    END IF;
+    
+    -- Only log if something actually changed
+    IF JSON_LENGTH(changed_fields) > 0 THEN
+        INSERT INTO issue_tracker_change_log (issue_id, change_type, changed_fields, old_values, new_values, changed_by)
+        VALUES (NEW.id, 'UPDATE', changed_fields, old_vals, new_vals, COALESCE(@current_user_id, OLD.reported_by));
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER issue_tracker_before_delete
+BEFORE DELETE ON issue_tracker
+FOR EACH ROW
+BEGIN
+    -- If issue is not already resolved/closed, update it before deletion
+    IF OLD.status NOT IN ('Resolved', 'Closed') THEN
+        -- Can't UPDATE in a BEFORE DELETE trigger, so we just ensure it was marked resolved
+        -- This will prevent accidental deletion of open issues
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Cannot delete open issues. Please close or resolve the issue first.';
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER issue_tracker_after_delete_log
+AFTER DELETE ON issue_tracker
+FOR EACH ROW
+BEGIN
+    DECLARE deleted_fields JSON DEFAULT JSON_ARRAY();
+    DECLARE old_vals JSON DEFAULT JSON_OBJECT();
+    
+    -- Log all fields from deleted issue
+    IF OLD.issue_type IS NOT NULL THEN
+        SET deleted_fields = JSON_ARRAY_APPEND(deleted_fields, '$', 'issue_type');
+        SET old_vals = JSON_SET(old_vals, '$.issue_type', OLD.issue_type);
+    END IF;
+    IF OLD.asset_id IS NOT NULL THEN
+        SET deleted_fields = JSON_ARRAY_APPEND(deleted_fields, '$', 'asset_id');
+        SET old_vals = JSON_SET(old_vals, '$.asset_id', OLD.asset_id);
+    END IF;
+    IF OLD.title IS NOT NULL THEN
+        SET deleted_fields = JSON_ARRAY_APPEND(deleted_fields, '$', 'title');
+        SET old_vals = JSON_SET(old_vals, '$.title', OLD.title);
+    END IF;
+    IF OLD.status IS NOT NULL THEN
+        SET deleted_fields = JSON_ARRAY_APPEND(deleted_fields, '$', 'status');
+        SET old_vals = JSON_SET(old_vals, '$.status', OLD.status);
+    END IF;
+    IF OLD.solution IS NOT NULL THEN
+        SET deleted_fields = JSON_ARRAY_APPEND(deleted_fields, '$', 'solution');
+        SET old_vals = JSON_SET(old_vals, '$.solution', OLD.solution);
+    END IF;
+    
+    INSERT INTO issue_tracker_change_log (issue_id, change_type, changed_fields, old_values, changed_by)
+    VALUES (OLD.id, 'DELETE', deleted_fields, old_vals, COALESCE(@current_user_id, OLD.reported_by));
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `issue_tracker_change_log` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `issue_id` int(11) NOT NULL,
+  `change_type` enum('INSERT','UPDATE','DELETE') NOT NULL,
+  `changed_fields` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`changed_fields`)),
+  `old_values` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`old_values`)),
+  `new_values` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`new_values`)),
+  `changed_by` int(11) DEFAULT NULL,
+  `change_date` timestamp NULL DEFAULT current_timestamp(),
+  PRIMARY KEY (`id`),
+  KEY `changed_by` (`changed_by`),
+  KEY `idx_issue` (`issue_id`),
+  KEY `idx_change_type` (`change_type`),
+  KEY `idx_change_date` (`change_date`),
+  CONSTRAINT `issue_tracker_change_log_ibfk_1` FOREIGN KEY (`issue_id`) REFERENCES `issue_tracker` (`id`) ON DELETE CASCADE,
+  CONSTRAINT `issue_tracker_change_log_ibfk_2` FOREIGN KEY (`changed_by`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `label_templates` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `template_code` varchar(100) NOT NULL COMMENT 'Unique code like "CABLE"',
+  `template_name` varchar(200) NOT NULL COMMENT 'Human readable name',
+  `layout_json` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'Universal label design: SVG graphics, auto-populated field placeholders, styling' CHECK (json_valid(`layout_json`)),
+  `created_at` timestamp NULL DEFAULT current_timestamp(),
+  `created_by` int(11) DEFAULT NULL,
+  `last_modified_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
+  `last_modified_by` int(11) DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `template_code` (`template_code`),
+  KEY `created_by` (`created_by`),
+  KEY `last_modified_by` (`last_modified_by`),
+  KEY `idx_template_code` (`template_code`),
+  KEY `idx_template_name` (`template_name`),
+  CONSTRAINT `label_templates_ibfk_1` FOREIGN KEY (`created_by`) REFERENCES `users` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `label_templates_ibfk_2` FOREIGN KEY (`last_modified_by`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `lending_history` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `asset_id` int(11) NOT NULL,
+  `borrower_id` int(11) NOT NULL,
+  `checkout_date` datetime NOT NULL DEFAULT current_timestamp(),
+  `due_date` date DEFAULT NULL,
+  `return_date` datetime DEFAULT NULL,
+  `checked_out_by` int(11) DEFAULT NULL,
+  `checked_in_by` int(11) DEFAULT NULL,
+  `notes` text DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `checked_out_by` (`checked_out_by`),
+  KEY `checked_in_by` (`checked_in_by`),
+  KEY `idx_asset` (`asset_id`),
+  KEY `idx_borrower` (`borrower_id`),
+  KEY `idx_checkout_date` (`checkout_date`),
+  KEY `idx_return_date` (`return_date`),
+  CONSTRAINT `lending_history_ibfk_1` FOREIGN KEY (`asset_id`) REFERENCES `assets` (`id`) ON DELETE CASCADE,
+  CONSTRAINT `lending_history_ibfk_2` FOREIGN KEY (`borrower_id`) REFERENCES `borrowers` (`id`),
+  CONSTRAINT `lending_history_ibfk_3` FOREIGN KEY (`checked_out_by`) REFERENCES `users` (`id`),
+  CONSTRAINT `lending_history_ibfk_4` FOREIGN KEY (`checked_in_by`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER lending_history_before_insert_meta
+BEFORE INSERT ON lending_history
+FOR EACH ROW
+BEGIN
+    IF NEW.checked_out_by IS NULL AND @current_user_id IS NOT NULL THEN
+        SET NEW.checked_out_by = @current_user_id;
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER lending_history_before_update_meta
+BEFORE UPDATE ON lending_history
+FOR EACH ROW
+BEGIN
+    IF OLD.return_date IS NULL AND NEW.return_date IS NOT NULL THEN
+        IF NEW.checked_in_by IS NULL AND @current_user_id IS NOT NULL THEN
+            SET NEW.checked_in_by = @current_user_id;
+        END IF;
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `physical_audit_logs` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `physical_audit_id` int(11) NOT NULL COMMENT 'Reference to the audit session',
+  `asset_id` int(11) NOT NULL,
+  `audit_date` datetime NOT NULL DEFAULT current_timestamp(),
+  `audited_by` int(11) NOT NULL,
+  `status_found` enum('Good','Attention','Faulty','Missing','Retired','In Repair','In Transit','Expired','Unmanaged') DEFAULT 'Good',
+  `audit_task_id` int(11) DEFAULT NULL COMMENT 'Which audit task was run on this asset',
+  `audit_task_responses` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'User responses to the JSON sequence questions' CHECK (json_valid(`audit_task_responses`)),
+  `exception_type` enum('wrong-zone','unexpected-asset','damaged','missing-label','other') DEFAULT NULL,
+  `exception_details` text DEFAULT NULL COMMENT 'Details about the exception found',
+  `found_in_zone_id` int(11) DEFAULT NULL COMMENT 'Which zone the asset was actually found in (if different from expected)',
+  `auditor_action` enum('physical-move','virtual-update','no-action') DEFAULT NULL COMMENT 'What the auditor chose to do about wrong-zone assets',
+  `notes` text DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `audit_task_id` (`audit_task_id`),
+  KEY `found_in_zone_id` (`found_in_zone_id`),
+  KEY `idx_physical_audit` (`physical_audit_id`),
+  KEY `idx_asset` (`asset_id`),
+  KEY `idx_audit_date` (`audit_date`),
+  KEY `idx_audited_by` (`audited_by`),
+  KEY `idx_status_found` (`status_found`),
+  KEY `idx_exception_type` (`exception_type`),
+  CONSTRAINT `physical_audit_logs_ibfk_1` FOREIGN KEY (`physical_audit_id`) REFERENCES `physical_audits` (`id`) ON DELETE CASCADE,
+  CONSTRAINT `physical_audit_logs_ibfk_2` FOREIGN KEY (`asset_id`) REFERENCES `assets` (`id`) ON DELETE CASCADE,
+  CONSTRAINT `physical_audit_logs_ibfk_3` FOREIGN KEY (`audited_by`) REFERENCES `users` (`id`),
+  CONSTRAINT `physical_audit_logs_ibfk_4` FOREIGN KEY (`audit_task_id`) REFERENCES `audit_tasks` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `physical_audit_logs_ibfk_5` FOREIGN KEY (`found_in_zone_id`) REFERENCES `zones` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER physical_audit_logs_before_insert_meta
+BEFORE INSERT ON physical_audit_logs
+FOR EACH ROW
+BEGIN
+    -- Auto-populate audited_by from session variable if not provided
+    IF NEW.audited_by IS NULL OR NEW.audited_by = 0 THEN
+        SET NEW.audited_by = COALESCE(@current_user_id, 1);
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER update_assets_found
+AFTER INSERT ON physical_audit_logs
+FOR EACH ROW
+BEGIN
+    UPDATE physical_audits 
+    SET assets_found = assets_found + 1
+    WHERE id = NEW.physical_audit_id;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER update_asset_from_audit
+AFTER INSERT ON physical_audit_logs
+FOR EACH ROW
+BEGIN
+    DECLARE current_status VARCHAR(100);
+    
+    -- Update asset's last_audit date
+    UPDATE assets 
+    SET last_audit = DATE(NEW.audit_date),
+        last_audit_status = NEW.status_found
+    WHERE id = NEW.asset_id;
+    
+    -- Compare found status with current asset status
+    SELECT status INTO current_status FROM assets WHERE id = NEW.asset_id LIMIT 1;
+    
+    IF NEW.status_found != current_status THEN
+        UPDATE assets 
+        SET status = NEW.status_found
+        WHERE id = NEW.asset_id;
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `physical_audits` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `audit_type` enum('full-zone','spot-check') NOT NULL,
+  `zone_id` int(11) DEFAULT NULL COMMENT 'Zone being audited (NULL for spot-check audits)',
+  `audit_name` varchar(255) DEFAULT NULL COMMENT 'Custom name for the audit session',
+  `started_by` int(11) NOT NULL,
+  `started_at` datetime NOT NULL DEFAULT current_timestamp(),
+  `completed_at` datetime DEFAULT NULL,
+  `status` enum('in-progress','all-good','timeout','attention','cancelled') DEFAULT 'in-progress',
+  `timeout_minutes` int(11) DEFAULT NULL COMMENT 'Timeout setting used for this audit',
+  `issues_found` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'Array of issues: missing_assets, moved_assets, damaged_assets, etc.' CHECK (json_valid(`issues_found`)),
+  `assets_expected` int(11) DEFAULT NULL COMMENT 'Total assets expected to be found in zone',
+  `assets_found` int(11) DEFAULT 0 COMMENT 'Total assets actually found and scanned',
+  `notes` text DEFAULT NULL,
+  `cancelled_reason` text DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `idx_audit_type` (`audit_type`),
+  KEY `idx_zone` (`zone_id`),
+  KEY `idx_status` (`status`),
+  KEY `idx_started_at` (`started_at`),
+  KEY `idx_started_by` (`started_by`),
+  CONSTRAINT `physical_audits_ibfk_1` FOREIGN KEY (`zone_id`) REFERENCES `zones` (`id`),
+  CONSTRAINT `physical_audits_ibfk_2` FOREIGN KEY (`started_by`) REFERENCES `users` (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER calculate_assets_expected
+BEFORE INSERT ON physical_audits
+FOR EACH ROW
+BEGIN
+    DECLARE expected_count INT DEFAULT 0;
+    DECLARE v_timeout INT;
+    
+    -- For full-zone audits, calculate expected assets in the zone
+    IF NEW.audit_type = 'full-zone' AND NEW.zone_id IS NOT NULL THEN
+        SELECT COUNT(*) INTO expected_count
+        FROM assets 
+        WHERE zone_id = NEW.zone_id 
+        AND status NOT IN ('Missing', 'Retired');
+        
+        SET NEW.assets_expected = expected_count;
+    END IF;
+    
+    -- Set timeout from zone settings if not specified
+    IF NEW.timeout_minutes IS NULL AND NEW.zone_id IS NOT NULL THEN
+        SELECT audit_timeout_minutes INTO v_timeout
+        FROM zones 
+        WHERE id = NEW.zone_id
+        LIMIT 1;
+        
+        IF v_timeout IS NOT NULL THEN
+            SET NEW.timeout_minutes = v_timeout;
+        END IF;
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER auto_detect_audit_issues
+AFTER UPDATE ON physical_audits
+FOR EACH ROW
+BEGIN
+    DECLARE missing_count INT DEFAULT 0;
+    DECLARE zone_name VARCHAR(200);
+    
+    -- Only process when audit status changes to completed states
+    IF OLD.status = 'in-progress' AND NEW.status IN ('all-good', 'attention', 'timeout') THEN
+        
+        -- Get zone name for reporting
+        IF NEW.zone_id IS NOT NULL THEN
+            SELECT zone_name INTO zone_name FROM zones WHERE id = NEW.zone_id;
+        END IF;
+        
+        -- For full-zone audits, check for missing assets
+        IF NEW.audit_type = 'full-zone' AND NEW.assets_expected IS NOT NULL THEN
+            SET missing_count = GREATEST(0, NEW.assets_expected - NEW.assets_found);
+        END IF;
+        
+        -- Create issue for missing assets
+        IF missing_count > 0 THEN
+            INSERT INTO issue_tracker (
+                issue_type, title, description, severity, priority, status,
+                reported_by, auto_detected, detection_trigger, created_date, notes
+            )
+            VALUES (
+                'System Issue', 
+                CONCAT('Audit: Missing Assets in ', COALESCE(zone_name, 'Unknown Zone')),
+                CONCAT('Full zone audit completed with ', missing_count, ' missing assets. Expected: ', NEW.assets_expected, ', Found: ', NEW.assets_found, '. Audit ID: ', NEW.id),
+                CASE WHEN missing_count >= 5 THEN 'Critical' WHEN missing_count >= 2 THEN 'High' ELSE 'Medium' END,
+                'High', 'Open',
+                NEW.started_by, TRUE, 'AUDIT_MISSING_ASSETS', NOW(),
+                CONCAT('Physical Audit ID: ', NEW.id, ' in zone: ', COALESCE(zone_name, NEW.zone_id))
+            );
+        END IF;
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `print_history` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `entity_type` enum('Asset','Template','Borrower','Zone','Report','Custom') NOT NULL,
+  `entity_id` int(11) DEFAULT NULL COMMENT 'ID of the asset/template/borrower/zone (NULL for reports)',
+  `label_template_id` int(11) DEFAULT NULL,
+  `printer_id` int(11) DEFAULT NULL,
+  `quantity` int(11) DEFAULT 1,
+  `print_status` enum('Success','Failed','Cancelled','Queued') NOT NULL,
+  `error_message` text DEFAULT NULL,
+  `rendered_data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'The actual data that was sent to printer (for debugging)' CHECK (json_valid(`rendered_data`)),
+  `printed_at` timestamp NULL DEFAULT current_timestamp(),
+  `printed_by` int(11) DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `label_template_id` (`label_template_id`),
+  KEY `idx_entity` (`entity_type`,`entity_id`),
+  KEY `idx_printed_at` (`printed_at`),
+  KEY `idx_printed_by` (`printed_by`),
+  KEY `idx_printer` (`printer_id`),
+  KEY `idx_status` (`print_status`),
+  CONSTRAINT `print_history_ibfk_1` FOREIGN KEY (`label_template_id`) REFERENCES `label_templates` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `print_history_ibfk_2` FOREIGN KEY (`printer_id`) REFERENCES `printer_settings` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `print_history_ibfk_3` FOREIGN KEY (`printed_by`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `printer_settings` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `printer_name` varchar(200) NOT NULL,
+  `description` text DEFAULT NULL,
+  `log` tinyint(1) DEFAULT 1 COMMENT 'Log all print jobs to this printer',
+  `can_be_used_for_reports` tinyint(1) DEFAULT 0 COMMENT 'Can this printer be used for printing reports',
+  `min_powerlevel_to_use` int(11) NOT NULL DEFAULT 75 COMMENT 'Minimum role power level required to use this printer',
+  `printer_plugin` enum('Ptouch','Brother','Zebra','System','PDF','Network','Custom') NOT NULL COMMENT 'Which printer plugin the client should send printer_settings to',
+  `printer_settings` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'Printer-specific settings: connection, paper size, DPI, margins, etc.' CHECK (json_valid(`printer_settings`)),
+  `created_at` timestamp NULL DEFAULT current_timestamp(),
+  `created_by` int(11) DEFAULT NULL,
+  `last_modified_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
+  `last_modified_by` int(11) DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `created_by` (`created_by`),
+  KEY `last_modified_by` (`last_modified_by`),
+  KEY `idx_printer_name` (`printer_name`),
+  KEY `idx_printer_plugin` (`printer_plugin`),
+  KEY `idx_min_powerlevel` (`min_powerlevel_to_use`),
+  KEY `idx_can_reports` (`can_be_used_for_reports`),
+  CONSTRAINT `printer_settings_ibfk_1` FOREIGN KEY (`created_by`) REFERENCES `users` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `printer_settings_ibfk_2` FOREIGN KEY (`last_modified_by`) REFERENCES `users` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `CONSTRAINT_1` CHECK (`min_powerlevel_to_use` >= 1 and `min_powerlevel_to_use` <= 100)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `roles` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `name` varchar(100) NOT NULL,
+  `power` int(11) NOT NULL CHECK (`power` >= 1 and `power` <= 100),
+  `created_at` timestamp NULL DEFAULT current_timestamp(),
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `name` (`name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `suppliers` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `name` varchar(200) NOT NULL,
+  `contact` varchar(200) DEFAULT NULL,
+  `email` varchar(255) DEFAULT NULL,
+  `phone` varchar(50) DEFAULT NULL,
+  `website` varchar(255) DEFAULT NULL,
+  `notes` text DEFAULT NULL,
+  `created_at` timestamp NULL DEFAULT current_timestamp(),
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `templates` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `template_code` varchar(50) DEFAULT NULL,
+  `asset_tag_generation_string` varchar(500) DEFAULT NULL,
+  `description` text DEFAULT NULL,
+  `active` tinyint(1) DEFAULT 1,
+  `asset_type` enum('N','B','L','C') DEFAULT NULL,
+  `name` varchar(255) DEFAULT NULL,
+  `category_id` int(11) DEFAULT NULL,
+  `manufacturer` varchar(200) DEFAULT NULL,
+  `model` varchar(200) DEFAULT NULL,
+  `zone_id` int(11) DEFAULT NULL,
+  `zone_plus` enum('Floating Local','Floating Global','Clarify') DEFAULT NULL,
+  `zone_note` text DEFAULT NULL,
+  `status` enum('Good','Attention','Faulty','Missing','Retired','In Repair','In Transit','Expired','Unmanaged') DEFAULT NULL,
+  `price` decimal(12,2) DEFAULT NULL CHECK (`price` is null or `price` >= 0),
+  `purchase_date` date DEFAULT NULL COMMENT 'Default purchase date for assets created from this template',
+  `purchase_date_now` tinyint(1) DEFAULT 0 COMMENT 'Auto-set purchase date to current date when creating assets',
+  `warranty_until` date DEFAULT NULL,
+  `warranty_auto` tinyint(1) DEFAULT 0 COMMENT 'Auto-calculate warranty_until from purchase_date',
+  `warranty_auto_amount` int(11) DEFAULT NULL COMMENT 'Number of days/years for warranty calculation',
+  `warranty_auto_unit` enum('days','years') DEFAULT 'years' COMMENT 'Unit for warranty auto-calculation',
+  `expiry_date` date DEFAULT NULL,
+  `expiry_auto` tinyint(1) DEFAULT 0 COMMENT 'Auto-calculate expiry_date from purchase_date',
+  `expiry_auto_amount` int(11) DEFAULT NULL COMMENT 'Number of days/years for expiry calculation',
+  `expiry_auto_unit` enum('days','years') DEFAULT 'years' COMMENT 'Unit for expiry auto-calculation',
+  `quantity_total` int(11) DEFAULT NULL,
+  `quantity_used` int(11) DEFAULT NULL,
+  `supplier_id` int(11) DEFAULT NULL,
+  `lendable` tinyint(1) DEFAULT NULL,
+  `lending_status` enum('Available','Borrowed','Overdue','Deployed','Illegally Handed Out','Stolen') DEFAULT 'Available' COMMENT 'Default lending status for assets created from this template',
+  `minimum_role_for_lending` int(11) DEFAULT NULL,
+  `audit_task_id` int(11) DEFAULT NULL,
+  `label_template_id` int(11) DEFAULT NULL,
+  `no_scan` enum('Yes','Ask','No') DEFAULT NULL,
+  `notes` text DEFAULT NULL,
+  `additional_fields` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`additional_fields`)),
+  `created_at` timestamp NULL DEFAULT current_timestamp(),
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `template_code` (`template_code`),
+  KEY `category_id` (`category_id`),
+  KEY `zone_id` (`zone_id`),
+  KEY `supplier_id` (`supplier_id`),
+  KEY `audit_task_id` (`audit_task_id`),
+  KEY `idx_template_code` (`template_code`),
+  KEY `idx_label_template` (`label_template_id`),
+  KEY `idx_asset_tag_generation` (`asset_tag_generation_string`),
+  CONSTRAINT `fk_template_label_template` FOREIGN KEY (`label_template_id`) REFERENCES `label_templates` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `templates_ibfk_1` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `templates_ibfk_2` FOREIGN KEY (`zone_id`) REFERENCES `zones` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `templates_ibfk_3` FOREIGN KEY (`supplier_id`) REFERENCES `suppliers` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `templates_ibfk_4` FOREIGN KEY (`audit_task_id`) REFERENCES `audit_tasks` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `users` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `name` varchar(200) NOT NULL,
+  `username` varchar(100) NOT NULL,
+  `password` varchar(255) NOT NULL,
+  `pin_code` varchar(8) DEFAULT NULL,
+  `login_string` varchar(255) DEFAULT NULL,
+  `role_id` int(11) NOT NULL,
+  `email` varchar(255) DEFAULT NULL,
+  `phone` varchar(50) DEFAULT NULL,
+  `notes` text DEFAULT NULL,
+  `active` tinyint(1) DEFAULT 1,
+  `last_login_date` datetime DEFAULT NULL,
+  `created_date` timestamp NULL DEFAULT current_timestamp(),
+  `password_reset_token` varchar(255) DEFAULT NULL,
+  `password_reset_expiry` datetime DEFAULT NULL,
+  `preferences` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'User personalization settings: common (all clients) + client-specific (web, mobile, desktop)' CHECK (json_valid(`preferences`)),
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `username` (`username`),
+  KEY `role_id` (`role_id`),
+  KEY `idx_username` (`username`),
+  KEY `idx_login_string` (`login_string`),
+  CONSTRAINT `users_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `zones` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `zone_name` varchar(200) NOT NULL,
+  `zone_notes` text DEFAULT NULL,
+  `zone_type` enum('Building','Floor','Room','Storage Area') NOT NULL,
+  `zone_code` varchar(50) DEFAULT NULL,
+  `mini_code` varchar(50) DEFAULT NULL,
+  `parent_id` int(11) DEFAULT NULL,
+  `include_in_parent` tinyint(1) DEFAULT 1,
+  `audit_timeout_minutes` int(11) DEFAULT 60 COMMENT 'Audit timeout in minutes for this zone',
+  PRIMARY KEY (`id`),
+  KEY `idx_parent` (`parent_id`),
+  KEY `idx_type` (`zone_type`),
+  CONSTRAINT `zones_ibfk_1` FOREIGN KEY (`parent_id`) REFERENCES `zones` (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+--
+-- WARNING: can't read the INFORMATION_SCHEMA.libraries table. It's most probably an old server 12.0.2-MariaDB-ubu2404.
+--
+
+/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
+/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
+/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
+/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
+

+ 32 - 0
backend/database/dev/export-clean-schema.sh

@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+# Export clean schema (DDL only, no data) from current dev database
+
+set -euo pipefail
+
+# Add mysql-client to PATH (keg-only on macOS)
+export PATH="/opt/homebrew/opt/mysql-client/bin:$PATH"
+
+DB_HOST="127.0.0.1"
+DB_PORT="3306"
+DB_USER="beepzone_user"
+DB_PASS="beepzone"
+DB_NAME="beepzone"
+
+echo "Exporting clean schema (DDL only, no data)..."
+mysqldump \
+  --host="$DB_HOST" \
+  --port="$DB_PORT" \
+  --user="$DB_USER" \
+  --password="$DB_PASS" \
+  --no-data \
+  --skip-comments \
+  --skip-dump-date \
+  --skip-add-locks \
+  --skip-add-drop-table \
+  --skip-set-charset \
+  --skip-tz-utc \
+  --routines \
+  --triggers \
+  "$DB_NAME" | sed 's/ AUTO_INCREMENT=[0-9]*//g' > beepzone-schema-clean.sql
+
+echo "✓ Exported to: beepzone-schema-clean.sql"

+ 37 - 0
backend/database/dev/export-full-dump.sh

@@ -0,0 +1,37 @@
+#!/usr/bin/env bash
+# Export full database dump (schema + data + triggers) from current dev database
+# This creates a single file that can be imported directly
+
+set -euo pipefail
+
+# Add mysql-client to PATH (keg-only on macOS)
+export PATH="/opt/homebrew/opt/mysql-client/bin:$PATH"
+
+DB_HOST="127.0.0.1"
+DB_PORT="3306"
+DB_USER="beepzone_user"
+DB_PASS="BeepZONE77"
+DB_NAME="beepzone"
+
+echo "Exporting full database dump (schema + data + triggers)..."
+mysqldump \
+  --host="$DB_HOST" \
+  --port="$DB_PORT" \
+  --user="$DB_USER" \
+  --password="$DB_PASS" \
+  --single-transaction \
+  --routines \
+  --triggers \
+  --events \
+  --skip-comments \
+  --skip-dump-date \
+  --skip-tz-utc \
+  "$DB_NAME" > beepzone-full-dump.sql
+
+echo "Exported to: beepzone-full-dump.sql"
+echo ""
+echo "This file contains:"
+echo "  - Complete schema (CREATE TABLE statements)"
+echo "  - All data (INSERT statements)"
+echo "  - Triggers and routines"
+echo "  - Ready for single-file import"

+ 2081 - 0
backend/database/schema/beepzone-schema-dump.sql

@@ -0,0 +1,2081 @@
+/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
+/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `asset_change_log` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `table_name` varchar(50) NOT NULL,
+  `action` enum('INSERT','UPDATE','DELETE') NOT NULL,
+  `record_id` int(11) NOT NULL,
+  `changed_fields` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'Only fields that actually changed' CHECK (json_valid(`changed_fields`)),
+  `old_values` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`old_values`)),
+  `new_values` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`new_values`)),
+  `changed_at` timestamp NULL DEFAULT current_timestamp(),
+  `changed_by_id` int(11) DEFAULT NULL,
+  `changed_by_username` varchar(100) DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `idx_table_action` (`table_name`,`action`),
+  KEY `idx_timestamp` (`changed_at`),
+  KEY `idx_record` (`record_id`),
+  KEY `idx_user` (`changed_by_id`),
+  CONSTRAINT `asset_change_log_ibfk_1` FOREIGN KEY (`changed_by_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `assets` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `asset_tag` varchar(200) DEFAULT NULL,
+  `asset_numeric_id` int(11) NOT NULL CHECK (`asset_numeric_id` between 10000000 and 99999999),
+  `asset_type` enum('N','B','L','C') NOT NULL,
+  `name` varchar(255) DEFAULT NULL,
+  `category_id` int(11) DEFAULT NULL,
+  `manufacturer` varchar(200) DEFAULT NULL,
+  `model` varchar(200) DEFAULT NULL,
+  `serial_number` varchar(200) DEFAULT NULL,
+  `zone_id` int(11) DEFAULT NULL,
+  `zone_plus` enum('Floating Local','Floating Global','Clarify') DEFAULT NULL,
+  `zone_note` text DEFAULT NULL,
+  `status` enum('Good','Attention','Faulty','Missing','Retired','In Repair','In Transit','Expired','Unmanaged') DEFAULT 'Good',
+  `last_audit` date DEFAULT NULL,
+  `last_audit_status` varchar(100) DEFAULT NULL,
+  `price` decimal(12,2) DEFAULT NULL CHECK (`price` is null or `price` >= 0),
+  `purchase_date` date DEFAULT NULL,
+  `warranty_until` date DEFAULT NULL,
+  `expiry_date` date DEFAULT NULL,
+  `quantity_available` int(11) DEFAULT NULL,
+  `quantity_total` int(11) DEFAULT NULL,
+  `quantity_used` int(11) DEFAULT 0,
+  `supplier_id` int(11) DEFAULT NULL,
+  `lendable` tinyint(1) DEFAULT 0,
+  `minimum_role_for_lending` int(11) DEFAULT 1 CHECK (`minimum_role_for_lending` >= 1 and `minimum_role_for_lending` <= 100),
+  `lending_status` enum('Available','Deployed','Borrowed','Overdue','Illegally Handed Out','Stolen') DEFAULT NULL,
+  `current_borrower_id` int(11) DEFAULT NULL,
+  `due_date` date DEFAULT NULL,
+  `previous_borrower_id` int(11) DEFAULT NULL,
+  `audit_task_id` int(11) DEFAULT NULL,
+  `label_template_id` int(11) DEFAULT NULL,
+  `no_scan` enum('Yes','Ask','No') DEFAULT 'No',
+  `notes` text DEFAULT NULL,
+  `additional_fields` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`additional_fields`)),
+  `file_attachment` mediumblob DEFAULT NULL,
+  `created_date` timestamp NULL DEFAULT current_timestamp(),
+  `created_by` int(11) DEFAULT NULL,
+  `last_modified_date` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
+  `last_modified_by` int(11) DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `asset_numeric_id` (`asset_numeric_id`),
+  UNIQUE KEY `asset_tag` (`asset_tag`),
+  KEY `supplier_id` (`supplier_id`),
+  KEY `current_borrower_id` (`current_borrower_id`),
+  KEY `previous_borrower_id` (`previous_borrower_id`),
+  KEY `audit_task_id` (`audit_task_id`),
+  KEY `created_by` (`created_by`),
+  KEY `last_modified_by` (`last_modified_by`),
+  KEY `idx_asset_tag` (`asset_tag`),
+  KEY `idx_asset_numeric` (`asset_numeric_id`),
+  KEY `idx_type` (`asset_type`),
+  KEY `idx_status` (`status`),
+  KEY `idx_zone` (`zone_id`),
+  KEY `idx_category` (`category_id`),
+  KEY `idx_lendable` (`lendable`),
+  KEY `idx_lending_status` (`lending_status`),
+  KEY `idx_label_template` (`label_template_id`),
+  CONSTRAINT `assets_ibfk_1` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`),
+  CONSTRAINT `assets_ibfk_2` FOREIGN KEY (`zone_id`) REFERENCES `zones` (`id`),
+  CONSTRAINT `assets_ibfk_3` FOREIGN KEY (`supplier_id`) REFERENCES `suppliers` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `assets_ibfk_4` FOREIGN KEY (`current_borrower_id`) REFERENCES `borrowers` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `assets_ibfk_5` FOREIGN KEY (`previous_borrower_id`) REFERENCES `borrowers` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `assets_ibfk_6` FOREIGN KEY (`audit_task_id`) REFERENCES `audit_tasks` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `assets_ibfk_7` FOREIGN KEY (`created_by`) REFERENCES `users` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `assets_ibfk_8` FOREIGN KEY (`last_modified_by`) REFERENCES `users` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `fk_asset_label_template` FOREIGN KEY (`label_template_id`) REFERENCES `label_templates` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER assets_before_insert_meta
+BEFORE INSERT ON assets
+FOR EACH ROW
+BEGIN
+    IF NEW.created_by IS NULL AND @current_user_id IS NOT NULL THEN
+        SET NEW.created_by = @current_user_id;
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER validate_zone_plus_insert
+BEFORE INSERT ON assets
+FOR EACH ROW
+BEGIN
+    IF NEW.zone_plus = 'Clarify' AND (NEW.zone_note IS NULL OR NEW.zone_note = '') THEN
+        SIGNAL SQLSTATE '45000'
+        SET MESSAGE_TEXT = 'zone_note is required when zone_plus is set to Clarify';
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER assets_after_insert_log
+AFTER INSERT ON assets
+FOR EACH ROW
+BEGIN
+    DECLARE username VARCHAR(100);
+    DECLARE set_fields_array JSON;
+    DECLARE new_vals JSON;
+    
+    IF @current_user_id IS NOT NULL THEN
+        SELECT users.username INTO username FROM users WHERE id = @current_user_id;
+    END IF;
+    
+    -- Build JSON objects only with non-NULL fields
+    SET set_fields_array = JSON_ARRAY();
+    SET new_vals = JSON_OBJECT();
+    
+    -- Always log these core fields
+    IF NEW.asset_tag IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'asset_tag');
+        SET new_vals = JSON_SET(new_vals, '$.asset_tag', NEW.asset_tag);
+    END IF;
+    
+    IF NEW.asset_numeric_id IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'asset_numeric_id');
+        SET new_vals = JSON_SET(new_vals, '$.asset_numeric_id', NEW.asset_numeric_id);
+    END IF;
+    
+    IF NEW.asset_type IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'asset_type');
+        SET new_vals = JSON_SET(new_vals, '$.asset_type', NEW.asset_type);
+    END IF;
+    
+    IF NEW.name IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'name');
+        SET new_vals = JSON_SET(new_vals, '$.name', NEW.name);
+    END IF;
+    
+    IF NEW.category_id IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'category_id');
+        SET new_vals = JSON_SET(new_vals, '$.category_id', NEW.category_id);
+    END IF;
+    
+    IF NEW.manufacturer IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'manufacturer');
+        SET new_vals = JSON_SET(new_vals, '$.manufacturer', NEW.manufacturer);
+    END IF;
+    
+    IF NEW.model IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'model');
+        SET new_vals = JSON_SET(new_vals, '$.model', NEW.model);
+    END IF;
+    
+    IF NEW.serial_number IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'serial_number');
+        SET new_vals = JSON_SET(new_vals, '$.serial_number', NEW.serial_number);
+    END IF;
+    
+    IF NEW.zone_id IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'zone_id');
+        SET new_vals = JSON_SET(new_vals, '$.zone_id', NEW.zone_id);
+    END IF;
+    
+    IF NEW.zone_plus IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'zone_plus');
+        SET new_vals = JSON_SET(new_vals, '$.zone_plus', NEW.zone_plus);
+    END IF;
+    
+    IF NEW.zone_note IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'zone_note');
+        SET new_vals = JSON_SET(new_vals, '$.zone_note', NEW.zone_note);
+    END IF;
+    
+    IF NEW.status IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'status');
+        SET new_vals = JSON_SET(new_vals, '$.status', NEW.status);
+    END IF;
+    
+    IF NEW.last_audit IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'last_audit');
+        SET new_vals = JSON_SET(new_vals, '$.last_audit', NEW.last_audit);
+    END IF;
+    
+    IF NEW.last_audit_status IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'last_audit_status');
+        SET new_vals = JSON_SET(new_vals, '$.last_audit_status', NEW.last_audit_status);
+    END IF;
+    
+    IF NEW.price IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'price');
+        SET new_vals = JSON_SET(new_vals, '$.price', NEW.price);
+    END IF;
+    
+    IF NEW.purchase_date IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'purchase_date');
+        SET new_vals = JSON_SET(new_vals, '$.purchase_date', NEW.purchase_date);
+    END IF;
+    
+    IF NEW.warranty_until IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'warranty_until');
+        SET new_vals = JSON_SET(new_vals, '$.warranty_until', NEW.warranty_until);
+    END IF;
+    
+    IF NEW.expiry_date IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'expiry_date');
+        SET new_vals = JSON_SET(new_vals, '$.expiry_date', NEW.expiry_date);
+    END IF;
+    
+    IF NEW.quantity_available IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'quantity_available');
+        SET new_vals = JSON_SET(new_vals, '$.quantity_available', NEW.quantity_available);
+    END IF;
+    
+    IF NEW.quantity_total IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'quantity_total');
+        SET new_vals = JSON_SET(new_vals, '$.quantity_total', NEW.quantity_total);
+    END IF;
+    
+    IF NEW.quantity_used IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'quantity_used');
+        SET new_vals = JSON_SET(new_vals, '$.quantity_used', NEW.quantity_used);
+    END IF;
+    
+    IF NEW.supplier_id IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'supplier_id');
+        SET new_vals = JSON_SET(new_vals, '$.supplier_id', NEW.supplier_id);
+    END IF;
+    
+    IF NEW.lendable IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'lendable');
+        SET new_vals = JSON_SET(new_vals, '$.lendable', NEW.lendable);
+    END IF;
+    
+    IF NEW.minimum_role_for_lending IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'minimum_role_for_lending');
+        SET new_vals = JSON_SET(new_vals, '$.minimum_role_for_lending', NEW.minimum_role_for_lending);
+    END IF;
+    
+    IF NEW.lending_status IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'lending_status');
+        SET new_vals = JSON_SET(new_vals, '$.lending_status', NEW.lending_status);
+    END IF;
+    
+    IF NEW.current_borrower_id IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'current_borrower_id');
+        SET new_vals = JSON_SET(new_vals, '$.current_borrower_id', NEW.current_borrower_id);
+    END IF;
+    
+    IF NEW.due_date IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'due_date');
+        SET new_vals = JSON_SET(new_vals, '$.due_date', NEW.due_date);
+    END IF;
+    
+    IF NEW.previous_borrower_id IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'previous_borrower_id');
+        SET new_vals = JSON_SET(new_vals, '$.previous_borrower_id', NEW.previous_borrower_id);
+    END IF;
+    
+    IF NEW.audit_task_id IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'audit_task_id');
+        SET new_vals = JSON_SET(new_vals, '$.audit_task_id', NEW.audit_task_id);
+    END IF;
+    
+    IF NEW.no_scan IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'no_scan');
+        SET new_vals = JSON_SET(new_vals, '$.no_scan', NEW.no_scan);
+    END IF;
+    
+    IF NEW.notes IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'notes');
+        SET new_vals = JSON_SET(new_vals, '$.notes', NEW.notes);
+    END IF;
+    
+    IF NEW.additional_fields IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'additional_fields');
+        SET new_vals = JSON_SET(new_vals, '$.additional_fields', NEW.additional_fields);
+    END IF;
+    
+    IF NEW.created_by IS NOT NULL THEN
+        SET set_fields_array = JSON_ARRAY_APPEND(set_fields_array, '$', 'created_by');
+        SET new_vals = JSON_SET(new_vals, '$.created_by', NEW.created_by);
+    END IF;
+    
+    -- Log the INSERT with only the fields that were set
+    INSERT INTO asset_change_log (
+        table_name, action, record_id, changed_fields, new_values,
+        changed_by_id, changed_by_username
+    )
+    VALUES (
+        'assets',
+        'INSERT',
+        NEW.id,
+        set_fields_array,
+        new_vals,
+        @current_user_id,
+        username
+    );
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER assets_before_update_meta
+BEFORE UPDATE ON assets
+FOR EACH ROW
+BEGIN
+    SET NEW.last_modified_date = NOW();
+    IF @current_user_id IS NOT NULL THEN
+        SET NEW.last_modified_by = @current_user_id;
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER prevent_lend_non_lendable_assets
+BEFORE UPDATE ON assets
+FOR EACH ROW
+BEGIN
+    -- Check if trying to set lending_status to any borrowed state on a non-lendable asset
+    IF (NEW.lendable = FALSE OR NEW.lendable IS NULL) AND 
+       NEW.lending_status IN ('Borrowed', 'Deployed', 'Overdue') AND
+       (OLD.lending_status NOT IN ('Borrowed', 'Deployed', 'Overdue') OR OLD.lending_status IS NULL) THEN
+        SIGNAL SQLSTATE '45000'
+        SET MESSAGE_TEXT = 'Cannot lend asset that is marked as non-lendable. Set lendable=TRUE first.';
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER validate_zone_plus_update
+BEFORE UPDATE ON assets
+FOR EACH ROW
+BEGIN
+    IF NEW.zone_plus = 'Clarify' AND (NEW.zone_note IS NULL OR NEW.zone_note = '') THEN
+        SIGNAL SQLSTATE '45000'
+        SET MESSAGE_TEXT = 'zone_note is required when zone_plus is set to Clarify';
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER assets_after_update_log
+AFTER UPDATE ON assets
+FOR EACH ROW
+BEGIN
+    DECLARE username VARCHAR(100);
+    DECLARE changed_fields_array JSON;
+    DECLARE old_vals JSON;
+    DECLARE new_vals JSON;
+    
+    IF @current_user_id IS NOT NULL THEN
+        SELECT users.username INTO username FROM users WHERE id = @current_user_id;
+    END IF;
+    
+    -- Build JSON objects only with changed fields
+    SET changed_fields_array = JSON_ARRAY();
+    SET old_vals = JSON_OBJECT();
+    SET new_vals = JSON_OBJECT();
+    
+    IF OLD.asset_tag <=> NEW.asset_tag IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'asset_tag');
+        SET old_vals = JSON_SET(old_vals, '$.asset_tag', OLD.asset_tag);
+        SET new_vals = JSON_SET(new_vals, '$.asset_tag', NEW.asset_tag);
+    END IF;
+    
+    IF OLD.asset_numeric_id <=> NEW.asset_numeric_id IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'asset_numeric_id');
+        SET old_vals = JSON_SET(old_vals, '$.asset_numeric_id', OLD.asset_numeric_id);
+        SET new_vals = JSON_SET(new_vals, '$.asset_numeric_id', NEW.asset_numeric_id);
+    END IF;
+    
+    IF OLD.asset_type <=> NEW.asset_type IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'asset_type');
+        SET old_vals = JSON_SET(old_vals, '$.asset_type', OLD.asset_type);
+        SET new_vals = JSON_SET(new_vals, '$.asset_type', NEW.asset_type);
+    END IF;
+    
+    IF OLD.name <=> NEW.name IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'name');
+        SET old_vals = JSON_SET(old_vals, '$.name', OLD.name);
+        SET new_vals = JSON_SET(new_vals, '$.name', NEW.name);
+    END IF;
+    
+    IF OLD.category_id <=> NEW.category_id IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'category_id');
+        SET old_vals = JSON_SET(old_vals, '$.category_id', OLD.category_id);
+        SET new_vals = JSON_SET(new_vals, '$.category_id', NEW.category_id);
+    END IF;
+    
+    IF OLD.manufacturer <=> NEW.manufacturer IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'manufacturer');
+        SET old_vals = JSON_SET(old_vals, '$.manufacturer', OLD.manufacturer);
+        SET new_vals = JSON_SET(new_vals, '$.manufacturer', NEW.manufacturer);
+    END IF;
+    
+    IF OLD.model <=> NEW.model IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'model');
+        SET old_vals = JSON_SET(old_vals, '$.model', OLD.model);
+        SET new_vals = JSON_SET(new_vals, '$.model', NEW.model);
+    END IF;
+    
+    IF OLD.serial_number <=> NEW.serial_number IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'serial_number');
+        SET old_vals = JSON_SET(old_vals, '$.serial_number', OLD.serial_number);
+        SET new_vals = JSON_SET(new_vals, '$.serial_number', NEW.serial_number);
+    END IF;
+    
+    IF OLD.zone_id <=> NEW.zone_id IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'zone_id');
+        SET old_vals = JSON_SET(old_vals, '$.zone_id', OLD.zone_id);
+        SET new_vals = JSON_SET(new_vals, '$.zone_id', NEW.zone_id);
+    END IF;
+    
+    IF OLD.zone_plus <=> NEW.zone_plus IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'zone_plus');
+        SET old_vals = JSON_SET(old_vals, '$.zone_plus', OLD.zone_plus);
+        SET new_vals = JSON_SET(new_vals, '$.zone_plus', NEW.zone_plus);
+    END IF;
+    
+    IF OLD.zone_note <=> NEW.zone_note IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'zone_note');
+        SET old_vals = JSON_SET(old_vals, '$.zone_note', OLD.zone_note);
+        SET new_vals = JSON_SET(new_vals, '$.zone_note', NEW.zone_note);
+    END IF;
+    
+    IF OLD.status <=> NEW.status IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'status');
+        SET old_vals = JSON_SET(old_vals, '$.status', OLD.status);
+        SET new_vals = JSON_SET(new_vals, '$.status', NEW.status);
+    END IF;
+    
+    IF OLD.last_audit <=> NEW.last_audit IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'last_audit');
+        SET old_vals = JSON_SET(old_vals, '$.last_audit', OLD.last_audit);
+        SET new_vals = JSON_SET(new_vals, '$.last_audit', NEW.last_audit);
+    END IF;
+    
+    IF OLD.last_audit_status <=> NEW.last_audit_status IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'last_audit_status');
+        SET old_vals = JSON_SET(old_vals, '$.last_audit_status', OLD.last_audit_status);
+        SET new_vals = JSON_SET(new_vals, '$.last_audit_status', NEW.last_audit_status);
+    END IF;
+    
+    IF OLD.price <=> NEW.price IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'price');
+        SET old_vals = JSON_SET(old_vals, '$.price', OLD.price);
+        SET new_vals = JSON_SET(new_vals, '$.price', NEW.price);
+    END IF;
+    
+    IF OLD.purchase_date <=> NEW.purchase_date IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'purchase_date');
+        SET old_vals = JSON_SET(old_vals, '$.purchase_date', OLD.purchase_date);
+        SET new_vals = JSON_SET(new_vals, '$.purchase_date', NEW.purchase_date);
+    END IF;
+    
+    IF OLD.warranty_until <=> NEW.warranty_until IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'warranty_until');
+        SET old_vals = JSON_SET(old_vals, '$.warranty_until', OLD.warranty_until);
+        SET new_vals = JSON_SET(new_vals, '$.warranty_until', NEW.warranty_until);
+    END IF;
+    
+    IF OLD.expiry_date <=> NEW.expiry_date IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'expiry_date');
+        SET old_vals = JSON_SET(old_vals, '$.expiry_date', OLD.expiry_date);
+        SET new_vals = JSON_SET(new_vals, '$.expiry_date', NEW.expiry_date);
+    END IF;
+    
+    IF OLD.quantity_available <=> NEW.quantity_available IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'quantity_available');
+        SET old_vals = JSON_SET(old_vals, '$.quantity_available', OLD.quantity_available);
+        SET new_vals = JSON_SET(new_vals, '$.quantity_available', NEW.quantity_available);
+    END IF;
+    
+    IF OLD.quantity_total <=> NEW.quantity_total IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'quantity_total');
+        SET old_vals = JSON_SET(old_vals, '$.quantity_total', OLD.quantity_total);
+        SET new_vals = JSON_SET(new_vals, '$.quantity_total', NEW.quantity_total);
+    END IF;
+    
+    IF OLD.quantity_used <=> NEW.quantity_used IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'quantity_used');
+        SET old_vals = JSON_SET(old_vals, '$.quantity_used', OLD.quantity_used);
+        SET new_vals = JSON_SET(new_vals, '$.quantity_used', NEW.quantity_used);
+    END IF;
+    
+    IF OLD.supplier_id <=> NEW.supplier_id IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'supplier_id');
+        SET old_vals = JSON_SET(old_vals, '$.supplier_id', OLD.supplier_id);
+        SET new_vals = JSON_SET(new_vals, '$.supplier_id', NEW.supplier_id);
+    END IF;
+    
+    IF OLD.lendable <=> NEW.lendable IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'lendable');
+        SET old_vals = JSON_SET(old_vals, '$.lendable', OLD.lendable);
+        SET new_vals = JSON_SET(new_vals, '$.lendable', NEW.lendable);
+    END IF;
+    
+    IF OLD.minimum_role_for_lending <=> NEW.minimum_role_for_lending IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'minimum_role_for_lending');
+        SET old_vals = JSON_SET(old_vals, '$.minimum_role_for_lending', OLD.minimum_role_for_lending);
+        SET new_vals = JSON_SET(new_vals, '$.minimum_role_for_lending', NEW.minimum_role_for_lending);
+    END IF;
+    
+    IF OLD.lending_status <=> NEW.lending_status IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'lending_status');
+        SET old_vals = JSON_SET(old_vals, '$.lending_status', OLD.lending_status);
+        SET new_vals = JSON_SET(new_vals, '$.lending_status', NEW.lending_status);
+    END IF;
+    
+    IF OLD.current_borrower_id <=> NEW.current_borrower_id IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'current_borrower_id');
+        SET old_vals = JSON_SET(old_vals, '$.current_borrower_id', OLD.current_borrower_id);
+        SET new_vals = JSON_SET(new_vals, '$.current_borrower_id', NEW.current_borrower_id);
+    END IF;
+    
+    IF OLD.due_date <=> NEW.due_date IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'due_date');
+        SET old_vals = JSON_SET(old_vals, '$.due_date', OLD.due_date);
+        SET new_vals = JSON_SET(new_vals, '$.due_date', NEW.due_date);
+    END IF;
+    
+    IF OLD.previous_borrower_id <=> NEW.previous_borrower_id IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'previous_borrower_id');
+        SET old_vals = JSON_SET(old_vals, '$.previous_borrower_id', OLD.previous_borrower_id);
+        SET new_vals = JSON_SET(new_vals, '$.previous_borrower_id', NEW.previous_borrower_id);
+    END IF;
+    
+    IF OLD.audit_task_id <=> NEW.audit_task_id IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'audit_task_id');
+        SET old_vals = JSON_SET(old_vals, '$.audit_task_id', OLD.audit_task_id);
+        SET new_vals = JSON_SET(new_vals, '$.audit_task_id', NEW.audit_task_id);
+    END IF;
+    
+    IF OLD.no_scan <=> NEW.no_scan IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'no_scan');
+        SET old_vals = JSON_SET(old_vals, '$.no_scan', OLD.no_scan);
+        SET new_vals = JSON_SET(new_vals, '$.no_scan', NEW.no_scan);
+    END IF;
+    
+    IF OLD.notes <=> NEW.notes IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'notes');
+        SET old_vals = JSON_SET(old_vals, '$.notes', OLD.notes);
+        SET new_vals = JSON_SET(new_vals, '$.notes', NEW.notes);
+    END IF;
+    
+    IF OLD.additional_fields <=> NEW.additional_fields IS FALSE THEN
+        SET changed_fields_array = JSON_ARRAY_APPEND(changed_fields_array, '$', 'additional_fields');
+        SET old_vals = JSON_SET(old_vals, '$.additional_fields', OLD.additional_fields);
+        SET new_vals = JSON_SET(new_vals, '$.additional_fields', NEW.additional_fields);
+    END IF;
+    
+    -- Only log if there were actual changes (excluding auto-updated fields)
+    IF JSON_LENGTH(changed_fields_array) > 0 THEN
+        INSERT INTO asset_change_log (
+            table_name, action, record_id, changed_fields, old_values, new_values,
+            changed_by_id, changed_by_username
+        )
+        VALUES (
+            'assets',
+            'UPDATE',
+            NEW.id,
+            changed_fields_array,
+            old_vals,
+            new_vals,
+            @current_user_id,
+            username
+        );
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER auto_detect_asset_issues
+AFTER UPDATE ON assets
+FOR EACH ROW
+BEGIN
+    DECLARE issue_title VARCHAR(255);
+    DECLARE issue_description TEXT;
+    DECLARE issue_severity ENUM('Critical', 'High', 'Medium', 'Low');
+    DECLARE detection_trigger_name VARCHAR(100);
+    
+    -- Check for lending_status changes to problematic states
+    IF (OLD.lending_status IS NULL OR OLD.lending_status != NEW.lending_status) 
+       AND NEW.lending_status IN ('Overdue', 'Illegally Handed Out', 'Stolen') THEN
+        
+        -- Determine issue details based on lending_status
+        CASE NEW.lending_status
+            WHEN 'Overdue' THEN
+                SET issue_title = CONCAT('Asset Overdue: ', COALESCE(NEW.name, NEW.asset_tag, CAST(NEW.asset_numeric_id AS CHAR)));
+                SET issue_description = CONCAT('Asset lending status changed to Overdue. Asset: ', NEW.asset_tag, 
+                    CASE WHEN NEW.name IS NOT NULL THEN CONCAT(' (', NEW.name, ')') ELSE '' END);
+                SET issue_severity = 'High';
+                SET detection_trigger_name = 'LENDING_OVERDUE';
+                
+            WHEN 'Illegally Handed Out' THEN
+                SET issue_title = CONCAT('Asset Illegally Handed Out: ', COALESCE(NEW.name, NEW.asset_tag, CAST(NEW.asset_numeric_id AS CHAR)));
+                SET issue_description = CONCAT('Asset lending status changed to Illegally Handed Out. Asset: ', NEW.asset_tag,
+                    CASE WHEN NEW.name IS NOT NULL THEN CONCAT(' (', NEW.name, ')') ELSE '' END);
+                SET issue_severity = 'Critical';
+                SET detection_trigger_name = 'LENDING_ILLEGAL';
+                
+            WHEN 'Stolen' THEN
+                SET issue_title = CONCAT('Asset Stolen: ', COALESCE(NEW.name, NEW.asset_tag, CAST(NEW.asset_numeric_id AS CHAR)));
+                SET issue_description = CONCAT('Asset lending status changed to Stolen (14+ days overdue). Asset: ', NEW.asset_tag,
+                    CASE WHEN NEW.name IS NOT NULL THEN CONCAT(' (', NEW.name, ')') ELSE '' END);
+                SET issue_severity = 'Critical';
+                SET detection_trigger_name = 'LENDING_STOLEN';
+        END CASE;
+        
+        -- Insert the auto-detected issue
+        INSERT INTO issue_tracker (
+            issue_type, asset_id, title, description, severity, priority, status,
+            reported_by, auto_detected, detection_trigger, created_date
+        )
+        VALUES (
+            'Asset Issue', NEW.id, issue_title, issue_description, issue_severity, 'Urgent', 'Open',
+            COALESCE(@current_user_id, 1), TRUE, detection_trigger_name, NOW()
+        );
+    END IF;
+    
+    -- Check for status changes to problematic states
+    IF OLD.status != NEW.status AND NEW.status IN ('Attention', 'Faulty', 'Missing', 'Retired', 'In Repair', 'Expired') THEN
+        
+        -- Determine issue details based on status
+        CASE NEW.status
+            WHEN 'Attention' THEN
+                SET issue_title = CONCAT('Asset Needs Attention: ', COALESCE(NEW.name, NEW.asset_tag, NEW.asset_numeric_id));
+                SET issue_description = CONCAT('Asset status changed to Attention. Asset: ', NEW.asset_numeric_id, CASE WHEN NEW.name IS NOT NULL THEN CONCAT(' (', NEW.name, ')') ELSE '' END);
+                SET issue_severity = 'Medium';
+                SET detection_trigger_name = 'STATUS_ATTENTION';
+                
+            WHEN 'Faulty' THEN
+                SET issue_title = CONCAT('Asset Faulty: ', COALESCE(NEW.name, NEW.asset_tag, NEW.asset_numeric_id));
+                SET issue_description = CONCAT('Asset status changed to Faulty. Asset: ', NEW.asset_numeric_id, CASE WHEN NEW.name IS NOT NULL THEN CONCAT(' (', NEW.name, ')') ELSE '' END);
+                SET issue_severity = 'High';
+                SET detection_trigger_name = 'STATUS_FAULTY';
+                
+            WHEN 'Missing' THEN
+                SET issue_title = CONCAT('Asset Missing: ', COALESCE(NEW.name, NEW.asset_tag, NEW.asset_numeric_id));
+                SET issue_description = CONCAT('Asset status changed to Missing. Asset: ', NEW.asset_numeric_id, CASE WHEN NEW.name IS NOT NULL THEN CONCAT(' (', NEW.name, ')') ELSE '' END);
+                SET issue_severity = 'Critical';
+                SET detection_trigger_name = 'STATUS_MISSING';
+                
+            WHEN 'Retired' THEN
+                SET issue_title = CONCAT('Asset Retired: ', COALESCE(NEW.name, NEW.asset_tag, NEW.asset_numeric_id));
+                SET issue_description = CONCAT('Asset status changed to Retired. Asset: ', NEW.asset_numeric_id, CASE WHEN NEW.name IS NOT NULL THEN CONCAT(' (', NEW.name, ')') ELSE '' END);
+                SET issue_severity = 'Low';
+                SET detection_trigger_name = 'STATUS_RETIRED';
+                
+            WHEN 'In Repair' THEN
+                SET issue_title = CONCAT('Asset In Repair: ', COALESCE(NEW.name, NEW.asset_tag, NEW.asset_numeric_id));
+                SET issue_description = CONCAT('Asset status changed to In Repair. Asset: ', NEW.asset_numeric_id, CASE WHEN NEW.name IS NOT NULL THEN CONCAT(' (', NEW.name, ')') ELSE '' END);
+                SET issue_severity = 'Medium';
+                SET detection_trigger_name = 'STATUS_IN_REPAIR';
+                
+            WHEN 'Expired' THEN
+                SET issue_title = CONCAT('Asset Expired: ', COALESCE(NEW.name, NEW.asset_tag, NEW.asset_numeric_id));
+                SET issue_description = CONCAT('Asset status changed to Expired. Asset: ', NEW.asset_numeric_id, CASE WHEN NEW.name IS NOT NULL THEN CONCAT(' (', NEW.name, ')') ELSE '' END);
+                SET issue_severity = 'Medium';
+                SET detection_trigger_name = 'STATUS_EXPIRED';
+        END CASE;
+        
+        -- Insert the auto-detected issue
+        INSERT INTO issue_tracker (
+            issue_type, asset_id, title, description, severity, priority, status,
+            reported_by, auto_detected, detection_trigger, created_date
+        )
+        VALUES (
+            'Asset Issue', NEW.id, issue_title, issue_description, issue_severity, 'Normal', 'Open',
+            COALESCE(@current_user_id, 1), TRUE, detection_trigger_name, NOW()
+        );
+    END IF;
+    
+    -- Auto-resolve issues when status becomes Good again
+    IF OLD.status != NEW.status AND NEW.status = 'Good' AND OLD.status IN ('Faulty', 'Missing', 'In Repair', 'Expired') THEN
+        UPDATE issue_tracker 
+        SET status = 'Resolved',
+            solution = 'Automatically Fixed',
+            solution_plus = CONCAT('Asset status automatically changed from ', OLD.status, ' to Good'),
+            resolved_date = NOW(),
+            resolved_by = COALESCE(@current_user_id, 1)
+        WHERE asset_id = NEW.id 
+        AND status IN ('Open', 'In Progress')
+        AND auto_detected = TRUE
+        AND detection_trigger IN ('STATUS_FAULTY', 'STATUS_MISSING', 'STATUS_IN_REPAIR', 'STATUS_EXPIRED');
+    END IF;
+    
+    -- Auto-resolve overdue/stolen/illegal issues when item is returned (lending_status becomes Available)
+    IF (OLD.lending_status IS NULL OR OLD.lending_status != NEW.lending_status) 
+       AND NEW.lending_status = 'Available' 
+       AND OLD.lending_status IN ('Overdue', 'Illegally Handed Out', 'Stolen') THEN
+        UPDATE issue_tracker 
+        SET status = 'Resolved',
+            solution = 'Items Returned',
+            solution_plus = CONCAT('Asset was returned - lending status changed from ', OLD.lending_status, ' to Available'),
+            resolved_date = NOW(),
+            resolved_by = COALESCE(@current_user_id, 1)
+        WHERE asset_id = NEW.id 
+        AND status IN ('Open', 'In Progress')
+        AND auto_detected = TRUE
+        AND detection_trigger IN ('LENDING_OVERDUE', 'LENDING_ILLEGAL', 'LENDING_STOLEN');
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER prevent_delete_borrowed_assets
+BEFORE DELETE ON assets
+FOR EACH ROW
+BEGIN
+    IF OLD.lending_status IN ('Borrowed', 'Deployed', 'Overdue') THEN
+        SIGNAL SQLSTATE '45000'
+        SET MESSAGE_TEXT = 'Cannot delete asset that is currently borrowed or deployed, maybe update to retired or unmanaged before';
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER assets_after_delete_log
+AFTER DELETE ON assets
+FOR EACH ROW
+BEGIN
+    DECLARE username VARCHAR(100);
+    DECLARE deleted_fields_array JSON;
+    DECLARE old_vals JSON;
+    
+    IF @current_user_id IS NOT NULL THEN
+        SELECT users.username INTO username FROM users WHERE id = @current_user_id;
+    END IF;
+    
+    -- Build JSON objects only with non-NULL fields (for restore capability)
+    SET deleted_fields_array = JSON_ARRAY();
+    SET old_vals = JSON_OBJECT();
+    
+    IF OLD.asset_tag IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'asset_tag');
+        SET old_vals = JSON_SET(old_vals, '$.asset_tag', OLD.asset_tag);
+    END IF;
+    
+    IF OLD.asset_numeric_id IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'asset_numeric_id');
+        SET old_vals = JSON_SET(old_vals, '$.asset_numeric_id', OLD.asset_numeric_id);
+    END IF;
+    
+    IF OLD.asset_type IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'asset_type');
+        SET old_vals = JSON_SET(old_vals, '$.asset_type', OLD.asset_type);
+    END IF;
+    
+    IF OLD.name IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'name');
+        SET old_vals = JSON_SET(old_vals, '$.name', OLD.name);
+    END IF;
+    
+    IF OLD.category_id IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'category_id');
+        SET old_vals = JSON_SET(old_vals, '$.category_id', OLD.category_id);
+    END IF;
+    
+    IF OLD.manufacturer IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'manufacturer');
+        SET old_vals = JSON_SET(old_vals, '$.manufacturer', OLD.manufacturer);
+    END IF;
+    
+    IF OLD.model IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'model');
+        SET old_vals = JSON_SET(old_vals, '$.model', OLD.model);
+    END IF;
+    
+    IF OLD.serial_number IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'serial_number');
+        SET old_vals = JSON_SET(old_vals, '$.serial_number', OLD.serial_number);
+    END IF;
+    
+    IF OLD.zone_id IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'zone_id');
+        SET old_vals = JSON_SET(old_vals, '$.zone_id', OLD.zone_id);
+    END IF;
+    
+    IF OLD.zone_plus IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'zone_plus');
+        SET old_vals = JSON_SET(old_vals, '$.zone_plus', OLD.zone_plus);
+    END IF;
+    
+    IF OLD.zone_note IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'zone_note');
+        SET old_vals = JSON_SET(old_vals, '$.zone_note', OLD.zone_note);
+    END IF;
+    
+    IF OLD.status IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'status');
+        SET old_vals = JSON_SET(old_vals, '$.status', OLD.status);
+    END IF;
+    
+    IF OLD.last_audit IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'last_audit');
+        SET old_vals = JSON_SET(old_vals, '$.last_audit', OLD.last_audit);
+    END IF;
+    
+    IF OLD.last_audit_status IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'last_audit_status');
+        SET old_vals = JSON_SET(old_vals, '$.last_audit_status', OLD.last_audit_status);
+    END IF;
+    
+    IF OLD.price IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'price');
+        SET old_vals = JSON_SET(old_vals, '$.price', OLD.price);
+    END IF;
+    
+    IF OLD.purchase_date IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'purchase_date');
+        SET old_vals = JSON_SET(old_vals, '$.purchase_date', OLD.purchase_date);
+    END IF;
+    
+    IF OLD.warranty_until IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'warranty_until');
+        SET old_vals = JSON_SET(old_vals, '$.warranty_until', OLD.warranty_until);
+    END IF;
+    
+    IF OLD.expiry_date IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'expiry_date');
+        SET old_vals = JSON_SET(old_vals, '$.expiry_date', OLD.expiry_date);
+    END IF;
+    
+    IF OLD.quantity_available IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'quantity_available');
+        SET old_vals = JSON_SET(old_vals, '$.quantity_available', OLD.quantity_available);
+    END IF;
+    
+    IF OLD.quantity_total IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'quantity_total');
+        SET old_vals = JSON_SET(old_vals, '$.quantity_total', OLD.quantity_total);
+    END IF;
+    
+    IF OLD.quantity_used IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'quantity_used');
+        SET old_vals = JSON_SET(old_vals, '$.quantity_used', OLD.quantity_used);
+    END IF;
+    
+    IF OLD.supplier_id IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'supplier_id');
+        SET old_vals = JSON_SET(old_vals, '$.supplier_id', OLD.supplier_id);
+    END IF;
+    
+    IF OLD.lendable IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'lendable');
+        SET old_vals = JSON_SET(old_vals, '$.lendable', OLD.lendable);
+    END IF;
+    
+    IF OLD.minimum_role_for_lending IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'minimum_role_for_lending');
+        SET old_vals = JSON_SET(old_vals, '$.minimum_role_for_lending', OLD.minimum_role_for_lending);
+    END IF;
+    
+    IF OLD.lending_status IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'lending_status');
+        SET old_vals = JSON_SET(old_vals, '$.lending_status', OLD.lending_status);
+    END IF;
+    
+    IF OLD.current_borrower_id IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'current_borrower_id');
+        SET old_vals = JSON_SET(old_vals, '$.current_borrower_id', OLD.current_borrower_id);
+    END IF;
+    
+    IF OLD.due_date IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'due_date');
+        SET old_vals = JSON_SET(old_vals, '$.due_date', OLD.due_date);
+    END IF;
+    
+    IF OLD.previous_borrower_id IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'previous_borrower_id');
+        SET old_vals = JSON_SET(old_vals, '$.previous_borrower_id', OLD.previous_borrower_id);
+    END IF;
+    
+    IF OLD.audit_task_id IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'audit_task_id');
+        SET old_vals = JSON_SET(old_vals, '$.audit_task_id', OLD.audit_task_id);
+    END IF;
+    
+    IF OLD.no_scan IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'no_scan');
+        SET old_vals = JSON_SET(old_vals, '$.no_scan', OLD.no_scan);
+    END IF;
+    
+    IF OLD.notes IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'notes');
+        SET old_vals = JSON_SET(old_vals, '$.notes', OLD.notes);
+    END IF;
+    
+    IF OLD.additional_fields IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'additional_fields');
+        SET old_vals = JSON_SET(old_vals, '$.additional_fields', OLD.additional_fields);
+    END IF;
+    
+    -- Always capture metadata fields for restore
+    IF OLD.created_date IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'created_date');
+        SET old_vals = JSON_SET(old_vals, '$.created_date', OLD.created_date);
+    END IF;
+    
+    IF OLD.created_by IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'created_by');
+        SET old_vals = JSON_SET(old_vals, '$.created_by', OLD.created_by);
+    END IF;
+    
+    IF OLD.last_modified_date IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'last_modified_date');
+        SET old_vals = JSON_SET(old_vals, '$.last_modified_date', OLD.last_modified_date);
+    END IF;
+    
+    IF OLD.last_modified_by IS NOT NULL THEN
+        SET deleted_fields_array = JSON_ARRAY_APPEND(deleted_fields_array, '$', 'last_modified_by');
+        SET old_vals = JSON_SET(old_vals, '$.last_modified_by', OLD.last_modified_by);
+    END IF;
+    
+    -- Log the DELETE with only non-NULL fields
+    INSERT INTO asset_change_log (
+        table_name, action, record_id, changed_fields, old_values,
+        changed_by_id, changed_by_username
+    )
+    VALUES (
+        'assets',
+        'DELETE',
+        OLD.id,
+        deleted_fields_array,
+        old_vals,
+        @current_user_id,
+        username
+    );
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `audit_tasks` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `task_name` varchar(200) NOT NULL,
+  `json_sequence` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL CHECK (json_valid(`json_sequence`)),
+  `created_at` timestamp NULL DEFAULT current_timestamp(),
+  `updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `borrowers` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `name` varchar(200) NOT NULL,
+  `email` varchar(255) DEFAULT NULL,
+  `phone_number` varchar(50) DEFAULT NULL,
+  `class_name` varchar(100) DEFAULT NULL,
+  `role` varchar(100) DEFAULT NULL,
+  `notes` text DEFAULT NULL,
+  `added_by` int(11) NOT NULL,
+  `added_date` timestamp NULL DEFAULT current_timestamp(),
+  `banned` tinyint(1) DEFAULT 0,
+  `unban_fine` decimal(10,2) DEFAULT 0.00,
+  `last_unban_by` int(11) DEFAULT NULL,
+  `last_unban_date` date DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `added_by` (`added_by`),
+  KEY `last_unban_by` (`last_unban_by`),
+  KEY `idx_name` (`name`),
+  KEY `idx_banned` (`banned`),
+  CONSTRAINT `borrowers_ibfk_1` FOREIGN KEY (`added_by`) REFERENCES `users` (`id`),
+  CONSTRAINT `borrowers_ibfk_2` FOREIGN KEY (`last_unban_by`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER borrowers_before_insert_meta
+BEFORE INSERT ON borrowers
+FOR EACH ROW
+BEGIN
+    IF NEW.added_by IS NULL AND @current_user_id IS NOT NULL THEN
+        SET NEW.added_by = @current_user_id;
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER borrowers_before_update_meta
+BEFORE UPDATE ON borrowers
+FOR EACH ROW
+BEGIN
+    IF OLD.banned = TRUE AND NEW.banned = FALSE THEN
+        IF NEW.last_unban_by IS NULL AND @current_user_id IS NOT NULL THEN
+            SET NEW.last_unban_by = @current_user_id;
+        END IF;
+        IF NEW.last_unban_date IS NULL THEN
+            SET NEW.last_unban_date = CURDATE();
+        END IF;
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER auto_detect_borrower_issues
+AFTER UPDATE ON borrowers
+FOR EACH ROW
+BEGIN
+    DECLARE issue_title VARCHAR(255);
+    DECLARE issue_description TEXT;
+    
+    -- Auto-detect when borrower gets banned
+    IF OLD.banned = FALSE AND NEW.banned = TRUE THEN
+        SET issue_title = CONCAT('Borrower Banned: ', NEW.name);
+        SET issue_description = CONCAT('Borrower has been banned. Name: ', NEW.name, CASE WHEN NEW.unban_fine > 0 THEN CONCAT(', Unban Fine: $', NEW.unban_fine) ELSE '' END);
+        
+        INSERT INTO issue_tracker (
+            issue_type, borrower_id, title, description, severity, priority, status,
+            reported_by, auto_detected, detection_trigger, created_date
+        )
+        VALUES (
+            'Borrower Issue', NEW.id, issue_title, issue_description, 'High', 'Normal', 'Open',
+            COALESCE(@current_user_id, 1), TRUE, 'BORROWER_BANNED', NOW()
+        );
+    END IF;
+    
+    -- Auto-resolve when borrower gets unbanned
+    IF OLD.banned = TRUE AND NEW.banned = FALSE THEN
+        UPDATE issue_tracker 
+        SET status = 'Resolved',
+            solution = 'Items Returned',
+            solution_plus = CONCAT('Borrower unbanned on ', COALESCE(NEW.last_unban_date, CURDATE()), CASE WHEN NEW.last_unban_by IS NOT NULL THEN CONCAT(' by user ID ', NEW.last_unban_by) ELSE '' END),
+            resolved_date = NOW(),
+            resolved_by = COALESCE(@current_user_id, NEW.last_unban_by, 1)
+        WHERE borrower_id = NEW.id 
+        AND status IN ('Open', 'In Progress')
+        AND auto_detected = TRUE
+        AND detection_trigger = 'BORROWER_BANNED';
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `categories` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `category_name` varchar(200) NOT NULL,
+  `category_description` text DEFAULT NULL,
+  `parent_id` int(11) DEFAULT NULL,
+  `category_code` varchar(50) DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `idx_parent` (`parent_id`),
+  KEY `idx_code` (`category_code`),
+  CONSTRAINT `categories_ibfk_1` FOREIGN KEY (`parent_id`) REFERENCES `categories` (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `issue_tracker` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `issue_type` enum('Asset Issue','Borrower Issue','System Issue','Maintenance','Other') NOT NULL,
+  `asset_id` int(11) DEFAULT NULL,
+  `borrower_id` int(11) DEFAULT NULL,
+  `title` varchar(255) NOT NULL,
+  `description` text NOT NULL,
+  `severity` enum('Critical','High','Medium','Low') DEFAULT NULL,
+  `priority` enum('Urgent','High','Normal','Low') DEFAULT 'Normal',
+  `status` enum('Open','In Progress','Resolved','Closed','On Hold') DEFAULT 'Open',
+  `solution` enum('Fixed','Replaced','Clarify','No Action Needed','Deferred','Items Returned','Automatically Fixed') DEFAULT NULL,
+  `solution_plus` text DEFAULT NULL,
+  `replacement_asset_id` int(11) DEFAULT NULL,
+  `reported_by` int(11) NOT NULL,
+  `assigned_to` int(11) DEFAULT NULL,
+  `resolved_by` int(11) DEFAULT NULL,
+  `cost` decimal(10,2) DEFAULT NULL,
+  `created_date` datetime NOT NULL DEFAULT current_timestamp(),
+  `updated_date` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
+  `resolved_date` datetime DEFAULT NULL,
+  `notes` text DEFAULT NULL,
+  `auto_detected` tinyint(1) DEFAULT 0,
+  `detection_trigger` varchar(100) DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `replacement_asset_id` (`replacement_asset_id`),
+  KEY `reported_by` (`reported_by`),
+  KEY `assigned_to` (`assigned_to`),
+  KEY `resolved_by` (`resolved_by`),
+  KEY `idx_issue_type` (`issue_type`),
+  KEY `idx_asset` (`asset_id`),
+  KEY `idx_borrower` (`borrower_id`),
+  KEY `idx_severity` (`severity`),
+  KEY `idx_status` (`status`),
+  KEY `idx_created_date` (`created_date`),
+  KEY `idx_auto_detected` (`auto_detected`),
+  CONSTRAINT `issue_tracker_ibfk_1` FOREIGN KEY (`asset_id`) REFERENCES `assets` (`id`) ON DELETE CASCADE,
+  CONSTRAINT `issue_tracker_ibfk_2` FOREIGN KEY (`borrower_id`) REFERENCES `borrowers` (`id`) ON DELETE CASCADE,
+  CONSTRAINT `issue_tracker_ibfk_3` FOREIGN KEY (`replacement_asset_id`) REFERENCES `assets` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `issue_tracker_ibfk_4` FOREIGN KEY (`reported_by`) REFERENCES `users` (`id`),
+  CONSTRAINT `issue_tracker_ibfk_5` FOREIGN KEY (`assigned_to`) REFERENCES `users` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `issue_tracker_ibfk_6` FOREIGN KEY (`resolved_by`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER issue_tracker_before_insert_meta
+BEFORE INSERT ON issue_tracker
+FOR EACH ROW
+BEGIN
+    IF NEW.reported_by IS NULL AND @current_user_id IS NOT NULL THEN
+        SET NEW.reported_by = @current_user_id;
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER validate_issue_tracker_insert
+BEFORE INSERT ON issue_tracker
+FOR EACH ROW
+BEGIN
+    -- Clarify solution requires solution_plus
+    IF NEW.solution = 'Clarify' AND (NEW.solution_plus IS NULL OR NEW.solution_plus = '') THEN
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'solution_plus is required when solution is set to Clarify';
+    END IF;
+    
+    -- Replacement solution requires replacement_asset_id
+    IF NEW.solution = 'Replaced' AND NEW.replacement_asset_id IS NULL THEN
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'replacement_asset_id is required when solution is set to Replaced';
+    END IF;
+    
+    -- Asset Issue requires asset_id
+    IF NEW.issue_type = 'Asset Issue' AND NEW.asset_id IS NULL THEN
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'asset_id is required for Asset Issue type';
+    END IF;
+    
+    -- Borrower Issue requires borrower_id
+    IF NEW.issue_type = 'Borrower Issue' AND NEW.borrower_id IS NULL THEN
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'borrower_id is required for Borrower Issue type';
+    END IF;
+    
+    -- Auto-set resolved_date when status becomes Resolved or Closed
+    IF NEW.status IN ('Resolved', 'Closed') AND NEW.resolved_date IS NULL THEN
+        SET NEW.resolved_date = NOW();
+    END IF;
+    
+    -- Auto-set resolved_by when status becomes Resolved or Closed
+    IF NEW.status IN ('Resolved', 'Closed') AND NEW.resolved_by IS NULL AND @current_user_id IS NOT NULL THEN
+        SET NEW.resolved_by = @current_user_id;
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER issue_tracker_after_insert_log
+AFTER INSERT ON issue_tracker
+FOR EACH ROW
+BEGIN
+    DECLARE set_fields JSON DEFAULT JSON_ARRAY();
+    DECLARE new_vals JSON DEFAULT JSON_OBJECT();
+    
+    -- Build JSON of non-NULL inserted fields
+    IF NEW.issue_type IS NOT NULL THEN
+        SET set_fields = JSON_ARRAY_APPEND(set_fields, '$', 'issue_type');
+        SET new_vals = JSON_SET(new_vals, '$.issue_type', NEW.issue_type);
+    END IF;
+    IF NEW.asset_id IS NOT NULL THEN
+        SET set_fields = JSON_ARRAY_APPEND(set_fields, '$', 'asset_id');
+        SET new_vals = JSON_SET(new_vals, '$.asset_id', NEW.asset_id);
+    END IF;
+    IF NEW.borrower_id IS NOT NULL THEN
+        SET set_fields = JSON_ARRAY_APPEND(set_fields, '$', 'borrower_id');
+        SET new_vals = JSON_SET(new_vals, '$.borrower_id', NEW.borrower_id);
+    END IF;
+    IF NEW.title IS NOT NULL THEN
+        SET set_fields = JSON_ARRAY_APPEND(set_fields, '$', 'title');
+        SET new_vals = JSON_SET(new_vals, '$.title', NEW.title);
+    END IF;
+    IF NEW.severity IS NOT NULL THEN
+        SET set_fields = JSON_ARRAY_APPEND(set_fields, '$', 'severity');
+        SET new_vals = JSON_SET(new_vals, '$.severity', NEW.severity);
+    END IF;
+    IF NEW.status IS NOT NULL THEN
+        SET set_fields = JSON_ARRAY_APPEND(set_fields, '$', 'status');
+        SET new_vals = JSON_SET(new_vals, '$.status', NEW.status);
+    END IF;
+    
+    INSERT INTO issue_tracker_change_log (issue_id, change_type, changed_fields, new_values, changed_by)
+    VALUES (NEW.id, 'INSERT', set_fields, new_vals, COALESCE(@current_user_id, NEW.reported_by));
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER validate_issue_tracker_update
+BEFORE UPDATE ON issue_tracker
+FOR EACH ROW
+BEGIN
+    -- Clarify solution requires solution_plus
+    IF NEW.solution = 'Clarify' AND (NEW.solution_plus IS NULL OR NEW.solution_plus = '') THEN
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'solution_plus is required when solution is set to Clarify';
+    END IF;
+    
+    -- Replacement solution requires replacement_asset_id
+    IF NEW.solution = 'Replaced' AND NEW.replacement_asset_id IS NULL THEN
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'replacement_asset_id is required when solution is set to Replaced';
+    END IF;
+    
+    -- Auto-set resolved_date when status changes to Resolved or Closed
+    IF OLD.status NOT IN ('Resolved', 'Closed') AND NEW.status IN ('Resolved', 'Closed') THEN
+        SET NEW.resolved_date = NOW();
+        IF @current_user_id IS NOT NULL THEN
+            SET NEW.resolved_by = @current_user_id;
+        END IF;
+    END IF;
+    
+    -- Clear resolved_date when status changes away from Resolved/Closed
+    IF OLD.status IN ('Resolved', 'Closed') AND NEW.status NOT IN ('Resolved', 'Closed') THEN
+        SET NEW.resolved_date = NULL;
+        SET NEW.resolved_by = NULL;
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER issue_tracker_after_update_log
+AFTER UPDATE ON issue_tracker
+FOR EACH ROW
+BEGIN
+    DECLARE changed_fields JSON DEFAULT JSON_ARRAY();
+    DECLARE old_vals JSON DEFAULT JSON_OBJECT();
+    DECLARE new_vals JSON DEFAULT JSON_OBJECT();
+    
+    -- Track all changed fields
+    IF OLD.status <=> NEW.status IS FALSE THEN
+        SET changed_fields = JSON_ARRAY_APPEND(changed_fields, '$', 'status');
+        SET old_vals = JSON_SET(old_vals, '$.status', OLD.status);
+        SET new_vals = JSON_SET(new_vals, '$.status', NEW.status);
+    END IF;
+    IF OLD.severity <=> NEW.severity IS FALSE THEN
+        SET changed_fields = JSON_ARRAY_APPEND(changed_fields, '$', 'severity');
+        SET old_vals = JSON_SET(old_vals, '$.severity', OLD.severity);
+        SET new_vals = JSON_SET(new_vals, '$.severity', NEW.severity);
+    END IF;
+    IF OLD.priority <=> NEW.priority IS FALSE THEN
+        SET changed_fields = JSON_ARRAY_APPEND(changed_fields, '$', 'priority');
+        SET old_vals = JSON_SET(old_vals, '$.priority', OLD.priority);
+        SET new_vals = JSON_SET(new_vals, '$.priority', NEW.priority);
+    END IF;
+    IF OLD.solution <=> NEW.solution IS FALSE THEN
+        SET changed_fields = JSON_ARRAY_APPEND(changed_fields, '$', 'solution');
+        SET old_vals = JSON_SET(old_vals, '$.solution', OLD.solution);
+        SET new_vals = JSON_SET(new_vals, '$.solution', NEW.solution);
+    END IF;
+    IF OLD.assigned_to <=> NEW.assigned_to IS FALSE THEN
+        SET changed_fields = JSON_ARRAY_APPEND(changed_fields, '$', 'assigned_to');
+        SET old_vals = JSON_SET(old_vals, '$.assigned_to', OLD.assigned_to);
+        SET new_vals = JSON_SET(new_vals, '$.assigned_to', NEW.assigned_to);
+    END IF;
+    IF OLD.resolved_by <=> NEW.resolved_by IS FALSE THEN
+        SET changed_fields = JSON_ARRAY_APPEND(changed_fields, '$', 'resolved_by');
+        SET old_vals = JSON_SET(old_vals, '$.resolved_by', OLD.resolved_by);
+        SET new_vals = JSON_SET(new_vals, '$.resolved_by', NEW.resolved_by);
+    END IF;
+    
+    -- Only log if something actually changed
+    IF JSON_LENGTH(changed_fields) > 0 THEN
+        INSERT INTO issue_tracker_change_log (issue_id, change_type, changed_fields, old_values, new_values, changed_by)
+        VALUES (NEW.id, 'UPDATE', changed_fields, old_vals, new_vals, COALESCE(@current_user_id, OLD.reported_by));
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER issue_tracker_before_delete
+BEFORE DELETE ON issue_tracker
+FOR EACH ROW
+BEGIN
+    -- If issue is not already resolved/closed, update it before deletion
+    IF OLD.status NOT IN ('Resolved', 'Closed') THEN
+        -- Can't UPDATE in a BEFORE DELETE trigger, so we just ensure it was marked resolved
+        -- This will prevent accidental deletion of open issues
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Cannot delete open issues. Please close or resolve the issue first.';
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER issue_tracker_after_delete_log
+AFTER DELETE ON issue_tracker
+FOR EACH ROW
+BEGIN
+    DECLARE deleted_fields JSON DEFAULT JSON_ARRAY();
+    DECLARE old_vals JSON DEFAULT JSON_OBJECT();
+    
+    -- Log all fields from deleted issue
+    IF OLD.issue_type IS NOT NULL THEN
+        SET deleted_fields = JSON_ARRAY_APPEND(deleted_fields, '$', 'issue_type');
+        SET old_vals = JSON_SET(old_vals, '$.issue_type', OLD.issue_type);
+    END IF;
+    IF OLD.asset_id IS NOT NULL THEN
+        SET deleted_fields = JSON_ARRAY_APPEND(deleted_fields, '$', 'asset_id');
+        SET old_vals = JSON_SET(old_vals, '$.asset_id', OLD.asset_id);
+    END IF;
+    IF OLD.title IS NOT NULL THEN
+        SET deleted_fields = JSON_ARRAY_APPEND(deleted_fields, '$', 'title');
+        SET old_vals = JSON_SET(old_vals, '$.title', OLD.title);
+    END IF;
+    IF OLD.status IS NOT NULL THEN
+        SET deleted_fields = JSON_ARRAY_APPEND(deleted_fields, '$', 'status');
+        SET old_vals = JSON_SET(old_vals, '$.status', OLD.status);
+    END IF;
+    IF OLD.solution IS NOT NULL THEN
+        SET deleted_fields = JSON_ARRAY_APPEND(deleted_fields, '$', 'solution');
+        SET old_vals = JSON_SET(old_vals, '$.solution', OLD.solution);
+    END IF;
+    
+    INSERT INTO issue_tracker_change_log (issue_id, change_type, changed_fields, old_values, changed_by)
+    VALUES (OLD.id, 'DELETE', deleted_fields, old_vals, COALESCE(@current_user_id, OLD.reported_by));
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `issue_tracker_change_log` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `issue_id` int(11) NOT NULL,
+  `change_type` enum('INSERT','UPDATE','DELETE') NOT NULL,
+  `changed_fields` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`changed_fields`)),
+  `old_values` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`old_values`)),
+  `new_values` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`new_values`)),
+  `changed_by` int(11) DEFAULT NULL,
+  `change_date` timestamp NULL DEFAULT current_timestamp(),
+  PRIMARY KEY (`id`),
+  KEY `changed_by` (`changed_by`),
+  KEY `idx_issue` (`issue_id`),
+  KEY `idx_change_type` (`change_type`),
+  KEY `idx_change_date` (`change_date`),
+  CONSTRAINT `issue_tracker_change_log_ibfk_1` FOREIGN KEY (`issue_id`) REFERENCES `issue_tracker` (`id`) ON DELETE CASCADE,
+  CONSTRAINT `issue_tracker_change_log_ibfk_2` FOREIGN KEY (`changed_by`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `label_templates` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `template_code` varchar(100) NOT NULL COMMENT 'Unique code like "CABLE"',
+  `template_name` varchar(200) NOT NULL COMMENT 'Human readable name',
+  `layout_json` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'Universal label design: SVG graphics, auto-populated field placeholders, styling' CHECK (json_valid(`layout_json`)),
+  `created_at` timestamp NULL DEFAULT current_timestamp(),
+  `created_by` int(11) DEFAULT NULL,
+  `last_modified_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
+  `last_modified_by` int(11) DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `template_code` (`template_code`),
+  KEY `created_by` (`created_by`),
+  KEY `last_modified_by` (`last_modified_by`),
+  KEY `idx_template_code` (`template_code`),
+  KEY `idx_template_name` (`template_name`),
+  CONSTRAINT `label_templates_ibfk_1` FOREIGN KEY (`created_by`) REFERENCES `users` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `label_templates_ibfk_2` FOREIGN KEY (`last_modified_by`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `lending_history` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `asset_id` int(11) NOT NULL,
+  `borrower_id` int(11) NOT NULL,
+  `checkout_date` datetime NOT NULL DEFAULT current_timestamp(),
+  `due_date` date DEFAULT NULL,
+  `return_date` datetime DEFAULT NULL,
+  `checked_out_by` int(11) DEFAULT NULL,
+  `checked_in_by` int(11) DEFAULT NULL,
+  `notes` text DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `checked_out_by` (`checked_out_by`),
+  KEY `checked_in_by` (`checked_in_by`),
+  KEY `idx_asset` (`asset_id`),
+  KEY `idx_borrower` (`borrower_id`),
+  KEY `idx_checkout_date` (`checkout_date`),
+  KEY `idx_return_date` (`return_date`),
+  CONSTRAINT `lending_history_ibfk_1` FOREIGN KEY (`asset_id`) REFERENCES `assets` (`id`) ON DELETE CASCADE,
+  CONSTRAINT `lending_history_ibfk_2` FOREIGN KEY (`borrower_id`) REFERENCES `borrowers` (`id`),
+  CONSTRAINT `lending_history_ibfk_3` FOREIGN KEY (`checked_out_by`) REFERENCES `users` (`id`),
+  CONSTRAINT `lending_history_ibfk_4` FOREIGN KEY (`checked_in_by`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER lending_history_before_insert_meta
+BEFORE INSERT ON lending_history
+FOR EACH ROW
+BEGIN
+    IF NEW.checked_out_by IS NULL AND @current_user_id IS NOT NULL THEN
+        SET NEW.checked_out_by = @current_user_id;
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER lending_history_before_update_meta
+BEFORE UPDATE ON lending_history
+FOR EACH ROW
+BEGIN
+    IF OLD.return_date IS NULL AND NEW.return_date IS NOT NULL THEN
+        IF NEW.checked_in_by IS NULL AND @current_user_id IS NOT NULL THEN
+            SET NEW.checked_in_by = @current_user_id;
+        END IF;
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `physical_audit_logs` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `physical_audit_id` int(11) NOT NULL COMMENT 'Reference to the audit session',
+  `asset_id` int(11) NOT NULL,
+  `audit_date` datetime NOT NULL DEFAULT current_timestamp(),
+  `audited_by` int(11) NOT NULL,
+  `status_found` enum('Good','Attention','Faulty','Missing','Retired','In Repair','In Transit','Expired','Unmanaged') DEFAULT 'Good',
+  `audit_task_id` int(11) DEFAULT NULL COMMENT 'Which audit task was run on this asset',
+  `audit_task_responses` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'User responses to the JSON sequence questions' CHECK (json_valid(`audit_task_responses`)),
+  `exception_type` enum('wrong-zone','unexpected-asset','damaged','missing-label','other') DEFAULT NULL,
+  `exception_details` text DEFAULT NULL COMMENT 'Details about the exception found',
+  `found_in_zone_id` int(11) DEFAULT NULL COMMENT 'Which zone the asset was actually found in (if different from expected)',
+  `auditor_action` enum('physical-move','virtual-update','no-action') DEFAULT NULL COMMENT 'What the auditor chose to do about wrong-zone assets',
+  `notes` text DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `audit_task_id` (`audit_task_id`),
+  KEY `found_in_zone_id` (`found_in_zone_id`),
+  KEY `idx_physical_audit` (`physical_audit_id`),
+  KEY `idx_asset` (`asset_id`),
+  KEY `idx_audit_date` (`audit_date`),
+  KEY `idx_audited_by` (`audited_by`),
+  KEY `idx_status_found` (`status_found`),
+  KEY `idx_exception_type` (`exception_type`),
+  CONSTRAINT `physical_audit_logs_ibfk_1` FOREIGN KEY (`physical_audit_id`) REFERENCES `physical_audits` (`id`) ON DELETE CASCADE,
+  CONSTRAINT `physical_audit_logs_ibfk_2` FOREIGN KEY (`asset_id`) REFERENCES `assets` (`id`) ON DELETE CASCADE,
+  CONSTRAINT `physical_audit_logs_ibfk_3` FOREIGN KEY (`audited_by`) REFERENCES `users` (`id`),
+  CONSTRAINT `physical_audit_logs_ibfk_4` FOREIGN KEY (`audit_task_id`) REFERENCES `audit_tasks` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `physical_audit_logs_ibfk_5` FOREIGN KEY (`found_in_zone_id`) REFERENCES `zones` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER physical_audit_logs_before_insert_meta
+BEFORE INSERT ON physical_audit_logs
+FOR EACH ROW
+BEGIN
+    -- Auto-populate audited_by from session variable if not provided
+    IF NEW.audited_by IS NULL OR NEW.audited_by = 0 THEN
+        SET NEW.audited_by = COALESCE(@current_user_id, 1);
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER update_assets_found
+AFTER INSERT ON physical_audit_logs
+FOR EACH ROW
+BEGIN
+    UPDATE physical_audits 
+    SET assets_found = assets_found + 1
+    WHERE id = NEW.physical_audit_id;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER update_asset_from_audit
+AFTER INSERT ON physical_audit_logs
+FOR EACH ROW
+BEGIN
+    DECLARE current_status VARCHAR(100);
+    
+    -- Update asset's last_audit date
+    UPDATE assets 
+    SET last_audit = DATE(NEW.audit_date),
+        last_audit_status = NEW.status_found
+    WHERE id = NEW.asset_id;
+    
+    -- Compare found status with current asset status
+    SELECT status INTO current_status FROM assets WHERE id = NEW.asset_id LIMIT 1;
+    
+    IF NEW.status_found != current_status THEN
+        UPDATE assets 
+        SET status = NEW.status_found
+        WHERE id = NEW.asset_id;
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `physical_audits` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `audit_type` enum('full-zone','spot-check') NOT NULL,
+  `zone_id` int(11) DEFAULT NULL COMMENT 'Zone being audited (NULL for spot-check audits)',
+  `audit_name` varchar(255) DEFAULT NULL COMMENT 'Custom name for the audit session',
+  `started_by` int(11) NOT NULL,
+  `started_at` datetime NOT NULL DEFAULT current_timestamp(),
+  `completed_at` datetime DEFAULT NULL,
+  `status` enum('in-progress','all-good','timeout','attention','cancelled') DEFAULT 'in-progress',
+  `timeout_minutes` int(11) DEFAULT NULL COMMENT 'Timeout setting used for this audit',
+  `issues_found` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'Array of issues: missing_assets, moved_assets, damaged_assets, etc.' CHECK (json_valid(`issues_found`)),
+  `assets_expected` int(11) DEFAULT NULL COMMENT 'Total assets expected to be found in zone',
+  `assets_found` int(11) DEFAULT 0 COMMENT 'Total assets actually found and scanned',
+  `notes` text DEFAULT NULL,
+  `cancelled_reason` text DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `idx_audit_type` (`audit_type`),
+  KEY `idx_zone` (`zone_id`),
+  KEY `idx_status` (`status`),
+  KEY `idx_started_at` (`started_at`),
+  KEY `idx_started_by` (`started_by`),
+  CONSTRAINT `physical_audits_ibfk_1` FOREIGN KEY (`zone_id`) REFERENCES `zones` (`id`),
+  CONSTRAINT `physical_audits_ibfk_2` FOREIGN KEY (`started_by`) REFERENCES `users` (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER calculate_assets_expected
+BEFORE INSERT ON physical_audits
+FOR EACH ROW
+BEGIN
+    DECLARE expected_count INT DEFAULT 0;
+    DECLARE v_timeout INT;
+    
+    -- For full-zone audits, calculate expected assets in the zone
+    IF NEW.audit_type = 'full-zone' AND NEW.zone_id IS NOT NULL THEN
+        SELECT COUNT(*) INTO expected_count
+        FROM assets 
+        WHERE zone_id = NEW.zone_id 
+        AND status NOT IN ('Missing', 'Retired');
+        
+        SET NEW.assets_expected = expected_count;
+    END IF;
+    
+    -- Set timeout from zone settings if not specified
+    IF NEW.timeout_minutes IS NULL AND NEW.zone_id IS NOT NULL THEN
+        SELECT audit_timeout_minutes INTO v_timeout
+        FROM zones 
+        WHERE id = NEW.zone_id
+        LIMIT 1;
+        
+        IF v_timeout IS NOT NULL THEN
+            SET NEW.timeout_minutes = v_timeout;
+        END IF;
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8mb4 */ ;
+/*!50003 SET character_set_results = utf8mb4 */ ;
+/*!50003 SET collation_connection  = utf8mb4_0900_ai_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`%`*/ /*!50003 TRIGGER auto_detect_audit_issues
+AFTER UPDATE ON physical_audits
+FOR EACH ROW
+BEGIN
+    DECLARE missing_count INT DEFAULT 0;
+    DECLARE zone_name VARCHAR(200);
+    
+    -- Only process when audit status changes to completed states
+    IF OLD.status = 'in-progress' AND NEW.status IN ('all-good', 'attention', 'timeout') THEN
+        
+        -- Get zone name for reporting
+        IF NEW.zone_id IS NOT NULL THEN
+            SELECT zone_name INTO zone_name FROM zones WHERE id = NEW.zone_id;
+        END IF;
+        
+        -- For full-zone audits, check for missing assets
+        IF NEW.audit_type = 'full-zone' AND NEW.assets_expected IS NOT NULL THEN
+            SET missing_count = GREATEST(0, NEW.assets_expected - NEW.assets_found);
+        END IF;
+        
+        -- Create issue for missing assets
+        IF missing_count > 0 THEN
+            INSERT INTO issue_tracker (
+                issue_type, title, description, severity, priority, status,
+                reported_by, auto_detected, detection_trigger, created_date, notes
+            )
+            VALUES (
+                'System Issue', 
+                CONCAT('Audit: Missing Assets in ', COALESCE(zone_name, 'Unknown Zone')),
+                CONCAT('Full zone audit completed with ', missing_count, ' missing assets. Expected: ', NEW.assets_expected, ', Found: ', NEW.assets_found, '. Audit ID: ', NEW.id),
+                CASE WHEN missing_count >= 5 THEN 'Critical' WHEN missing_count >= 2 THEN 'High' ELSE 'Medium' END,
+                'High', 'Open',
+                NEW.started_by, TRUE, 'AUDIT_MISSING_ASSETS', NOW(),
+                CONCAT('Physical Audit ID: ', NEW.id, ' in zone: ', COALESCE(zone_name, NEW.zone_id))
+            );
+        END IF;
+    END IF;
+END */;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `print_history` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `entity_type` enum('Asset','Template','Borrower','Zone','Report','Custom') NOT NULL,
+  `entity_id` int(11) DEFAULT NULL COMMENT 'ID of the asset/template/borrower/zone (NULL for reports)',
+  `label_template_id` int(11) DEFAULT NULL,
+  `printer_id` int(11) DEFAULT NULL,
+  `quantity` int(11) DEFAULT 1,
+  `print_status` enum('Success','Failed','Cancelled','Queued') NOT NULL,
+  `error_message` text DEFAULT NULL,
+  `rendered_data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'The actual data that was sent to printer (for debugging)' CHECK (json_valid(`rendered_data`)),
+  `printed_at` timestamp NULL DEFAULT current_timestamp(),
+  `printed_by` int(11) DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `label_template_id` (`label_template_id`),
+  KEY `idx_entity` (`entity_type`,`entity_id`),
+  KEY `idx_printed_at` (`printed_at`),
+  KEY `idx_printed_by` (`printed_by`),
+  KEY `idx_printer` (`printer_id`),
+  KEY `idx_status` (`print_status`),
+  CONSTRAINT `print_history_ibfk_1` FOREIGN KEY (`label_template_id`) REFERENCES `label_templates` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `print_history_ibfk_2` FOREIGN KEY (`printer_id`) REFERENCES `printer_settings` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `print_history_ibfk_3` FOREIGN KEY (`printed_by`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `printer_settings` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `printer_name` varchar(200) NOT NULL,
+  `description` text DEFAULT NULL,
+  `log` tinyint(1) DEFAULT 1 COMMENT 'Log all print jobs to this printer',
+  `can_be_used_for_reports` tinyint(1) DEFAULT 0 COMMENT 'Can this printer be used for printing reports',
+  `min_powerlevel_to_use` int(11) NOT NULL DEFAULT 75 COMMENT 'Minimum role power level required to use this printer',
+  `printer_plugin` enum('Ptouch','Brother','Zebra','System','PDF','Network','Custom') NOT NULL COMMENT 'Which printer plugin the client should send printer_settings to',
+  `printer_settings` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'Printer-specific settings: connection, paper size, DPI, margins, etc.' CHECK (json_valid(`printer_settings`)),
+  `created_at` timestamp NULL DEFAULT current_timestamp(),
+  `created_by` int(11) DEFAULT NULL,
+  `last_modified_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
+  `last_modified_by` int(11) DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `created_by` (`created_by`),
+  KEY `last_modified_by` (`last_modified_by`),
+  KEY `idx_printer_name` (`printer_name`),
+  KEY `idx_printer_plugin` (`printer_plugin`),
+  KEY `idx_min_powerlevel` (`min_powerlevel_to_use`),
+  KEY `idx_can_reports` (`can_be_used_for_reports`),
+  CONSTRAINT `printer_settings_ibfk_1` FOREIGN KEY (`created_by`) REFERENCES `users` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `printer_settings_ibfk_2` FOREIGN KEY (`last_modified_by`) REFERENCES `users` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `CONSTRAINT_1` CHECK (`min_powerlevel_to_use` >= 1 and `min_powerlevel_to_use` <= 100)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `roles` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `name` varchar(100) NOT NULL,
+  `power` int(11) NOT NULL CHECK (`power` >= 1 and `power` <= 100),
+  `created_at` timestamp NULL DEFAULT current_timestamp(),
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `name` (`name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `suppliers` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `name` varchar(200) NOT NULL,
+  `contact` varchar(200) DEFAULT NULL,
+  `email` varchar(255) DEFAULT NULL,
+  `phone` varchar(50) DEFAULT NULL,
+  `website` varchar(255) DEFAULT NULL,
+  `notes` text DEFAULT NULL,
+  `created_at` timestamp NULL DEFAULT current_timestamp(),
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `templates` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `template_code` varchar(50) DEFAULT NULL,
+  `asset_tag_generation_string` varchar(500) DEFAULT NULL,
+  `description` text DEFAULT NULL,
+  `active` tinyint(1) DEFAULT 1,
+  `asset_type` enum('N','B','L','C') DEFAULT NULL,
+  `name` varchar(255) DEFAULT NULL,
+  `category_id` int(11) DEFAULT NULL,
+  `manufacturer` varchar(200) DEFAULT NULL,
+  `model` varchar(200) DEFAULT NULL,
+  `zone_id` int(11) DEFAULT NULL,
+  `zone_plus` enum('Floating Local','Floating Global','Clarify') DEFAULT NULL,
+  `zone_note` text DEFAULT NULL,
+  `status` enum('Good','Attention','Faulty','Missing','Retired','In Repair','In Transit','Expired','Unmanaged') DEFAULT NULL,
+  `price` decimal(12,2) DEFAULT NULL CHECK (`price` is null or `price` >= 0),
+  `purchase_date` date DEFAULT NULL COMMENT 'Default purchase date for assets created from this template',
+  `purchase_date_now` tinyint(1) DEFAULT 0 COMMENT 'Auto-set purchase date to current date when creating assets',
+  `warranty_until` date DEFAULT NULL,
+  `warranty_auto` tinyint(1) DEFAULT 0 COMMENT 'Auto-calculate warranty_until from purchase_date',
+  `warranty_auto_amount` int(11) DEFAULT NULL COMMENT 'Number of days/years for warranty calculation',
+  `warranty_auto_unit` enum('days','years') DEFAULT 'years' COMMENT 'Unit for warranty auto-calculation',
+  `expiry_date` date DEFAULT NULL,
+  `expiry_auto` tinyint(1) DEFAULT 0 COMMENT 'Auto-calculate expiry_date from purchase_date',
+  `expiry_auto_amount` int(11) DEFAULT NULL COMMENT 'Number of days/years for expiry calculation',
+  `expiry_auto_unit` enum('days','years') DEFAULT 'years' COMMENT 'Unit for expiry auto-calculation',
+  `quantity_total` int(11) DEFAULT NULL,
+  `quantity_used` int(11) DEFAULT NULL,
+  `supplier_id` int(11) DEFAULT NULL,
+  `lendable` tinyint(1) DEFAULT NULL,
+  `lending_status` enum('Available','Borrowed','Overdue','Deployed','Illegally Handed Out','Stolen') DEFAULT 'Available' COMMENT 'Default lending status for assets created from this template',
+  `minimum_role_for_lending` int(11) DEFAULT NULL,
+  `audit_task_id` int(11) DEFAULT NULL,
+  `label_template_id` int(11) DEFAULT NULL,
+  `no_scan` enum('Yes','Ask','No') DEFAULT NULL,
+  `notes` text DEFAULT NULL,
+  `additional_fields` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`additional_fields`)),
+  `created_at` timestamp NULL DEFAULT current_timestamp(),
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `template_code` (`template_code`),
+  KEY `category_id` (`category_id`),
+  KEY `zone_id` (`zone_id`),
+  KEY `supplier_id` (`supplier_id`),
+  KEY `audit_task_id` (`audit_task_id`),
+  KEY `idx_template_code` (`template_code`),
+  KEY `idx_label_template` (`label_template_id`),
+  KEY `idx_asset_tag_generation` (`asset_tag_generation_string`),
+  CONSTRAINT `fk_template_label_template` FOREIGN KEY (`label_template_id`) REFERENCES `label_templates` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `templates_ibfk_1` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `templates_ibfk_2` FOREIGN KEY (`zone_id`) REFERENCES `zones` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `templates_ibfk_3` FOREIGN KEY (`supplier_id`) REFERENCES `suppliers` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `templates_ibfk_4` FOREIGN KEY (`audit_task_id`) REFERENCES `audit_tasks` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `users` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `name` varchar(200) NOT NULL,
+  `username` varchar(100) NOT NULL,
+  `password` varchar(255) NOT NULL,
+  `pin_code` varchar(8) DEFAULT NULL,
+  `login_string` varchar(255) DEFAULT NULL,
+  `role_id` int(11) NOT NULL,
+  `email` varchar(255) DEFAULT NULL,
+  `phone` varchar(50) DEFAULT NULL,
+  `notes` text DEFAULT NULL,
+  `active` tinyint(1) DEFAULT 1,
+  `last_login_date` datetime DEFAULT NULL,
+  `created_date` timestamp NULL DEFAULT current_timestamp(),
+  `password_reset_token` varchar(255) DEFAULT NULL,
+  `password_reset_expiry` datetime DEFAULT NULL,
+  `preferences` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'User personalization settings: common (all clients) + client-specific (web, mobile, desktop)' CHECK (json_valid(`preferences`)),
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `username` (`username`),
+  KEY `role_id` (`role_id`),
+  KEY `idx_username` (`username`),
+  KEY `idx_login_string` (`login_string`),
+  CONSTRAINT `users_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `zones` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `zone_name` varchar(200) NOT NULL,
+  `zone_notes` text DEFAULT NULL,
+  `zone_type` enum('Building','Floor','Room','Storage Area') NOT NULL,
+  `zone_code` varchar(50) DEFAULT NULL,
+  `mini_code` varchar(50) DEFAULT NULL,
+  `parent_id` int(11) DEFAULT NULL,
+  `include_in_parent` tinyint(1) DEFAULT 1,
+  `audit_timeout_minutes` int(11) DEFAULT 60 COMMENT 'Audit timeout in minutes for this zone',
+  PRIMARY KEY (`id`),
+  KEY `idx_parent` (`parent_id`),
+  KEY `idx_type` (`zone_type`),
+  CONSTRAINT `zones_ibfk_1` FOREIGN KEY (`parent_id`) REFERENCES `zones` (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+--
+-- WARNING: can't read the INFORMATION_SCHEMA.libraries table. It's most probably an old server 12.0.2-MariaDB-ubu2404.
+--
+
+/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
+/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
+/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
+/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
+

+ 39 - 0
backend/seckelapi/Containerfile

@@ -0,0 +1,39 @@
+# Multi-stage build for SeckelAPI
+FROM docker.io/library/rust:1.92-slim-trixie AS builder
+
+WORKDIR /build
+
+# Install build dependencies
+RUN apt-get update && \
+    apt-get install -y pkg-config libssl-dev && \
+    rm -rf /var/lib/apt/lists/*
+
+# Copy source code
+COPY sources/ .
+
+# Build release binary
+RUN cargo build --release
+
+# Runtime stage - minimal Debian image
+FROM docker.io/library/debian:trixie-slim
+
+WORKDIR /app
+
+# Install runtime dependencies
+RUN apt-get update && \
+    apt-get install -y ca-certificates libssl3 && \
+    rm -rf /var/lib/apt/lists/*
+
+# Copy binary and config from builder
+COPY --from=builder /build/target/release/seckelapi /app/seckelapi
+COPY sources/config/ /app/config/
+
+# Expose API port
+EXPOSE 5777
+
+# Run as non-root user
+RUN useradd -r -u 1000 seckelapi && \
+    chown -R seckelapi:seckelapi /app
+USER seckelapi
+
+CMD ["/app/seckelapi"]

+ 18 - 0
backend/seckelapi/config/basics.toml

@@ -0,0 +1,18 @@
+# Basic ahh configs
+
+[server]
+host = "0.0.0.0"
+port = 5777
+request_body_limit_mb = 10 
+
+[database]
+host = "localhost"
+port = 3306
+database = "beepzone"
+username = "beepzone"
+password = "changeme123"
+min_connections = 1
+max_connections = 10
+connection_timeout_seconds = 2
+connection_timeout_wait = 2
+connection_check = 1

+ 45 - 0
backend/seckelapi/config/functions.toml

@@ -0,0 +1,45 @@
+# auto generation of things
+[auto_generation]
+
+[auto_generation.assets]
+field = "asset_numeric_id"
+type = "numeric"
+length = 8
+range_min = 10000000
+range_max = 99999999
+max_attempts = 10
+# on what event seckel api schould try to generate auto gen value incaase client send empty value
+on_action = "insert"
+
+[scheduled_queries]
+
+# Single idempotent task that sets the correct state atomically to avoid double-trigger inserts
+[[scheduled_queries.tasks]]
+name = "sync_overdue_and_stolen"
+description = "Atomically set lending_status to Overdue (1-13 days late) or Stolen (>=14 days late) only if it changed"
+query = """
+  -- Use max lateness per asset to avoid flip-flopping due to multiple open lending rows
+  -- Removed issue_tracker check from WHERE clause to avoid MySQL trigger conflict
+  UPDATE assets a
+  INNER JOIN (
+      SELECT lh.asset_id, MAX(DATEDIFF(CURDATE(), lh.due_date)) AS days_late
+      FROM lending_history lh
+      WHERE lh.return_date IS NULL
+        AND lh.due_date IS NOT NULL
+      GROUP BY lh.asset_id
+  ) late ON a.id = late.asset_id
+  SET a.lending_status = CASE
+      WHEN a.asset_type IN ('N','B') AND late.days_late >= 14 THEN 'Stolen'
+      WHEN a.asset_type IN ('N','B') AND late.days_late BETWEEN 1 AND 13 THEN 'Overdue'
+      ELSE a.lending_status
+  END
+  WHERE a.asset_type IN ('N','B')
+    AND (
+      (late.days_late >= 14 AND a.lending_status <> 'Stolen')
+      OR
+      (late.days_late BETWEEN 1 AND 13 AND a.lending_status <> 'Overdue')
+    )
+"""
+interval_minutes = 2
+run_on_startup = true
+enabled = true

+ 31 - 0
backend/seckelapi/config/logging.toml

@@ -0,0 +1,31 @@
+# Logging Configuration
+[logging]
+# all logs can be commented out to disable them if you want yk, because you probably dont need more than the combined log
+request_log = "./logs/request.log"
+query_log = "./logs/queries.log"
+error_log = "./logs/error.log"
+warning_log = "./logs/warning.log"
+info_log = "./logs/info.log"
+combined_log = "./logs/sequel.log"
+
+# Log levels: debug, info, warn, error
+level = "info"
+
+# mask fields that are sensitive in logs (they are hashed anyways but why log bcrypt hashes in ur logs thats dumb)
+mask_passwords = true
+
+# other values that we might not want in query logs (also applies to request logs)
+sensitive_fields = ["login_string", "password_reset_token", "pin_code"]
+
+# Custom log filters, route specific log entries to separate files using regex ... yes I have autism why are you asking?
+[[logging.custom_filters]]
+name = "security_violations"
+output_file = "./logs/security_violations.log"
+pattern = "(Permission denied|Too many WHERE|Authentication failed|invalid credentials|invalid PIN|invalid token)"
+enabled = true
+
+[[logging.custom_filters]]
+name = "admin_transactions"
+output_file = "./logs/admin_activity.log"
+pattern = "user=admin|power=100"
+enabled = true

+ 210 - 0
backend/seckelapi/config/security.toml

@@ -0,0 +1,210 @@
+# prepare for evil ass autism configs!
+[security]
+# Yk what this is, if not read the fkn readme
+whitelisted_pin_ips = ["192.168.1.0/24", "127.0.0.1"]
+whitelisted_string_ips = ["192.168.5.0/24", "127.0.0.1"]
+
+# session stuffs
+session_timeout_minutes = 60  # def session timeout (makes session key go bye bye)
+refresh_session_on_activity = true  # most useless thing ever most likely as nobody will ever disable this but sure you can just kill a users session during active use right?
+max_concurrent_sessions = 3  # how many gooning session to allow per user (you can set custom ones per powerlevel btw)
+session_cleanup_interval_minutes = 5  # how often to actually check on the session timeout, we aint gotta spam it none stop tbh
+
+# PIN and Token Auth
+hash_pins = false  # weather or not to use bcrypt for pin field (left off for dev work)
+hash_tokens = false  # Same as above
+pin_column = "pin_code"
+token_column = "login_string"
+
+# Rate Limiting, need i say more?
+enable_rate_limiting = true  # Do yuo wahnt raten limitierung or not?
+
+# If i have to explain these to you just dont use this software
+auth_rate_limit_per_minute = 10000
+auth_rate_limit_per_second = 50000
+
+# api rape limitz
+api_rate_limit_per_minute = 100000
+api_rate_limit_per_second = 100000
+
+# default query limits to avoid someone spamming quieries on a table with 271k rows
+default_max_limit = 10000
+default_max_where_conditions = 1000
+
+# own user preferences level
+# Determines what an user can do with their own little preference store
+# - "read-own-only": kiosk ah ruling
+# - "read-write-own": what you probably want for most users
+# - "read-write-all": adminier maybe ?
+default_user_settings_access = "read-write-own"
+
+# define what tables exist
+# known tables for wildcard permissions (*:rw) and to prevent SQL injection via table names cuz thats a thing
+known_tables = [
+    "users", "roles", "assets", "categories", "zones",
+    "suppliers", "templates", "audit_tasks", "borrowers", 
+    "lending_history", "audit_history", "maintenance_log",
+    "asset_change_log", "issue_tracker", "issue_tracker_change_log",
+    "physical_audits", "physical_audit_logs",
+    "label_templates", "printer_settings", "print_history"
+]
+
+# tables you cant write or change using proxi in any way not even user overrides below
+read_only_tables = ["asset_change_log", "issue_tracker_change_log", "print_history"]
+
+# column names banned from being written to by default (this is however overwritable on a per table per column per user type schizo settings below)
+global_write_protected_columns = [
+    "id",
+    "created_date",
+    "created_at",
+    "last_modified_date",
+    "updated_at",
+    "last_modified_at",
+]
+
+# note to myself how the rbac system kinda works
+# Format: role_power contains both basic table rules and advanced column rules
+# Basic rules: "table:permission" (r = read, w = write, rw = read+write, * = all tables (for like admins or smth))
+# Advanced rules: "table.column:permission" for more granular column level control
+# Column permissions: r = read, w = write, rw = read+write, block = blocked (obviously)
+# Use "table.*:block" to block all columns, then "table.specific_column:r" to allow specific ones
+# Use "table.*:r" to allow all columns, then "table.sensitive_column:block" to block specific ones
+
+# In the future even more advaned rules called schizo_rules will be implemented where you can define sql logic based rules
+# like "only allow access to rows where user_id = current_user_id" or "only allow access to assets where status != 'Stolen'"
+
+# i let an llm comment on the crap below so i can understand what ive done in like 3 months when i forget everything
+
+[permissions]
+
+[permissions."100"]
+# Admin - full access to everything
+basic_rules = [
+    "*:rw",  # Example of wildcard full access to all known tables
+    "asset_change_log:r",  # More or less redundant but whatever
+    "issue_tracker_change_log:r"  # Same as above
+]
+advanced_rules = [
+    # Further granularity wow!
+    "assets.asset_numeric_id:r",
+    "assets.created_by:r",
+    "assets.last_modified_by:r",
+    "users.password_hash:block",
+]
+max_limit = 500
+max_where_conditions = 50
+session_timeout_minutes = 120  # Admins get longer sessions (2 hours)
+max_concurrent_sessions = 5  # Admins can have more concurrent sessions
+rollback_on_error = true  # Rollback batch operations on any error
+allow_batch_operations = true  # Admins can use batch operations
+user_settings_access = "read-write-all"  # Admins can modify any user's preferences
+
+[permissions."75"]
+# Manager - full asset management, limited user access
+rollback_on_error = true  # Rollback batch operations on any error
+allow_batch_operations = true  # Managers can use batch operations
+basic_rules = [
+    "assets:rw",
+    "lending_history:rw", 
+    "audit_history:rw",
+    "maintenance_log:rw",
+    "borrowers:rw",
+    "categories:rw",
+    "zones:rw",
+    "suppliers:rw",
+    "templates:rw",
+    "audit_tasks:rw",
+    "issue_tracker:rw",
+    "physical_audits:rw",
+    "physical_audit_logs:rw",
+    "label_templates:rw",
+    "printer_settings:rw",
+    "print_history:r",
+    "users:r",  # Basic read access, then restricted by advanced rules below
+    "roles:r",
+    "asset_change_log:r",
+    "issue_tracker_change_log:r"
+]
+advanced_rules = [
+    # Table-specific protected (same as admin)
+    "assets.asset_numeric_id:r",
+    "assets.created_by:r",
+    "assets.last_modified_by:r",
+    # Users table - can read most info but not sensitive auth data
+    "users.password:block",
+    "users.password_hash:block",
+    "users.pin_code:block", 
+    "users.login_string:block",
+    "users.password_reset_token:block",
+    "users.password_reset_expiry:block",
+]
+# Query limits (moderate for managers)
+max_limit = 200
+max_where_conditions = 20
+user_settings_access = "read-write-own"  # Managers can only modify their own preferences
+
+[permissions."50"]
+# Staff - asset and lending management, NO user access
+rollback_on_error = false  # Don't rollback batch operations on error (continue processing)
+allow_batch_operations = true  # Staff can use batch operations
+basic_rules = [
+    "assets:rw",
+    "lending_history:rw",
+    "audit_history:rw",
+    "maintenance_log:rw",
+    "borrowers:rw",
+    "categories:r",
+    "zones:r",
+    "suppliers:r",
+    "templates:r",
+    "audit_tasks:r",
+    "issue_tracker:r",
+    "physical_audits:r",
+    "physical_audit_logs:r",
+    "label_templates:r",
+    "printer_settings:r",
+    "print_history:r",
+    "asset_change_log:r",
+    "issue_tracker_change_log:r"
+]
+advanced_rules = [
+    # Table-specific protected (same as admin/manager)
+    "assets.asset_numeric_id:r",
+    "assets.created_by:r",
+    "assets.last_modified_by:r",
+]
+# No users table access for staff - security requirement
+# Query limits (standard for staff)
+max_limit = 100
+max_where_conditions = 10
+user_settings_access = "read-write-own"  # Staff can only modify their own preferences
+
+[permissions."25"]
+# Student - read-only access, no financial data, no user access, no change logs
+rollback_on_error = true  # Rollback batch operations on any error
+allow_batch_operations = false  # Students cannot use batch operations
+basic_rules = [
+    "assets:r",
+    "lending_history:r",
+    "borrowers:r",
+    "categories:r",
+    "zones:r"
+]
+advanced_rules = [
+    # Assets table - hide financial and sensitive info
+    "assets.price:block",
+    "assets.purchase_date:block",
+    "assets.supplier_id:block",
+    "assets.warranty_expiry:block",
+    # Borrowers table - hide personal contact info
+    "borrowers.email:block",
+    "borrowers.phone_number:block",
+    "borrowers.notes:block"
+]
+
+# Query limits (restricted for students)
+max_limit = 50
+max_where_conditions = 5
+user_settings_access = "read-own-only"  # Students can only read their own preferences, not modify
+
+

+ 720 - 0
beepzone-helper.sh

@@ -0,0 +1,720 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+# Simple TUI-based BeepZone setup helper using `dialog`.
+# Targets macOS + Debian-ish Linux (podman, rustup mysql-client already installed).
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+WORK_DIR="${SCRIPT_DIR}"
+
+: "${DIALOG:=dialog}"
+
+if ! command -v "$DIALOG" >/dev/null 2>&1; then
+  echo "\n[ERROR] 'dialog' is not installed or not in PATH." >&2
+  echo "On macOS:   brew install dialog" >&2
+  echo "On Debian:  sudo apt-get install dialog" >&2
+  exit 1
+fi
+
+if ! command -v podman >/dev/null 2>&1; then
+  "$DIALOG" --title "BeepZone Setup" --msgbox "podman is required but not found in PATH.\nPlease install and configure podman (podman-desktop recommended)." 10 60
+  exit 1
+fi
+
+# Check for MariaDB/MySQL client - try common brew locations on macOS
+MYSQL_CLIENT=""
+if command -v mariadb >/dev/null 2>&1; then
+  MYSQL_CLIENT="mariadb"
+elif command -v mysql >/dev/null 2>&1; then
+  MYSQL_CLIENT="mysql"
+elif [[ -x /opt/homebrew/opt/mysql-client/bin/mysql ]]; then
+  MYSQL_CLIENT="/opt/homebrew/opt/mysql-client/bin/mysql"
+  export PATH="/opt/homebrew/opt/mysql-client/bin:$PATH"
+elif [[ -x /opt/homebrew/opt/mariadb/bin/mariadb ]]; then
+  MYSQL_CLIENT="/opt/homebrew/opt/mariadb/bin/mariadb"
+  export PATH="/opt/homebrew/opt/mariadb/bin:$PATH"
+elif [[ -x /opt/homebrew/bin/mariadb ]]; then
+  MYSQL_CLIENT="/opt/homebrew/bin/mariadb"
+  export PATH="/opt/homebrew/bin:$PATH"
+elif [[ -x /opt/homebrew/bin/mysql ]]; then
+  MYSQL_CLIENT="/opt/homebrew/bin/mysql"
+  export PATH="/opt/homebrew/bin:$PATH"
+elif [[ -x /usr/local/opt/mysql-client/bin/mysql ]]; then
+  MYSQL_CLIENT="/usr/local/opt/mysql-client/bin/mysql"
+  export PATH="/usr/local/opt/mysql-client/bin:$PATH"
+elif [[ -x /usr/local/bin/mariadb ]]; then
+  MYSQL_CLIENT="/usr/local/bin/mariadb"
+  export PATH="/usr/local/bin:$PATH"
+elif [[ -x /usr/local/bin/mysql ]]; then
+  MYSQL_CLIENT="/usr/local/bin/mysql"
+  export PATH="/usr/local/bin:$PATH"
+else
+  "$DIALOG" --title "BeepZone Setup" --msgbox "MariaDB/MySQL client is required but not found.\n\nSearched locations:\n- System PATH\n- /opt/homebrew/opt/mysql-client/bin/\n- /opt/homebrew/opt/mariadb/bin/\n- /opt/homebrew/bin/\n- /usr/local/opt/mysql-client/bin/\n- /usr/local/bin/\n\nOn macOS:   brew install mysql-client\nOn Debian:  sudo apt-get install mariadb-client" 18 70
+  exit 1
+fi
+
+TMP_FILE="$(mktemp)"
+CHOICE_FILE="$(mktemp)"
+trap 'rm -f "$TMP_FILE" "$CHOICE_FILE"' EXIT
+
+LOG_FILE="/tmp/beepzone-helper.log"
+> "$LOG_FILE"
+
+ENV_FILE="$SCRIPT_DIR/.env"
+
+# Load existing settings from .env if present
+if [[ -f "$ENV_FILE" ]]; then
+  source "$ENV_FILE"
+fi
+
+# Set defaults if not loaded from .env
+: "${BEEPZONE_DB_CONTAINER_NAME:=beepzone-mariadb}"
+: "${BEEPZONE_DB_IMAGE:=mariadb:12}"
+: "${DB_HOST:=127.0.0.1}"
+: "${DB_PORT:=3306}"
+: "${DB_NAME:=beepzone}"
+: "${DB_USER:=beepzone}"
+: "${DB_PASS:=beepzone}"
+: "${DB_ROOT_PASSWORD:=root}"
+: "${SECKELAPI_REPO:=https://git.teleco.ch/crt/seckelapi.git}"
+: "${CLIENT_REPO:=https://git.teleco.ch/crt/beepzone-client-egui-emo.git}"
+: "${DEPLOYMENT_TYPE:=clean}"
+
+save_env() {
+  cat > "$ENV_FILE" << EOF
+# BeepZone Setup Configuration
+# Auto-generated by beepzone-helper.sh
+
+BEEPZONE_DB_CONTAINER_NAME="$BEEPZONE_DB_CONTAINER_NAME"
+BEEPZONE_DB_IMAGE="$BEEPZONE_DB_IMAGE"
+DB_HOST="$DB_HOST"
+DB_PORT="$DB_PORT"
+DB_NAME="$DB_NAME"
+DB_USER="$DB_USER"
+DB_PASS="$DB_PASS"
+DB_ROOT_PASSWORD="$DB_ROOT_PASSWORD"
+SECKELAPI_REPO="$SECKELAPI_REPO"
+CLIENT_REPO="$CLIENT_REPO"
+DEPLOYMENT_TYPE="$DEPLOYMENT_TYPE"
+EOF
+}
+
+ask_main_menu() {
+  while true; do
+    $DIALOG --clear \
+      --title "BeepZone Setup" \
+      --menu "Choose an action" 16 76 6 \
+      1 "Configure & run MariaDB (podman)" \
+      2 "Import DB schema & data" \
+      3 "Manage users & roles" \
+      4 "Configure & setup SeckelAPI" \
+      5 "Build desktop client" \
+      6 "Quit" 2>"$CHOICE_FILE"
+
+    choice=$(<"$CHOICE_FILE")
+    case $choice in
+      1) configure_and_run_db ;;
+      2) import_schema_and_seed ;;
+      3) manage_users_and_roles ;;
+      4) setup_seckelapi ;;
+      5) build_desktop_client ;;
+      6) exit 0 ;;
+    esac
+  done
+}
+
+configure_and_run_db() {
+  $DIALOG --form "MariaDB container configuration" 18 70 8 \
+    "DB Host"        1 1  "$DB_HOST"         1 18  30 30 \
+    "DB Port"        2 1  "$DB_PORT"         2 18  30 30 \
+    "DB Name"        3 1  "$DB_NAME"         3 18  30 30 \
+    "DB User"        4 1  "$DB_USER"         4 18  30 30 \
+    "DB Password"    5 1  "$DB_PASS"         5 18  30 30 \
+    "Root Password"  6 1  "$DB_ROOT_PASSWORD" 6 18  30 30 \
+    "Container Name" 7 1  "$BEEPZONE_DB_CONTAINER_NAME" 7 18  30 30 \
+    "Image"          8 1  "$BEEPZONE_DB_IMAGE" 8 18  30 30 2>"$TMP_FILE" || return
+
+  # Parse dialog output (8 lines, one per field)
+  {
+    read -r DB_HOST
+    read -r DB_PORT
+    read -r DB_NAME
+    read -r DB_USER
+    read -r DB_PASS
+    read -r DB_ROOT_PASSWORD
+    read -r BEEPZONE_DB_CONTAINER_NAME
+    read -r BEEPZONE_DB_IMAGE
+  } < "$TMP_FILE"
+
+  save_env
+
+  if podman ps -a --format '{{.Names}}' | grep -q "^${BEEPZONE_DB_CONTAINER_NAME}$"; then
+    $DIALOG --yesno "Container '$BEEPZONE_DB_CONTAINER_NAME' already exists.\n\nStart (or restart) it now?" 10 60
+    if [[ $? -eq 0 ]]; then
+      podman start "$BEEPZONE_DB_CONTAINER_NAME" >/dev/null 2>&1 || podman restart "$BEEPZONE_DB_CONTAINER_NAME" >/dev/null 2>&1 || true
+      $DIALOG --msgbox "Container '$BEEPZONE_DB_CONTAINER_NAME' started (or already running)." 7 60
+    fi
+    return
+  fi
+
+  run_cmd=(
+    podman run -d
+    --name "${BEEPZONE_DB_CONTAINER_NAME}"
+    -e "MARIADB_ROOT_PASSWORD=${DB_ROOT_PASSWORD}"
+    -e "MARIADB_DATABASE=${DB_NAME}"
+    -e "MARIADB_USER=${DB_USER}"
+    -e "MARIADB_PASSWORD=${DB_PASS}"
+    -p "${DB_PORT}:3306"
+    "${BEEPZONE_DB_IMAGE}"
+  )
+
+  pretty_cmd="podman run -d \\
+--name ${BEEPZONE_DB_CONTAINER_NAME} \\
+-e MARIADB_ROOT_PASSWORD=${DB_ROOT_PASSWORD} \\
+-e MARIADB_DATABASE=${DB_NAME} \\
+-e MARIADB_USER=${DB_USER} \\
+-e MARIADB_PASSWORD=${DB_PASS} \\
+-p ${DB_PORT}:3306 ${BEEPZONE_DB_IMAGE}"
+
+  if $DIALOG --yesno "About to run:\n\n${pretty_cmd}\n\nProceed?" 17 76; then
+    if "${run_cmd[@]}" >>"$LOG_FILE" 2>&1; then
+      $DIALOG --msgbox "MariaDB container '${BEEPZONE_DB_CONTAINER_NAME}' started successfully." 8 70
+    else
+      error_log=$(tail -20 "$LOG_FILE")
+      $DIALOG --title "Error" --msgbox "Failed to start MariaDB container.\n\nError:\n${error_log}" 20 76
+    fi
+  fi
+}
+
+import_schema_and_seed() {
+  local import_type schema_file full_dump_file
+  schema_file="$WORK_DIR/backend/database/schema/beepzone-schema-dump.sql"
+  full_dump_file="$WORK_DIR/backend/database/dev/beepzone-full-dump.sql"
+
+  # Ask what type of import
+  $DIALOG --clear \
+    --title "Database Import" \
+    --menu "Select import type" 12 70 2 \
+    1 "Full dump (schema + data in one file)" \
+    2 "Clean schema only (no data)" 2>"$CHOICE_FILE" || return
+
+  import_type=$(<"$CHOICE_FILE")
+
+  if [[ "$import_type" == "1" ]]; then
+    # Full dump import
+    if [[ ! -f "$full_dump_file" ]]; then
+      $DIALOG --msgbox "full dump file not found at:\n$full_dump_file" 10 70
+      return
+    fi
+
+    $DIALOG --yesno "import full database dump from:\n$full_dump_file\n\nThis will DROP and recreate the database.\n\nProceed?" 12 70 || return
+
+    {
+      echo "DROP DATABASE IF EXISTS \`$DB_NAME\`;"
+      echo "CREATE DATABASE \`$DB_NAME\` CHARACTER SET utf8mb4 COLLATE utf8mb4_uca1400_ai_ci;"
+      echo "USE \`$DB_NAME\`;"
+      echo "SET FOREIGN_KEY_CHECKS=0;"
+      cat "$full_dump_file"
+      echo "SET FOREIGN_KEY_CHECKS=1;"
+    } | podman exec -i "$BEEPZONE_DB_CONTAINER_NAME" \
+      mariadb -h"$DB_HOST" -P"$DB_PORT" -uroot -p"$DB_ROOT_PASSWORD" >>"$LOG_FILE" 2>&1 || {
+        error_log=$(tail -30 "$LOG_FILE")
+        $DIALOG --title "Error" --msgbox "full dump import failed.\n\nError:\n${error_log}" 22 76
+        return
+    }
+
+    DEPLOYMENT_TYPE="dev"
+    save_env
+    $DIALOG --msgbox "full database dump imported successfully!" 8 70
+
+  else
+    # Clean schema import
+    if [[ ! -f "$schema_file" ]]; then
+      $DIALOG --msgbox "schema file not found at:\n$schema_file" 10 60
+      return
+    fi
+
+    $DIALOG --yesno "import clean schema from:\n$schema_file\n\nThis will DROP and recreate the database.\n\nProceed?" 12 70 || return
+
+    {
+      echo "DROP DATABASE IF EXISTS \`$DB_NAME\`;"
+      echo "CREATE DATABASE \`$DB_NAME\` CHARACTER SET utf8mb4 COLLATE utf8mb4_uca1400_ai_ci;"
+      echo "USE \`$DB_NAME\`;"
+      echo "SET FOREIGN_KEY_CHECKS=0;"
+      cat "$schema_file"
+      echo "SET FOREIGN_KEY_CHECKS=1;"
+    } | podman exec -i "$BEEPZONE_DB_CONTAINER_NAME" \
+      mariadb -h"$DB_HOST" -P"$DB_PORT" -uroot -p"$DB_ROOT_PASSWORD" >>"$LOG_FILE" 2>&1 || {
+        error_log=$(tail -30 "$LOG_FILE")
+        $DIALOG --title "Error" --msgbox "schema import failed.\n\nError:\n${error_log}" 22 76
+        return
+    }
+
+    DEPLOYMENT_TYPE="clean"
+    save_env
+    $DIALOG --msgbox "schema imported successfully!\n\nI recommend you make an Admin role with power 100 and and Admin user as the next step." 8 70
+  fi
+}
+
+manage_users_and_roles() {
+  while true; do
+    $DIALOG --clear \
+      --title "Manage Users & Roles" \
+      --menu "Choose an action:" 17 60 6 \
+      1 "Create a role" \
+      2 "Create a user" \
+      3 "Delete a role" \
+      4 "Delete a user" \
+      5 "Back to main menu" 2>"$CHOICE_FILE"
+    
+    [[ ! -s "$CHOICE_FILE" ]] && return 0
+    choice=$(<"$CHOICE_FILE")
+    
+    case $choice in
+      1) create_role ;;
+      2) create_user ;;
+      3) delete_role ;;
+      4) delete_user ;;
+      5) return 0 ;;
+    esac
+  done
+}
+
+create_role() {
+  $DIALOG --title "Create Role" \
+    --form "Enter role details:" 12 60 2 \
+    "Role name:" 1 1 "" 1 20 30 0 \
+    "Power (1-100):" 2 1 "" 2 20 10 0 \
+    2>"$TMP_FILE"
+
+  [[ ! -s "$TMP_FILE" ]] && return 1
+
+  local name power
+  {
+    read -r name
+    read -r power
+  } < "$TMP_FILE"
+
+  [[ -z "$name" || -z "$power" ]] && {
+    $DIALOG --msgbox "Role name and power are required." 8 50
+    return 1
+  }
+
+  # Validate power is a number between 1-100
+  if ! [[ "$power" =~ ^[0-9]+$ ]] || [[ "$power" -lt 1 || "$power" -gt 100 ]]; then
+    $DIALOG --msgbox "Power must be a number between 1 and 100." 8 50
+    return 1
+  fi
+
+  local sql="INSERT INTO roles (name, power) VALUES ('$name', $power);"
+
+  echo "$sql" | podman exec -i "$BEEPZONE_DB_CONTAINER_NAME" \
+    mariadb -h"$DB_HOST" -P"$DB_PORT" -uroot -p"$DB_ROOT_PASSWORD" "$DB_NAME" >>"$LOG_FILE" 2>&1 || {
+      error_log=$(tail -20 "$LOG_FILE")
+      $DIALOG --title "Error" --msgbox "Failed to create role.\n\nError:\n${error_log}" 20 76
+      return 1
+  }
+
+  $DIALOG --msgbox "Role '$name' created successfully!" 8 50
+}
+
+delete_role() {
+  # Fetch roles from the database
+  local roles_list
+  roles_list=$(podman exec -i "$BEEPZONE_DB_CONTAINER_NAME" \
+    mariadb -h"$DB_HOST" -P"$DB_PORT" -uroot -p"$DB_ROOT_PASSWORD" "$DB_NAME" \
+    -e "SELECT id, name, power FROM roles ORDER BY power DESC;" -s -N 2>>"$LOG_FILE") || {
+      $DIALOG --msgbox "Failed to fetch roles from database." 10 60
+      return
+  }
+
+  # Build role selection menu
+  local role_options=()
+  while IFS=$'\t' read -r role_id role_name role_power; do
+    role_options+=("$role_id" "$role_name (power: $role_power)")
+  done <<< "$roles_list"
+
+  if [[ ${#role_options[@]} -eq 0 ]]; then
+    $DIALOG --msgbox "No roles found in database." 10 60
+    return
+  fi
+
+  # Select role to delete
+  $DIALOG --menu "Select role to delete:" 20 70 10 "${role_options[@]}" 2>"$TMP_FILE" || return
+  local selected_role_id
+  selected_role_id=$(<"$TMP_FILE")
+  
+  # Get role name for confirmation
+  local selected_role_name
+  selected_role_name=$(echo "$roles_list" | awk -v id="$selected_role_id" -F'\t' '$1 == id {print $2}')
+
+  # Check if any users are using this role
+  local user_count
+  user_count=$(podman exec -i "$BEEPZONE_DB_CONTAINER_NAME" \
+    mariadb -h"$DB_HOST" -P"$DB_PORT" -uroot -p"$DB_ROOT_PASSWORD" "$DB_NAME" \
+    -e "SELECT COUNT(*) FROM users WHERE role_id = $selected_role_id;" -s -N 2>>"$LOG_FILE")
+  
+  if [[ "$user_count" -gt 0 ]]; then
+    $DIALOG --msgbox "Cannot delete role '$selected_role_name'.\n$user_count user(s) are currently assigned this role." 10 60
+    return
+  fi
+
+  # Confirm deletion
+  $DIALOG --yesno "Are you sure you want to delete role '$selected_role_name' (ID: $selected_role_id)?\n\nThis action cannot be undone." 10 60 || return
+
+  # Delete role
+  local sql="DELETE FROM roles WHERE id = $selected_role_id;"
+  
+  echo "$sql" | podman exec -i "$BEEPZONE_DB_CONTAINER_NAME" \
+    mariadb -h"$DB_HOST" -P"$DB_PORT" -uroot -p"$DB_ROOT_PASSWORD" "$DB_NAME" >>"$LOG_FILE" 2>&1 || {
+      error_log=$(tail -20 "$LOG_FILE")
+      $DIALOG --title "Error" --msgbox "Failed to delete role.\n\nError:\n${error_log}" 20 76
+      return
+  }
+
+  $DIALOG --msgbox "Role '$selected_role_name' deleted successfully!" 8 60
+}
+
+create_user() {
+  # Get available roles
+  local roles_list
+  roles_list=$(podman exec -i "$BEEPZONE_DB_CONTAINER_NAME" \
+    mariadb -h"$DB_HOST" -P"$DB_PORT" -uroot -p"$DB_ROOT_PASSWORD" "$DB_NAME" \
+    -e "SELECT id, name, power FROM roles ORDER BY power DESC;" -s -N 2>>"$LOG_FILE") || {
+      $DIALOG --msgbox "Failed to fetch roles from database.\nEnsure schema is imported and roles exist." 10 60
+      return
+  }
+
+  # Build role selection menu
+  local role_options=()
+  while IFS=$'\t' read -r role_id role_name role_power; do
+    role_options+=("$role_id" "$role_name (power: $role_power)")
+  done <<< "$roles_list"
+
+  if [[ ${#role_options[@]} -eq 0 ]]; then
+    $DIALOG --msgbox "No roles found in database.\nPlease create a role first." 10 60
+    return
+  fi
+
+  # Select role
+  $DIALOG --menu "Select user role" 15 60 5 "${role_options[@]}" 2>"$TMP_FILE" || return
+  local selected_role_id
+  selected_role_id=$(<"$TMP_FILE")
+
+  # Get user details
+  $DIALOG --form "Create BeepZone user" 14 70 5 \
+    "Name (full name)" 1 1 "" 1 20 40 40 \
+    "Username"         2 1 "" 2 20 40 40 \
+    "Password"         3 1 "" 3 20 40 40 \
+    "Email"            4 1 "" 4 20 40 40 \
+    "Phone"            5 1 "" 5 20 40 40 2>"$TMP_FILE" || return
+
+  local name username password email phone
+  {
+    read -r name
+    read -r username
+    read -r password
+    read -r email
+    read -r phone
+  } < "$TMP_FILE"
+
+  if [[ -z "$name" || -z "$username" || -z "$password" ]]; then
+    $DIALOG --msgbox "Name, username, and password are required." 8 60
+    return
+  fi
+
+  # Hash password with bcrypt (cost 12)
+  local password_hash
+  
+  # Try htpasswd first (native Unix tool)
+  if command -v htpasswd >/dev/null 2>&1; then
+    password_hash=$(htpasswd -nbB -C 12 "$username" "$password" 2>>"$LOG_FILE" | cut -d: -f2)
+  # Fall back to Python bcrypt
+  elif command -v python3 >/dev/null 2>&1; then
+    password_hash=$(python3 -c "import bcrypt; print(bcrypt.hashpw('$password'.encode('utf-8'), bcrypt.gensalt(12)).decode('utf-8'))" 2>>"$LOG_FILE")
+  fi
+  
+  if [[ -z "$password_hash" ]]; then
+    $DIALOG --msgbox "Error: Failed to hash password. No bcrypt tool found.\n\nInstall options:\n- htpasswd: brew install httpd (macOS) or apt install apache2-utils (Linux)\n- Python bcrypt: pip3 install bcrypt" 12 70
+    return
+  fi
+
+  # Insert user with hashed password
+  local sql
+  sql="INSERT INTO users (name, username, password, role_id, email, phone, active) VALUES 
+    ('$name', '$username', '$password_hash', $selected_role_id, "
+  [[ -n "$email" ]] && sql+="'$email'" || sql+="NULL"
+  sql+=", "
+  [[ -n "$phone" ]] && sql+="'$phone'" || sql+="NULL"
+  sql+=", 1);"
+
+  echo "$sql" | podman exec -i "$BEEPZONE_DB_CONTAINER_NAME" \
+    mariadb -h"$DB_HOST" -P"$DB_PORT" -uroot -p"$DB_ROOT_PASSWORD" "$DB_NAME" >>"$LOG_FILE" 2>&1 || {
+      error_log=$(tail -20 "$LOG_FILE")
+      $DIALOG --title "Error" --msgbox "Failed to create user.\n\nError:\n${error_log}" 20 76
+      return
+  }
+
+  $DIALOG --msgbox "User '$username' created successfully!" 8 60
+}
+
+delete_user() {
+  # Fetch users from the database
+  local users_list
+  users_list=$(podman exec -i "$BEEPZONE_DB_CONTAINER_NAME" \
+    mariadb -h"$DB_HOST" -P"$DB_PORT" -uroot -p"$DB_ROOT_PASSWORD" "$DB_NAME" \
+    -e "SELECT id, username, name FROM users ORDER BY id;" -s -N 2>>"$LOG_FILE") || {
+      $DIALOG --msgbox "Failed to fetch users from database." 10 60
+      return
+  }
+
+  # Build user selection menu
+  local user_options=()
+  while IFS=$'\t' read -r user_id username name; do
+    user_options+=("$user_id" "$username - $name")
+  done <<< "$users_list"
+
+  if [[ ${#user_options[@]} -eq 0 ]]; then
+    $DIALOG --msgbox "No users found in database." 10 60
+    return
+  fi
+
+  # Select user to delete
+  $DIALOG --menu "Select user to delete:" 20 70 10 "${user_options[@]}" 2>"$TMP_FILE" || return
+  local selected_user_id
+  selected_user_id=$(<"$TMP_FILE")
+  
+  # Get username for confirmation
+  local selected_username
+  selected_username=$(echo "$users_list" | awk -v id="$selected_user_id" -F'\t' '$1 == id {print $2}')
+
+  # Confirm deletion
+  $DIALOG --yesno "Are you sure you want to delete user '$selected_username' (ID: $selected_user_id)?\n\nThis action cannot be undone." 10 60 || return
+
+  # Delete user
+  local sql="DELETE FROM users WHERE id = $selected_user_id;"
+  
+  echo "$sql" | podman exec -i "$BEEPZONE_DB_CONTAINER_NAME" \
+    mariadb -h"$DB_HOST" -P"$DB_PORT" -uroot -p"$DB_ROOT_PASSWORD" "$DB_NAME" >>"$LOG_FILE" 2>&1 || {
+      error_log=$(tail -20 "$LOG_FILE")
+      $DIALOG --title "Error" --msgbox "Failed to delete user.\n\nError:\n${error_log}" 20 76
+      return
+  }
+
+  $DIALOG --msgbox "User '$selected_username' deleted successfully!" 8 60
+}
+
+clone_if_missing() {
+  local repo_url="$1" dest_dir="$2"
+  if [[ -d "$dest_dir/.git" ]]; then
+    return
+  fi
+  git clone "$repo_url" "$dest_dir" >>"$LOG_FILE" 2>&1 || {
+    error_log=$(tail -20 "$LOG_FILE")
+    $DIALOG --title "Error" --msgbox "Failed to clone $repo_url.\n\nError:\n${error_log}" 20 76
+    return 1
+  }
+}
+
+setup_seckelapi() {
+  local sources_dir="$WORK_DIR/backend/seckelapi/sources"
+  local config_dir="$WORK_DIR/backend/seckelapi/config"
+  local sources_basics_config="$sources_dir/config/basics.toml"
+  
+  # Check if sources are already cloned
+  if [[ ! -d "$sources_dir/.git" ]]; then
+    mkdir -p "$sources_dir"
+    clone_if_missing "$SECKELAPI_REPO" "$sources_dir" || return
+  fi
+
+  # Ask about config update
+  $DIALOG --clear \
+    --title "SeckelAPI Configuration" \
+    --menu "How do you want to handle the config?" 12 70 2 \
+    1 "Auto-update from database settings" \
+    2 "Manually edit config file" 2>"$CHOICE_FILE"
+  
+  [[ ! -s "$CHOICE_FILE" ]] && return 0
+  config_choice=$(<"$CHOICE_FILE")
+  
+  # Ensure sources config directory exists and copy all template configs
+  mkdir -p "$sources_dir/config"
+  
+  # Always copy all template configs from config/ to sources/config/
+  for conf_file in "$config_dir"/*.toml; do
+    conf_name=$(basename "$conf_file")
+    cp "$conf_file" "$sources_dir/config/$conf_name" 2>>"$LOG_FILE"
+  done
+  
+  if [[ "$config_choice" == "1" ]]; then
+    # Auto-update basics.toml in sources with database settings (only [database] section)
+    if [[ -f "$sources_basics_config" ]]; then
+      # Determine database host - use host.containers.internal for container deployments
+      local db_host_value="$DB_HOST"
+      
+      sed -i.bak \
+        -e '/^\[database\]/,/^\[/ {
+          /^host = /s|= .*|= "'"$db_host_value"'"|
+          /^port = /s|= .*|= '"$DB_PORT"'|
+          /^database = /s|= .*|= "'"$DB_NAME"'"|
+          /^username = /s|= .*|= "'"$DB_USER"'"|
+          /^password = /s|= .*|= "'"$DB_PASS"'"|
+        }' \
+        "$sources_basics_config"
+      $DIALOG --msgbox "Config updated with database settings from .env" 8 60
+    else
+      $DIALOG --msgbox "Error: basics.toml not found at $sources_basics_config" 8 60
+      return 1
+    fi
+  else
+    # Open config file for manual editing (in sources/config/)
+    if [[ -f "$sources_basics_config" ]]; then
+      ${EDITOR:-nano} "$sources_basics_config"
+    else
+      $DIALOG --msgbox "Error: basics.toml not found at $sources_basics_config" 8 60
+      return 1
+    fi
+  fi
+
+  # Ask about build and deployment
+  $DIALOG --clear \
+    --title "Build & Deploy SeckelAPI" \
+    --menu "Choose an action:" 12 70 2 \
+    1 "Build and deploy to podman container" \
+    2 "Build natively on host" 2>"$CHOICE_FILE"
+  
+  [[ ! -s "$CHOICE_FILE" ]] && return 0
+  build_choice=$(<"$CHOICE_FILE")
+  
+  if [[ "$build_choice" == "2" ]]; then
+    # Native host build with live output
+    clear
+    echo "Building SeckelAPI..."
+    echo "===================="
+    echo ""
+    
+    if (cd "$sources_dir" && cargo build --release); then
+      # Copy config files to the build output directory
+      local target_dir="$sources_dir/target/release"
+      mkdir -p "$target_dir/config"
+      cp "$sources_dir/config"/*.toml "$target_dir/config/" 2>>"$LOG_FILE"
+      
+      echo ""
+      echo "Build completed successfully!"
+      echo "Binary location: $target_dir/seckelapi"
+      echo "Config location: $target_dir/config/"
+      read -p "Press Enter to continue..."
+    else
+      echo ""
+      echo "Build failed! Check the output above for errors."
+      read -p "Press Enter to continue..."
+      return 1
+    fi
+  elif [[ "$build_choice" == "1" ]]; then
+    # Build and deploy to podman container
+    clear
+    echo "Building SeckelAPI container..."
+    echo "================================"
+    echo ""
+    
+    # Get the host gateway IP for container to access host services
+    # For Podman, we'll use the gateway IP from the default bridge network
+    local host_gateway="host.containers.internal"
+    
+    # Update database host for container networking
+    if [[ -f "$sources_basics_config" ]]; then
+      echo "Updating database host to: $host_gateway"
+      sed -i.container-bak \
+        -e '/^\[database\]/,/^\[/ {
+          /^host = /s|= .*|= "'"$host_gateway"'"|
+        }' \
+        "$sources_basics_config"
+    fi
+    
+    local container_name="beepzone-seckelapi"
+    local image_name="beepzone-seckelapi:latest"
+    local containerfile="$WORK_DIR/backend/seckelapi/Containerfile"
+    
+    # Stop and remove existing container if running
+    if podman ps -a --format "{{.Names}}" | grep -q "^${container_name}$"; then
+      echo "Stopping and removing existing container..."
+      podman stop "$container_name" 2>/dev/null || true
+      podman rm "$container_name" 2>/dev/null || true
+    fi
+    
+    # Build container image
+    echo "Building container image..."
+    if podman build -t "$image_name" -f "$containerfile" "$WORK_DIR/backend/seckelapi"; then
+      echo ""
+      echo "Container image built successfully!"
+      echo ""
+      
+      # Ask to run the container
+      if $DIALOG --yesno "Start the SeckelAPI container now?" 8 50; then
+        echo "Starting container..."
+        
+        # Run container with port mapping and host gateway
+        if podman run -d \
+          --name "$container_name" \
+          --add-host host.containers.internal:host-gateway \
+          -p 5777:5777 \
+          "$image_name"; then
+          
+          echo ""
+          echo "Container started successfully!"
+          echo "Container name: $container_name"
+          echo "API listening on: http://0.0.0.0:5777"
+          echo ""
+          echo "Useful commands:"
+          echo "  podman logs $container_name       - View logs"
+          echo "  podman stop $container_name       - Stop container"
+          echo "  podman start $container_name      - Start container"
+          echo "  podman restart $container_name    - Restart container"
+          read -p "Press Enter to continue..."
+        else
+          echo ""
+          echo "Failed to start container!"
+          read -p "Press Enter to continue..."
+          return 1
+        fi
+      fi
+    else
+      echo ""
+      echo "Container build failed! Check the output above for errors."
+      read -p "Press Enter to continue..."
+      return 1
+    fi
+  fi
+}
+
+build_desktop_client() {
+  local sources_dir="$WORK_DIR/frontend/desktop-client/sources"
+  
+  # Check if sources are already cloned
+  if [[ ! -d "$sources_dir/.git" ]]; then
+    mkdir -p "$sources_dir"
+    clone_if_missing "$CLIENT_REPO" "$sources_dir" || return
+  fi
+
+  if $DIALOG --yesno "Build BeepZone desktop client in release mode now?" 8 70; then
+    clear
+    echo "Building BeepZone Desktop Client..."
+    echo "===================================="
+    echo ""
+    
+    if (cd "$sources_dir" && cargo build --release); then
+      echo ""
+      echo "Build completed successfully!"
+      echo "Binary location: $sources_dir/target/release/"
+      read -p "Press Enter to continue..."
+    else
+      echo ""
+      echo "Build failed! Check the output above for errors."
+      read -p "Press Enter to continue..."
+      return 1
+    fi
+  fi
+}
+
+ask_main_menu

+ 24 - 0
run-client.sh

@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+CLIENT_BIN="$SCRIPT_DIR/frontend/desktop-client/sources/target/release/beepzone-egui"
+CLIENT_SOURCES="$SCRIPT_DIR/frontend/desktop-client/sources"
+
+if [[ ! -f "$CLIENT_BIN" ]]; then
+  echo "Error: BeepZone client binary not found at:"
+  echo "  $CLIENT_BIN"
+  echo ""
+  echo "Please build the desktop client first using the setup helper:"
+  echo "  ./beepzone-helper.sh"
+  exit 1
+fi
+
+echo "Starting BeepZone Desktop Client..."
+echo "Binary: $CLIENT_BIN"
+echo "Working directory: $CLIENT_SOURCES"
+echo ""
+
+cd "$CLIENT_SOURCES"
+exec ./target/release/beepzone-egui

+ 24 - 0
run-seckelapi.sh

@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+SECKELAPI_BIN="$SCRIPT_DIR/backend/seckelapi/sources/target/release/seckelapi"
+SECKELAPI_SOURCES="$SCRIPT_DIR/backend/seckelapi/sources"
+
+if [[ ! -f "$SECKELAPI_BIN" ]]; then
+  echo "Error: SeckelAPI binary not found at:"
+  echo "  $SECKELAPI_BIN"
+  echo ""
+  echo "Please build SeckelAPI first using the setup helper:"
+  echo "  ./beepzone-helper.sh"
+  exit 1
+fi
+
+echo "Starting SeckelAPI..."
+echo "Binary: $SECKELAPI_BIN"
+echo "Working directory: $SECKELAPI_SOURCES"
+echo ""
+
+cd "$SECKELAPI_SOURCES"
+exec ./target/release/seckelapi

Some files were not shown because too many files changed in this diff