Files
Verde-Web/resources/views/admin/trips.blade.php

601 lines
30 KiB
PHP

@extends('admin.layouts.app', ['pageTitle' => 'Trips'])
@section('page')
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin="">
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin=""></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet-routing-machine@3.2.12/dist/leaflet-routing-machine.css" />
<script src="https://unpkg.com/leaflet-routing-machine@3.2.12/dist/leaflet-routing-machine.js"></script>
<div class="mx-auto max-w-7xl">
<header class="mb-6 flex items-end justify-between">
<div>
<h2 class="text-2xl font-semibold tracking-tight text-neutral-900">Trips</h2>
<p class="mt-1 text-sm text-neutral-500">Schedule and monitor collection runs.</p>
</div>
<button id="new-btn" class="btn-primary">+ Schedule Trip</button>
</header>
<div class="card mb-4 flex flex-wrap items-center gap-3 p-4">
<select id="filter-status" class="form-select w-44">
<option value="">All statuses</option>
<option value="scheduled">Scheduled</option>
<option value="in_progress">In progress</option>
<option value="at_dumpsite">At dumpsite</option>
<option value="completed">Completed</option>
<option value="cancelled">Cancelled</option>
</select>
<input id="filter-date" type="date" class="form-input w-44">
<button id="filter-apply" class="btn-primary">Apply</button>
</div>
<div class="table-wrap">
<table class="table">
<thead>
<tr>
<th>Trip #</th>
<th>Date</th>
<th>LGU</th>
<th>Route</th>
<th>Team</th>
<th>Load</th>
<th>Status</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody id="rows">
<tr><td colspan="7" class="py-10 text-center text-sm text-neutral-400">Loading…</td></tr>
</tbody>
</table>
</div>
</div>
<div id="form-modal" class="hidden">
<div class="slide-over-mask" data-close></div>
<div class="slide-over-panel translate-x-0 p-6 overflow-y-auto">
<div class="mb-4 flex items-center justify-between">
<h3 class="text-base font-semibold text-neutral-900">Schedule trip</h3>
<button data-close class="text-neutral-400 hover:text-neutral-600">
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
</button>
</div>
<div id="conflict-banner" class="mb-4 hidden rounded border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800">
<div id="conflict-message" class="mb-2"></div>
<button type="button" id="override-btn" class="btn-primary py-1 px-3 text-xs" style="background-color: #d97706; border-color: #d97706;">Override & Schedule</button>
</div>
<form id="create-form" class="space-y-4">
<div><label class="form-label">Route</label>
<select name="route_id" id="route-select" required class="form-select"><option value=""> Select </option></select>
</div>
<div id="preview-map-container" class="hidden">
<label class="form-label">Map Preview</label>
<div id="preview-map" class="h-48 w-full rounded border" style="z-index: 0;"></div>
</div>
<div><label class="form-label">Team</label>
<select name="team_id" id="team-select" required class="form-select"><option value=""> Select </option></select>
</div>
<div><label class="form-label">Date</label><input name="scheduled_date" type="date" required class="form-input"></div>
<div><label class="form-label">Start time</label><input name="scheduled_start_time" type="time" class="form-input" placeholder="06:00"></div>
<div><label class="form-label">Notes</label><textarea name="notes" rows="2" class="form-textarea"></textarea></div>
<div class="flex justify-end gap-2 pt-2">
<button type="button" data-close class="btn-ghost">Cancel</button>
<button type="submit" class="btn-primary">Schedule</button>
</div>
</form>
</div>
</div>
{{-- Detail panel --}}
<div id="detail-modal" class="hidden">
<div class="slide-over-mask" data-close></div>
<div class="slide-over-panel translate-x-0 p-6 overflow-y-auto" style="max-width: 600px;">
<div class="mb-4 flex items-center justify-between">
<h3 id="detail-title" class="text-base font-semibold text-neutral-900">Trip detail</h3>
<button data-close class="text-neutral-400 hover:text-neutral-600">
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
</button>
</div>
<div id="detail-body" class="text-sm"></div>
</div>
</div>
{{-- Hand Off / Continuation Modal --}}
<div id="handoff-modal" class="hidden">
<div class="slide-over-mask" onclick="document.getElementById('handoff-modal').classList.add('hidden')"></div>
<div class="slide-over-panel translate-x-0 p-6 overflow-y-auto">
<div class="mb-4 flex items-center justify-between">
<h3 class="text-base font-semibold text-neutral-900"> Hand Off Trip to Another Team</h3>
<button onclick="document.getElementById('handoff-modal').classList.add('hidden')" class="text-neutral-400 hover:text-neutral-600">
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
</button>
</div>
<p class="mb-4 text-xs text-neutral-500">This will close the current trip and create a new continuation trip with the remaining pending stops assigned to the selected team.</p>
<form id="handoff-form" class="space-y-4">
<div>
<label class="form-label">Reason for Handoff</label>
<select name="end_reason" required class="form-select">
<option value=""> Select reason </option>
<option value="truck_full">Truck Full (going to dumpsite)</option>
<option value="breakdown">Truck Breakdown</option>
<option value="incident">Incident</option>
</select>
</div>
<div>
<label class="form-label">New Team</label>
<select name="new_team_id" required class="form-select" id="handoff-team-select">
<option value=""> Select team </option>
</select>
</div>
<div>
<label class="form-label">New Truck <span class="text-neutral-400">(optional)</span></label>
<select name="new_truck_id" class="form-select" id="handoff-truck-select">
<option value=""> No change </option>
</select>
</div>
<div class="flex justify-end gap-2 pt-2">
<button type="button" onclick="document.getElementById('handoff-modal').classList.add('hidden')" class="btn-ghost">Cancel</button>
<button type="submit" class="btn-primary" style="background:#d97706">Create Continuation Trip</button>
</div>
</form>
</div>
</div>
<script type="module">
const rows = document.getElementById('rows');
const modal = document.getElementById('form-modal');
const detail = document.getElementById('detail-modal');
function badge(s) {
const m = {
scheduled: 'badge-info', in_progress: 'badge-pending',
at_dumpsite: 'badge-pending', completed: 'badge-active',
handed_off: 'badge-pending', cancelled: 'badge-rejected',
};
return `<span class="badge ${m[s] || 'badge-inactive'}">${s.replace('_', ' ')}</span>`;
}
async function loadRoutes() {
const res = await window.Verde.apiFetch('/api/v1/admin/routes?status=active&per_page=200');
if (!res.ok) return;
const sel = document.getElementById('route-select');
(res.body.data ?? []).forEach(r => {
const opt = document.createElement('option');
opt.value = r.db_id;
opt.dataset.uuid = r.id;
opt.textContent = `${r.code} — ${r.name}`;
sel.appendChild(opt);
});
}
async function loadTeams() {
const res = await window.Verde.apiFetch('/api/v1/admin/teams?status=active&per_page=200');
if (!res.ok) return;
const sel = document.getElementById('team-select');
(res.body.data ?? []).forEach(t => {
const opt = document.createElement('option');
opt.value = t.db_id;
opt.textContent = t.name;
sel.appendChild(opt);
});
}
async function load() {
rows.innerHTML = `<tr><td colspan="7" class="py-10 text-center text-sm text-neutral-400">Loading…</td></tr>`;
const params = new URLSearchParams();
const s = document.getElementById('filter-status').value;
const d = document.getElementById('filter-date').value;
if (s) params.set('status', s);
if (d) params.set('date', d);
params.set('per_page', '50');
const res = await window.Verde.apiFetch(`/api/v1/admin/trips?${params}`);
if (!res.ok) { rows.innerHTML = `<tr><td colspan="7" class="py-10 text-center text-sm text-red-500">Failed to load.</td></tr>`; return; }
const items = res.body.data ?? [];
if (items.length === 0) { rows.innerHTML = `<tr><td colspan="7" class="py-10 text-center text-sm text-neutral-400">No trips match. Schedule one to get started.</td></tr>`; return; }
rows.innerHTML = items.map(t => `
<tr>
<td class="font-mono text-xs font-medium text-neutral-900">${window.Verde.escapeHtml(t.trip_number)}</td>
<td>${window.Verde.escapeHtml(t.scheduled_date)}</td>
<td class="text-xs text-neutral-500">${window.Verde.escapeHtml(t.tenant?.name || 'N/A')}</td>
<td>${t.route ? window.Verde.escapeHtml(t.route.name) : '—'}</td>
<td>${t.team ? window.Verde.escapeHtml(t.team.name) : '—'}</td>
<td>${t.total_load_kg ? `${t.total_load_kg} kg` : '—'}</td>
<td>${badge(t.status)}</td>
<td class="text-right space-x-1">
<button data-id="${t.id}" data-action="view" class="btn-ghost px-3 py-1 text-xs">View</button>
${!['completed','cancelled'].includes(t.status)
? `<button data-id="${t.id}" data-action="cancel" class="btn-ghost px-3 py-1 text-xs">Cancel</button>`
: ''}
</td>
</tr>
`).join('');
rows.querySelectorAll('button[data-action]').forEach(btn => {
btn.addEventListener('click', () => onAction(btn.dataset.action, btn.dataset.id));
});
}
async function onAction(action, uuid) {
if (action === 'cancel') {
if (!confirm('Cancel this trip?')) return;
const r = await window.Verde.apiFetch(`/api/v1/admin/trips/${uuid}/cancel`, { method: 'POST' });
if (r.ok) { window.Verde.toast('Cancelled', 'success'); load(); }
else window.Verde.toast(r.body?.message ?? 'Cancel failed', 'error');
}
if (action === 'view') {
const r = await window.Verde.apiFetch(`/api/v1/admin/trips/${uuid}`);
if (!r.ok) { window.Verde.toast('Failed to load', 'error'); return; }
renderDetail(r.body.data);
detail.classList.remove('hidden');
}
}
let editingTripId = null;
let tripMapInstance = null;
let tripRoutingControl = null;
function renderDetail(t) {
window.currentTrip = t;
document.getElementById('detail-title').textContent = t.trip_number;
const stops = (t.stops ?? []).map(s => `
<li class="rounded border border-neutral-200 p-2 text-xs">
<div class="flex items-center justify-between">
<span class="font-mono text-neutral-400">#${s.sequence}</span>
${badge(s.status)}
</div>
<div class="font-medium text-neutral-900">${window.Verde.escapeHtml(s.drop_off_point?.name ?? '—')}</div>
${s.actual_arrival ? `<div class="mt-1 text-neutral-500">Arrived ${window.Verde.formatDate(s.actual_arrival)}</div>` : ''}
${s.skip_reason ? `<div class="mt-1 italic text-red-600">Skipped: ${window.Verde.escapeHtml(s.skip_reason)}</div>` : ''}
</li>
`).join('');
const timeline = (t.timeline ?? []).map(e => `
<li class="border-l-2 border-verde-200 pl-3 text-xs">
<div class="flex items-baseline gap-2">
<span class="font-mono text-neutral-400">${window.Verde.formatDate(e.event_at, { hour: '2-digit', minute: '2-digit' })}</span>
<span class="font-medium text-neutral-900">${e.event_type.replace(/_/g, ' ')}</span>
</div>
${e.notes ? `<div class="mt-0.5 text-neutral-600">${window.Verde.escapeHtml(e.notes)}</div>` : ''}
</li>
`).join('');
let actionsHtml = '';
if (t.status === 'scheduled') {
actionsHtml = `
<div class="mb-4 flex gap-2">
<button type="button" class="btn-ghost text-xs py-1 px-2" onclick="window.editTrip(${t.db_id})">Edit</button>
<button type="button" class="btn-rejected text-xs py-1 px-2" onclick="window.deleteTrip(${t.db_id})">Delete</button>
</div>
`;
} else if (['in_progress', 'at_dumpsite'].includes(t.status)) {
actionsHtml = `
<div class="mb-4 flex gap-2">
<button type="button" class="text-xs py-1 px-3 rounded bg-amber-50 border border-amber-300 text-amber-800 hover:bg-amber-100 font-medium" onclick="window.openHandOff('${t.id}')">⚠ Hand Off to Another Team</button>
</div>
`;
}
document.getElementById('detail-body').innerHTML = `
${actionsHtml}
${t.parent_trip ? `<div class="mb-3 rounded border border-blue-200 bg-blue-50 px-3 py-2 text-xs text-blue-800">📦 Continuation of Trip <strong>${window.Verde.escapeHtml(t.parent_trip.trip_number)}</strong>${t.end_reason ? ` &mdash; <em>${t.end_reason.replace('_', ' ')}</em>` : ''}</div>` : ''}
${(t.continuation_trips ?? []).length ? `<div class="mb-3 rounded border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800">🔄 Continued by: ${t.continuation_trips.map(c => `<strong>${window.Verde.escapeHtml(c.trip_number)}</strong> (${window.Verde.escapeHtml(c.team_name ?? '—')} · ${c.status})`).join(', ')}</div>` : ''}
<div id="trip-map" class="mb-4 rounded border" style="height: 250px; z-index: 0;"></div>
<div class="mb-4 grid grid-cols-2 gap-y-2 text-sm">
<dt class="text-neutral-500">Status</dt><dd>${badge(t.status)}</dd>
<dt class="text-neutral-500">Date</dt><dd>${window.Verde.escapeHtml(t.scheduled_date)}</dd>
<dt class="text-neutral-500">Route</dt><dd>${window.Verde.escapeHtml(t.route?.name ?? '—')}</dd>
<dt class="text-neutral-500">Team</dt><dd>${window.Verde.escapeHtml(t.team?.name ?? '—')}</dd>
<dt class="text-neutral-500">Load</dt><dd>${t.total_load_kg
? `${t.total_load_kg} kg${t.releases?.length ? ` (${t.releases.length} release${t.releases.length > 1 ? 's' : ''})` : ''}`
: '—'}</dd>
</div>
<h4 class="mb-2 text-xs font-semibold uppercase tracking-wider text-neutral-500">Stops</h4>
<ul class="mb-4 space-y-1.5">${stops || '<li class="text-xs text-neutral-400">No stops</li>'}</ul>
<h4 class="mb-2 text-xs font-semibold uppercase tracking-wider text-neutral-500">Timeline</h4>
<ul class="space-y-2">${timeline || '<li class="text-xs text-neutral-400">No events yet</li>'}</ul>
<h4 class="mt-4 mb-2 text-xs font-semibold uppercase tracking-wider text-neutral-500">Dumpsite Releases</h4>
${t.releases?.length
? `<ul class="space-y-2">${t.releases.map(r => `
<li class="rounded border border-neutral-200 bg-neutral-50 p-2.5 text-xs">
<div class="flex items-center justify-between mb-1">
<span class="font-semibold text-neutral-800">${window.Verde.escapeHtml(r.dumpsite_name || '—')}</span>
<span class="font-mono text-neutral-400">${window.Verde.formatDate(r.released_at, { hour: '2-digit', minute: '2-digit' })}</span>
</div>
<div class="flex items-center gap-3">
<span class="font-bold text-verde-700">${r.weight_kg} kg</span>
${r.gate_pass_number ? `<span class="text-neutral-500">Gate pass: ${window.Verde.escapeHtml(r.gate_pass_number)}</span>` : ''}
</div>
<div class="mt-1 text-neutral-500">Driver: ${window.Verde.escapeHtml(r.driver_name || '—')}</div>
${r.dumpsite_attendant ? `<div class="text-neutral-500">Attendant: ${window.Verde.escapeHtml(r.dumpsite_attendant)}</div>` : ''}
${r.notes ? `<div class="mt-1 italic text-neutral-400">${window.Verde.escapeHtml(r.notes)}</div>` : ''}
</li>
`).join('')}</ul>`
: '<p class="text-xs text-neutral-400">No releases recorded yet</p>'
}
`;
if (tripMapInstance) {
tripMapInstance.remove();
tripMapInstance = null;
}
setTimeout(() => {
tripMapInstance = L.map('trip-map').setView([14.5995, 120.9842], 12);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap'
}).addTo(tripMapInstance);
const wps = [];
(t.stops ?? []).forEach(s => {
if (s.drop_off_point?.coordinates) {
wps.push(L.latLng(s.drop_off_point.coordinates.lat, s.drop_off_point.coordinates.lng));
}
});
if (t.dumpsite?.coordinates) {
wps.push(L.latLng(t.dumpsite.coordinates.lat, t.dumpsite.coordinates.lng));
}
if (wps.length >= 2) {
tripRoutingControl = L.Routing.control({
router: L.Routing.osrmv1({ serviceUrl: 'https://routing.openstreetmap.de/routed-car/route/v1', profile: 'driving' }),
waypoints: wps,
routeWhileDragging: false,
addWaypoints: false,
show: false,
createMarker: function(i, wp, n) {
return L.marker(wp.latLng, {
icon: L.icon({
iconUrl: i === n - 1
? 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png'
: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-blue.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34]
})
});
}
}).addTo(tripMapInstance);
} else if (wps.length === 1) {
L.marker(wps[0]).addTo(tripMapInstance);
tripMapInstance.setView(wps[0], 15);
}
}, 100);
}
window.deleteTrip = async (id) => {
if (!confirm('Are you sure you want to delete this trip?')) return;
const res = await window.Verde.apiFetch('/api/v1/admin/trips/' + id, { method: 'DELETE' });
if (res.ok) {
window.Verde.toast('Trip deleted', 'success');
document.getElementById('detail-modal').classList.add('hidden');
load();
}
};
window.editTrip = (id) => {
const t = window.currentTrip;
if (!t) return;
editingTripId = t.db_id;
document.querySelector('#form-modal h3').textContent = 'Edit trip';
const form = document.getElementById('create-form');
form.route_id.value = t.route?.db_id || '';
form.team_id.value = t.team?.db_id || '';
form.scheduled_date.value = t.scheduled_date || '';
form.scheduled_start_time.value = t.scheduled_start_time ? t.scheduled_start_time.substring(0, 5) : '';
form.notes.value = t.notes || '';
previewRoute(t.route?.id || '');
document.getElementById('detail-modal').classList.add('hidden');
document.getElementById('form-modal').classList.remove('hidden');
};
document.getElementById('new-btn').addEventListener('click', () => {
editingTripId = null;
document.getElementById('create-form').reset();
document.getElementById('conflict-banner').classList.add('hidden');
previewRoute('');
document.querySelector('#form-modal h3').textContent = 'Schedule trip';
modal.classList.remove('hidden');
});
[modal, detail].forEach(m => m.querySelectorAll('[data-close]').forEach(el => el.addEventListener('click', () => m.classList.add('hidden'))));
document.getElementById('filter-apply').addEventListener('click', load);
document.getElementById('filter-status').addEventListener('change', load);
document.getElementById('route-select').addEventListener('change', (e) => {
const opt = e.target.options[e.target.selectedIndex];
previewRoute(opt && opt.dataset.uuid ? opt.dataset.uuid : '');
});
let previewMapInstance = null;
let previewRoutingControl = null;
async function previewRoute(routeId) {
const container = document.getElementById('preview-map-container');
if (!routeId) {
container.classList.add('hidden');
if (previewMapInstance) {
previewMapInstance.remove();
previewMapInstance = null;
}
return;
}
container.classList.remove('hidden');
if (previewMapInstance) {
previewMapInstance.remove();
previewMapInstance = null;
}
previewMapInstance = L.map('preview-map').setView([14.5995, 120.9842], 12);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap'
}).addTo(previewMapInstance);
const res = await window.Verde.apiFetch(`/api/v1/admin/routes/${routeId}`);
if (!res.ok) return;
const route = res.body.data;
const wps = [];
(route.stops ?? []).forEach(s => {
if (s.drop_off_point?.coordinates) {
wps.push(L.latLng(s.drop_off_point.coordinates.lat, s.drop_off_point.coordinates.lng));
}
});
if (route.default_dumpsite?.coordinates) {
wps.push(L.latLng(route.default_dumpsite.coordinates.lat, route.default_dumpsite.coordinates.lng));
}
if (wps.length >= 2) {
previewRoutingControl = L.Routing.control({
router: L.Routing.osrmv1({ serviceUrl: 'https://routing.openstreetmap.de/routed-car/route/v1', profile: 'driving' }),
waypoints: wps,
routeWhileDragging: false,
addWaypoints: false,
show: false,
createMarker: function(i, wp, n) {
return L.marker(wp.latLng, {
icon: L.icon({
iconUrl: i === n - 1
? 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png'
: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-blue.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34]
})
});
}
}).addTo(previewMapInstance);
} else if (wps.length === 1) {
L.marker(wps[0]).addTo(previewMapInstance);
previewMapInstance.setView(wps[0], 15);
}
}
document.getElementById('create-form').addEventListener('submit', async (e) => {
e.preventDefault();
async function submitTrip(override = false) {
const fd = new FormData(e.target);
const payload = Object.fromEntries(fd.entries());
payload.route_id = parseInt(payload.route_id, 10);
payload.team_id = parseInt(payload.team_id, 10);
if (payload.scheduled_start_time) {
payload.scheduled_start_time = payload.scheduled_start_time.substring(0, 5);
} else {
delete payload.scheduled_start_time;
}
if (!payload.notes) delete payload.notes;
if (override) payload.override_conflicts = true;
const res = await window.Verde.apiFetch(editingTripId ? '/api/v1/admin/trips/' + editingTripId : '/api/v1/admin/trips', {
method: editingTripId ? 'PUT' : 'POST', body: JSON.stringify(payload),
});
if (res.ok) {
window.Verde.toast(editingTripId ? 'Trip updated' : 'Trip scheduled', 'success');
modal.classList.add('hidden');
document.getElementById('conflict-banner').classList.add('hidden');
e.target.reset();
load();
} else {
if (res.status === 422 && res.body?.data?.conflicts) {
const banner = document.getElementById('conflict-banner');
document.getElementById('conflict-message').innerText = res.body.data.conflicts.join("\n");
banner.classList.remove('hidden');
} else if (res.status === 422 && res.body?.errors) {
const msgs = Object.values(res.body.errors).flat().join('\n');
window.Verde.toast(msgs, 'error');
} else {
window.Verde.toast(res.body?.message ?? 'Schedule failed', 'error');
}
}
}
submitTrip();
});
document.getElementById('override-btn').addEventListener('click', () => {
document.getElementById('conflict-banner').classList.add('hidden');
document.getElementById('create-form').dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
// The event listener doesn't pass 'override', so we need a cleaner way.
// Let's set a hidden input.
let input = document.getElementById('override_conflicts_input');
if (!input) {
input = document.createElement('input');
input.type = 'hidden';
input.id = 'override_conflicts_input';
input.name = 'override_conflicts';
document.getElementById('create-form').appendChild(input);
}
input.value = 'true';
document.getElementById('create-form').querySelector('button[type="submit"]').click();
setTimeout(() => { if(input) input.remove(); }, 100);
});
Promise.all([loadRoutes(), loadTeams()]);
load();
// ── Hand Off / Continuation ────────────────────────────────────────────
let handoffTripId = null;
async function populateHandoffSelects() {
const [teamsRes, trucksRes] = await Promise.all([
window.Verde.apiFetch('/api/v1/admin/teams?status=active&per_page=200'),
window.Verde.apiFetch('/api/v1/admin/trucks?per_page=200'),
]);
const teamSel = document.getElementById('handoff-team-select');
const truckSel = document.getElementById('handoff-truck-select');
if (teamsRes.ok) {
(teamsRes.body.data ?? []).forEach(t => {
const o = document.createElement('option');
o.value = t.db_id;
o.textContent = t.name;
teamSel.appendChild(o);
});
}
if (trucksRes.ok) {
(trucksRes.body.data ?? []).forEach(t => {
const o = document.createElement('option');
o.value = t.db_id;
o.textContent = `${t.plate_number}${t.capacity_kg ? ' (' + t.capacity_kg + ' kg)' : ''}`;
truckSel.appendChild(o);
});
}
}
populateHandoffSelects();
window.openHandOff = (tripUuid) => {
handoffTripId = tripUuid;
document.getElementById('handoff-form').reset();
document.getElementById('handoff-modal').classList.remove('hidden');
};
document.getElementById('handoff-form').addEventListener('submit', async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
const payload = {
end_reason: fd.get('end_reason'),
new_team_id: parseInt(fd.get('new_team_id'), 10),
};
const newTruck = fd.get('new_truck_id');
if (newTruck) payload.new_truck_id = parseInt(newTruck, 10);
const res = await window.Verde.apiFetch(`/api/v1/admin/trips/${handoffTripId}/continue`, {
method: 'POST', body: JSON.stringify(payload),
});
if (res.ok) {
const newNum = res.body?.data?.trip_number ?? '';
window.Verde.toast(`Continuation trip ${newNum} created`, 'success');
document.getElementById('handoff-modal').classList.add('hidden');
document.getElementById('detail-modal').classList.add('hidden');
load();
} else {
window.Verde.toast(res.body?.message ?? 'Hand off failed', 'error');
}
});
</script>
@endsection