| Line 388... |
Line 388... |
| 388 |
Map<String, Object> result = new HashMap<>();
|
388 |
Map<String, Object> result = new HashMap<>();
|
| 389 |
result.put("dtrUserId", dtrUserId);
|
389 |
result.put("dtrUserId", dtrUserId);
|
| 390 |
result.put("authUserId", authUserId);
|
390 |
result.put("authUserId", authUserId);
|
| 391 |
result.put("userName", au.getFirstName() + " " + au.getLastName());
|
391 |
result.put("userName", au.getFirstName() + " " + au.getLastName());
|
| 392 |
result.put("parties", parties);
|
392 |
result.put("parties", parties);
|
| - |
|
393 |
result.put("agendaOptions", com.spice.profitmandi.dao.enumuration.dtr.VisitAgenda.labels());
|
| - |
|
394 |
// Managers can't assign a visit while a scheduled beat's agenda is unfilled; the
|
| - |
|
395 |
// modal uses this flag to show the warning and disable the assign action. (A
|
| - |
|
396 |
// user with no beat that day is allowed — nothing to fill.)
|
| - |
|
397 |
boolean agendaFilled = agendaAllowsAssign(authUserId, parsedDate != null ? parsedDate : LocalDate.now());
|
| - |
|
398 |
result.put("agendaFilled", agendaFilled);
|
| - |
|
399 |
result.put("agendaNotFilledMessage", AGENDA_NOT_FILLED_MSG);
|
| 393 |
// Only non-auto agendas are manually assignable; auto ones surface via
|
400 |
// Only non-auto agendas are manually assignable; auto ones surface via
|
| 394 |
// openAgendas (chips) and are woven into task_name server-side on submit.
|
401 |
// openAgendas (chips) and are woven into task_name server-side on submit.
|
| 395 |
result.put("agendaOptions", AgendaType.manualAssignableLabels());
|
402 |
result.put("agendaOptions", AgendaType.manualAssignableLabels());
|
| 396 |
return responseSender.ok(result);
|
403 |
return responseSender.ok(result);
|
| 397 |
}
|
404 |
}
|
| Line 465... |
Line 472... |
| 465 |
// button on the UI isn't enough — block at the API too.
|
472 |
// button on the UI isn't enough — block at the API too.
|
| 466 |
if (!taskDate.equals(LocalDate.now())) {
|
473 |
if (!taskDate.equals(LocalDate.now())) {
|
| 467 |
return responseSender.badRequest("Visits can only be assigned for today's date (" + LocalDate.now() + ")");
|
474 |
return responseSender.badRequest("Visits can only be assigned for today's date (" + LocalDate.now() + ")");
|
| 468 |
}
|
475 |
}
|
| 469 |
|
476 |
|
| - |
|
477 |
// Block when the executive has a beat scheduled today but hasn't filled its
|
| - |
|
478 |
// agenda yet (enforced here so it applies to every caller, not just the UI).
|
| - |
|
479 |
if (!agendaAllowsAssign(authUserId, taskDate)) {
|
| - |
|
480 |
return responseSender.badRequest(AGENDA_NOT_FILLED_MSG);
|
| - |
|
481 |
}
|
| - |
|
482 |
|
| 470 |
LocalDateTime now = LocalDateTime.now();
|
483 |
LocalDateTime now = LocalDateTime.now();
|
| 471 |
int createdCount = 0, updatedCount = 0;
|
484 |
int createdCount = 0, updatedCount = 0;
|
| 472 |
|
485 |
|
| 473 |
for (Map<String, Object> p : selected) {
|
486 |
for (Map<String, Object> p : selected) {
|
| 474 |
Integer fofoStoreId = ((Number) p.get("fofoStoreId")).intValue();
|
487 |
Integer fofoStoreId = ((Number) p.get("fofoStoreId")).intValue();
|
| Line 606... |
Line 619... |
| 606 |
result.put("status", true);
|
619 |
result.put("status", true);
|
| 607 |
result.put("instanceId", instanceId);
|
620 |
result.put("instanceId", instanceId);
|
| 608 |
return responseSender.ok(result);
|
621 |
return responseSender.ok(result);
|
| 609 |
}
|
622 |
}
|
| 610 |
|
623 |
|
| - |
|
624 |
// Warning shown (and enforced) when a manager tries to assign a visit before the
|
| - |
|
625 |
// executive has filled their agenda for the day.
|
| - |
|
626 |
static final String AGENDA_NOT_FILLED_MSG = "Agenda not filled yet you cant assign visit";
|
| - |
|
627 |
|
| - |
|
628 |
// True when the agenda rule permits a manager to assign a visit to `authUserId`
|
| - |
|
629 |
// on `date`: either the executive has no beat scheduled that day (nothing to
|
| - |
|
630 |
// fill), or at least one of their scheduled beats has its agenda_filled_timestamp
|
| - |
|
631 |
// set. ONLY a scheduled-but-unfilled agenda blocks assignment.
|
| - |
|
632 |
private boolean agendaAllowsAssign(int authUserId, LocalDate date) {
|
| - |
|
633 |
if (date == null) return true;
|
| - |
|
634 |
List<Integer> beatIds = beatPlanQueryService.getAllScheduledBeats(date, date).stream()
|
| - |
|
635 |
.filter(b -> b.getAuthUserId() == authUserId)
|
| - |
|
636 |
.map(com.spice.profitmandi.dao.model.BeatDayDetails::getBeatId)
|
| - |
|
637 |
.distinct().collect(Collectors.toList());
|
| - |
|
638 |
if (beatIds.isEmpty()) return true; // no beat scheduled → agenda rule doesn't apply
|
| - |
|
639 |
for (BeatSchedule bs : beatScheduleRepository.selectByBeatIds(beatIds)) {
|
| - |
|
640 |
if (bs.getStartDate() != null && bs.getStartDate().equals(date)
|
| - |
|
641 |
&& bs.getAgendaFilledTimestamp() != null) {
|
| - |
|
642 |
return true;
|
| - |
|
643 |
}
|
| - |
|
644 |
}
|
| - |
|
645 |
return false;
|
| - |
|
646 |
}
|
| - |
|
647 |
|
| 611 |
// ====================== DEFERRED PARTNERS ======================
|
648 |
// ====================== DEFERRED PARTNERS ======================
|
| 612 |
// Heads review partners that weren't visited on their planned day and act on
|
649 |
// Heads review partners that weren't visited on their planned day and act on
|
| 613 |
// them. The deferral lifecycle lives in user.beat_deferred_visit (separate
|
650 |
// them. The deferral lifecycle lives in user.beat_deferred_visit (separate
|
| 614 |
// from the raw location_tracking event log). Detection = explicit
|
651 |
// from the raw location_tracking event log). Detection = explicit
|
| 615 |
// (mark_type='DEFERRED') + derived (planned beat_route minus completed visits).
|
652 |
// (mark_type='DEFERRED') + derived (planned beat_route minus completed visits).
|
| Line 1036... |
Line 1073... |
| 1036 |
// Shared, idempotent lead_route write. Returns true when a new row is created,
|
1073 |
// Shared, idempotent lead_route write. Returns true when a new row is created,
|
| 1037 |
// false when the lead is already on that (beat, date). Best-effort nudges the
|
1074 |
// false when the lead is already on that (beat, date). Best-effort nudges the
|
| 1038 |
// lead forward to BEAT_PLANNED. Callers are responsible for the beat-runs-on-date
|
1075 |
// lead forward to BEAT_PLANNED. Callers are responsible for the beat-runs-on-date
|
| 1039 |
// and future-date guards. Mirrors deferredAssignToBeat's lead branch.
|
1076 |
// and future-date guards. Mirrors deferredAssignToBeat's lead branch.
|
| 1040 |
private boolean writeLeadRoute(int beatId, int leadId, LocalDate date, int approverAuthId) {
|
1077 |
private boolean writeLeadRoute(int beatId, int leadId, LocalDate date, int approverAuthId) {
|
| - |
|
1078 |
return writeLeadRoute(beatId, leadId, date, approverAuthId, 9999);
|
| - |
|
1079 |
}
|
| - |
|
1080 |
|
| - |
|
1081 |
// As above, but with an explicit route position — used when building a Lead beat
|
| - |
|
1082 |
// where the leads carry a real ordering (vs 9999 "append" for one-off attaches).
|
| - |
|
1083 |
private boolean writeLeadRoute(int beatId, int leadId, LocalDate date, int approverAuthId, int sequenceOrder) {
|
| 1041 |
boolean exists = leadRouteRepository.selectByBeatId(beatId).stream()
|
1084 |
boolean exists = leadRouteRepository.selectByBeatId(beatId).stream()
|
| 1042 |
.anyMatch(lr -> lr.getLeadId() == leadId
|
1085 |
.anyMatch(lr -> lr.getLeadId() == leadId
|
| 1043 |
&& date.equals(lr.getScheduleDate())
|
1086 |
&& date.equals(lr.getScheduleDate())
|
| 1044 |
&& !"CANCELLED".equals(lr.getStatus()));
|
1087 |
&& !"CANCELLED".equals(lr.getStatus()));
|
| 1045 |
if (exists) return false;
|
1088 |
if (exists) return false;
|
| Line 1047... |
Line 1090... |
| 1047 |
LocalDateTime now = LocalDateTime.now();
|
1090 |
LocalDateTime now = LocalDateTime.now();
|
| 1048 |
LeadRoute lr = new LeadRoute();
|
1091 |
LeadRoute lr = new LeadRoute();
|
| 1049 |
lr.setBeatId(beatId);
|
1092 |
lr.setBeatId(beatId);
|
| 1050 |
lr.setLeadId(leadId);
|
1093 |
lr.setLeadId(leadId);
|
| 1051 |
lr.setScheduleDate(date);
|
1094 |
lr.setScheduleDate(date);
|
| 1052 |
lr.setSequenceOrder(9999); // append; planner can reorder
|
1095 |
lr.setSequenceOrder(sequenceOrder);
|
| 1053 |
lr.setStatus("APPROVED");
|
1096 |
lr.setStatus("APPROVED");
|
| 1054 |
lr.setApprovedBy(approverAuthId);
|
1097 |
lr.setApprovedBy(approverAuthId);
|
| 1055 |
lr.setApprovedTimestamp(now);
|
1098 |
lr.setApprovedTimestamp(now);
|
| 1056 |
lr.setCreatedTimestamp(now);
|
1099 |
lr.setCreatedTimestamp(now);
|
| 1057 |
lr.setUpdatedTimestamp(now);
|
1100 |
lr.setUpdatedTimestamp(now);
|
| Line 2465... |
Line 2508... |
| 2465 |
// client so reports/dashboards don't have to recompute from lat/lng.
|
2508 |
// client so reports/dashboards don't have to recompute from lat/lng.
|
| 2466 |
List<Map<String, Object>> visits = (List<Map<String, Object>>) day.get("visits");
|
2509 |
List<Map<String, Object>> visits = (List<Map<String, Object>>) day.get("visits");
|
| 2467 |
if (visits != null) {
|
2510 |
if (visits != null) {
|
| 2468 |
for (int i = 0; i < visits.size(); i++) {
|
2511 |
for (int i = 0; i < visits.size(); i++) {
|
| 2469 |
Map<String, Object> visit = visits.get(i);
|
2512 |
Map<String, Object> visit = visits.get(i);
|
| - |
|
2513 |
// Lead stop → lead_route (separate table, keyed by scheduleDate).
|
| - |
|
2514 |
// planDate is guaranteed non-null for lead visits (validated above).
|
| - |
|
2515 |
if ("lead".equals(visit.get("type"))) {
|
| - |
|
2516 |
int leadId = ((Number) visit.get("id")).intValue();
|
| - |
|
2517 |
writeLeadRoute(beat.getId(), leadId, planDate, currentUser.getId(), i);
|
| - |
|
2518 |
continue;
|
| - |
|
2519 |
}
|
| 2470 |
BeatRoute route = new BeatRoute();
|
2520 |
BeatRoute route = new BeatRoute();
|
| 2471 |
route.setBeatId(beat.getId());
|
2521 |
route.setBeatId(beat.getId());
|
| 2472 |
route.setFofoId(((Number) visit.get("id")).intValue());
|
2522 |
route.setFofoId(((Number) visit.get("id")).intValue());
|
| 2473 |
route.setVisitType("office".equals(visit.get("type"))
|
2523 |
route.setVisitType("office".equals(visit.get("type"))
|
| 2474 |
? com.spice.profitmandi.dao.enumuration.dtr.BeatVisitType.OFFICE
|
2524 |
? com.spice.profitmandi.dao.enumuration.dtr.BeatVisitType.OFFICE
|