solverforge-fsr / static /app-render-routes.js
blackopsrepl's picture
feat(fsr): add snapshot-scoped route geometry
ae32abe
/* app-render-routes.js - route list rendering for the Bergamo FSR demo */
(function () {
'use strict';
var FSR = window.FSR = window.FSR || {};
var utils = FSR.utils;
FSR.createRouteListRenderer = function (options) {
var SF = options.SF;
return { renderRouteCards: renderRouteCards };
function renderRouteCards(plan, routeGeometry) {
var routeGeometryById = geometryByRouteId(routeGeometry);
options.routeCards.innerHTML = '';
(plan.technician_routes || []).forEach(function (route, routeIdx) {
var stats = utils.routeStats(plan, route);
var routeId = routeKey(route, routeIdx);
var focusedRouteId = options.getFocusedRouteId ? options.getFocusedRouteId() : null;
var isFocused = focusedRouteId === routeId;
var card = SF.el('div', {
className: 'fsr-route-row' + (isFocused ? ' is-focused' : ''),
role: 'button',
tabIndex: 0,
dataset: { routeId: routeId },
});
card.addEventListener('click', function () { focusRoute(routeId); });
card.addEventListener('keydown', function (event) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
focusRoute(routeId);
}
});
var top = SF.el('div', { className: 'fsr-route-row__top' });
top.appendChild(SF.el('strong', null, route.technician_name || route.id || ('Technician ' + (routeIdx + 1))));
top.appendChild(SF.el('span', { className: 'fsr-route-tag' }, String((route.visits || []).length) + ' stops'));
card.appendChild(top);
var meta = SF.el('div', { className: 'fsr-route-row__meta' });
meta.appendChild(SF.el('span', null, utils.formatDuration(stats.travelMinutes) + ' travel'));
meta.appendChild(SF.el('span', null, utils.formatDuration(stats.serviceMinutes) + ' service'));
meta.appendChild(SF.el('span', null, route.territory || 'no territory'));
if (stats.lateMinutes) meta.appendChild(SF.el('span', null, utils.formatDuration(stats.lateMinutes) + ' late'));
if (stats.overtimeMinutes) meta.appendChild(SF.el('span', null, utils.formatDuration(stats.overtimeMinutes) + ' overtime'));
if (stats.unreachable || stats.missingSkills || stats.missingParts) {
meta.appendChild(SF.el('span', null, String(stats.unreachable + stats.missingSkills + stats.missingParts) + ' hard issues'));
}
if (hasGeometryGaps(routeGeometryById[routeId])) meta.appendChild(SF.el('span', null, 'Geometry gaps'));
card.appendChild(meta);
var action = SF.createButton({
text: isFocused ? 'Show All' : 'Highlight',
variant: isFocused ? 'default' : 'ghost',
});
action.addEventListener('click', function (event) {
event.stopPropagation();
focusRoute(routeId);
});
card.appendChild(SF.el('div', { className: 'fsr-route-row__actions' }, action));
options.routeCards.appendChild(card);
});
renderUnassignedCard(plan);
}
function renderUnassignedCard(plan) {
var assigned = utils.assignedVisitSet(plan.technician_routes || []);
var rows = (plan.service_visits || []).reduce(function (items, visit, idx) {
if (assigned[idx]) return items;
items.push([
visit.customer || visit.name || visit.id,
utils.timeLabel(visit.earliest_minute) + '-' + utils.timeLabel(visit.latest_minute),
utils.formatDuration(visit.duration_minutes || 0),
]);
return items;
}, []);
if (!rows.length) return;
var card = SF.el('div', { className: 'fsr-route-empty' });
card.appendChild(SF.el('strong', null, 'Unassigned visits'));
card.appendChild(SF.createTable({ columns: ['Visit', 'Window', 'Duration'], rows: rows }));
options.routeCards.appendChild(card);
}
function geometryByRouteId(routeGeometry) {
return ((routeGeometry && routeGeometry.routes) || []).reduce(function (index, route) {
index[String(route.routeId)] = route;
return index;
}, {});
}
function hasGeometryGaps(routeGeometry) {
return !!routeGeometry && (routeGeometry.segments || []).some(function (segment) {
return segment.geometryStatus !== 'ROUTED';
});
}
function focusRoute(routeId) {
if (options.onFocusRoute) options.onFocusRoute(routeId);
}
function routeKey(route, idx) {
return String(route.id || route.technician_name || ('route-' + idx));
}
};
})();