| Line 120... |
Line 120... |
| 120 |
* Math.sin(dLng / 2) * Math.sin(dLng / 2);
|
120 |
* Math.sin(dLng / 2) * Math.sin(dLng / 2);
|
| 121 |
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
121 |
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
| 122 |
return R * c;
|
122 |
return R * c;
|
| 123 |
}
|
123 |
}
|
| 124 |
|
124 |
|
| - |
|
125 |
// Mirrors the JS recalcDay() formula. Used by schedule/repeat endpoints
|
| - |
|
126 |
// which create fresh BeatSchedule rows — they need to fill totals from the
|
| - |
|
127 |
// existing beat_route table, not from anything the client posted.
|
| - |
|
128 |
// Returns {totalKm, totalMins}.
|
| - |
|
129 |
private double[] computeDayTotals(int beatId, int dayNumber, String endAction) {
|
| - |
|
130 |
Beat beat = beatRepository.selectById(beatId);
|
| - |
|
131 |
if (beat == null) return new double[]{0d, 0d};
|
| - |
|
132 |
|
| - |
|
133 |
List<BeatRoute> dayRoutes = beatRouteRepository.selectByBeatId(beatId).stream()
|
| - |
|
134 |
.filter(r -> r.getDayNumber() == dayNumber)
|
| - |
|
135 |
.sorted(java.util.Comparator.comparingInt(BeatRoute::getSequenceOrder))
|
| - |
|
136 |
.collect(Collectors.toList());
|
| - |
|
137 |
if (dayRoutes.isEmpty()) return new double[]{0d, 0d};
|
| - |
|
138 |
|
| - |
|
139 |
List<Integer> fofoIds = dayRoutes.stream().map(BeatRoute::getFofoId).distinct().collect(Collectors.toList());
|
| - |
|
140 |
Map<Integer, FofoStore> storeMap = new HashMap<>();
|
| - |
|
141 |
try {
|
| - |
|
142 |
for (FofoStore fs : fofoStoreRepository.selectByRetailerIds(fofoIds)) {
|
| - |
|
143 |
storeMap.put(fs.getId(), fs);
|
| - |
|
144 |
}
|
| - |
|
145 |
} catch (Exception ignored) { /* fall through with empty map */ }
|
| - |
|
146 |
|
| - |
|
147 |
double ROAD_FACTOR = 1.3;
|
| - |
|
148 |
double AVG_SPEED = 30.0; // km/h
|
| - |
|
149 |
int VISIT_MINS = 30;
|
| - |
|
150 |
|
| - |
|
151 |
Double prevLat = parseDoubleOrNull(beat.getStartLatitude());
|
| - |
|
152 |
Double prevLng = parseDoubleOrNull(beat.getStartLongitude());
|
| - |
|
153 |
|
| - |
|
154 |
double totalKm = 0d;
|
| - |
|
155 |
for (BeatRoute r : dayRoutes) {
|
| - |
|
156 |
FofoStore fs = storeMap.get(r.getFofoId());
|
| - |
|
157 |
if (fs == null) continue;
|
| - |
|
158 |
Double curLat = parseDoubleOrNull(fs.getLatitude());
|
| - |
|
159 |
Double curLng = parseDoubleOrNull(fs.getLongitude());
|
| - |
|
160 |
if (prevLat != null && prevLng != null && curLat != null && curLng != null) {
|
| - |
|
161 |
totalKm += haversineKm(prevLat, prevLng, curLat, curLng) * ROAD_FACTOR;
|
| - |
|
162 |
}
|
| - |
|
163 |
if (curLat != null && curLng != null) {
|
| - |
|
164 |
prevLat = curLat;
|
| - |
|
165 |
prevLng = curLng;
|
| - |
|
166 |
}
|
| - |
|
167 |
}
|
| - |
|
168 |
|
| - |
|
169 |
// Return-home leg only when end_action='HOME'
|
| - |
|
170 |
if ("HOME".equalsIgnoreCase(endAction)) {
|
| - |
|
171 |
Double homeLat = parseDoubleOrNull(beat.getStartLatitude());
|
| - |
|
172 |
Double homeLng = parseDoubleOrNull(beat.getStartLongitude());
|
| - |
|
173 |
if (prevLat != null && prevLng != null && homeLat != null && homeLng != null) {
|
| - |
|
174 |
totalKm += haversineKm(prevLat, prevLng, homeLat, homeLng) * ROAD_FACTOR;
|
| - |
|
175 |
}
|
| - |
|
176 |
}
|
| - |
|
177 |
|
| - |
|
178 |
double totalMins = (totalKm / AVG_SPEED) * 60.0 + dayRoutes.size() * VISIT_MINS;
|
| - |
|
179 |
return new double[]{Math.round(totalKm * 1000d) / 1000d, Math.round(totalMins)};
|
| - |
|
180 |
}
|
| - |
|
181 |
|
| 125 |
// ====================== ASSIGN VISIT ======================
|
182 |
// ====================== ASSIGN VISIT ======================
|
| 126 |
// Day View "Assign Visit" — lets an admin pick parties (stores) for a specific
|
183 |
// Day View "Assign Visit" — lets an admin pick parties (stores) for a specific
|
| 127 |
// auth user on a specific date and pushes them as visit tasks to the v2
|
184 |
// auth user on a specific date and pushes them as visit tasks to the v2
|
| 128 |
// /profitmandi-web/v2/beat-tracking/batch endpoint.
|
185 |
// /profitmandi-web/v2/beat-tracking/batch endpoint.
|
| 129 |
|
186 |
|
| Line 470... |
Line 527... |
| 470 |
+ "Please create a new beat for additional days.");
|
527 |
+ "Please create a new beat for additional days.");
|
| 471 |
}
|
528 |
}
|
| 472 |
beat.setTotalDays(newTotalDays);
|
529 |
beat.setTotalDays(newTotalDays);
|
| 473 |
|
530 |
|
| 474 |
// Replace routes (partner stops). Schedules stay intact (except for
|
531 |
// Replace routes (partner stops). Schedules stay intact (except for
|
| 475 |
// dayNumber > newTotalDays cleanup below if the beat shrank).
|
532 |
// dayNumber > newTotalDays cleanup + total km/min refresh below).
|
| 476 |
beatRouteRepository.deleteByBeatId(beatId);
|
533 |
beatRouteRepository.deleteByBeatId(beatId);
|
| 477 |
// Collect lead IDs the user kept on the plan
|
534 |
// Collect lead IDs the user kept on the plan
|
| 478 |
Set<Integer> keptLeadIds = new HashSet<>();
|
535 |
Set<Integer> keptLeadIds = new HashSet<>();
|
| 479 |
for (int d = 0; d < days.size(); d++) {
|
536 |
for (int d = 0; d < days.size(); d++) {
|
| 480 |
Map<String, Object> day = days.get(d);
|
537 |
Map<String, Object> day = days.get(d);
|
| Line 492... |
Line 549... |
| 492 |
route.setBeatId(beatId);
|
549 |
route.setBeatId(beatId);
|
| 493 |
route.setFofoId(((Number) v.get("id")).intValue());
|
550 |
route.setFofoId(((Number) v.get("id")).intValue());
|
| 494 |
route.setSequenceOrder(partnerSeq++);
|
551 |
route.setSequenceOrder(partnerSeq++);
|
| 495 |
route.setDayNumber(dayNumber);
|
552 |
route.setDayNumber(dayNumber);
|
| 496 |
route.setActive(true);
|
553 |
route.setActive(true);
|
| - |
|
554 |
if (v.get("distanceFromPrevKm") != null)
|
| - |
|
555 |
route.setDistanceFromPrevKm(((Number) v.get("distanceFromPrevKm")).doubleValue());
|
| - |
|
556 |
if (v.get("timeFromPrevMins") != null)
|
| - |
|
557 |
route.setTimeFromPrevMins(((Number) v.get("timeFromPrevMins")).intValue());
|
| 497 |
beatRouteRepository.persist(route);
|
558 |
beatRouteRepository.persist(route);
|
| 498 |
}
|
559 |
}
|
| 499 |
}
|
560 |
}
|
| 500 |
|
561 |
|
| 501 |
// If the beat shrank, drop schedule rows for day numbers that no longer exist
|
562 |
// If the beat shrank, drop schedule rows for day numbers that no longer exist
|
| Line 504... |
Line 565... |
| 504 |
for (BeatSchedule s : currentSchedules) {
|
565 |
for (BeatSchedule s : currentSchedules) {
|
| 505 |
if (s.getDayNumber() > newTotalDays) beatScheduleRepository.delete(s);
|
566 |
if (s.getDayNumber() > newTotalDays) beatScheduleRepository.delete(s);
|
| 506 |
}
|
567 |
}
|
| 507 |
}
|
568 |
}
|
| 508 |
|
569 |
|
| - |
|
570 |
// Refresh the day-level totals on every remaining schedule row so they
|
| - |
|
571 |
// reflect the post-edit route. Previously updateBeat left these stale
|
| - |
|
572 |
// (or NULL, for beats created before this fix), which is what the user
|
| - |
|
573 |
// reported. Keyed by dayNumber so multi-instance beats all get updated.
|
| - |
|
574 |
Map<Integer, Map<String, Object>> dayByNumber = new HashMap<>();
|
| - |
|
575 |
for (int d = 0; d < days.size(); d++) {
|
| - |
|
576 |
dayByNumber.put(d + 1, days.get(d));
|
| - |
|
577 |
}
|
| - |
|
578 |
List<BeatSchedule> allSchedules = beatScheduleRepository.selectByBeatId(beatId);
|
| - |
|
579 |
for (BeatSchedule s : allSchedules) {
|
| - |
|
580 |
Map<String, Object> day = dayByNumber.get(s.getDayNumber());
|
| - |
|
581 |
if (day == null) continue;
|
| - |
|
582 |
if (day.get("totalDistanceKm") != null)
|
| - |
|
583 |
s.setTotalDistanceKm(((Number) day.get("totalDistanceKm")).doubleValue());
|
| - |
|
584 |
if (day.get("totalTimeMins") != null)
|
| - |
|
585 |
s.setTotalTimeMins(((Number) day.get("totalTimeMins")).intValue());
|
| - |
|
586 |
}
|
| - |
|
587 |
|
| 509 |
// Process per-lead actions sent from the editor's removed-leads popup.
|
588 |
// Process per-lead actions sent from the editor's removed-leads popup.
|
| 510 |
// Each entry: {leadId, action: "cancel"|"reschedule", toDate?: "yyyy-MM-dd"}.
|
589 |
// Each entry: {leadId, action: "cancel"|"reschedule", toDate?: "yyyy-MM-dd"}.
|
| 511 |
// - cancel: mark the lead's current APPROVED row for this beat as CANCELLED.
|
590 |
// - cancel: mark the lead's current APPROVED row for this beat as CANCELLED.
|
| 512 |
// - reschedule: cancel here, then create a fresh APPROVED LeadRoute on
|
591 |
// - reschedule: cancel here, then create a fresh APPROVED LeadRoute on
|
| 513 |
// whichever beat this user has scheduled on toDate. If no beat exists
|
592 |
// whichever beat this user has scheduled on toDate. If no beat exists
|
| Line 1264... |
Line 1343... |
| 1264 |
if (day.get("totalTimeMins") != null)
|
1343 |
if (day.get("totalTimeMins") != null)
|
| 1265 |
schedule.setTotalTimeMins(((Number) day.get("totalTimeMins")).intValue());
|
1344 |
schedule.setTotalTimeMins(((Number) day.get("totalTimeMins")).intValue());
|
| 1266 |
schedule.setCreatedTimestamp(LocalDateTime.now());
|
1345 |
schedule.setCreatedTimestamp(LocalDateTime.now());
|
| 1267 |
beatScheduleRepository.persist(schedule);
|
1346 |
beatScheduleRepository.persist(schedule);
|
| 1268 |
|
1347 |
|
| 1269 |
// Routes (stops)
|
1348 |
// Routes (stops) — also persist per-leg distance/time supplied by the
|
| - |
|
1349 |
// client so reports/dashboards don't have to recompute from lat/lng.
|
| 1270 |
List<Map<String, Object>> visits = (List<Map<String, Object>>) day.get("visits");
|
1350 |
List<Map<String, Object>> visits = (List<Map<String, Object>>) day.get("visits");
|
| 1271 |
if (visits != null) {
|
1351 |
if (visits != null) {
|
| 1272 |
for (int i = 0; i < visits.size(); i++) {
|
1352 |
for (int i = 0; i < visits.size(); i++) {
|
| 1273 |
Map<String, Object> visit = visits.get(i);
|
1353 |
Map<String, Object> visit = visits.get(i);
|
| 1274 |
BeatRoute route = new BeatRoute();
|
1354 |
BeatRoute route = new BeatRoute();
|
| 1275 |
route.setBeatId(beat.getId());
|
1355 |
route.setBeatId(beat.getId());
|
| 1276 |
route.setFofoId(((Number) visit.get("id")).intValue());
|
1356 |
route.setFofoId(((Number) visit.get("id")).intValue());
|
| 1277 |
route.setSequenceOrder(i);
|
1357 |
route.setSequenceOrder(i);
|
| 1278 |
route.setDayNumber(dayNumber);
|
1358 |
route.setDayNumber(dayNumber);
|
| 1279 |
route.setActive(true);
|
1359 |
route.setActive(true);
|
| - |
|
1360 |
if (visit.get("distanceFromPrevKm") != null)
|
| - |
|
1361 |
route.setDistanceFromPrevKm(((Number) visit.get("distanceFromPrevKm")).doubleValue());
|
| - |
|
1362 |
if (visit.get("timeFromPrevMins") != null)
|
| - |
|
1363 |
route.setTimeFromPrevMins(((Number) visit.get("timeFromPrevMins")).intValue());
|
| 1280 |
beatRouteRepository.persist(route);
|
1364 |
beatRouteRepository.persist(route);
|
| 1281 |
}
|
1365 |
}
|
| 1282 |
}
|
1366 |
}
|
| 1283 |
}
|
1367 |
}
|
| 1284 |
|
1368 |
|
| Line 1812... |
Line 1896... |
| 1812 |
dayInfo.put("dayNumber", s.getDayNumber());
|
1896 |
dayInfo.put("dayNumber", s.getDayNumber());
|
| 1813 |
boolean isUnscheduled = s.getStartDate().getYear() == 9999;
|
1897 |
boolean isUnscheduled = s.getStartDate().getYear() == 9999;
|
| 1814 |
dayInfo.put("planDate", isUnscheduled ? null : s.getStartDate().toString());
|
1898 |
dayInfo.put("planDate", isUnscheduled ? null : s.getStartDate().toString());
|
| 1815 |
dayInfo.put("totalKm", s.getTotalDistanceKm());
|
1899 |
dayInfo.put("totalKm", s.getTotalDistanceKm());
|
| 1816 |
dayInfo.put("totalMins", s.getTotalTimeMins());
|
1900 |
dayInfo.put("totalMins", s.getTotalTimeMins());
|
| - |
|
1901 |
// endAction tells the planner whether to draw the return-to-home line
|
| - |
|
1902 |
// for this day (HOME) or end at the last stop (DAYBREAK).
|
| - |
|
1903 |
dayInfo.put("endAction", s.getEndAction());
|
| 1817 |
long visitCount = routes.stream().filter(r -> r.getDayNumber() == s.getDayNumber()).count();
|
1904 |
long visitCount = routes.stream().filter(r -> r.getDayNumber() == s.getDayNumber()).count();
|
| 1818 |
dayInfo.put("visitCount", (int) visitCount);
|
1905 |
dayInfo.put("visitCount", (int) visitCount);
|
| 1819 |
dayInfoList.add(dayInfo);
|
1906 |
dayInfoList.add(dayInfo);
|
| 1820 |
}
|
1907 |
}
|
| 1821 |
if (schedules.isEmpty()) {
|
1908 |
if (schedules.isEmpty()) {
|
| Line 1870... |
Line 1957... |
| 1870 |
|
1957 |
|
| 1871 |
// Delete old schedules and create new
|
1958 |
// Delete old schedules and create new
|
| 1872 |
beatScheduleRepository.deleteByBeatId(beatId);
|
1959 |
beatScheduleRepository.deleteByBeatId(beatId);
|
| 1873 |
LocalDate schEndDate = dateList.isEmpty() ? null : LocalDate.parse(dateList.get(dateList.size() - 1));
|
1960 |
LocalDate schEndDate = dateList.isEmpty() ? null : LocalDate.parse(dateList.get(dateList.size() - 1));
|
| 1874 |
for (int i = 0; i < dateList.size() && i < beat.getTotalDays(); i++) {
|
1961 |
for (int i = 0; i < dateList.size() && i < beat.getTotalDays(); i++) {
|
| - |
|
1962 |
int dayNumber = i + 1;
|
| - |
|
1963 |
String endAction = (i == dateList.size() - 1) ? "HOME" : "DAYBREAK";
|
| 1875 |
BeatSchedule schedule = new BeatSchedule();
|
1964 |
BeatSchedule schedule = new BeatSchedule();
|
| 1876 |
schedule.setBeatId(beatId);
|
1965 |
schedule.setBeatId(beatId);
|
| 1877 |
schedule.setStartDate(LocalDate.parse(dateList.get(i)));
|
1966 |
schedule.setStartDate(LocalDate.parse(dateList.get(i)));
|
| 1878 |
schedule.setEndDate(schEndDate);
|
1967 |
schedule.setEndDate(schEndDate);
|
| 1879 |
schedule.setDayNumber(i + 1);
|
1968 |
schedule.setDayNumber(dayNumber);
|
| - |
|
1969 |
schedule.setEndAction(endAction);
|
| - |
|
1970 |
// Fill total_distance_km / total_time_mins from beat_route so the new
|
| 1880 |
schedule.setEndAction(i == dateList.size() - 1 ? "HOME" : "DAYBREAK");
|
1971 |
// schedule row isn't NULL (this was the bug — these were left unset).
|
| - |
|
1972 |
double[] totals = computeDayTotals(beatId, dayNumber, endAction);
|
| - |
|
1973 |
schedule.setTotalDistanceKm(totals[0]);
|
| - |
|
1974 |
schedule.setTotalTimeMins((int) totals[1]);
|
| 1881 |
schedule.setCreatedTimestamp(LocalDateTime.now());
|
1975 |
schedule.setCreatedTimestamp(LocalDateTime.now());
|
| 1882 |
beatScheduleRepository.persist(schedule);
|
1976 |
beatScheduleRepository.persist(schedule);
|
| 1883 |
}
|
1977 |
}
|
| 1884 |
|
1978 |
|
| 1885 |
Map<String, Object> response = new HashMap<>();
|
1979 |
Map<String, Object> response = new HashMap<>();
|
| Line 1910... |
Line 2004... |
| 1910 |
if (s.getStartDate() != null && s.getStartDate().getYear() == 9999) {
|
2004 |
if (s.getStartDate() != null && s.getStartDate().getYear() == 9999) {
|
| 1911 |
beatScheduleRepository.delete(s);
|
2005 |
beatScheduleRepository.delete(s);
|
| 1912 |
}
|
2006 |
}
|
| 1913 |
}
|
2007 |
}
|
| 1914 |
|
2008 |
|
| 1915 |
// Add new real-date schedule rows for the existing beat
|
2009 |
// Add new real-date schedule rows for the existing beat — fill totals
|
| - |
|
2010 |
// from beat_route so total_distance_km / total_time_mins aren't NULL.
|
| 1916 |
LocalDate repeatEndDate = dateList.isEmpty() ? null : LocalDate.parse(dateList.get(dateList.size() - 1));
|
2011 |
LocalDate repeatEndDate = dateList.isEmpty() ? null : LocalDate.parse(dateList.get(dateList.size() - 1));
|
| 1917 |
for (int i = 0; i < dateList.size(); i++) {
|
2012 |
for (int i = 0; i < dateList.size(); i++) {
|
| - |
|
2013 |
int dayNumber = i + 1;
|
| - |
|
2014 |
String endAction = (i == dateList.size() - 1) ? "HOME" : "DAYBREAK";
|
| 1918 |
BeatSchedule schedule = new BeatSchedule();
|
2015 |
BeatSchedule schedule = new BeatSchedule();
|
| 1919 |
schedule.setBeatId(beatId);
|
2016 |
schedule.setBeatId(beatId);
|
| 1920 |
schedule.setStartDate(LocalDate.parse(dateList.get(i)));
|
2017 |
schedule.setStartDate(LocalDate.parse(dateList.get(i)));
|
| 1921 |
schedule.setEndDate(repeatEndDate);
|
2018 |
schedule.setEndDate(repeatEndDate);
|
| 1922 |
schedule.setDayNumber(i + 1);
|
2019 |
schedule.setDayNumber(dayNumber);
|
| - |
|
2020 |
schedule.setEndAction(endAction);
|
| 1923 |
schedule.setEndAction(i == dateList.size() - 1 ? "HOME" : "DAYBREAK");
|
2021 |
double[] totals = computeDayTotals(beatId, dayNumber, endAction);
|
| - |
|
2022 |
schedule.setTotalDistanceKm(totals[0]);
|
| - |
|
2023 |
schedule.setTotalTimeMins((int) totals[1]);
|
| 1924 |
schedule.setCreatedTimestamp(LocalDateTime.now());
|
2024 |
schedule.setCreatedTimestamp(LocalDateTime.now());
|
| 1925 |
beatScheduleRepository.persist(schedule);
|
2025 |
beatScheduleRepository.persist(schedule);
|
| 1926 |
}
|
2026 |
}
|
| 1927 |
|
2027 |
|
| 1928 |
Map<String, Object> response = new HashMap<>();
|
2028 |
Map<String, Object> response = new HashMap<>();
|