| Line 318... |
Line 318... |
| 318 |
List<Map<String, Object>> selected = (List<Map<String, Object>>) body.get("parties");
|
318 |
List<Map<String, Object>> selected = (List<Map<String, Object>>) body.get("parties");
|
| 319 |
if (authUserId == null || planDate == null || selected == null || selected.isEmpty()) {
|
319 |
if (authUserId == null || planDate == null || selected == null || selected.isEmpty()) {
|
| 320 |
return responseSender.badRequest("authUserId, planDate and parties are required");
|
320 |
return responseSender.badRequest("authUserId, planDate and parties are required");
|
| 321 |
}
|
321 |
}
|
| 322 |
|
322 |
|
| - |
|
323 |
// Defence-in-depth: Assign Visit is L2-and-above only (mirrors the hidden UI).
|
| - |
|
324 |
if (!canEditBeat(currentUser(request))) {
|
| - |
|
325 |
return responseSender.badRequest("Assigning a visit is restricted to L2 and above.");
|
| - |
|
326 |
}
|
| - |
|
327 |
|
| 323 |
AuthUser au = authRepository.selectById(authUserId);
|
328 |
AuthUser au = authRepository.selectById(authUserId);
|
| 324 |
if (au == null) return responseSender.badRequest("Auth user not found");
|
329 |
if (au == null) return responseSender.badRequest("Auth user not found");
|
| 325 |
|
330 |
|
| 326 |
// Map auth → dtr.users.id (this is the userId the v2 endpoint expects)
|
331 |
// Map auth → dtr.users.id (this is the userId the v2 endpoint expects)
|
| 327 |
com.spice.profitmandi.dao.entity.dtr.User dtrUser;
|
332 |
com.spice.profitmandi.dao.entity.dtr.User dtrUser;
|
| Line 826... |
Line 831... |
| 826 |
}
|
831 |
}
|
| 827 |
|
832 |
|
| 828 |
@GetMapping(value = "/beatPlanWindow")
|
833 |
@GetMapping(value = "/beatPlanWindow")
|
| 829 |
public String beatPlanWindow(HttpServletRequest request, Model model) throws ProfitMandiBusinessException {
|
834 |
public String beatPlanWindow(HttpServletRequest request, Model model) throws ProfitMandiBusinessException {
|
| 830 |
model.addAttribute("escalationTypes", visibleLevelsFor(request));
|
835 |
model.addAttribute("escalationTypes", visibleLevelsFor(request));
|
| - |
|
836 |
model.addAttribute("canScheduleToday", canScheduleToday(currentUser(request)));
|
| 831 |
return "beat-plan-window";
|
837 |
return "beat-plan-window";
|
| 832 |
}
|
838 |
}
|
| 833 |
|
839 |
|
| 834 |
// Helpers for XLSX bulk upload
|
840 |
// Helpers for XLSX bulk upload
|
| 835 |
private static String readCell(org.apache.poi.ss.usermodel.Cell cell) {
|
841 |
private static String readCell(org.apache.poi.ss.usermodel.Cell cell) {
|
| Line 1482... |
Line 1488... |
| 1482 |
@GetMapping(value = "/beatPlan/dayView")
|
1488 |
@GetMapping(value = "/beatPlan/dayView")
|
| 1483 |
public String beatPlanDayView(HttpServletRequest request, Model model) throws ProfitMandiBusinessException {
|
1489 |
public String beatPlanDayView(HttpServletRequest request, Model model) throws ProfitMandiBusinessException {
|
| 1484 |
model.addAttribute("escalationTypes", visibleLevelsFor(request));
|
1490 |
model.addAttribute("escalationTypes", visibleLevelsFor(request));
|
| 1485 |
// Matches /beatPlan/getAuthUsers and the Beat Report — default Sales category.
|
1491 |
// Matches /beatPlan/getAuthUsers and the Beat Report — default Sales category.
|
| 1486 |
model.addAttribute("categoryId", com.spice.profitmandi.common.model.ProfitMandiConstants.TICKET_CATEGORY_SALES);
|
1492 |
model.addAttribute("categoryId", com.spice.profitmandi.common.model.ProfitMandiConstants.TICKET_CATEGORY_SALES);
|
| - |
|
1493 |
model.addAttribute("canEditBeat", canEditBeat(currentUser(request)));
|
| 1487 |
return "beat-plan-day-view";
|
1494 |
return "beat-plan-day-view";
|
| 1488 |
}
|
1495 |
}
|
| 1489 |
|
1496 |
|
| 1490 |
// Returns visits for a beat.
|
1497 |
// Returns visits for a beat.
|
| 1491 |
// - Partner stops (beat_route) belong to the beat template — always returned.
|
1498 |
// - Partner stops (beat_route) belong to the beat template — always returned.
|
| Line 1728... |
Line 1735... |
| 1728 |
if (mine.isGreaterThanEqualTo(e) && !e.equals(mine)) below.add(e);
|
1735 |
if (mine.isGreaterThanEqualTo(e) && !e.equals(mine)) below.add(e);
|
| 1729 |
}
|
1736 |
}
|
| 1730 |
return below;
|
1737 |
return below;
|
| 1731 |
}
|
1738 |
}
|
| 1732 |
|
1739 |
|
| 1733 |
private List<EscalationType> visibleLevelsFor(HttpServletRequest request) throws ProfitMandiBusinessException {
|
1740 |
private AuthUser currentUser(HttpServletRequest request) throws ProfitMandiBusinessException {
|
| 1734 |
LoginDetails ld = cookiesProcessor.getCookiesObject(request);
|
1741 |
LoginDetails ld = cookiesProcessor.getCookiesObject(request);
|
| 1735 |
AuthUser me = (ld != null) ? authRepository.selectByEmailOrMobile(ld.getEmailId()) : null;
|
1742 |
return (ld != null) ? authRepository.selectByEmailOrMobile(ld.getEmailId()) : null;
|
| - |
|
1743 |
}
|
| - |
|
1744 |
|
| - |
|
1745 |
private List<EscalationType> visibleLevelsFor(HttpServletRequest request) throws ProfitMandiBusinessException {
|
| - |
|
1746 |
AuthUser me = currentUser(request);
|
| 1736 |
return me == null ? java.util.Collections.emptyList() : getVisibleEscalationLevels(me);
|
1747 |
return me == null ? java.util.Collections.emptyList() : getVisibleEscalationLevels(me);
|
| 1737 |
}
|
1748 |
}
|
| 1738 |
|
1749 |
|
| - |
|
1750 |
// Same-day scheduling is restricted to L4+ operators (or super-admin). Everyone
|
| - |
|
1751 |
// else can only schedule beats for future dates.
|
| - |
|
1752 |
private boolean canScheduleToday(AuthUser me) {
|
| - |
|
1753 |
if (me == null) return false;
|
| - |
|
1754 |
if (isSuperAdmin(me)) return true;
|
| - |
|
1755 |
EscalationType lvl = getHighestEscalation(me.getId());
|
| - |
|
1756 |
return lvl != null && lvl.isGreaterThanEqualTo(EscalationType.L4);
|
| - |
|
1757 |
}
|
| - |
|
1758 |
|
| - |
|
1759 |
// Editing a beat / assigning a visit from the day-view is restricted to L2 and
|
| - |
|
1760 |
// above (or super-admin). L1 executives can view but not edit.
|
| - |
|
1761 |
private boolean canEditBeat(AuthUser me) {
|
| - |
|
1762 |
if (me == null) return false;
|
| - |
|
1763 |
if (isSuperAdmin(me)) return true;
|
| - |
|
1764 |
EscalationType lvl = getHighestEscalation(me.getId());
|
| - |
|
1765 |
return lvl != null && lvl.isGreaterThanEqualTo(EscalationType.L2);
|
| - |
|
1766 |
}
|
| - |
|
1767 |
|
| 1739 |
// Shared permission check for base-location admin actions: Sales L3+ OR super-admin.
|
1768 |
// Shared permission check for base-location admin actions: Sales L3+ OR super-admin.
|
| 1740 |
private boolean isBaseLocationManager(AuthUser me) {
|
1769 |
private boolean isBaseLocationManager(AuthUser me) {
|
| 1741 |
if (isSuperAdmin(me)) return true;
|
1770 |
if (isSuperAdmin(me)) return true;
|
| 1742 |
return csService.getAuthUserIds(
|
1771 |
return csService.getAuthUserIds(
|
| 1743 |
com.spice.profitmandi.common.model.ProfitMandiConstants.TICKET_CATEGORY_SALES,
|
1772 |
com.spice.profitmandi.common.model.ProfitMandiConstants.TICKET_CATEGORY_SALES,
|
| Line 2676... |
Line 2705... |
| 2676 |
try {
|
2705 |
try {
|
| 2677 |
requested.add(LocalDate.parse(s));
|
2706 |
requested.add(LocalDate.parse(s));
|
| 2678 |
} catch (Exception ignored) {
|
2707 |
} catch (Exception ignored) {
|
| 2679 |
}
|
2708 |
}
|
| 2680 |
}
|
2709 |
}
|
| - |
|
2710 |
// Same-day scheduling is restricted to L4+ operators.
|
| - |
|
2711 |
if (requested.contains(LocalDate.now()) && !canScheduleToday(currentUser(request))) {
|
| - |
|
2712 |
return responseSender.badRequest("Scheduling a beat for today is restricted to L4 and above.");
|
| - |
|
2713 |
}
|
| 2681 |
Map<String, Object> conflict = findScheduleConflict(beat.getAuthUserId(), requested, beatId);
|
2714 |
Map<String, Object> conflict = findScheduleConflict(beat.getAuthUserId(), requested, beatId);
|
| 2682 |
if (conflict != null) return responseSender.badRequest(scheduleConflictMessage(conflict));
|
2715 |
if (conflict != null) return responseSender.badRequest(scheduleConflictMessage(conflict));
|
| 2683 |
|
2716 |
|
| 2684 |
// Delete old schedules and create new
|
2717 |
// Delete old schedules and create new
|
| 2685 |
beatScheduleRepository.deleteByBeatId(beatId);
|
2718 |
beatScheduleRepository.deleteByBeatId(beatId);
|