trutrak_api.php
29.16 KB
<?php
require_once __DIR__ . '/../bootstrap.php';
require_once __DIR__ . '/../lib/trutrak_soap.php';
header('Content-Type: application/json; charset=utf-8');
// Never leak PHP warnings/notices into JSON (breaks fetch().json()).
@ini_set('display_errors', '0');
@ini_set('html_errors', '0');
error_reporting(E_ALL);
$__tt_warnings = [];
set_error_handler(function ($severity, $message, $file, $line) use (&$__tt_warnings) {
// Collect warnings/notices and keep output clean.
$__tt_warnings[] = ['severity' => $severity, 'message' => $message, 'file' => basename($file), 'line' => $line];
return true; // handled
});
/**
* Ensure this endpoint ALWAYS returns JSON (even on fatal errors).
*/
register_shutdown_function(function () {
$err = error_get_last();
if ($err && in_array($err['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR], true)) {
if (!headers_sent()) {
http_response_code(500);
header('Content-Type: application/json; charset=utf-8');
}
echo json_encode(['ok' => false, 'error' => 'Server error', 'detail' => $err['message'] ?? 'fatal'], JSON_UNESCAPED_SLASHES);
exit;
}
});
$action = $_GET['action'] ?? 'overview';
// Permission gates
if (in_array($action, ['config', 'save_config', 'test', 'clear_token', 'geofence_meta_save'], true)) {
requirePermission('trutrak.manage');
} elseif (in_array($action, ['history', 'history_csv', 'trail', 'journeys', 'dailytrail'], true)) {
// heavy endpoints
if (hasPermission('trutrak.history')) {
// ok
} else {
requirePermission('trutrak.view');
http_response_code(403);
echo json_encode(['ok' => false, 'error' => 'Missing permission: trutrak.history']);
exit;
}
} else {
requirePermission('trutrak.view');
}
if (!function_exists('curl_init')) {
http_response_code(500);
echo json_encode(['ok' => false, 'error' => 'Server missing cURL (curl_init). Enable PHP cURL extension.']);
exit;
}
if (!class_exists('DOMDocument')) {
http_response_code(500);
echo json_encode(['ok' => false, 'error' => 'Server missing DOMDocument. Enable PHP DOM/XML extension.']);
exit;
}
function tt_json($arr, int $code = 200): void {
http_response_code($code);
global $__tt_warnings;
if (!empty($__tt_warnings) && is_array($arr)) {
// Only attach warnings for easier debugging; safe to ignore client-side.
$arr['_warnings'] = $__tt_warnings;
}
echo json_encode($arr, JSON_UNESCAPED_SLASHES);
exit;
}
function tt_bad(string $msg, int $code = 400, array $extra = []): void {
tt_json(array_merge(['ok' => false, 'error' => $msg], $extra), $code);
}
function tt_csv(array $rows, string $filename = 'trutrak_export.csv'): void {
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="' . $filename . '"');
$out = fopen('php://output', 'w');
if (!$out) exit;
if (empty($rows)) {
fclose($out);
exit;
}
$headers = array_keys($rows[0]);
fputcsv($out, $headers);
foreach ($rows as $r) {
$line = [];
foreach ($headers as $h) $line[] = $r[$h] ?? '';
fputcsv($out, $line);
}
fclose($out);
exit;
}
// ─────────────────────────────────────────────────────────────────────────────
// DB cache helpers (keeps last known locations loaded without page refresh)
// ─────────────────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────
// Geofence meta (category + icon) helpers
// ─────────────────────────────────────────────────────────────────────────────
function tt_geofence_meta_init(PDO $pdo): void {
$pdo->exec("CREATE TABLE IF NOT EXISTS trutrak_geofence_meta (\n"
. " geofence_key VARCHAR(64) NOT NULL PRIMARY KEY,\n"
. " category VARCHAR(32) NOT NULL DEFAULT 'other',\n"
. " icon VARCHAR(32) NOT NULL DEFAULT '📍',\n"
. " label VARCHAR(255) NULL,\n"
. " updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP\n"
. ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
}
function tt_geofence_key(array $row): string {
$id = (string)($row['ID'] ?? $row['id'] ?? $row['WAYPOINT_ID'] ?? $row['waypoint_id'] ?? $row['GEOFENCE_ID'] ?? $row['geofence_id'] ?? '');
if ($id !== '') {
// keep it compact
return 'id:' . substr($id, 0, 58);
}
$wkt = (string)($row['GEO_COORDINATES'] ?? $row['geo_coordinates'] ?? $row['Geo_Coordinates'] ?? $row['GEO_COORDINATE'] ?? $row['geo'] ?? '');
$wkt = trim($wkt);
if ($wkt !== '') {
return 'w:' . sha1($wkt);
}
// fallback to address-based key
$addr = implode('|', [
(string)($row['ADDRESS1'] ?? $row['address1'] ?? ''),
(string)($row['ADDRESS2'] ?? $row['address2'] ?? ''),
(string)($row['TOWN'] ?? $row['town'] ?? ''),
(string)($row['POSTCODE'] ?? $row['postcode'] ?? ''),
(string)($row['COUNTY'] ?? $row['county'] ?? ''),
(string)($row['COUNTRY'] ?? $row['country'] ?? ''),
]);
return 'a:' . sha1(trim($addr));
}
function tt_geofence_meta_fetch(PDO $pdo, array $keys): array {
if (empty($keys)) return [];
tt_geofence_meta_init($pdo);
$keys = array_values(array_unique(array_filter($keys, fn($x) => (string)$x !== '')));
if (empty($keys)) return [];
$in = implode(',', array_fill(0, count($keys), '?'));
$st = $pdo->prepare("SELECT geofence_key, category, icon, label FROM trutrak_geofence_meta WHERE geofence_key IN ($in)");
$st->execute($keys);
$out = [];
foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $r) {
$out[(string)$r['geofence_key']] = $r;
}
return $out;
}
function tt_cache_init(PDO $pdo): void {
// Safe to call even if migration hasn't run yet.
$pdo->exec("CREATE TABLE IF NOT EXISTS trutrak_device_cache (\n"
. " device_id VARCHAR(64) NOT NULL PRIMARY KEY,\n"
. " unique_id VARCHAR(64) NULL,\n"
. " name VARCHAR(255) NULL,\n"
. " reg VARCHAR(64) NULL,\n"
. " latitude DECIMAL(10,7) NULL,\n"
. " longitude DECIMAL(10,7) NULL,\n"
. " speed VARCHAR(32) NULL,\n"
. " heading VARCHAR(32) NULL,\n"
. " status VARCHAR(64) NULL,\n"
. " address TEXT NULL,\n"
. " last_seen DATETIME NULL,\n"
. " raw_json LONGTEXT NULL,\n"
. " updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP\n"
. ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
}
function tt_cache_write(PDO $pdo, array $items): void {
tt_cache_init($pdo);
$sql = "INSERT INTO trutrak_device_cache (device_id, unique_id, name, reg, latitude, longitude, speed, heading, status, address, last_seen, raw_json)\n"
. "VALUES (?,?,?,?,?,?,?,?,?,?,?,?)\n"
. "ON DUPLICATE KEY UPDATE unique_id=VALUES(unique_id), name=VALUES(name), reg=VALUES(reg), latitude=VALUES(latitude), longitude=VALUES(longitude),\n"
. "speed=VALUES(speed), heading=VALUES(heading), status=VALUES(status), address=VALUES(address), last_seen=VALUES(last_seen), raw_json=VALUES(raw_json), updated_at=CURRENT_TIMESTAMP";
$st = $pdo->prepare($sql);
foreach ($items as $r) {
$deviceId = (string)($r['deviceId'] ?? $r['device_id'] ?? '');
if ($deviceId === '') continue;
// TruTrak payloads vary by account/endpoint. Some use uppercase keys.
$regVal = (string)($r['reg'] ?? $r['REG'] ?? $r['Reg'] ?? '');
$nameVal = (string)($r['name'] ?? $r['NAME'] ?? $r['Name'] ?? '');
$uniqueVal = (string)($r['uniqueId'] ?? $r['UNIQUE_ID'] ?? $r['UniqueId'] ?? '');
$last = $r['deviceTime'] ?? $r['fixTime'] ?? $r['time'] ?? $r['timestamp'] ?? $r['serverTime'] ?? null;
$lastSeen = null;
if ($last) {
try {
$ts = strtotime((string)$last);
if ($ts !== false) $lastSeen = date('Y-m-d H:i:s', $ts);
} catch (Throwable $e) {}
}
$st->execute([
$deviceId,
$uniqueVal,
$nameVal,
$regVal,
(isset($r['latitude']) ? (float)$r['latitude'] : null),
(isset($r['longitude']) ? (float)$r['longitude'] : null),
(string)($r['speed'] ?? $r['spd'] ?? ''),
(string)($r['heading'] ?? $r['hdg'] ?? ''),
(string)($r['status'] ?? ''),
(string)($r['address'] ?? ''),
$lastSeen,
json_encode($r, JSON_UNESCAPED_SLASHES),
]);
}
// Track last cache refresh
tt_sys_set('trutrak_cache_updated_at', (string)time());
}
function tt_cache_read(PDO $pdo): array {
try {
tt_cache_init($pdo);
$rows = $pdo->query(
"SELECT device_id AS deviceId, unique_id AS uniqueId, name, reg, latitude, longitude, speed, heading, status, address, last_seen AS deviceTime, updated_at
"
. "FROM trutrak_device_cache ORDER BY COALESCE(NULLIF(name,''), NULLIF(reg,''), device_id)"
)->fetchAll(PDO::FETCH_ASSOC);
// Default behaviour: only show devices that are linked to an ACTIVE vehicle.
// Admins (trutrak.manage) can bypass with ?show_all=1 for debugging.
$showAll = ((string)($_GET['show_all'] ?? '0') === '1' || (string)($_GET['include_unlinked'] ?? '0') === '1') && hasPermission('trutrak.manage');
$stats = [
'total' => count($rows),
'shown' => count($rows),
'hidden_unlinked' => 0,
'hidden_inactive' => 0,
];
// Load mapping + vehicle meta
$mapRows = null;
try {
$mapRows = $pdo->query(
"SELECT m.trutrak_device_id AS deviceId, v.id AS vehicleId, v.plate_full AS plateFull, v.plate_short AS plateShort, v.make AS make, v.model AS model, v.tag_color AS tagColor, v.is_active AS isActive
"
. "FROM vehicle_trutrak_map m JOIN vans v ON v.id = m.vehicle_id"
)->fetchAll(PDO::FETCH_ASSOC);
} catch (Throwable $e) {
$mapRows = null;
}
$activeByDevice = [];
$anyByDevice = [];
foreach ($mapRows as $m) {
$did = (string)($m['deviceId'] ?? '');
if ($did === '') continue;
$anyByDevice[$did] = $m;
if ((int)($m['isActive'] ?? 0) === 1) $activeByDevice[$did] = $m;
}
// If mapping tables are missing (older installs), fail open and show everything.
if ($mapRows === null) {
$stats['shown'] = count($rows);
$updated = (int)tt_sys_get('trutrak_cache_updated_at', '0');
return ['ok' => true, 'updated_at' => $updated, 'devices' => $rows, 'stats' => $stats, 'show_all' => $showAll];
}
$out = [];
if ($showAll) {
foreach ($rows as $r) {
$dev = (string)($r['deviceId'] ?? '');
if ($dev !== '' && isset($anyByDevice[$dev])) {
$m = $anyByDevice[$dev];
$r['vehicleId'] = $m['vehicleId'];
$r['plateFull'] = $m['plateFull'];
$r['plateShort'] = $m['plateShort'];
$r['make'] = $m['make'] ?? '';
$r['model'] = $m['model'] ?? '';
$r['tagColor'] = $m['tagColor'] ?? '#ff8c1a';
$r['vehicleActive'] = (int)($m['isActive'] ?? 0);
}
$out[] = $r;
}
} else {
foreach ($rows as $r) {
$dev = (string)($r['deviceId'] ?? '');
if ($dev !== '' && isset($activeByDevice[$dev])) {
$m = $activeByDevice[$dev];
$r['vehicleId'] = $m['vehicleId'];
$r['plateFull'] = $m['plateFull'];
$r['plateShort'] = $m['plateShort'];
$r['make'] = $m['make'] ?? '';
$r['model'] = $m['model'] ?? '';
$r['tagColor'] = $m['tagColor'] ?? '#ff8c1a';
$r['vehicleActive'] = 1;
$out[] = $r;
} else {
if ($dev !== '' && isset($anyByDevice[$dev]) && (int)($anyByDevice[$dev]['isActive'] ?? 0) === 0) {
$stats['hidden_inactive']++;
} else {
$stats['hidden_unlinked']++;
}
}
}
}
$stats['shown'] = count($out);
$updated = (int)tt_sys_get('trutrak_cache_updated_at', '0');
return ['ok' => true, 'updated_at' => $updated, 'devices' => $out, 'stats' => $stats, 'show_all' => $showAll];
} catch (Throwable $e) {
return ['ok' => true, 'updated_at' => 0, 'devices' => [], 'stats' => ['total' => 0, 'shown' => 0, 'hidden_unlinked' => 0, 'hidden_inactive' => 0]];
}
}
$cfg = tt_get_cfg();
if (empty($cfg['base_url'])) {
tt_bad('TruTrak base URL not configured. Go to Administration → TruTrak.', 400);
}
// ─────────────────────────────────────────────────────────────────────────────
// Config endpoints
// ─────────────────────────────────────────────────────────────────────────────
if ($action === 'config') {
tt_json([
'ok' => true,
'config' => [
'base_url' => $cfg['base_url'],
'login' => $cfg['login'] ? '***' : '',
'password' => $cfg['password'] ? '***' : '',
'skey' => $cfg['skey'] ? '***' : '',
'auth_mode' => $cfg['auth_mode'],
'xmltype' => $cfg['xmltype'],
'token_cached' => ($cfg['token'] !== '' && $cfg['token_exp'] > time() + 300),
]
]);
}
if ($action === 'save_config') {
// Accept either JSON or form post
$in = $_POST;
if (empty($in) && str_contains($_SERVER['CONTENT_TYPE'] ?? '', 'application/json')) {
$raw = file_get_contents('php://input');
$j = json_decode($raw ?: '', true);
if (is_array($j)) $in = $j;
}
$base = trim((string)($in['base_url'] ?? $cfg['base_url']));
if ($base && !preg_match('#^https?://#i', $base)) $base = 'https://' . $base;
$base = rtrim($base, '/');
if ($base === '') tt_bad('Base URL is required');
$auth = strtoupper(trim((string)($in['auth_mode'] ?? $cfg['auth_mode'])));
if (!in_array($auth, ['C','U'], true)) $auth = 'U';
$xmltype = (int)($in['xmltype'] ?? $cfg['xmltype']);
if (!in_array($xmltype, [0,1,2], true)) $xmltype = 0;
// Only overwrite secrets if user supplied non-empty values
$login = $cfg['login'];
if (array_key_exists('login', $in) && trim((string)$in['login']) !== '') $login = trim((string)$in['login']);
$pass = $cfg['password'];
if (array_key_exists('password', $in) && trim((string)$in['password']) !== '') $pass = trim((string)$in['password']);
$skey = $cfg['skey'];
if (array_key_exists('skey', $in) && trim((string)$in['skey']) !== '') $skey = trim((string)$in['skey']);
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', $pass);
tt_sys_set('trutrak_secret_key', $skey);
// Clear token if creds changed
tt_sys_set('trutrak_token', '');
tt_sys_set('trutrak_token_expires', '0');
tt_json(['ok' => true]);
}
if ($action === 'clear_token') {
tt_sys_set('trutrak_token', '');
tt_sys_set('trutrak_token_expires', '0');
tt_json(['ok' => true]);
}
if ($action === 'test') {
// Ensure WSDL loads + token works + assets list returns something
$wsdl = tt_load_wsdl($cfg['base_url'], true);
if (!$wsdl['ok']) tt_bad('Failed to load WSDL: ' . ($wsdl['error'] ?? 'Unknown error'), 502);
$t = tt_get_token($cfg, true);
if (!$t['ok']) tt_bad('Auth failed: ' . ($t['error'] ?? 'Unknown error'), 502);
$cfg = tt_get_cfg(); // reload (token now cached)
$token = $t['token'];
$assets = tt_call($cfg, 'TTAssetsList', ['token' => $token, 'filter_registration' => '', 'filter_depots' => '']);
tt_json([
'ok' => true,
'wsdl_url' => $wsdl['url'] ?? null,
'token_cached' => !$t['cached'],
'assets_sample' => $assets['ok'] ? array_slice($assets['rows'] ?? [], 0, 3) : null,
'assets_error' => $assets['ok'] ? null : ($assets['error'] ?? 'AssetsList failed'),
]);
}
// ─────────────────────────────────────────────────────────────────────────────
// Data endpoints
// ─────────────────────────────────────────────────────────────────────────────
// Cached last-known locations (DB-backed) — returns immediately without hitting TruTrak.
if ($action === 'cache') {
global $pdo;
$c = tt_cache_read($pdo);
tt_json([
'ok' => true,
'cached' => true,
'updated_at' => $c['updated_at'],
'devices' => $c['devices'],
'rows' => $c['devices'],
'items' => $c['devices'],
]);
}
// Save geofence meta (category + icon). Does NOT require a TruTrak token.
if ($action === 'geofence_meta_save') {
global $pdo;
tt_geofence_meta_init($pdo);
$raw = file_get_contents('php://input');
$in = [];
if ($raw) {
$j = json_decode($raw, true);
if (is_array($j)) $in = $j;
}
if (empty($in)) $in = $_POST;
$key = trim((string)($in['key'] ?? ''));
if ($key === '' || strlen($key) > 64) tt_bad('Invalid geofence key', 400);
$category = strtolower(trim((string)($in['category'] ?? 'other')));
$allowedCats = ['office','yard','other'];
if (!in_array($category, $allowedCats, true)) $category = 'other';
$icon = trim((string)($in['icon'] ?? '📍'));
if ($icon === '' || mb_strlen($icon) > 32) $icon = '📍';
$label = trim((string)($in['label'] ?? ''));
if ($label !== '' && mb_strlen($label) > 255) $label = mb_substr($label, 0, 255);
$st = $pdo->prepare("INSERT INTO trutrak_geofence_meta (geofence_key, category, icon, label) VALUES (?,?,?,?)\n"
. "ON DUPLICATE KEY UPDATE category=VALUES(category), icon=VALUES(icon), label=VALUES(label), updated_at=CURRENT_TIMESTAMP");
$st->execute([$key, $category, $icon, ($label === '' ? null : $label)]);
tt_json(['ok' => true]);
}
// Ensure token for most endpoints
$token = '';
if (!in_array($action, ['config','save_config','clear_token','geofence_meta_save'], true)) {
$tok = tt_get_token($cfg, false);
if (!$tok['ok']) tt_bad($tok['error'] ?? 'Authentication required', 502);
$token = $tok['token'];
$cfg = tt_get_cfg(); // reload cached token
}
// Method helper: pick best available last-location method
function tt_best_last_location_method(array $cfg, bool $withWp = false): array {
$wsdl = tt_load_wsdl($cfg['base_url'], false);
if (!$wsdl['ok']) return ['ok' => false, 'error' => $wsdl['error'] ?? 'Failed to load WSDL'];
$w = $wsdl['raw'];
$cands = [];
if ($withWp) {
$cands = ['TTAssetsLastLocationWithWP_Extended', 'TTAssetsLastLocationWithWP', 'TTAssetsLastLocation_Extended', 'TTAssetsLastLocation'];
} else {
$cands = ['TTAssetsLastLocation_Extended', 'TTAssetsLastLocation', 'TTAssetsLastLocationWithWP_Extended', 'TTAssetsLastLocationWithWP'];
}
foreach ($cands as $m) {
if (tt_wsdl_has_method($w, $m)) return ['ok' => true, 'method' => $m];
}
return ['ok' => false, 'error' => 'No last-location method found in WSDL'];
}
if ($action === 'overview' || $action === 'live') {
$withWp = ($_GET['with_wp'] ?? '0') === '1';
$filterAssets = trim((string)($_GET['filter_assets'] ?? ''));
$pick = tt_best_last_location_method($cfg, $withWp);
if (!$pick['ok']) tt_bad($pick['error'] ?? 'No suitable method found', 502);
$res = tt_call($cfg, $pick['method'], [
'token' => $token,
'filter_assets' => $filterAssets,
'xmltype' => (string)$cfg['xmltype'],
]);
if (!$res['ok']) tt_bad($res['error'] ?? 'API call failed', 502);
$items = [];
foreach (($res['rows'] ?? []) as $r) $items[] = tt_norm_location($r);
// Keep a full copy for DB cache (includes unlinked devices for mapping UI)
$itemsForCache = $items;
// Add mapped vehicle metadata (plate + tag colour) and optionally filter.
// Default: only show mapped devices for ACTIVE vehicles (keeps map clean).
$showAll = ((string)($_GET['show_all'] ?? '0') === '1' || (string)($_GET['include_unlinked'] ?? '0') === '1') && hasPermission('trutrak.manage');
$stats = [
'total' => count($items),
'shown' => count($items),
'hidden_unlinked' => 0,
'hidden_inactive' => 0,
];
try {
$map = $pdo->query(
"SELECT m.trutrak_device_id AS deviceId, v.id AS vehicleId, v.plate_full AS plateFull, v.plate_short AS plateShort, v.make AS make, v.model AS model, v.tag_color AS tagColor, v.is_active AS isActive
"
. "FROM vehicle_trutrak_map m JOIN vans v ON v.id = m.vehicle_id"
)->fetchAll(PDO::FETCH_ASSOC);
$activeByDevice = [];
$anyByDevice = [];
foreach ($map as $m) {
$did = (string)($m['deviceId'] ?? '');
if ($did === '') continue;
$anyByDevice[$did] = $m;
if ((int)($m['isActive'] ?? 0) === 1) $activeByDevice[$did] = $m;
}
$out = [];
foreach ($items as $r) {
$dev = (string)($r['deviceId'] ?? $r['device_id'] ?? '');
if ($dev !== '' && isset($anyByDevice[$dev])) {
$m = $anyByDevice[$dev];
$r['vehicleId'] = $m['vehicleId'];
$r['plateFull'] = $m['plateFull'];
$r['plateShort'] = $m['plateShort'];
$r['make'] = $m['make'] ?? '';
$r['model'] = $m['model'] ?? '';
$r['tagColor'] = $m['tagColor'] ?? '#ff8c1a';
$r['vehicleActive'] = (int)($m['isActive'] ?? 0);
}
if ($showAll) {
$out[] = $r;
continue;
}
// Filter out unlinked + inactive vehicles
if ($dev !== '' && isset($activeByDevice[$dev])) {
$out[] = $r;
} else {
if ($dev !== '' && isset($anyByDevice[$dev]) && (int)($anyByDevice[$dev]['isActive'] ?? 0) === 0) {
$stats['hidden_inactive']++;
} else {
$stats['hidden_unlinked']++;
}
}
}
$items = $out;
$stats['shown'] = count($items);
} catch (Throwable $e) {
// If mapping tables don't exist yet, fail open (show everything).
}
// Persist to DB cache so pages can load immediately without a full refresh.
try {
global $pdo;
tt_cache_write($pdo, $itemsForCache);
} catch (Throwable $e) {}
$updated = (int)tt_sys_get('trutrak_cache_updated_at', '0');
tt_json([
'ok' => true,
'method' => $pick['method'],
'cached' => false,
'show_all' => $showAll,
'stats' => $stats,
'updated_at' => $updated,
'items' => $items,
'rows' => $items,
'devices' => $items,
'meta' => [
'base_url' => $cfg['base_url'],
'xmltype' => $cfg['xmltype'],
],
]);
}
if ($action === 'assets') {
$filterReg = trim((string)($_GET['filter_registration'] ?? ''));
$filterDepots = trim((string)($_GET['filter_depots'] ?? ''));
$res = tt_call($cfg, 'TTAssetsList', [
'token' => $token,
'filter_registration' => $filterReg,
'filter_depots' => $filterDepots,
'xmltype' => (string)$cfg['xmltype'],
]);
if (!$res['ok']) tt_bad($res['error'] ?? 'AssetsList failed', 502);
tt_json(['ok' => true, 'rows' => $res['rows'] ?? []]);
}
if ($action === 'depots') {
$filterDepot = trim((string)($_GET['filter_depot'] ?? ''));
$res = tt_call($cfg, 'TTDepotList', [
'token' => $token,
'filter_depot' => $filterDepot,
'xmltype' => (string)$cfg['xmltype'],
]);
if (!$res['ok']) tt_bad($res['error'] ?? 'DepotList failed', 502);
tt_json(['ok' => true, 'rows' => $res['rows'] ?? []]);
}
if (in_array($action, ['waypoints', 'geofences'], true)) {
// TruTrak call is still named TTWaypoints, but we present it as Geofences in the UI.
$filterWp = trim((string)($_GET['filter_geofences'] ?? ($_GET['filter_waypoints'] ?? '')));
$res = tt_call($cfg, 'TTWaypoints', [
'token' => $token,
'filter_waypoints' => $filterWp,
'xmltype' => (string)$cfg['xmltype'],
]);
if (!$res['ok']) tt_bad($res['error'] ?? 'Geofences failed', 502);
// Attach local meta (category + icon) so the UI can show proper geofence icons.
$rows = $res['rows'] ?? [];
$keys = [];
foreach ($rows as $r) $keys[] = tt_geofence_key($r);
$meta = [];
try {
global $pdo;
$meta = tt_geofence_meta_fetch($pdo, $keys);
} catch (Throwable $e) {
$meta = [];
}
$out = [];
foreach ($rows as $r) {
$k = tt_geofence_key($r);
$r['_key'] = $k;
if (isset($meta[$k])) {
$r['_category'] = $meta[$k]['category'] ?? 'other';
$r['_icon'] = $meta[$k]['icon'] ?? '📍';
$r['_label'] = $meta[$k]['label'] ?? null;
}
$out[] = $r;
}
tt_json(['ok' => true, 'rows' => $out]);
}
// Streaming (polling) – returns all locations after a record id
if ($action === 'stream') {
$after = trim((string)($_GET['after_record'] ?? '0'));
// Prefer extended if available
$wsdl = tt_load_wsdl($cfg['base_url'], false);
if (!$wsdl['ok']) tt_bad($wsdl['error'] ?? 'Failed to load WSDL', 502);
$method = tt_wsdl_has_method($wsdl['raw'], 'TTAssetsAllLocations_extended') ? 'TTAssetsAllLocations_extended' : 'TTAssetsAllLocations';
$res = tt_call($cfg, $method, [
'token' => $token,
'after_record' => $after,
'xmltype' => (string)$cfg['xmltype'],
]);
if (!$res['ok']) tt_bad($res['error'] ?? 'Stream call failed', 502);
$items = [];
foreach (($res['rows'] ?? []) as $r) $items[] = tt_norm_location($r);
tt_json(['ok' => true, 'method' => $method, 'items' => $items, 'after_record' => $after]);
}
// History endpoints
function tt_parse_dt(string $s): string {
// TruTrak typically accepts ISO; keep what the user provides but normalize spacing
$s = trim($s);
if ($s === '') return '';
// allow HTML5 datetime-local (YYYY-MM-DDTHH:MM)
$s = str_replace('T', ' ', $s);
return $s;
}
if (in_array($action, ['trail','journeys','dailytrail','history','history_csv'], true)) {
// Method selection
$method = $_GET['method'] ?? '';
if ($action === 'trail') $method = 'TTAssetTrail';
if ($action === 'journeys') $method = 'TTAssetJourneys';
if ($action === 'dailytrail') $method = 'TTAssetDailyTrail';
if ($method === '') {
// default
$method = 'TTAssetJourneys';
}
$filterAssets = trim((string)($_GET['filter_assets'] ?? ''));
$start = tt_parse_dt((string)($_GET['start'] ?? ''));
$end = tt_parse_dt((string)($_GET['end'] ?? ''));
$date = trim((string)($_GET['date'] ?? ''));
// DailyTrail: enforce 1 call per 24h (best-effort)
if ($method === 'TTAssetDailyTrail') {
$last = (int)$cfg['daily_ts'];
if ($last > 0 && (time() - $last) < 86400) {
tt_bad('TTAssetDailyTrail is limited to 1 call every 24H (per TruTrak). Try again later.', 429);
}
}
$res = tt_call($cfg, $method, [
'token' => $token,
'filter_assets' => $filterAssets,
'start' => $start,
'end' => $end,
'date' => $date,
'xmltype' => (string)$cfg['xmltype'],
]);
if (!$res['ok']) tt_bad($res['error'] ?? 'History call failed', 502);
if ($method === 'TTAssetDailyTrail') {
tt_sys_set('trutrak_dailytrail_last_ts', (string)time());
}
$rows = $res['rows'] ?? [];
if ($action === 'history_csv') {
tt_csv($rows, 'trutrak_' . strtolower($method) . '_' . date('Ymd_His') . '.csv');
}
// Limit payload for browser
$max = 500;
tt_json([
'ok' => true,
'method' => $method,
'count' => count($rows),
'rows' => array_slice($rows, 0, $max),
'truncated' => count($rows) > $max,
]);
}
tt_bad('Unknown action', 400, ['action' => $action]);