# GasERP UI/UX Fix Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Fix the critical security, performance, mobile, and print issues found in the UI/UX audit (scored 55/100).

**Architecture:** The project is a procedural PHP app with no framework, no test suite, and no build system. All CSS is inline in layout.php/sidebar.php/header.php. All JS is vanilla inline. We will fix issues surgically — extracting CSS to external files, consolidating queries, fixing responsive breakpoints, and patching security holes.

**Tech Stack:** PHP 8+, MySQL/MariaDB, vanilla CSS/JS, Google Material Symbols

---

## File Structure

| File | Action | Purpose |
|------|--------|---------|
| `config/layout.php` | Modify | Remove inline CSS (move to external), fix print `@page`, add tablet breakpoint |
| `config/sidebar.php` | Modify | Remove inline CSS (move to external), add logout button HTML, fix `<div onclick>` → `<button>` |
| `config/header.php` | Modify | Remove inline CSS (move to external), consolidate badge queries |
| `config/footer.php` | Modify | Remove duplicate badge AJAX (consolidated into header) |
| `config/database.php` | Modify | Add `getPendingCounts()` helper |
| `config/migrations.php` | Create | One-time migration file (runs once, stores lock) |
| `assets/css/main.css` | Create | Extracted design system CSS |
| `assets/css/sidebar.css` | Create | Extracted sidebar CSS |
| `assets/css/header.css` | Create | Extracted header CSS |
| `transactions/delivery.php` | Modify | Fix SQL injection, convert DELETE GET → POST |
| `transactions/movements.php` | Modify | Fix undefined CSS variables |
| `dashboard/index.php` | Modify | Remove hardcoded `repeat(4,1fr)` inline styles |
| `announcements/index.php` | Modify | Remove hardcoded `repeat(4,1fr)` inline styles |
| `contracts/index.php` | Modify | Remove hardcoded `repeat(4,1fr)` inline styles |
| `transactions/index.php` | Modify | Remove hardcoded `repeat(4,1fr)` inline styles |
| `drivers/index.php` | Modify | Wrap table in `.table-container`, remove duplicate `openModal`/`closeModal` |
| `knowledge/index.php` | Modify | Wrap table in `.table-container` |
| `contracts/view.php` | Modify | Wrap 5 tables in `.table-container` |
| `cylinders/types.php` | Modify | Wrap table in `.table-container` |
| `cylinders/inventory.php` | Modify | Wrap table in `.table-container` |

---

### Task 1: Security — Fix SQL Injection in delivery.php

**Files:**
- Modify: `G:/xampp/htdocs/GasERP/transactions/delivery.php:165-170`

The status filter at line 170 uses `$statusFilter` from `$_GET['status']` without escaping.

- [ ] **Step 1: Read the current filter code**

```php
// Current (line ~165-170):
$statusFilter = $_GET['status'] ?? '';
if ($statusFilter) { $where .= " AND status = '$statusFilter'"; }
```

- [ ] **Step 2: Add escaping**

Replace the unescaped interpolation with:
```php
$statusFilter = $_GET['status'] ?? '';
if ($statusFilter) {
    $where .= " AND status = '" . mysqli_real_escape_string($conn, $statusFilter) . "'";
}
```

- [ ] **Step 3: Verify no other unescaped GET/POST in the same file**

Search for `$_GET` and `$_POST` usage in `delivery.php` and confirm all other uses are either `intval()` or `mysqli_real_escape_string()`.

---

### Task 2: Security — Convert DELETE via GET to POST

**Files:**
- Modify: `G:/xampp/htdocs/GasERP/transactions/delivery.php:180-189, 566`

Transaction deletion is triggered by `?delete=ID` GET parameter. This must become a POST form with confirmation.

- [ ] **Step 1: Change server-side handler from GET to POST**

Replace (lines ~180-189):
```php
if(isset($_GET['delete']) && hasPermission($currentUser, 'can_manage_transactions')){
    $id = intval($_GET['delete']);
    // ... delete logic ...
}
```

With:
```php
if($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delete_transaction' && hasPermission($currentUser, 'can_manage_transactions')){
    $id = intval($_POST['transaction_id'] ?? 0);
    if ($id > 0) {
        // ... existing delete logic (lines 183-188) ...
    }
}
```

- [ ] **Step 2: Change the UI delete link to a POST form**

Replace the delete link (line ~566) from:
```php
<a href="?delete=<?= $t['id'] ?>" onclick="return confirm('Delete this transaction?')" ...>
```

With:
```php
<form method="POST" style="display:inline;" onsubmit="return confirm('Delete this transaction?')">
    <input type="hidden" name="action" value="delete_transaction">
    <input type="hidden" name="transaction_id" value="<?= $t['id'] ?>">
    <button type="submit" class="act-btn act-btn-delete" title="Delete"><span class="material-symbols-rounded">delete</span></button>
</form>
```

---

### Task 3: Performance — Consolidate Badge Count Queries

**Files:**
- Modify: `G:/xampp/htdocs/GasERP/config/database.php` — add `getPendingCounts()` function
- Modify: `G:/xampp/htdocs/GasERP/config/sidebar.php` — use shared function
- Modify: `G:/xampp/htdocs/GasERP/config/header.php` — use shared function
- Modify: `G:/xampp/htdocs/GasERP/config/footer.php` — remove duplicate AJAX call

Currently 50-60 queries run per page load from badge/notification counting across sidebar, header, and footer AJAX. Consolidate into a single function that runs one query per table.

- [ ] **Step 1: Add `getPendingCounts()` to database.php**

Add this function after the existing helper functions (after line ~79):

```php
function getPendingCounts($conn) {
    static $cached = null;
    if ($cached !== null) return $cached;

    $counts = [
        'movements' => 0, 'bonus' => 0, 'drivers' => 0,
        'customers' => 0, 'cylinders' => 0, 'warehouses' => 0,
        'stock_ops' => 0,
        'total_pending' => 0,
        // Notification counts (new this week)
        'new_drivers' => 0, 'new_customers' => 0, 'new_cylinders' => 0,
        'new_transactions' => 0, 'new_movements' => 0, 'new_announcements' => 0,
        'total_new' => 0,
        'unread_notifications' => 0,
    ];

    // Pending approvals — one query per table
    $tables = [
        'movements' => ['table' => 'cylinder_movements', 'col' => 'status'],
        'bonus'     => ['table' => 'transactions',       'col' => 'approval_status'],
        'drivers'   => ['table' => 'drivers',            'col' => 'approval_status'],
        'customers' => ['table' => 'customers',          'col' => 'approval_status'],
        'cylinders' => ['table' => 'cylinders',          'col' => 'approval_status'],
        'warehouses'=> ['table' => 'warehouses',         'col' => 'approval_status'],
    ];

    foreach ($tables as $key => $cfg) {
        $r = @mysqli_query($conn, "SELECT COUNT(*) as cnt FROM `{$cfg['table']}` WHERE `{$cfg['col']}` = 'Pending Approval'");
        if ($r) $counts[$key] = intval(@mysqli_fetch_assoc($r)['cnt']);
    }

    // stock_operations may not exist
    $r = @mysqli_query($conn, "SELECT COUNT(*) as cnt FROM stock_operations WHERE approval_status = 'Pending Approval'");
    if ($r) $counts['stock_ops'] = intval(@mysqli_fetch_assoc($r)['cnt']);

    $counts['total_pending'] = array_sum([$counts['movements'], $counts['bonus'], $counts['drivers'], $counts['customers'], $counts['cylinders'], $counts['warehouses'], $counts['stock_ops']]);

    // New items this week
    $weekAgo = date('Y-m-d', strtotime('-7 days'));
    $newItems = [
        'new_drivers'       => ['table' => 'drivers',            'where' => "created_at >= '$weekAgo'"],
        'new_customers'     => ['table' => 'customers',          'where' => "created_at >= '$weekAgo'"],
        'new_cylinders'     => ['table' => 'cylinders',          'where' => "created_at >= '$weekAgo'"],
        'new_transactions'  => ['table' => 'transactions',       'where' => "created_at >= '$weekAgo'"],
        'new_movements'     => ['table' => 'cylinder_movements', 'where' => "created_at >= '$weekAgo'"],
        'new_announcements' => ['table' => 'announcements',      'where' => "status='published' AND created_at >= '$weekAgo'"],
    ];

    foreach ($newItems as $key => $cfg) {
        // Ensure created_at column exists
        $colCheck = @mysqli_query($conn, "SHOW COLUMNS FROM `{$cfg['table']}` LIKE 'created_at'");
        if ($colCheck && mysqli_num_rows($colCheck) == 0) {
            @mysqli_query($conn, "ALTER TABLE `{$cfg['table']}` ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP");
            @mysqli_query($conn, "UPDATE `{$cfg['table']}` SET created_at = NOW() WHERE created_at IS NULL");
        }
        $r = @mysqli_query($conn, "SELECT COUNT(*) as cnt FROM `{$cfg['table']}` WHERE {$cfg['where']}");
        if ($r) $counts[$key] = intval(@mysqli_fetch_assoc($r)['cnt']);
    }

    $counts['total_new'] = $counts['new_drivers'] + $counts['new_customers'] + $counts['new_cylinders'] + $counts['new_transactions'] + $counts['new_movements'] + $counts['new_announcements'];

    // Unread notifications
    $uid = intval($_SESSION['user_id'] ?? 0);
    if ($uid) {
        $r = @mysqli_query($conn, "SELECT COUNT(*) as cnt FROM notifications WHERE user_id = $uid AND is_read = 0");
        if ($r) $counts['unread_notifications'] = intval(@mysqli_fetch_assoc($r)['cnt']);
    }

    $cached = $counts;
    return $counts;
}
```

- [ ] **Step 2: Replace sidebar.php badge queries (lines 652-667) with shared function**

Replace the 8 COUNT queries in the approvals menu item with:
```php
<?php
$badgeCounts = getPendingCounts($conn);
$pCnt = $badgeCounts['total_pending'];
?>
```

And for notifications (lines 680-685), replace with:
```php
<?php $unCnt = $badgeCounts['unread_notifications']; ?>
```

- [ ] **Step 3: Replace header.php badge queries (lines 24-48) with shared function**

Replace the entire notification counting block with:
```php
<?php
$notifCounts = getPendingCounts($conn);
$notifCount = $notifCounts['total_pending'] + $notifCounts['total_new'];
$notifItems = [
    'approval_movements' => $notifCounts['movements'],
    'approval_bonus'     => $notifCounts['bonus'],
    'approval_drivers'   => $notifCounts['drivers'],
    'approval_customers' => $notifCounts['customers'],
    'approval_warehouses'=> $notifCounts['warehouses'],
    'drivers'       => $notifCounts['new_drivers'],
    'customers'     => $notifCounts['new_customers'],
    'cylinders'     => $notifCounts['new_cylinders'],
    'transactions'  => $notifCounts['new_transactions'],
    'movements'     => $notifCounts['new_movements'],
    'announcements' => $notifCounts['new_announcements'],
];
?>
```

- [ ] **Step 4: Remove duplicate AJAX badge refresh from footer.php**

In `footer.php` lines 91-105, the AJAX call to `pending_count.php` duplicates what the sidebar already shows. Remove or replace with a lightweight approach that only fetches the total count:

```javascript
// Replace the existing AJAX block with:
(function(){
    var badge = document.getElementById('approvalBadge');
    if(!badge) return;
    // Badge is already rendered server-side from getPendingCounts()
    // Only refresh via AJAX every 60 seconds
    setInterval(function(){
        fetch('../config/pending_count.php')
            .then(function(r){ return r.json(); })
            .then(function(d){
                if(d.count > 0){
                    badge.textContent = d.count;
                    badge.style.display = '';
                } else {
                    badge.style.display = 'none';
                }
            }).catch(function(){});
    }, 60000);
})();
```

---

### Task 4: Performance — Extract CSS to External Files

**Files:**
- Create: `G:/xampp/htdocs/GasERP/assets/css/main.css`
- Create: `G:/xampp/htdocs/GasERP/assets/css/sidebar.css`
- Create: `G:/xampp/htdocs/GasERP/assets/css/header.css`
- Modify: `G:/xampp/htdocs/GasERP/config/layout.php` — replace `<style>` with `<link>`
- Modify: `G:/xampp/htdocs/GasERP/config/sidebar.php` — remove `<style>` block
- Modify: `G:/xampp/htdocs/GasERP/config/header.php` — remove `<style>` block

55 KB of inline CSS is parsed on every page navigation. Extract to cacheable external files.

- [ ] **Step 1: Create `assets/css/main.css`**

Copy the entire `<style>` block from `config/layout.php` (lines 139-355) into this new file. Remove the `<style>` and `</style>` tags. Keep the CSS content exactly as-is.

- [ ] **Step 2: Create `assets/css/sidebar.css`**

Copy the entire `<style>` block from `config/sidebar.php` (lines 20-511) into this new file.

- [ ] **Step 3: Create `assets/css/header.css`**

Copy the entire `<style>` block from `config/header.php` (lines 52-357) into this new file.

- [ ] **Step 4: Update layout.php to use external CSS**

Replace the inline `<style>` block (lines 139-355) with:
```html
<link rel="stylesheet" href="../assets/css/main.css">
```

The brand color CSS variables must still be injected inline since they come from the database. Add a small inline `<style>` block just for the variables:
```html
<style>
:root {
    --primary: <?= $brandColors['primary'] ?>; --primary-dark: <?= $brandColors['primaryDark'] ?>; --primary-light: <?= $brandColors['primaryLight'] ?>; --primary-50: <?= $brandColors['primary50'] ?>;
    --secondary: <?= $brandColors['secondary'] ?>; --secondary-dark: <?= $brandColors['secondaryDark'] ?>; --secondary-light: <?= $brandColors['secondaryLight'] ?>; --secondary-50: <?= $brandColors['secondary50'] ?>;
    --brand-gradient: linear-gradient(135deg, var(--primary), var(--secondary));
    --brand-gradient-dark: linear-gradient(135deg, var(--primary-dark), var(--secondary-dark));
    --brand-shadow: 0 2px 8px rgba(99,102,241,0.3);
    --brand-shadow-hover: 0 4px 12px rgba(99,102,241,0.4);
}
</style>
```

- [ ] **Step 5: Update sidebar.php to link external CSS**

Remove the `<style>` block (lines 20-511) and add at the top of the file (before the HTML):
```html
<link rel="stylesheet" href="../assets/css/sidebar.css">
<style>
:root {
    --sidebar-bg: #ffffff;
    --sidebar-card: #f4f6fc;
    --sidebar-border: rgba(99,102,241,0.06);
    --accent: <?= $primaryColor ?>;
    --accent-glow: rgba(<?= hexdec(substr($primaryColor, 1, 2)) ?>, <?= hexdec(substr($primaryColor, 3, 2)) ?>, <?= hexdec(substr($primaryColor, 5, 2)) ?>, 0.1);
    --accent-hover: <?= $secondaryColor ?>;
    --text-primary: #1e1b4b;
    --sidebar-text-secondary: #5a5885;
    --sidebar-text-muted: #8b88a8;
}
</style>
```

- [ ] **Step 6: Update header.php to link external CSS**

Remove the `<style>` block (lines 52-357) and add:
```html
<link rel="stylesheet" href="../assets/css/header.css">
```

---

### Task 5: Performance — Consolidate Migration Logic

**Files:**
- Create: `G:/xampp/htdocs/GasERP/config/migrations.php`
- Modify: `G:/xampp/htdocs/GasERP/config/layout.php` — replace inline migrations with include

Currently 15-20 migration checks run on every page load across layout.php, sidebar.php, header.php, and individual pages. Create a one-time migration file.

- [ ] **Step 1: Create `config/migrations.php`**

```php
<?php
/**
 * One-time schema migrations. Runs once per request cycle using a static flag.
 * Individual pages that need specific columns should still check locally,
 * but this consolidates the common migrations from layout.php.
 */
function runMigrations($conn) {
    static $ran = false;
    if ($ran) return;
    $ran = true;

    // Ensure approval columns exist on core tables
    foreach(['transactions','drivers','customers','cylinders','warehouses'] as $table) {
        $check = @mysqli_query($conn, "SHOW COLUMNS FROM `$table` LIKE 'approval_status'");
        if ($check && mysqli_num_rows($check) == 0) {
            @mysqli_query($conn, "ALTER TABLE `$table` ADD COLUMN approval_status ENUM('Pending Approval','Approved','Rejected') DEFAULT 'Pending Approval'");
        } else {
            $col = @mysqli_fetch_assoc($check);
            if ($col && strpos($col['Default'] ?? '', 'Approved') !== false && $col['Default'] !== 'Pending Approval') {
                @mysqli_query($conn, "ALTER TABLE `$table` MODIFY COLUMN approval_status ENUM('Pending Approval','Approved','Rejected') DEFAULT 'Pending Approval'");
            }
        }
    }

    // Ensure warehouses has approval support columns
    $whCols = @mysqli_query($conn, "SHOW COLUMNS FROM warehouses");
    if ($whCols) {
        $whColArr = [];
        while ($c = mysqli_fetch_assoc($whCols)) $whColArr[] = $c['Field'];
        $whNeedsMigration = !in_array('approved_by', $whColArr);
        if ($whNeedsMigration) @mysqli_query($conn, "ALTER TABLE warehouses ADD COLUMN approved_by INT DEFAULT NULL");
        if (!in_array('approved_at', $whColArr)) @mysqli_query($conn, "ALTER TABLE warehouses ADD COLUMN approved_at DATETIME DEFAULT NULL");
        if (!in_array('rejection_reason', $whColArr)) @mysqli_query($conn, "ALTER TABLE warehouses ADD COLUMN rejection_reason TEXT DEFAULT NULL");
        if (!in_array('created_at', $whColArr)) @mysqli_query($conn, "ALTER TABLE warehouses ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP");
        if (!in_array('transaction_no', $whColArr)) @mysqli_query($conn, "ALTER TABLE warehouses ADD COLUMN transaction_no VARCHAR(100) DEFAULT NULL");
        if (!in_array('date', $whColArr)) @mysqli_query($conn, "ALTER TABLE warehouses ADD COLUMN date DATE DEFAULT NULL");
        if ($whNeedsMigration) {
            @mysqli_query($conn, "UPDATE warehouses SET approval_status = 'Approved' WHERE approved_by IS NULL AND approval_status = 'Pending Approval'");
        }
    }

    // Ensure roles have approval columns
    $check = @mysqli_query($conn, "SHOW COLUMNS FROM roles LIKE 'can_approve_bonus'");
    if ($check && mysqli_num_rows($check) == 0) {
        @mysqli_query($conn, "ALTER TABLE roles ADD COLUMN can_approve_bonus TINYINT(1) DEFAULT 0");
    }
    $check = @mysqli_query($conn, "SHOW COLUMNS FROM roles LIKE 'can_approve_stock'");
    if ($check && mysqli_num_rows($check) == 0) {
        @mysqli_query($conn, "ALTER TABLE roles ADD COLUMN can_approve_stock TINYINT(1) DEFAULT 0");
    }
    @mysqli_query($conn, "UPDATE roles SET can_approve_stock = 1 WHERE can_approve_stock = 0");

    // Ensure stock_balances columns
    $sbCheck = @mysqli_query($conn, "SHOW COLUMNS FROM stock_balances");
    if ($sbCheck) {
        $sbCols = [];
        while ($c = mysqli_fetch_assoc($sbCheck)) $sbCols[] = $c['Field'];
        if (!in_array('opening_stock', $sbCols)) @mysqli_query($conn, "ALTER TABLE stock_balances ADD COLUMN opening_stock INT DEFAULT 0 AFTER gas_type_id");
        if (!in_array('total_delivered', $sbCols)) @mysqli_query($conn, "ALTER TABLE stock_balances ADD COLUMN total_delivered INT DEFAULT 0 AFTER total_cylinders");
        if (!in_array('total_returned', $sbCols)) @mysqli_query($conn, "ALTER TABLE stock_balances ADD COLUMN total_returned INT DEFAULT 0 AFTER total_delivered");
    }

    // Ensure cylinder_balances has warehouse_id
    $cybCheck = @mysqli_query($conn, "SHOW COLUMNS FROM cylinder_balances");
    if ($cybCheck) {
        $cybCols = [];
        while ($c = mysqli_fetch_assoc($cybCheck)) $cybCols[] = $c['Field'];
        if (!in_array('warehouse_id', $cybCols)) {
            @mysqli_query($conn, "ALTER TABLE cylinder_balances ADD COLUMN warehouse_id INT DEFAULT NULL AFTER customer_id");
            @mysqli_query($conn, "ALTER TABLE cylinder_balances DROP INDEX uq_bal, ADD UNIQUE KEY uq_bal (customer_id, warehouse_id, product_id, month, year)");
        }
    }

    // Ensure cylinder_movements has to_warehouse_id
    $cmCheck = @mysqli_query($conn, "SHOW COLUMNS FROM cylinder_movements");
    if ($cmCheck) {
        $cmCols = [];
        while ($c = mysqli_fetch_assoc($cmCheck)) $cmCols[] = $c['Field'];
        if (!in_array('to_warehouse_id', $cmCols)) @mysqli_query($conn, "ALTER TABLE cylinder_movements ADD COLUMN to_warehouse_id INT DEFAULT NULL AFTER warehouse_id");
    }

    // Ensure customer_balances columns
    $cbCheck = @mysqli_query($conn, "SHOW COLUMNS FROM customer_balances");
    if ($cbCheck) {
        $cbCols = [];
        while ($c = mysqli_fetch_assoc($cbCheck)) $cbCols[] = $c['Field'];
        if (!in_array('total_sent', $cbCols)) @mysqli_query($conn, "ALTER TABLE customer_balances ADD COLUMN total_sent INT DEFAULT 0 AFTER gas_type_id");
        if (!in_array('total_returned', $cbCols)) @mysqli_query($conn, "ALTER TABLE customer_balances ADD COLUMN total_returned INT DEFAULT 0 AFTER total_sent");
    }
}
```

- [ ] **Step 2: Replace layout.php inline migrations (lines 16-90) with include**

Replace lines 16-90 with:
```php
include __DIR__ . '/migrations.php';
runMigrations($conn);
```

---

### Task 6: Mobile — Fix Hardcoded Grid Columns

**Files:**
- Modify: `G:/xampp/htdocs/GasERP/dashboard/index.php` (lines 175, 265)
- Modify: `G:/xampp/htdocs/GasERP/announcements/index.php` (line 140)
- Modify: `G:/xampp/htdocs/GasERP/contracts/index.php` (line 43)
- Modify: `G:/xampp/htdocs/GasERP/transactions/index.php` (line 67)

Inline `style="grid-template-columns:repeat(4,1fr)"` overrides the responsive CSS. Remove these inline styles so the CSS `auto-fit` rules take effect.

- [ ] **Step 1: Update `main.css` to handle 4-column stats grids responsively**

Add to `main.css` (or the inline variable block in layout.php):
```css
.stats-grid-4 {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    gap: 16px;
    margin-bottom: 24px;
}
@media (max-width: 768px) {
    .stats-grid-4 { grid-template-columns: repeat(2, 1fr); }
}
```

- [ ] **Step 2: Replace inline styles with the new class**

In each file, replace `style="grid-template-columns:repeat(4,1fr);"` (and any other inline style properties on the same element) with the `stats-grid-4` class:

`dashboard/index.php` line 175:
```php
<div class="stats-grid stats-grid-4">
```

`dashboard/index.php` line 265:
```php
<div class="stats-grid stats-grid-4">
```

`announcements/index.php` line 140:
```php
<div class="stats-grid stats-grid-4" style="margin-bottom:16px;">
```

`contracts/index.php` line 43:
```php
<div class="stats-grid stats-grid-4">
```

`transactions/index.php` line 67:
```php
<div class="stats-grid stats-grid-4">
```

---

### Task 7: Mobile — Wrap Missing Tables in `.table-container`

**Files:**
- Modify: `G:/xampp/htdocs/GasERP/drivers/index.php` (line 259)
- Modify: `G:/xampp/htdocs/GasERP/knowledge/index.php` (line 217)
- Modify: `G:/xampp/htdocs/GasERP/contracts/view.php` (lines 99, 120, 143, 182, 206)
- Modify: `G:/xampp/htdocs/GasERP/cylinders/types.php` (line 219)
- Modify: `G:/xampp/htdocs/GasERP/cylinders/inventory.php` (line 478)

Each `<table class="table">` must be wrapped in `<div class="table-container">` for horizontal scrolling on mobile.

- [ ] **Step 1: Wrap drivers/index.php table**

Around line 259, wrap the table:
```html
<div class="table-container">
<table class="table">
...
</table>
</div>
```

- [ ] **Step 2: Wrap knowledge/index.php table**

Around line 217, same wrapping pattern.

- [ ] **Step 3: Wrap contracts/view.php tables (5 tables)**

Each of the 5 tables at lines 99, 120, 143, 182, 206 needs wrapping.

- [ ] **Step 4: Wrap cylinders/types.php table**

Around line 219.

- [ ] **Step 5: Wrap cylinders/inventory.php table**

Around line 478.

---

### Task 8: Mobile — Add Tablet Breakpoint

**Files:**
- Modify: `G:/xampp/htdocs/GasERP/assets/css/main.css` (or layout.php inline styles)

Add a 1024px breakpoint between mobile and desktop.

- [ ] **Step 1: Add tablet breakpoint to main.css**

```css
@media (max-width: 1024px) and (min-width: 769px) {
    .main-content { padding: 72px 20px 20px; }
    .stats-grid { grid-template-columns: repeat(2, 1fr); }
    .modal { width: 95%; max-width: 500px; }
}
```

---

### Task 9: Print — Add @page Rules and Fix Overflow

**Files:**
- Modify: `G:/xampp/htdocs/GasERP/assets/css/main.css` (print section)

- [ ] **Step 1: Add @page and page-break rules to the print media query**

Replace the existing `@media print` block in main.css with:
```css
@media print {
    @page {
        size: A4;
        margin: 15mm;
    }
    .sidebar, .sidebar-overlay, .mobile-toggle, .top-navbar, .page-header .btn, .btn, .no-print, .toast, .collapse-toggle { display: none !important; }
    .main-content { margin-left: 0 !important; padding: 0 !important; }
    .card { box-shadow: none !important; border: 1px solid #ddd !important; break-inside: avoid; page-break-inside: avoid; }
    .table-container { overflow: visible !important; }
    .table { font-size: 10px !important; }
    .table th, .table td { padding: 6px 10px !important; }
    body { font-size: 11px !important; }
    h1 { font-size: 18px !important; }
    h2 { font-size: 15px !important; }
    .stats-grid { break-inside: avoid; page-break-inside: avoid; }
    tr { break-inside: avoid; page-break-inside: avoid; }
}
```

---

### Task 10: Fix Undefined CSS Variables in movements.php

**Files:**
- Modify: `G:/xampp/htdocs/GasERP/transactions/movements.php`

The file uses `var(--g200)`, `var(--g50)`, `var(--accent)`, `var(--text-color)` which are not defined in the layout.

- [ ] **Step 1: Replace undefined variables with valid ones**

Use find-and-replace across the file:

| Old | New |
|-----|-----|
| `var(--g200)` | `var(--border)` |
| `var(--g50)` | `var(--bg)` |
| `var(--accent)` | `var(--primary)` |
| `var(--text-color)` | `var(--text)` |

---

### Task 11: Add Sidebar Logout Button HTML

**Files:**
- Modify: `G:/xampp/htdocs/GasERP/config/sidebar.php`

The CSS for `.sidebar-footer` and `.logout-btn` exists (lines 361-394) but no HTML is rendered.

- [ ] **Step 1: Add logout button HTML before `</nav>` (before line 737)**

```html
<div class="sidebar-footer">
    <a href="../auth/logout.php" class="logout-btn">
        <span class="menu-icon"><span class="material-symbols-rounded">logout</span></span>
        <span class="menu-label">Logout</span>
    </a>
</div>
```

---

### Task 12: Fix Non-Semantic Elements

**Files:**
- Modify: `G:/xampp/htdocs/GasERP/config/sidebar.php` (lines 524, 540)

- [ ] **Step 1: Change collapse toggle `<div>` to `<button>`**

Line 524, replace:
```html
<div class="collapse-toggle" onclick="toggleSidebar()" title="Toggle sidebar">
```
With:
```html
<button class="collapse-toggle" onclick="toggleSidebar()" title="Toggle sidebar" type="button">
```

And the closing tag (line 526) from `</div>` to `</button>`.

- [ ] **Step 2: Change user profile `<div>` to `<a>`**

Line 540, replace:
```html
<div class="user-profile" onclick="location.href='../users/profile.php'" ...>
```
With:
```html
<a href="../users/profile.php" class="user-profile" ...>
```

And the closing `</div>` to `</a>`.

---

### Task 13: Fix Duplicate openModal/closeModal in drivers/index.php

**Files:**
- Modify: `G:/xampp/htdocs/GasERP/drivers/index.php` (lines 472-473)

- [ ] **Step 1: Remove the second duplicate definition**

Delete lines 472-473 (the second `openModal`/`closeModal` definitions). The first definition at lines 446-447 and the global definitions in footer.php already cover this.

---

## Execution Order

1. **Tasks 1-2** (Security) — do first, highest risk
2. **Tasks 3-5** (Performance) — biggest impact on load time
3. **Tasks 6-8** (Mobile) — responsive fixes
4. **Task 9** (Print) — print layout
5. **Tasks 10-13** (Bug fixes) — cleanup
