BROOKO icon
BROOKO UK NETWORK
Where code meets creativity & adventure
File viewer

trutrak.php

Type
php
Size
19.47 KB
Modified
15 May
trutrak.php 19.47 KB
<?php
require_once __DIR__ . '/../bootstrap.php';
requirePermission('trutrak.manage');
require_once __DIR__ . '/../lib/trutrak_soap.php';

global $pdo;
$pageTitle = 'TruTrak Admin';
$message = null;
$error = null;

function wp_tt_ensure_mapping_table(PDO $pdo): void {
    try {
        $pdo->exec("CREATE TABLE IF NOT EXISTS `vehicle_trutrak_map` (
          `id` int(11) NOT NULL AUTO_INCREMENT,
          `vehicle_id` int(11) NOT NULL,
          `trutrak_device_id` varchar(32) NOT NULL,
          `linked_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
          PRIMARY KEY (`id`),
          UNIQUE KEY `uniq_vehicle` (`vehicle_id`),
          UNIQUE KEY `uniq_device` (`trutrak_device_id`),
          KEY `idx_vehicle` (`vehicle_id`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;");
    } catch (Throwable $e) {}
}

wp_tt_ensure_mapping_table($pdo);

$cfg = tt_get_cfg();
$gmapsKey = tt_sys_get('trutrak_gmaps_key', '');

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $action = (string)($_POST['action'] ?? '');

    if ($action === 'save_connector') {
        $base = trim((string)($_POST['base_url'] ?? $cfg['base_url']));
        if ($base !== '' && !preg_match('#^https?://#i', $base)) $base = 'https://' . $base;
        $base = rtrim($base, '/');

        $auth = strtoupper(trim((string)($_POST['auth_mode'] ?? $cfg['auth_mode'])));
        if (!in_array($auth, ['C','U'], true)) $auth = 'U';

        $xmltype = (int)($_POST['xmltype'] ?? $cfg['xmltype']);
        if (!in_array($xmltype, [0,1,2], true)) $xmltype = 0;

        $login = $cfg['login'];
        if (array_key_exists('login', $_POST) && trim((string)$_POST['login']) !== '') $login = trim((string)$_POST['login']);

        $password = $cfg['password'];
        if (array_key_exists('password', $_POST) && trim((string)$_POST['password']) !== '') $password = trim((string)$_POST['password']);

        $skey = $cfg['skey'];
        if (array_key_exists('skey', $_POST) && trim((string)$_POST['skey']) !== '') $skey = trim((string)$_POST['skey']);

        $gmaps = $gmapsKey;
        if (!empty($_POST['clear_gmaps_key'])) $gmaps = '';
        elseif (array_key_exists('gmaps_key', $_POST) && trim((string)$_POST['gmaps_key']) !== '') $gmaps = trim((string)$_POST['gmaps_key']);

        if ($base === '') {
            $error = 'Base URL is required (e.g. https://ttapi.trutrakpro.co.uk).';
        } else {
            tt_sys_set('trutrak_base_url', $base);
            tt_sys_set('trutrak_auth_mode', $auth);
            tt_sys_set('trutrak_xmltype', (string)$xmltype);
            tt_sys_set('trutrak_login', $login);
            tt_sys_set('trutrak_password', $password);
            tt_sys_set('trutrak_secret_key', $skey);
            tt_sys_set('trutrak_gmaps_key', $gmaps);
            tt_sys_set('trutrak_token', '');
            tt_sys_set('trutrak_token_expires', '0');

            $message = 'Saved TruTrak connector settings.';
            $cfg = tt_get_cfg();
            $gmapsKey = tt_sys_get('trutrak_gmaps_key', '');
        }
    }

    if ($action === 'save_trutrak_map') {
        $vehicleId = (int)($_POST['vehicle_id'] ?? 0);
        $deviceId = trim((string)($_POST['device_id'] ?? ''));
        if ($vehicleId <= 0) {
            $error = 'Invalid vehicle.';
        } else {
            try {
                $pdo->beginTransaction();
                $stmt = $pdo->prepare('DELETE FROM vehicle_trutrak_map WHERE vehicle_id = ?');
                $stmt->execute([$vehicleId]);
                if ($deviceId !== '') {
                    $stmt = $pdo->prepare('DELETE FROM vehicle_trutrak_map WHERE trutrak_device_id = ?');
                    $stmt->execute([$deviceId]);
                    $stmt = $pdo->prepare('INSERT INTO vehicle_trutrak_map (vehicle_id, trutrak_device_id) VALUES (?, ?)');
                    $stmt->execute([$vehicleId, $deviceId]);
                }
                $pdo->commit();
                $message = 'Vehicle mapping saved.';
                logActivity('trutrak.map', 'van', $vehicleId, $deviceId ? "Linked TruTrak device $deviceId" : 'Unlinked TruTrak device');
            } catch (Throwable $e) {
                if ($pdo->inTransaction()) $pdo->rollBack();
                $error = 'Failed to save mapping: ' . $e->getMessage();
            }
        }
    }

    if ($action === 'apply_trutrak_suggestions') {
        try {
            $vRows = $pdo->query("SELECT id, plate_full FROM vans WHERE is_active = 1 ORDER BY plate_full ASC")->fetchAll(PDO::FETCH_ASSOC);
            $dRows = $pdo->query("SELECT device_id AS deviceId, reg, name FROM trutrak_device_cache")->fetchAll(PDO::FETCH_ASSOC);
            $mRows = $pdo->query("SELECT vehicle_id, trutrak_device_id FROM vehicle_trutrak_map")->fetchAll(PDO::FETCH_ASSOC);
            $mappedVehicle = [];
            $mappedDevice = [];
            foreach ($mRows as $m) { $mappedVehicle[(int)$m['vehicle_id']] = (string)$m['trutrak_device_id']; $mappedDevice[(string)$m['trutrak_device_id']] = (int)$m['vehicle_id']; }
            $devByReg = [];
            foreach ($dRows as $d) {
                $did = trim((string)($d['deviceId'] ?? ''));
                $regRaw = trim((string)($d['reg'] ?? ''));
                $regClean = preg_replace('/^VAN[\s\-_:]*/i', '', $regRaw);
                $regNorm = strtoupper(preg_replace('/[^A-Z0-9]/i', '', $regClean));
                if ($did !== '' && $regNorm !== '' && !isset($devByReg[$regNorm])) $devByReg[$regNorm] = $did;
            }
            $pairs = [];
            foreach ($vRows as $v) {
                $vid = (int)$v['id'];
                if (isset($mappedVehicle[$vid])) continue;
                $plateNorm = strtoupper(preg_replace('/[^A-Z0-9]/i', '', (string)($v['plate_full'] ?? '')));
                $did = $devByReg[$plateNorm] ?? '';
                if ($did === '' || isset($mappedDevice[$did])) continue;
                $pairs[] = [$vid, $did];
                $mappedDevice[$did] = $vid;
            }
            if (!$pairs) {
                $message = 'No auto-link suggestions found.';
            } else {
                $pdo->beginTransaction();
                $delVeh = $pdo->prepare('DELETE FROM vehicle_trutrak_map WHERE vehicle_id = ?');
                $delDev = $pdo->prepare('DELETE FROM vehicle_trutrak_map WHERE trutrak_device_id = ?');
                $ins = $pdo->prepare('INSERT INTO vehicle_trutrak_map (vehicle_id, trutrak_device_id) VALUES (?, ?)');
                foreach ($pairs as [$vid, $did]) { $delVeh->execute([$vid]); $delDev->execute([$did]); $ins->execute([$vid, $did]); }
                $pdo->commit();
                $message = 'Applied ' . count($pairs) . ' TruTrak auto-link suggestion' . (count($pairs) === 1 ? '' : 's') . '.';
            }
        } catch (Throwable $e) {
            if ($pdo->inTransaction()) $pdo->rollBack();
            $error = 'Failed to apply suggestions: ' . $e->getMessage();
        }
    }
}

$vans = [];
try { $vans = $pdo->query("SELECT id, plate_full, plate_short, make, model, is_active FROM vans ORDER BY is_active DESC, plate_full ASC")->fetchAll(PDO::FETCH_ASSOC); } catch (Throwable $e) {}
$devices = [];
try { $devices = $pdo->query("SELECT device_id AS deviceId, reg, name, status, last_seen AS deviceTime, updated_at FROM trutrak_device_cache ORDER BY COALESCE(NULLIF(reg,''), NULLIF(name,''), device_id)")->fetchAll(PDO::FETCH_ASSOC); } catch (Throwable $e) {}
$map = [];
try { foreach ($pdo->query("SELECT vehicle_id, trutrak_device_id FROM vehicle_trutrak_map")->fetchAll(PDO::FETCH_ASSOC) as $r) $map[(int)$r['vehicle_id']] = (string)$r['trutrak_device_id']; } catch (Throwable $e) {}

$linked = array_flip(array_values(array_filter($map, static fn($x) => (string)$x !== '')));
$unlinked = [];
foreach ($devices as $d) { $did = (string)($d['deviceId'] ?? ''); if ($did !== '' && !isset($linked[$did])) $unlinked[] = $d; }

$hasSecrets = ($cfg['login'] !== '' || $cfg['password'] !== '' || $cfg['skey'] !== '');

$extraHead = '<style>
.smart-admin-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;}
.smart-card-note{margin-top:8px;color:var(--text-muted);font-size:.9rem;}
@media(max-width:900px){.smart-admin-grid{grid-template-columns:1fr;}}
</style>';
include __DIR__ . '/../partials/header.php';
?>

<div class="content-header">
    <div>
        <h1 class="content-title">πŸ“ TruTrak Admin</h1>
        <p class="content-subtitle">Connector settings and vehicle/device mapping in one tab.</p>
    </div>
    <div class="content-actions"><a class="btn btn-secondary" href="<?= e(app_url('trutrak/map')) ?>">πŸ—ΊοΈ Open TruTrak Map</a></div>
</div>

<?php if ($message): ?><div class="alert alert-success"><?= e($message) ?></div><?php endif; ?>
<?php if ($error): ?><div class="alert alert-error"><?= e($error) ?></div><?php endif; ?>

<div class="smart-admin-grid">
    <div class="card">
        <div class="card-header"><h3 class="card-title">API Connector</h3></div>
        <div class="card-body">
            <form method="post" class="grid" style="gap:14px;">
                <input type="hidden" name="action" value="save_connector">
                <div>
                    <label class="field-label" for="base_url">Base URL</label>
                    <input class="input" id="base_url" name="base_url" type="text" value="<?= e($cfg['base_url']) ?>" placeholder="https://ttapi.trutrakpro.co.uk">
                    <div class="smart-card-note">WSDL: <code><?= e(rtrim($cfg['base_url'], '/')) ?>/?WSDL</code></div>
                </div>
                <div class="grid grid-2" style="gap:12px;">
                    <div><label class="field-label" for="login">Login</label><input class="input" id="login" name="login" type="text" value="" placeholder="<?= e($cfg['login'] ? 'Saved (leave blank)' : 'Enter login') ?>"></div>
                    <div><label class="field-label" for="password">Password</label><input class="input" id="password" name="password" type="password" value="" placeholder="<?= e($cfg['password'] ? 'Saved (leave blank)' : 'Enter password') ?>"></div>
                </div>
                <div><label class="field-label" for="skey">Secret key (SKey)</label><input class="input" id="skey" name="skey" type="text" value="" placeholder="<?= e($cfg['skey'] ? 'Saved (leave blank)' : 'Enter secret key') ?>"></div>
                <div>
                    <label class="field-label" for="gmaps_key">Google Maps API key (optional)</label>
                    <input class="input" id="gmaps_key" name="gmaps_key" type="text" value="" placeholder="<?= e($gmapsKey ? 'Saved (leave blank)' : 'Paste Google Maps JS API key') ?>">
                    <?php if ($gmapsKey): ?><label class="d-flex align-center gap-2" style="cursor:pointer;margin-top:8px;"><input type="checkbox" name="clear_gmaps_key" value="1"><span class="text-muted">Clear saved key</span></label><?php endif; ?>
                </div>
                <div class="grid grid-2" style="gap:12px;">
                    <div><label class="field-label" for="auth_mode">Auth mode</label><select class="input" id="auth_mode" name="auth_mode"><option value="C" <?= $cfg['auth_mode'] === 'C' ? 'selected' : '' ?>>C β€” customer login</option><option value="U" <?= $cfg['auth_mode'] === 'U' ? 'selected' : '' ?>>U β€” user login</option></select></div>
                    <div><label class="field-label" for="xmltype">XMLTYPE</label><select class="input" id="xmltype" name="xmltype"><option value="0" <?= $cfg['xmltype'] === 0 ? 'selected' : '' ?>>0 β€” Nested</option><option value="1" <?= $cfg['xmltype'] === 1 ? 'selected' : '' ?>>1 β€” Inline</option><option value="2" <?= $cfg['xmltype'] === 2 ? 'selected' : '' ?>>2 β€” Schema + Inline</option></select></div>
                </div>
                <div class="d-flex gap-md" style="flex-wrap:wrap;align-items:center;">
                    <button class="btn btn-primary" type="submit">πŸ’Ύ Save Connector</button>
                    <button class="btn btn-secondary" type="button" id="ttTestBtn">πŸ§ͺ Test</button>
                    <button class="btn btn-secondary" type="button" id="ttClearTokenBtn">β™» Clear Token</button>
                    <span class="text-muted small">Token: <?= ($cfg['token'] !== '' && $cfg['token_exp'] > time() + 300) ? 'cached' : 'none' ?></span>
                </div>
            </form>
            <div id="ttTestOut" style="display:none;margin-top:14px;"></div>
        </div>
    </div>

    <div class="card">
        <div class="card-header"><h3 class="card-title">Quick Status</h3></div>
        <div class="card-body">
            <div class="grid" style="gap:12px;">
                <div><strong>Connector:</strong> <span class="text-muted"><?= ($cfg['base_url'] && $hasSecrets) ? 'Configured' : 'Not complete' ?></span></div>
                <div><strong>Cached devices:</strong> <span class="text-muted"><?= count($devices) ?></span></div>
                <div><strong>Linked vehicles:</strong> <span class="text-muted"><?= count(array_filter($map)) ?></span></div>
                <div class="smart-card-note">Vehicle mapping now lives here, not under Management β†’ Vehicles.</div>
            </div>
        </div>
    </div>
</div>

<div class="card" id="vehicle-mapping" style="margin-top:16px;">
    <div class="card-header">
        <h3 class="card-title">🧭 Vehicle ↔ TruTrak Device Mapping</h3>
        <form method="post" onsubmit="return confirm('Apply all plate/device suggestions now?');">
            <input type="hidden" name="action" value="apply_trutrak_suggestions">
            <button class="btn btn-secondary btn-sm" type="submit">Auto-link suggestions</button>
        </form>
    </div>
    <div class="card-body" style="overflow:auto;">
        <?php if (!$devices): ?><div class="alert alert-warning">No TruTrak devices are cached yet. Open the map and refresh once, then return here.</div><?php endif; ?>
        <table class="table" style="min-width:900px;">
            <thead><tr><th>Vehicle</th><th>Details</th><th>Linked Device</th><th style="width:140px;">Action</th></tr></thead>
            <tbody>
            <?php if (!$vans): ?>
                <tr><td colspan="4" class="text-muted">No vehicles found.</td></tr>
            <?php else: foreach ($vans as $v):
                $vid = (int)$v['id'];
                $current = $map[$vid] ?? '';
                $label = trim((string)($v['plate_short'] ?: '') . ' ' . (string)($v['plate_full'] ?: '')) ?: ('Vehicle #' . $vid);
                $details = trim((string)($v['make'] ?? '') . ' ' . (string)($v['model'] ?? ''));
            ?>
                <tr>
                    <td><strong><?= e($label) ?></strong><?php if (!(int)$v['is_active']): ?><span class="badge badge-muted" style="margin-left:8px;">Inactive</span><?php endif; ?></td>
                    <td class="text-muted"><?= e($details ?: 'β€”') ?></td>
                    <td>
                        <form method="post" style="display:flex;gap:8px;align-items:center;">
                            <input type="hidden" name="action" value="save_trutrak_map">
                            <input type="hidden" name="vehicle_id" value="<?= $vid ?>">
                            <select name="device_id" class="input" style="max-width:560px;">
                                <option value="">β€” Not linked β€”</option>
                                <?php foreach ($devices as $d):
                                    $did = (string)$d['deviceId'];
                                    $dLabel = trim((string)($d['reg'] ?? '') . ' ' . (string)($d['name'] ?? '')) ?: ('Asset ' . $did);
                                ?>
                                    <option value="<?= e($did) ?>" <?= $current === $did ? 'selected' : '' ?>><?= e($dLabel) ?> (<?= e($did) ?>)</option>
                                <?php endforeach; ?>
                            </select>
                    </td>
                    <td><button class="btn btn-primary" type="submit">Save</button></form></td>
                </tr>
            <?php endforeach; endif; ?>
            </tbody>
        </table>
    </div>
</div>

<div class="card" style="margin-top:16px;">
    <div class="card-header"><h3 class="card-title">Unlinked Cached Devices</h3></div>
    <div class="card-body">
        <?php if (!$devices): ?>
            <p class="text-muted">No cached devices yet.</p>
        <?php elseif (!$unlinked): ?>
            <p class="text-muted">All cached devices are linked.</p>
        <?php else: ?>
            <ul class="list">
                <?php foreach ($unlinked as $d): $did = (string)$d['deviceId']; $dLabel = trim((string)($d['reg'] ?? '') . ' ' . (string)($d['name'] ?? '')) ?: ('Asset ' . $did); ?>
                    <li><strong><?= e($dLabel) ?></strong> <span class="text-muted">ID: <?= e($did) ?></span></li>
                <?php endforeach; ?>
            </ul>
        <?php endif; ?>
    </div>
</div>

<div class="card" style="margin-top:16px;">
    <div class="card-header"><h3 class="card-title">Permissions</h3></div>
    <div class="card-body">
        <ul style="margin:0;padding-left:18px;">
            <li><code>trutrak.view</code> β€” view the TruTrak Map</li>
            <li><code>trutrak.history</code> β€” access journey/history views where enabled</li>
            <li><code>trutrak.manage</code> β€” edit connector settings and vehicle mapping</li>
        </ul>
    </div>
</div>

<script>
(function(){
    const out = document.getElementById('ttTestOut');
    const testBtn = document.getElementById('ttTestBtn');
    const clearBtn = document.getElementById('ttClearTokenBtn');
    function esc(s){return String(s??'').replace(/[&<>\"']/g, c=>({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;"}[c]));}
    async function test(){
        if(!out) return; out.style.display='block'; out.innerHTML='<div class="alert alert-info">Testing…</div>';
        try{
            const res=await fetch('<?= e(app_api_url('trutrak')) ?>?action=test',{headers:{'Accept':'application/json'}});
            const txt=await res.text(); let data=null; try{data=txt?JSON.parse(txt):null;}catch(parseErr){throw new Error(`API returned non-JSON (HTTP ${res.status}). ${txt ? txt.slice(0,200) : 'Empty response.'}`);}
            if(!data || !data.ok) throw new Error(data?.error || 'Test failed');
            const sample=(data.assets_sample&&data.assets_sample.length)?'<pre style="margin:10px 0 0;max-height:220px;overflow:auto;">'+esc(JSON.stringify(data.assets_sample,null,2))+'</pre>':'<div class="text-muted" style="margin-top:10px;">No assets returned.</div>';
            out.innerHTML='<div class="alert alert-success"><strong>Connected βœ…</strong><div class="text-muted" style="margin-top:6px;">WSDL: <code>'+esc(data.wsdl_url||'')+'</code></div>'+sample+'</div>';
        }catch(e){ out.innerHTML='<div class="alert alert-error">'+esc(e?.message || 'Test failed')+'</div>'; }
    }
    async function clearToken(){
        if(!out) return; out.style.display='block'; out.innerHTML='<div class="alert alert-info">Clearing token…</div>';
        try{
            const res=await fetch('<?= e(app_api_url('trutrak')) ?>?action=clear_token',{headers:{'Accept':'application/json'}});
            const txt=await res.text(); let data=null; try{data=txt?JSON.parse(txt):null;}catch(parseErr){throw new Error(`API returned non-JSON (HTTP ${res.status}). ${txt ? txt.slice(0,200) : 'Empty response.'}`);}
            if(!data || !data.ok) throw new Error(data?.error || 'Failed');
            out.innerHTML='<div class="alert alert-success">Token cleared.</div>';
        }catch(e){ out.innerHTML='<div class="alert alert-error">'+esc(e?.message || 'Failed')+'</div>'; }
    }
    testBtn?.addEventListener('click', test);
    clearBtn?.addEventListener('click', clearToken);
})();
</script>

<?php include __DIR__ . '/../partials/footer.php'; ?>