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=>({"&":"&","<":"<",">":">","\"":""","'":"'"}[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'; ?>