| Line 600... |
Line 600... |
| 600 |
try {
|
600 |
try {
|
| 601 |
toDate = LocalDate.parse(toDateStr);
|
601 |
toDate = LocalDate.parse(toDateStr);
|
| 602 |
} catch (Exception e) {
|
602 |
} catch (Exception e) {
|
| 603 |
return responseSender.badRequest("Invalid toDate (yyyy-MM-dd)");
|
603 |
return responseSender.badRequest("Invalid toDate (yyyy-MM-dd)");
|
| 604 |
}
|
604 |
}
|
| - |
|
605 |
// Reschedule must land on a STRICTLY future date — today is already in progress.
|
| - |
|
606 |
if (!toDate.isAfter(LocalDate.now()))
|
| 605 |
if (toDate.isBefore(LocalDate.now())) return responseSender.badRequest("Reschedule date cannot be in the past");
|
607 |
return responseSender.badRequest("Reschedule date must be in the future (today is already in progress).");
|
| 606 |
|
608 |
|
| 607 |
if ("reschedule_beat".equalsIgnoreCase(action)) {
|
609 |
if ("reschedule_beat".equalsIgnoreCase(action)) {
|
| 608 |
boolean hasBeat = beatRepository.selectActiveByAuthUserId(d.getAuthUserId()).stream()
|
610 |
boolean hasBeat = beatRepository.selectActiveByAuthUserId(d.getAuthUserId()).stream()
|
| 609 |
.flatMap(b -> beatScheduleRepository.selectByBeatId(b.getId()).stream())
|
611 |
.flatMap(b -> beatScheduleRepository.selectByBeatId(b.getId()).stream())
|
| 610 |
.anyMatch(s -> s.getStartDate() != null && s.getStartDate().equals(toDate));
|
612 |
.anyMatch(s -> s.getStartDate() != null && s.getStartDate().equals(toDate));
|
| Line 705... |
Line 707... |
| 705 |
try {
|
707 |
try {
|
| 706 |
date = LocalDate.parse(dateStr);
|
708 |
date = LocalDate.parse(dateStr);
|
| 707 |
} catch (Exception e) {
|
709 |
} catch (Exception e) {
|
| 708 |
return responseSender.badRequest("Invalid date (yyyy-MM-dd)");
|
710 |
return responseSender.badRequest("Invalid date (yyyy-MM-dd)");
|
| 709 |
}
|
711 |
}
|
| - |
|
712 |
// Drop-onto-beat must land on a STRICTLY future date — today is already in progress.
|
| - |
|
713 |
if (!date.isAfter(LocalDate.now()))
|
| 710 |
if (date.isBefore(LocalDate.now())) return responseSender.badRequest("Pick an upcoming date");
|
714 |
return responseSender.badRequest("Pick a future date — today is already in progress.");
|
| 711 |
|
715 |
|
| 712 |
BeatDeferredVisit d = beatDeferredVisitRepository.selectById(deferredId);
|
716 |
BeatDeferredVisit d = beatDeferredVisitRepository.selectById(deferredId);
|
| 713 |
if (d == null) return responseSender.badRequest("Deferred record not found");
|
717 |
if (d == null) return responseSender.badRequest("Deferred record not found");
|
| 714 |
|
718 |
|
| 715 |
// A deferral can only move FORWARD — never onto the day it was deferred or earlier.
|
719 |
// A deferral can only move FORWARD — never onto the day it was deferred or earlier.
|
| Line 1000... |
Line 1004... |
| 1000 |
@RequestParam String planData) throws Exception {
|
1004 |
@RequestParam String planData) throws Exception {
|
| 1001 |
|
1005 |
|
| 1002 |
Beat beat = beatRepository.selectById(beatId);
|
1006 |
Beat beat = beatRepository.selectById(beatId);
|
| 1003 |
if (beat == null) return responseSender.badRequest("Beat not found");
|
1007 |
if (beat == null) return responseSender.badRequest("Beat not found");
|
| 1004 |
|
1008 |
|
| - |
|
1009 |
// Refuse edits while the beat is "live" — i.e., a schedule row covers today.
|
| - |
|
1010 |
// The salesperson is already on-route; mutating the plan mid-day would
|
| - |
|
1011 |
// break their location_tracking timeline. Edits resume tomorrow.
|
| - |
|
1012 |
LocalDate today = LocalDate.now();
|
| - |
|
1013 |
boolean runningToday = beatScheduleRepository.selectByBeatId(beatId).stream()
|
| - |
|
1014 |
.anyMatch(s -> s.getStartDate() != null && s.getStartDate().equals(today));
|
| - |
|
1015 |
if (runningToday) {
|
| - |
|
1016 |
return responseSender.badRequest(
|
| - |
|
1017 |
"This beat is scheduled to run today — editing is locked until tomorrow. "
|
| - |
|
1018 |
+ "Use the Deferred panel for today's adjustments.");
|
| - |
|
1019 |
}
|
| - |
|
1020 |
|
| 1005 |
Gson gson = new Gson();
|
1021 |
Gson gson = new Gson();
|
| 1006 |
Type type = new TypeToken<Map<String, Object>>() {
|
1022 |
Type type = new TypeToken<Map<String, Object>>() {
|
| 1007 |
}.getType();
|
1023 |
}.getType();
|
| 1008 |
Map<String, Object> plan = gson.fromJson(planData, type);
|
1024 |
Map<String, Object> plan = gson.fromJson(planData, type);
|
| 1009 |
|
1025 |
|
| Line 1259... |
Line 1275... |
| 1259 |
model.addAttribute("escalationTypes", visibleLevelsFor(request));
|
1275 |
model.addAttribute("escalationTypes", visibleLevelsFor(request));
|
| 1260 |
return "beat-plan-base-location";
|
1276 |
return "beat-plan-base-location";
|
| 1261 |
}
|
1277 |
}
|
| 1262 |
|
1278 |
|
| 1263 |
// Tabular JSON: one row per (beat, scheduled date) in [startDate, endDate].
|
1279 |
// Tabular JSON: one row per (beat, scheduled date) in [startDate, endDate].
|
| - |
|
1280 |
// Hierarchy-scoped: a manager sees only their downline + self; super-admins see all.
|
| - |
|
1281 |
// Optional categoryId + escalationType further narrow the listing to users at
|
| - |
|
1282 |
// a specific level (e.g. all Sales L1 in scope).
|
| 1264 |
@GetMapping(value = "/beatPlan/scheduledList")
|
1283 |
@GetMapping(value = "/beatPlan/scheduledList")
|
| 1265 |
public ResponseEntity<?> scheduledList(
|
1284 |
public ResponseEntity<?> scheduledList(
|
| - |
|
1285 |
HttpServletRequest request,
|
| 1266 |
@RequestParam(required = false) String startDate,
|
1286 |
@RequestParam(required = false) String startDate,
|
| 1267 |
@RequestParam(required = false) String endDate) {
|
1287 |
@RequestParam(required = false) String endDate,
|
| - |
|
1288 |
@RequestParam(required = false) Integer categoryId,
|
| - |
|
1289 |
@RequestParam(required = false) com.spice.profitmandi.dao.enumuration.cs.EscalationType escalationType) throws ProfitMandiBusinessException {
|
| 1268 |
|
1290 |
|
| 1269 |
LocalDate start, end;
|
1291 |
LocalDate start, end;
|
| 1270 |
try {
|
1292 |
try {
|
| 1271 |
start = (startDate == null || startDate.isEmpty()) ? LocalDate.now() : LocalDate.parse(startDate);
|
1293 |
start = (startDate == null || startDate.isEmpty()) ? LocalDate.now() : LocalDate.parse(startDate);
|
| 1272 |
end = (endDate == null || endDate.isEmpty()) ? start.plusDays(7) : LocalDate.parse(endDate);
|
1294 |
end = (endDate == null || endDate.isEmpty()) ? start.plusDays(7) : LocalDate.parse(endDate);
|
| 1273 |
} catch (Exception e) {
|
1295 |
} catch (Exception e) {
|
| 1274 |
return responseSender.badRequest("Invalid date — expected yyyy-MM-dd");
|
1296 |
return responseSender.badRequest("Invalid date — expected yyyy-MM-dd");
|
| 1275 |
}
|
1297 |
}
|
| 1276 |
|
1298 |
|
| - |
|
1299 |
// ---- Scope: which auth users does the caller get to see ----
|
| - |
|
1300 |
LoginDetails ld = cookiesProcessor.getCookiesObject(request);
|
| - |
|
1301 |
AuthUser me = (ld != null) ? authRepository.selectByEmailOrMobile(ld.getEmailId()) : null;
|
| - |
|
1302 |
Set<Integer> visible = null;
|
| - |
|
1303 |
if (me != null && !isSuperAdmin(me)) {
|
| - |
|
1304 |
visible = new HashSet<>(authService.getAllReportees(me.getId()));
|
| - |
|
1305 |
visible.add(me.getId());
|
| - |
|
1306 |
}
|
| - |
|
1307 |
// If a (category, level) was picked, further restrict to that population.
|
| - |
|
1308 |
Set<Integer> levelFilter = null;
|
| - |
|
1309 |
if (categoryId != null && escalationType != null) {
|
| - |
|
1310 |
levelFilter = csService.getAuthUserByCategoryId(categoryId, escalationType).stream()
|
| - |
|
1311 |
.filter(AuthUser::getActive)
|
| - |
|
1312 |
.map(AuthUser::getId)
|
| - |
|
1313 |
.collect(java.util.stream.Collectors.toSet());
|
| - |
|
1314 |
}
|
| - |
|
1315 |
|
| 1277 |
List<com.spice.profitmandi.dao.model.BeatDayDetails> beats =
|
1316 |
List<com.spice.profitmandi.dao.model.BeatDayDetails> beats =
|
| 1278 |
beatPlanQueryService.getAllScheduledBeats(start, end);
|
1317 |
beatPlanQueryService.getAllScheduledBeats(start, end);
|
| 1279 |
|
1318 |
|
| - |
|
1319 |
final Set<Integer> visibleF = visible;
|
| - |
|
1320 |
final Set<Integer> levelF = levelFilter;
|
| - |
|
1321 |
beats = beats.stream()
|
| - |
|
1322 |
.filter(b -> visibleF == null || visibleF.contains(b.getAuthUserId()))
|
| - |
|
1323 |
.filter(b -> levelF == null || levelF.contains(b.getAuthUserId()))
|
| - |
|
1324 |
.collect(java.util.stream.Collectors.toList());
|
| - |
|
1325 |
|
| 1280 |
// Resolve user names in bulk
|
1326 |
// Resolve user names in bulk
|
| 1281 |
Set<Integer> userIds = beats.stream()
|
1327 |
Set<Integer> userIds = beats.stream()
|
| 1282 |
.map(com.spice.profitmandi.dao.model.BeatDayDetails::getAuthUserId)
|
1328 |
.map(com.spice.profitmandi.dao.model.BeatDayDetails::getAuthUserId)
|
| 1283 |
.collect(java.util.stream.Collectors.toSet());
|
1329 |
.collect(java.util.stream.Collectors.toSet());
|
| 1284 |
Map<Integer, AuthUser> userMap = new HashMap<>();
|
1330 |
Map<Integer, AuthUser> userMap = new HashMap<>();
|
| Line 1434... |
Line 1480... |
| 1434 |
// scheduled in a date range across all users. Each row has a View button that
|
1480 |
// scheduled in a date range across all users. Each row has a View button that
|
| 1435 |
// opens that user's calendar in a modal.
|
1481 |
// opens that user's calendar in a modal.
|
| 1436 |
@GetMapping(value = "/beatPlan/dayView")
|
1482 |
@GetMapping(value = "/beatPlan/dayView")
|
| 1437 |
public String beatPlanDayView(HttpServletRequest request, Model model) throws ProfitMandiBusinessException {
|
1483 |
public String beatPlanDayView(HttpServletRequest request, Model model) throws ProfitMandiBusinessException {
|
| 1438 |
model.addAttribute("escalationTypes", visibleLevelsFor(request));
|
1484 |
model.addAttribute("escalationTypes", visibleLevelsFor(request));
|
| - |
|
1485 |
// Matches /beatPlan/getAuthUsers and the Beat Report — default Sales category.
|
| - |
|
1486 |
model.addAttribute("categoryId", com.spice.profitmandi.common.model.ProfitMandiConstants.TICKET_CATEGORY_SALES);
|
| 1439 |
return "beat-plan-day-view";
|
1487 |
return "beat-plan-day-view";
|
| 1440 |
}
|
1488 |
}
|
| 1441 |
|
1489 |
|
| 1442 |
// Returns visits for a beat.
|
1490 |
// Returns visits for a beat.
|
| 1443 |
// - Partner stops (beat_route) belong to the beat template — always returned.
|
1491 |
// - Partner stops (beat_route) belong to the beat template — always returned.
|