| Line 427... |
Line 427... |
| 427 |
state.days.push({
|
427 |
state.days.push({
|
| 428 |
dayNumber: d.dayNumber,
|
428 |
dayNumber: d.dayNumber,
|
| 429 |
startLat: state.homeLat, startLng: state.homeLng,
|
429 |
startLat: state.homeLat, startLng: state.homeLng,
|
| 430 |
startName: state.homeName,
|
430 |
startName: state.homeName,
|
| 431 |
visits: builtVisits,
|
431 |
visits: builtVisits,
|
| 432 |
endAction: d.dayNumber < beat.days.length ? 'DAYBREAK' : 'HOME',
|
432 |
endAction: d.endAction || (d.dayNumber < beat.days.length ? 'DAYBREAK' : 'HOME'),
|
| 433 |
totalKm: 0, totalMins: 0
|
433 |
totalKm: 0, totalMins: 0
|
| 434 |
});
|
434 |
});
|
| 435 |
});
|
435 |
});
|
| 436 |
state.currentDay = 1;
|
436 |
state.currentDay = 1;
|
| 437 |
|
437 |
|
| Line 639... |
Line 639... |
| 639 |
dayNumber: d.dayNumber,
|
639 |
dayNumber: d.dayNumber,
|
| 640 |
startLat: startLat,
|
640 |
startLat: startLat,
|
| 641 |
startLng: startLng,
|
641 |
startLng: startLng,
|
| 642 |
startName: startName,
|
642 |
startName: startName,
|
| 643 |
visits: builtVisits,
|
643 |
visits: builtVisits,
|
| - |
|
644 |
// Use the actual end_action stored on this schedule row.
|
| - |
|
645 |
// The dayNumber-based inference is wrong for multi-instance
|
| - |
|
646 |
// beats (same beat scheduled on several dates) — every
|
| - |
|
647 |
// instance would have day N but only one was treated HOME.
|
| 644 |
endAction: d.dayNumber < beat.days.length ? 'DAYBREAK' : 'HOME',
|
648 |
endAction: d.endAction || (d.dayNumber < beat.days.length ? 'DAYBREAK' : 'HOME'),
|
| 645 |
totalKm: d.totalKm || 0,
|
649 |
totalKm: d.totalKm || 0,
|
| 646 |
totalMins: d.totalMins || 0
|
650 |
totalMins: d.totalMins || 0
|
| 647 |
});
|
651 |
});
|
| 648 |
});
|
652 |
});
|
| 649 |
|
653 |
|
| Line 959... |
Line 963... |
| 959 |
var addr = partner.address || '';
|
963 |
var addr = partner.address || '';
|
| 960 |
|
964 |
|
| 961 |
var html = '<div class="marker-popup">';
|
965 |
var html = '<div class="marker-popup">';
|
| 962 |
html += '<h4>' + partner.code + ' - ' + name + '</h4>';
|
966 |
html += '<h4>' + partner.code + ' - ' + name + '</h4>';
|
| 963 |
html += '<div class="popup-meta">' + addr + '</div>';
|
967 |
html += '<div class="popup-meta">' + addr + '</div>';
|
| 964 |
html += '<div class="popup-distance">~' + dist.toFixed(1) + ' km | ~' + fmtMins(travelMins) + ' travel + ' + VISIT_MINS + 'min visit</div>';
|
968 |
html += '<div class="popup-distance">~' + dist.toFixed(1) + ' km | ~' + fmtMins(travelMins) + ' travel + ' + VISIT_MINS + ' Minutes Discussion Time</div>';
|
| 965 |
html += '<button class="popup-next" onclick="addVisit(' + partner.fofoId + ',\'next\')">Next Visit</button>';
|
969 |
html += '<button class="popup-next" onclick="addVisit(' + partner.fofoId + ',\'next\')">Next Visit</button>';
|
| 966 |
html += '<button class="popup-last" onclick="addVisit(' + partner.fofoId + ',\'last\')">Last Visit</button>';
|
970 |
html += '<button class="popup-last" onclick="addVisit(' + partner.fofoId + ',\'last\')">Last Visit</button>';
|
| 967 |
html += '</div>';
|
971 |
html += '</div>';
|
| 968 |
|
972 |
|
| 969 |
infoWindow.setContent(html);
|
973 |
infoWindow.setContent(html);
|
| Line 1233... |
Line 1237... |
| 1233 |
if (prevLat && prevLng && v.lat && v.lng) {
|
1237 |
if (prevLat && prevLng && v.lat && v.lng) {
|
| 1234 |
segDist = haversine(prevLat, prevLng, v.lat, v.lng) * ROAD_FACTOR;
|
1238 |
segDist = haversine(prevLat, prevLng, v.lat, v.lng) * ROAD_FACTOR;
|
| 1235 |
segTime = (segDist / AVG_SPEED) * 60;
|
1239 |
segTime = (segDist / AVG_SPEED) * 60;
|
| 1236 |
}
|
1240 |
}
|
| 1237 |
if (segDist > 0) {
|
1241 |
if (segDist > 0) {
|
| - |
|
1242 |
// Show drive ETA and discussion-time dwell separately so the
|
| - |
|
1243 |
// planner can see where the time goes.
|
| 1238 |
html += '<div class="route-segment"><span class="seg-line"></span><span class="seg-dist">' + segDist.toFixed(1) + ' km | ' + fmtMins(segTime) + '</span></div>';
|
1244 |
html += '<div class="route-segment"><span class="seg-line"></span><span class="seg-dist">'
|
| - |
|
1245 |
+ segDist.toFixed(1) + ' km | '
|
| - |
|
1246 |
+ fmtMins(segTime) + ' drive '
|
| - |
|
1247 |
+ '<span style="color:#94a3b8;">+ ' + VISIT_MINS + ' Minutes Discussion Time</span>'
|
| - |
|
1248 |
+ '</span></div>';
|
| 1239 |
}
|
1249 |
}
|
| 1240 |
|
1250 |
|
| 1241 |
var isLeadVisit = v.isLead || v.type === 'lead';
|
1251 |
var isLeadVisit = v.isLead || v.type === 'lead';
|
| - |
|
1252 |
// Outlet/business name is the primary label; code is the muted subtitle.
|
| - |
|
1253 |
// v.name is stored as "CODE - Outlet Name" — strip the code prefix.
|
| - |
|
1254 |
var outletLabel = v.name || '';
|
| - |
|
1255 |
if (v.code && outletLabel.indexOf(v.code + ' - ') === 0) {
|
| - |
|
1256 |
outletLabel = outletLabel.substring(v.code.length + 3);
|
| - |
|
1257 |
}
|
| - |
|
1258 |
if (!outletLabel) outletLabel = v.code || '';
|
| 1242 |
html += '<div class="route-stop route-stop-clickable" data-fofoid="' + v.id + '" data-daynum="' + day.dayNumber + '" style="cursor:pointer;' + (isLeadVisit ? 'background:#fff3cd;border-left:3px solid #e67e22;' : '') + '">';
|
1259 |
html += '<div class="route-stop route-stop-clickable" data-fofoid="' + v.id + '" data-daynum="' + day.dayNumber + '" style="cursor:pointer;' + (isLeadVisit ? 'background:#fff3cd;border-left:3px solid #e67e22;' : '') + '">';
|
| 1243 |
html += '<span class="stop-num" style="' + (isLeadVisit ? 'background:#e67e22;' : '') + '">' + (i + 1) + '</span>';
|
1260 |
html += '<span class="stop-num" style="' + (isLeadVisit ? 'background:#e67e22;' : '') + '">' + (i + 1) + '</span>';
|
| 1244 |
html += '<div class="stop-info"><div class="stop-name">' + (v.code || '') + (isLeadVisit ? ' <span style="color:#e67e22;font-size:10px;">LEAD VISIT</span>' : '') + '</div>';
|
1261 |
html += '<div class="stop-info"><div class="stop-name">' + outletLabel + (isLeadVisit ? ' <span style="color:#e67e22;font-size:10px;">LEAD VISIT</span>' : '') + '</div>';
|
| 1245 |
html += '<div class="stop-meta">' + (v.name || '') + '</div>';
|
1262 |
html += '<div class="stop-meta">' + (v.code || '') + '</div>';
|
| 1246 |
|
1263 |
|
| 1247 |
// Show "Last Visit" + "Day Break" buttons on last stop of current day
|
1264 |
// Show "Last Visit" + "Day Break" buttons on last stop of current day
|
| 1248 |
if (isLastStop && isCurrent && !day.endAction) {
|
1265 |
if (isLastStop && isCurrent && !day.endAction) {
|
| 1249 |
html += '<div style="margin-top:4px;display:flex;gap:4px;">';
|
1266 |
html += '<div style="margin-top:4px;display:flex;gap:4px;">';
|
| 1250 |
html += '<button class="btn-last-visit" data-daynum="' + day.dayNumber + '" style="font-size:10px;padding:2px 8px;border:none;border-radius:3px;background:#22c55e;color:#fff;cursor:pointer;font-weight:600;">Last Visit</button>';
|
1267 |
html += '<button class="btn-last-visit" data-daynum="' + day.dayNumber + '" style="font-size:10px;padding:2px 8px;border:none;border-radius:3px;background:#22c55e;color:#fff;cursor:pointer;font-weight:600;">Last Visit</button>';
|
| Line 1269... |
Line 1286... |
| 1269 |
if (lastV.lat && lastV.lng && state.homeLat && state.homeLng) {
|
1286 |
if (lastV.lat && lastV.lng && state.homeLat && state.homeLng) {
|
| 1270 |
retDist = haversine(lastV.lat, lastV.lng, state.homeLat, state.homeLng) * ROAD_FACTOR;
|
1287 |
retDist = haversine(lastV.lat, lastV.lng, state.homeLat, state.homeLng) * ROAD_FACTOR;
|
| 1271 |
retTime = (retDist / AVG_SPEED) * 60;
|
1288 |
retTime = (retDist / AVG_SPEED) * 60;
|
| 1272 |
}
|
1289 |
}
|
| 1273 |
if (retDist > 0) {
|
1290 |
if (retDist > 0) {
|
| - |
|
1291 |
// Return-home leg = travel time only (no discussion at home).
|
| 1274 |
html += '<div class="route-segment"><span class="seg-line seg-line-home"></span><span class="seg-dist">' + retDist.toFixed(1) + ' km | ' + fmtMins(retTime) + ' (return)</span></div>';
|
1292 |
html += '<div class="route-segment"><span class="seg-line seg-line-home"></span><span class="seg-dist">'
|
| - |
|
1293 |
+ retDist.toFixed(1) + ' km | ' + fmtMins(retTime) + ' drive (return)'
|
| - |
|
1294 |
+ '</span></div>';
|
| 1275 |
}
|
1295 |
}
|
| 1276 |
html += '<div class="route-stop route-stop-home"><span class="stop-num">H</span><div class="stop-info"><div class="stop-name">Home</div></div></div>';
|
1296 |
html += '<div class="route-stop route-stop-home"><span class="stop-num">H</span><div class="stop-info"><div class="stop-name">Home</div></div></div>';
|
| 1277 |
}
|
1297 |
}
|
| 1278 |
|
1298 |
|
| 1279 |
// Day break indicator
|
1299 |
// Day break indicator
|
| Line 1422... |
Line 1442... |
| 1422 |
+ 'Current: ' + state.days.length + ' day(s)\n\n'
|
1442 |
+ 'Current: ' + state.days.length + ' day(s)\n\n'
|
| 1423 |
+ 'Please create a new beat for additional days.');
|
1443 |
+ 'Please create a new beat for additional days.');
|
| 1424 |
return;
|
1444 |
return;
|
| 1425 |
}
|
1445 |
}
|
| 1426 |
|
1446 |
|
| - |
|
1447 |
// Build plan data — recompute every day's totals + per-leg distance/time
|
| - |
|
1448 |
// fresh from the current state. recalcDay() only refreshes the CURRENT day,
|
| - |
|
1449 |
// so older days could carry stale values; doing it here guarantees DB rows
|
| 1427 |
// Build plan data
|
1450 |
// match what the user sees in the panel.
|
| 1428 |
var daysData = [];
|
1451 |
var daysData = [];
|
| 1429 |
state.days.forEach(function (day) {
|
1452 |
state.days.forEach(function (day) {
|
| 1430 |
if (day.visits.length === 0) return;
|
1453 |
if (day.visits.length === 0) return;
|
| - |
|
1454 |
|
| - |
|
1455 |
// Walk the visits in order, computing each leg from the previous point.
|
| - |
|
1456 |
// Day always starts at (startLat, startLng); for the LAST stop, if
|
| - |
|
1457 |
// endAction === 'HOME' we ALSO add the return-to-home leg into the day total
|
| - |
|
1458 |
// (but not stored per-visit, just rolled into totalDistanceKm/totalTimeMins).
|
| - |
|
1459 |
var prevLat = day.startLat, prevLng = day.startLng;
|
| - |
|
1460 |
var dayKm = 0, dayMins = 0;
|
| - |
|
1461 |
var visitsOut = [];
|
| - |
|
1462 |
day.visits.forEach(function (v) {
|
| - |
|
1463 |
var legKm = null, legMins = null;
|
| - |
|
1464 |
if (prevLat != null && prevLng != null && v.lat != null && v.lng != null) {
|
| - |
|
1465 |
legKm = haversine(prevLat, prevLng, v.lat, v.lng) * ROAD_FACTOR;
|
| - |
|
1466 |
legMins = Math.round((legKm / AVG_SPEED) * 60);
|
| - |
|
1467 |
dayKm += legKm;
|
| - |
|
1468 |
dayMins += legMins;
|
| - |
|
1469 |
}
|
| - |
|
1470 |
visitsOut.push({
|
| - |
|
1471 |
id: v.id,
|
| - |
|
1472 |
type: v.type || 'partner',
|
| - |
|
1473 |
distanceFromPrevKm: legKm != null ? Number(legKm.toFixed(3)) : null,
|
| - |
|
1474 |
timeFromPrevMins: legMins
|
| - |
|
1475 |
});
|
| - |
|
1476 |
if (v.lat != null && v.lng != null) {
|
| - |
|
1477 |
prevLat = v.lat;
|
| - |
|
1478 |
prevLng = v.lng;
|
| - |
|
1479 |
}
|
| - |
|
1480 |
});
|
| - |
|
1481 |
// Return-home leg if applicable
|
| - |
|
1482 |
if (day.endAction === 'HOME' && day.visits.length > 0
|
| - |
|
1483 |
&& state.homeLat != null && state.homeLng != null) {
|
| - |
|
1484 |
var last = day.visits[day.visits.length - 1];
|
| - |
|
1485 |
if (last.lat != null && last.lng != null) {
|
| - |
|
1486 |
var retKm = haversine(last.lat, last.lng, state.homeLat, state.homeLng) * ROAD_FACTOR;
|
| - |
|
1487 |
dayKm += retKm;
|
| - |
|
1488 |
dayMins += (retKm / AVG_SPEED) * 60;
|
| - |
|
1489 |
}
|
| - |
|
1490 |
}
|
| - |
|
1491 |
// Add per-stop dwell time
|
| - |
|
1492 |
dayMins += day.visits.length * VISIT_MINS;
|
| - |
|
1493 |
|
| 1431 |
daysData.push({
|
1494 |
daysData.push({
|
| 1432 |
dayNumber: day.dayNumber,
|
1495 |
dayNumber: day.dayNumber,
|
| 1433 |
startLocationName: day.startName,
|
1496 |
startLocationName: day.startName,
|
| 1434 |
startLatitude: day.startLat ? day.startLat.toString() : null,
|
1497 |
startLatitude: day.startLat ? day.startLat.toString() : null,
|
| 1435 |
startLongitude: day.startLng ? day.startLng.toString() : null,
|
1498 |
startLongitude: day.startLng ? day.startLng.toString() : null,
|
| 1436 |
endAction: day.endAction,
|
1499 |
endAction: day.endAction,
|
| 1437 |
stayLocationName: day.stayName,
|
1500 |
stayLocationName: day.stayName,
|
| 1438 |
stayLatitude: day.stayLat ? day.stayLat.toString() : null,
|
1501 |
stayLatitude: day.stayLat ? day.stayLat.toString() : null,
|
| 1439 |
stayLongitude: day.stayLng ? day.stayLng.toString() : null,
|
1502 |
stayLongitude: day.stayLng ? day.stayLng.toString() : null,
|
| 1440 |
totalDistanceKm: day.totalKm,
|
1503 |
totalDistanceKm: Number(dayKm.toFixed(3)),
|
| 1441 |
totalTimeMins: Math.round(day.totalMins),
|
1504 |
totalTimeMins: Math.round(dayMins),
|
| 1442 |
visits: day.visits.map(function (v) {
|
1505 |
visits: visitsOut
|
| 1443 |
return {id: v.id, type: v.type || 'partner'};
|
- |
|
| 1444 |
})
|
- |
|
| 1445 |
});
|
1506 |
});
|
| 1446 |
});
|
1507 |
});
|
| 1447 |
|
1508 |
|
| 1448 |
// EDIT RULE: detect removed leads → show popup with cancel/reschedule per lead
|
1509 |
// EDIT RULE: detect removed leads → show popup with cancel/reschedule per lead
|
| 1449 |
var removedLeads = [];
|
1510 |
var removedLeads = [];
|