| Line 83... |
Line 83... |
| 83 |
private CookiesProcessor cookiesProcessor;
|
83 |
private CookiesProcessor cookiesProcessor;
|
| 84 |
@Autowired
|
84 |
@Autowired
|
| 85 |
private ResponseSender responseSender;
|
85 |
private ResponseSender responseSender;
|
| 86 |
@Autowired
|
86 |
@Autowired
|
| 87 |
private FofoStoreRepository fofoStoreRepository;
|
87 |
private FofoStoreRepository fofoStoreRepository;
|
| - |
|
88 |
@Autowired
|
| - |
|
89 |
private com.spice.profitmandi.dao.repository.logistics.CompanyOfficeRepository companyOfficeRepository;
|
| 88 |
|
90 |
|
| 89 |
@GetMapping(value = "/beatPlan")
|
91 |
@GetMapping(value = "/beatPlan")
|
| 90 |
public String beatPlan(HttpServletRequest request, Model model) throws ProfitMandiBusinessException {
|
92 |
public String beatPlan(HttpServletRequest request, Model model) throws ProfitMandiBusinessException {
|
| 91 |
model.addAttribute("escalationTypes", visibleLevelsFor(request));
|
93 |
model.addAttribute("escalationTypes", visibleLevelsFor(request));
|
| 92 |
return "beat-plan";
|
94 |
return "beat-plan";
|
| Line 525... |
Line 527... |
| 525 |
|
527 |
|
| 526 |
List<Map<String, Object>> out = new ArrayList<>();
|
528 |
List<Map<String, Object>> out = new ArrayList<>();
|
| 527 |
for (BeatDeferredVisit r : rows) {
|
529 |
for (BeatDeferredVisit r : rows) {
|
| 528 |
AuthUser u = userMap.get(r.getAuthUserId());
|
530 |
AuthUser u = userMap.get(r.getAuthUserId());
|
| 529 |
boolean isLead = "lead".equalsIgnoreCase(r.getTaskType());
|
531 |
boolean isLead = "lead".equalsIgnoreCase(r.getTaskType());
|
| - |
|
532 |
boolean isOffice = "office-visit".equalsIgnoreCase(r.getTaskType());
|
| 530 |
Map<String, Object> row = new HashMap<>();
|
533 |
Map<String, Object> row = new HashMap<>();
|
| 531 |
row.put("id", r.getId());
|
534 |
row.put("id", r.getId());
|
| 532 |
row.put("authUserId", r.getAuthUserId());
|
535 |
row.put("authUserId", r.getAuthUserId());
|
| 533 |
row.put("userName", u != null ? (u.getFirstName() + " " + u.getLastName()) : ("User #" + r.getAuthUserId()));
|
536 |
row.put("userName", u != null ? (u.getFirstName() + " " + u.getLastName()) : ("User #" + r.getAuthUserId()));
|
| 534 |
row.put("fofoStoreId", r.getFofoId());
|
537 |
row.put("fofoStoreId", r.getFofoId());
|
| 535 |
row.put("name", r.getDisplayName() != null ? r.getDisplayName() : ("#" + r.getFofoId()));
|
538 |
row.put("name", r.getDisplayName() != null ? r.getDisplayName() : ("#" + r.getFofoId()));
|
| 536 |
row.put("type", isLead ? "Lead" : "Visit");
|
539 |
row.put("type", isLead ? "Lead" : (isOffice ? "Office" : "Visit"));
|
| 537 |
row.put("deferredDate", r.getDeferredDate() != null ? r.getDeferredDate().toString() : null);
|
540 |
row.put("deferredDate", r.getDeferredDate() != null ? r.getDeferredDate().toString() : null);
|
| 538 |
row.put("reason", r.getReason());
|
541 |
row.put("reason", r.getReason());
|
| 539 |
row.put("status", r.getStatus());
|
542 |
row.put("status", r.getStatus());
|
| 540 |
LocalDate next = nextByRowId.get(r.getId());
|
543 |
LocalDate next = nextByRowId.get(r.getId());
|
| 541 |
row.put("nextScheduledDate", next != null ? next.toString() : null);
|
544 |
row.put("nextScheduledDate", next != null ? next.toString() : null);
|
| Line 614... |
Line 617... |
| 614 |
// leads, looking up fofo_store would be the wrong id space). For visits we
|
617 |
// leads, looking up fofo_store would be the wrong id space). For visits we
|
| 615 |
// still try to pull lat/lng for the visit location.
|
618 |
// still try to pull lat/lng for the visit location.
|
| 616 |
Integer dtrId = resolveDtrId(d.getAuthUserId(), new HashMap<>());
|
619 |
Integer dtrId = resolveDtrId(d.getAuthUserId(), new HashMap<>());
|
| 617 |
if (dtrId == null) return responseSender.badRequest("No dtr.users record for this sales person");
|
620 |
if (dtrId == null) return responseSender.badRequest("No dtr.users record for this sales person");
|
| 618 |
boolean isLead = "lead".equalsIgnoreCase(d.getTaskType());
|
621 |
boolean isLead = "lead".equalsIgnoreCase(d.getTaskType());
|
| - |
|
622 |
boolean isOffice = "office-visit".equalsIgnoreCase(d.getTaskType());
|
| 619 |
String visitLocation = "0.0000,0.0000";
|
623 |
String visitLocation = "0.0000,0.0000";
|
| - |
|
624 |
if (isOffice) {
|
| - |
|
625 |
// Office stops resolve lat/lng from logistics.company_office.
|
| - |
|
626 |
try {
|
| - |
|
627 |
com.spice.profitmandi.dao.entity.logistics.CompanyOffice o = companyOfficeRepository.selectById(d.getFofoId());
|
| - |
|
628 |
if (o != null) visitLocation = o.getLat() + "," + o.getLng();
|
| - |
|
629 |
} catch (Exception ignored) {
|
| - |
|
630 |
}
|
| 620 |
if (!isLead) {
|
631 |
} else if (!isLead) {
|
| 621 |
try {
|
632 |
try {
|
| 622 |
List<FofoStore> ss = fofoStoreRepository.selectByRetailerIds(java.util.Collections.singletonList(d.getFofoId()));
|
633 |
List<FofoStore> ss = fofoStoreRepository.selectByRetailerIds(java.util.Collections.singletonList(d.getFofoId()));
|
| 623 |
if (!ss.isEmpty()) {
|
634 |
if (!ss.isEmpty()) {
|
| 624 |
FofoStore fs = ss.get(0);
|
635 |
FofoStore fs = ss.get(0);
|
| 625 |
if (fs.getLatitude() != null && fs.getLongitude() != null
|
636 |
if (fs.getLatitude() != null && fs.getLongitude() != null
|
| Line 748... |
Line 759... |
| 748 |
.filter(r -> r.getDayNumber() == sched.getDayNumber())
|
759 |
.filter(r -> r.getDayNumber() == sched.getDayNumber())
|
| 749 |
.mapToInt(BeatRoute::getSequenceOrder).max().orElse(-1) + 1;
|
760 |
.mapToInt(BeatRoute::getSequenceOrder).max().orElse(-1) + 1;
|
| 750 |
BeatRoute br = new BeatRoute();
|
761 |
BeatRoute br = new BeatRoute();
|
| 751 |
br.setBeatId(beatId);
|
762 |
br.setBeatId(beatId);
|
| 752 |
br.setFofoId(d.getFofoId());
|
763 |
br.setFofoId(d.getFofoId());
|
| - |
|
764 |
br.setVisitType(com.spice.profitmandi.dao.enumuration.dtr.BeatVisitType.PARTNER);
|
| 753 |
br.setDayNumber(sched.getDayNumber());
|
765 |
br.setDayNumber(sched.getDayNumber());
|
| 754 |
br.setSequenceOrder(nextSeq);
|
766 |
br.setSequenceOrder(nextSeq);
|
| 755 |
br.setActive(true);
|
767 |
br.setActive(true);
|
| 756 |
beatRouteRepository.persist(br);
|
768 |
beatRouteRepository.persist(br);
|
| 757 |
}
|
769 |
}
|
| Line 1051... |
Line 1063... |
| 1051 |
continue; // leads live in lead_route, handled below
|
1063 |
continue; // leads live in lead_route, handled below
|
| 1052 |
}
|
1064 |
}
|
| 1053 |
BeatRoute route = new BeatRoute();
|
1065 |
BeatRoute route = new BeatRoute();
|
| 1054 |
route.setBeatId(beatId);
|
1066 |
route.setBeatId(beatId);
|
| 1055 |
route.setFofoId(((Number) v.get("id")).intValue());
|
1067 |
route.setFofoId(((Number) v.get("id")).intValue());
|
| - |
|
1068 |
route.setVisitType("office".equals(v.get("type"))
|
| - |
|
1069 |
? com.spice.profitmandi.dao.enumuration.dtr.BeatVisitType.OFFICE
|
| - |
|
1070 |
: com.spice.profitmandi.dao.enumuration.dtr.BeatVisitType.PARTNER);
|
| 1056 |
route.setSequenceOrder(partnerSeq++);
|
1071 |
route.setSequenceOrder(partnerSeq++);
|
| 1057 |
route.setDayNumber(dayNumber);
|
1072 |
route.setDayNumber(dayNumber);
|
| 1058 |
route.setActive(true);
|
1073 |
route.setActive(true);
|
| 1059 |
if (v.get("distanceFromPrevKm") != null)
|
1074 |
if (v.get("distanceFromPrevKm") != null)
|
| 1060 |
route.setDistanceFromPrevKm(((Number) v.get("distanceFromPrevKm")).doubleValue());
|
1075 |
route.setDistanceFromPrevKm(((Number) v.get("distanceFromPrevKm")).doubleValue());
|
| Line 1441... |
Line 1456... |
| 1441 |
}
|
1456 |
}
|
| 1442 |
|
1457 |
|
| 1443 |
List<BeatRoute> routes = beatRouteRepository.selectByBeatId(beatId);
|
1458 |
List<BeatRoute> routes = beatRouteRepository.selectByBeatId(beatId);
|
| 1444 |
List<Map<String, Object>> result = new ArrayList<>();
|
1459 |
List<Map<String, Object>> result = new ArrayList<>();
|
| 1445 |
|
1460 |
|
| - |
|
1461 |
// Stops — partner OR office, dispatched by visit_type. Partners are
|
| 1446 |
// Partner stops — always (they belong to the beat template)
|
1462 |
// enriched on the client from the partner map (already in scope);
|
| - |
|
1463 |
// offices are enriched here because the client has no office map.
|
| 1447 |
for (BeatRoute r : routes) {
|
1464 |
for (BeatRoute r : routes) {
|
| 1448 |
Map<String, Object> map = new HashMap<>();
|
1465 |
Map<String, Object> map = new HashMap<>();
|
| 1449 |
map.put("fofoId", r.getFofoId());
|
1466 |
map.put("fofoId", r.getFofoId());
|
| 1450 |
map.put("dayNumber", r.getDayNumber());
|
1467 |
map.put("dayNumber", r.getDayNumber());
|
| 1451 |
map.put("sequenceOrder", r.getSequenceOrder());
|
1468 |
map.put("sequenceOrder", r.getSequenceOrder());
|
| - |
|
1469 |
if (r.getVisitType() == com.spice.profitmandi.dao.enumuration.dtr.BeatVisitType.OFFICE) {
|
| - |
|
1470 |
map.put("visitType", "office");
|
| - |
|
1471 |
try {
|
| - |
|
1472 |
com.spice.profitmandi.dao.entity.logistics.CompanyOffice o =
|
| - |
|
1473 |
companyOfficeRepository.selectById(r.getFofoId());
|
| - |
|
1474 |
if (o != null) {
|
| - |
|
1475 |
map.put("code", o.getCode());
|
| - |
|
1476 |
map.put("name", o.getName());
|
| - |
|
1477 |
map.put("latitude", String.valueOf(o.getLat()));
|
| - |
|
1478 |
map.put("longitude", String.valueOf(o.getLng()));
|
| - |
|
1479 |
}
|
| - |
|
1480 |
} catch (Exception ignored) {
|
| - |
|
1481 |
}
|
| - |
|
1482 |
} else {
|
| 1452 |
map.put("visitType", "partner");
|
1483 |
map.put("visitType", "partner");
|
| - |
|
1484 |
}
|
| 1453 |
result.add(map);
|
1485 |
result.add(map);
|
| 1454 |
}
|
1486 |
}
|
| 1455 |
|
1487 |
|
| 1456 |
// Lead stops — only for the requested run date
|
1488 |
// Lead stops — only for the requested run date
|
| 1457 |
if (planDate != null && !planDate.isEmpty()) {
|
1489 |
if (planDate != null && !planDate.isEmpty()) {
|
| Line 1880... |
Line 1912... |
| 1880 |
for (int i = 0; i < visits.size(); i++) {
|
1912 |
for (int i = 0; i < visits.size(); i++) {
|
| 1881 |
Map<String, Object> visit = visits.get(i);
|
1913 |
Map<String, Object> visit = visits.get(i);
|
| 1882 |
BeatRoute route = new BeatRoute();
|
1914 |
BeatRoute route = new BeatRoute();
|
| 1883 |
route.setBeatId(beat.getId());
|
1915 |
route.setBeatId(beat.getId());
|
| 1884 |
route.setFofoId(((Number) visit.get("id")).intValue());
|
1916 |
route.setFofoId(((Number) visit.get("id")).intValue());
|
| - |
|
1917 |
route.setVisitType("office".equals(visit.get("type"))
|
| - |
|
1918 |
? com.spice.profitmandi.dao.enumuration.dtr.BeatVisitType.OFFICE
|
| - |
|
1919 |
: com.spice.profitmandi.dao.enumuration.dtr.BeatVisitType.PARTNER);
|
| 1885 |
route.setSequenceOrder(i);
|
1920 |
route.setSequenceOrder(i);
|
| 1886 |
route.setDayNumber(dayNumber);
|
1921 |
route.setDayNumber(dayNumber);
|
| 1887 |
route.setActive(true);
|
1922 |
route.setActive(true);
|
| 1888 |
if (visit.get("distanceFromPrevKm") != null)
|
1923 |
if (visit.get("distanceFromPrevKm") != null)
|
| 1889 |
route.setDistanceFromPrevKm(((Number) visit.get("distanceFromPrevKm")).doubleValue());
|
1924 |
route.setDistanceFromPrevKm(((Number) visit.get("distanceFromPrevKm")).doubleValue());
|
| Line 2095... |
Line 2130... |
| 2095 |
}
|
2130 |
}
|
| 2096 |
g.addPartner(day, seq, code.trim(), r + 1);
|
2131 |
g.addPartner(day, seq, code.trim(), r + 1);
|
| 2097 |
}
|
2132 |
}
|
| 2098 |
workbook.close();
|
2133 |
workbook.close();
|
| 2099 |
|
2134 |
|
| - |
|
2135 |
// Partner-code lookup (legacy).
|
| 2100 |
List<FofoStore> allStores = fofoStoreRepository.selectAll();
|
2136 |
List<FofoStore> allStores = fofoStoreRepository.selectAll();
|
| 2101 |
Map<String, Integer> codeToId = new HashMap<>();
|
2137 |
Map<String, Integer> codeToId = new HashMap<>();
|
| 2102 |
for (FofoStore store : allStores) codeToId.put(store.getCode(), store.getId());
|
2138 |
for (FofoStore store : allStores) codeToId.put(store.getCode(), store.getId());
|
| 2103 |
|
2139 |
|
| - |
|
2140 |
// Office-code lookup — office stops share the same `partner_code` column in the bulk
|
| - |
|
2141 |
// sheet; resolution dispatches by which catalogue the code belongs to. A code present
|
| - |
|
2142 |
// in BOTH catalogues is treated as an error so the planner fixes the collision.
|
| - |
|
2143 |
Map<String, Integer> officeCodeToId = new HashMap<>();
|
| - |
|
2144 |
for (com.spice.profitmandi.dao.entity.logistics.CompanyOffice o : companyOfficeRepository.selectAll()) {
|
| - |
|
2145 |
if (o.getCode() != null && !o.getCode().isEmpty()) officeCodeToId.put(o.getCode(), o.getId());
|
| - |
|
2146 |
}
|
| - |
|
2147 |
|
| 2104 |
LocalDate holidayStart = LocalDate.now();
|
2148 |
LocalDate holidayStart = LocalDate.now();
|
| 2105 |
List<PublicHolidays> holidays = publicHolidaysRepository.selectAllBetweenDates(holidayStart, holidayStart.plusMonths(6));
|
2149 |
List<PublicHolidays> holidays = publicHolidaysRepository.selectAllBetweenDates(holidayStart, holidayStart.plusMonths(6));
|
| 2106 |
Set<LocalDate> holidayDates = holidays.stream().map(PublicHolidays::getDate).collect(Collectors.toSet());
|
2150 |
Set<LocalDate> holidayDates = holidays.stream().map(PublicHolidays::getDate).collect(Collectors.toSet());
|
| 2107 |
|
2151 |
|
| 2108 |
// =====================================================================
|
2152 |
// =====================================================================
|
| Line 2168... |
Line 2212... |
| 2168 |
if (bulkConflict != null) {
|
2212 |
if (bulkConflict != null) {
|
| 2169 |
errorMessages.add("Beat '" + beatName + "': " + scheduleConflictMessage(bulkConflict));
|
2213 |
errorMessages.add("Beat '" + beatName + "': " + scheduleConflictMessage(bulkConflict));
|
| 2170 |
continue;
|
2214 |
continue;
|
| 2171 |
}
|
2215 |
}
|
| 2172 |
|
2216 |
|
| 2173 |
// Validate partner codes upfront so we don't half-persist.
|
2217 |
// Validate codes upfront so we don't half-persist. A code may belong to
|
| - |
|
2218 |
// fofo_store (PARTNER) or company_office (OFFICE) but not both.
|
| 2174 |
List<String> badCodes = new ArrayList<>();
|
2219 |
List<String> badCodes = new ArrayList<>();
|
| - |
|
2220 |
List<String> ambiguousCodes = new ArrayList<>();
|
| 2175 |
for (List<BulkPartner> ps : g.dayToPartners.values()) {
|
2221 |
for (List<BulkPartner> ps : g.dayToPartners.values()) {
|
| 2176 |
for (BulkPartner p : ps) {
|
2222 |
for (BulkPartner p : ps) {
|
| 2177 |
if (!codeToId.containsKey(p.code)) {
|
2223 |
boolean inPartner = codeToId.containsKey(p.code);
|
| - |
|
2224 |
boolean inOffice = officeCodeToId.containsKey(p.code);
|
| - |
|
2225 |
if (inPartner && inOffice) {
|
| - |
|
2226 |
ambiguousCodes.add(p.code + " (row " + p.rowNum + ")");
|
| - |
|
2227 |
} else if (!inPartner && !inOffice) {
|
| 2178 |
badCodes.add(p.code + " (row " + p.rowNum + ")");
|
2228 |
badCodes.add(p.code + " (row " + p.rowNum + ")");
|
| 2179 |
}
|
2229 |
}
|
| 2180 |
}
|
2230 |
}
|
| 2181 |
}
|
2231 |
}
|
| 2182 |
if (!badCodes.isEmpty()) {
|
2232 |
if (!badCodes.isEmpty()) {
|
| 2183 |
errorMessages.add("Beat '" + beatName + "': unknown partner code(s) — " + String.join(", ", badCodes) + ".");
|
2233 |
errorMessages.add("Beat '" + beatName + "': unknown code(s) — " + String.join(", ", badCodes) + ".");
|
| - |
|
2234 |
continue;
|
| - |
|
2235 |
}
|
| - |
|
2236 |
if (!ambiguousCodes.isEmpty()) {
|
| - |
|
2237 |
errorMessages.add("Beat '" + beatName + "': code(s) exist in both partner and office catalogues — " + String.join(", ", ambiguousCodes) + ".");
|
| 2184 |
continue;
|
2238 |
continue;
|
| 2185 |
}
|
2239 |
}
|
| 2186 |
|
2240 |
|
| 2187 |
ready.add(new ValidatedBulkBeat(g, authUserId, sortedDays, scheduleDates));
|
2241 |
ready.add(new ValidatedBulkBeat(g, authUserId, sortedDays, scheduleDates));
|
| 2188 |
}
|
2242 |
}
|
| Line 2259... |
Line 2313... |
| 2259 |
return Integer.compare(a.rowNum, b.rowNum);
|
2313 |
return Integer.compare(a.rowNum, b.rowNum);
|
| 2260 |
});
|
2314 |
});
|
| 2261 |
|
2315 |
|
| 2262 |
int autoSeq = 0;
|
2316 |
int autoSeq = 0;
|
| 2263 |
for (BulkPartner p : partners) {
|
2317 |
for (BulkPartner p : partners) {
|
| 2264 |
Integer fofoId = codeToId.get(p.code);
|
2318 |
Integer partnerId = codeToId.get(p.code);
|
| - |
|
2319 |
Integer officeId = officeCodeToId.get(p.code);
|
| 2265 |
// Codes were validated in Phase 1, this is just a safety net.
|
2320 |
// Codes were validated in Phase 1, this is just a safety net.
|
| 2266 |
if (fofoId == null) continue;
|
2321 |
if (partnerId == null && officeId == null) continue;
|
| 2267 |
BeatRoute route = new BeatRoute();
|
2322 |
BeatRoute route = new BeatRoute();
|
| 2268 |
route.setBeatId(beat.getId());
|
2323 |
route.setBeatId(beat.getId());
|
| - |
|
2324 |
if (partnerId != null) {
|
| - |
|
2325 |
route.setFofoId(partnerId);
|
| - |
|
2326 |
route.setVisitType(com.spice.profitmandi.dao.enumuration.dtr.BeatVisitType.PARTNER);
|
| - |
|
2327 |
} else {
|
| 2269 |
route.setFofoId(fofoId);
|
2328 |
route.setFofoId(officeId);
|
| - |
|
2329 |
route.setVisitType(com.spice.profitmandi.dao.enumuration.dtr.BeatVisitType.OFFICE);
|
| - |
|
2330 |
}
|
| 2270 |
route.setSequenceOrder(p.seq >= 0 ? p.seq : autoSeq);
|
2331 |
route.setSequenceOrder(p.seq >= 0 ? p.seq : autoSeq);
|
| 2271 |
route.setDayNumber(dayNumber);
|
2332 |
route.setDayNumber(dayNumber);
|
| 2272 |
route.setActive(true);
|
2333 |
route.setActive(true);
|
| 2273 |
beatRouteRepository.persist(route);
|
2334 |
beatRouteRepository.persist(route);
|
| 2274 |
autoSeq++;
|
2335 |
autoSeq++;
|