| Line 281... |
Line 281... |
| 281 |
EscalationType[] escalationTypes = EscalationType.values();
|
281 |
EscalationType[] escalationTypes = EscalationType.values();
|
| 282 |
model.addAttribute("escalationTypes", escalationTypes);
|
282 |
model.addAttribute("escalationTypes", escalationTypes);
|
| 283 |
return "beat-plan-base-location";
|
283 |
return "beat-plan-base-location";
|
| 284 |
}
|
284 |
}
|
| 285 |
|
285 |
|
| 286 |
@PostMapping(value = "/beatPlan/updateBaseLocation")
|
286 |
// Helpers for XLSX bulk upload
|
| 287 |
public ResponseEntity<?> updateBaseLocation(
|
287 |
private static String readCell(org.apache.poi.ss.usermodel.Cell cell) {
|
| 288 |
HttpServletRequest request,
|
288 |
if (cell == null) return null;
|
| 289 |
@RequestParam int authUserId,
|
289 |
switch (cell.getCellType()) {
|
| 290 |
@RequestParam String locationName,
|
290 |
case org.apache.poi.ss.usermodel.Cell.CELL_TYPE_STRING:
|
| 291 |
@RequestParam String latitude,
|
291 |
return cell.getStringCellValue();
|
| 292 |
@RequestParam String longitude,
|
- |
|
| 293 |
@RequestParam(required = false) String address) throws Exception {
|
292 |
case org.apache.poi.ss.usermodel.Cell.CELL_TYPE_NUMERIC:
|
| 294 |
|
- |
|
| 295 |
LoginDetails ld = cookiesProcessor.getCookiesObject(request);
|
293 |
if (org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(cell)) {
|
| 296 |
AuthUser me = authRepository.selectByEmailOrMobile(ld.getEmailId());
|
294 |
return cell.getDateCellValue().toInstant()
|
| 297 |
if (me == null) return responseSender.badRequest("Not logged in");
|
295 |
.atZone(java.time.ZoneId.systemDefault()).toLocalDate().toString();
|
| 298 |
|
296 |
}
|
| 299 |
// Permission gate: only Sales L3 and above
|
297 |
double n = cell.getNumericCellValue();
|
| 300 |
boolean isSalesL3Plus = csService.getAuthUserIds(
|
298 |
return (n == Math.floor(n)) ? String.valueOf((long) n) : String.valueOf(n);
|
| 301 |
com.spice.profitmandi.common.model.ProfitMandiConstants.TICKET_CATEGORY_SALES,
|
299 |
case org.apache.poi.ss.usermodel.Cell.CELL_TYPE_BOOLEAN:
|
| 302 |
Arrays.asList(EscalationType.L3, EscalationType.L4))
|
300 |
return String.valueOf(cell.getBooleanCellValue());
|
| 303 |
.stream().anyMatch(u -> u.getId() == me.getId());
|
301 |
case org.apache.poi.ss.usermodel.Cell.CELL_TYPE_FORMULA:
|
| 304 |
if (!isSalesL3Plus) {
|
302 |
return cell.getCellFormula();
|
| - |
|
303 |
default:
|
| 305 |
return responseSender.badRequest("Only Sales L3 and above can update base location");
|
304 |
return null;
|
| 306 |
}
|
305 |
}
|
| 307 |
|
- |
|
| 308 |
AuthUserLocation loc = new AuthUserLocation();
|
- |
|
| 309 |
loc.setAuthUserId(authUserId);
|
- |
|
| 310 |
loc.setLocationType("BASE");
|
- |
|
| 311 |
loc.setLocationName(locationName);
|
- |
|
| 312 |
loc.setLatitude(latitude);
|
- |
|
| 313 |
loc.setLongitude(longitude);
|
- |
|
| 314 |
loc.setAddress(address);
|
- |
|
| 315 |
loc.setCreatedTimestamp(LocalDateTime.now());
|
- |
|
| 316 |
authUserLocationRepository.persist(loc);
|
- |
|
| 317 |
|
- |
|
| 318 |
Map<String, Object> result = new HashMap<>();
|
- |
|
| 319 |
result.put("status", true);
|
- |
|
| 320 |
result.put("id", loc.getId());
|
- |
|
| 321 |
result.put("message", "Base location updated");
|
- |
|
| 322 |
return responseSender.ok(result);
|
- |
|
| 323 |
}
|
306 |
}
|
| 324 |
|
307 |
|
| 325 |
// ====================== ONE-TIME LAT/LNG MIGRATION ======================
|
308 |
// ====================== ONE-TIME LAT/LNG MIGRATION ======================
|
| 326 |
// For each active fofo_store, compare its stored lat/lng with the geocoded
|
309 |
// For each active fofo_store, compare its stored lat/lng with the geocoded
|
| 327 |
// address lat/lng (cached in Redis). If the gap is > thresholdKm (default 5)
|
310 |
// address lat/lng (cached in Redis). If the gap is > thresholdKm (default 5)
|
| Line 1011... |
Line 994... |
| 1011 |
@GetMapping(value = "/beatPlan/bulkUpload")
|
994 |
@GetMapping(value = "/beatPlan/bulkUpload")
|
| 1012 |
public String bulkUploadPage(HttpServletRequest request, Model model) {
|
995 |
public String bulkUploadPage(HttpServletRequest request, Model model) {
|
| 1013 |
return "beat-plan-bulk";
|
996 |
return "beat-plan-bulk";
|
| 1014 |
}
|
997 |
}
|
| 1015 |
|
998 |
|
| - |
|
999 |
@PostMapping(value = "/beatPlan/updateBaseLocation")
|
| - |
|
1000 |
public ResponseEntity<?> updateBaseLocation(
|
| - |
|
1001 |
HttpServletRequest request,
|
| - |
|
1002 |
@RequestParam int authUserId,
|
| - |
|
1003 |
@RequestParam String locationName,
|
| - |
|
1004 |
@RequestParam String latitude,
|
| - |
|
1005 |
@RequestParam String longitude,
|
| - |
|
1006 |
@RequestParam(required = false) String address) throws Exception {
|
| - |
|
1007 |
|
| - |
|
1008 |
LoginDetails ld = cookiesProcessor.getCookiesObject(request);
|
| - |
|
1009 |
AuthUser me = authRepository.selectByEmailOrMobile(ld.getEmailId());
|
| - |
|
1010 |
if (me == null) return responseSender.unauthorized("Not logged in");
|
| - |
|
1011 |
|
| - |
|
1012 |
// Permission gate: Sales L3+ OR explicit allow-list
|
| - |
|
1013 |
// Allow-list (lowercase emails) — people outside Sales L3/L4 who still need
|
| - |
|
1014 |
// to manage base locations (e.g., Tarun Verma).
|
| - |
|
1015 |
Set<String> baseLocationAllowList = new HashSet<>(Arrays.asList(
|
| - |
|
1016 |
"tarun.verma@smartdukaan.com"
|
| - |
|
1017 |
));
|
| - |
|
1018 |
|
| - |
|
1019 |
String myEmail = me.getEmailId() != null ? me.getEmailId().toLowerCase() : "";
|
| - |
|
1020 |
boolean isWhitelisted = baseLocationAllowList.contains(myEmail);
|
| - |
|
1021 |
|
| - |
|
1022 |
boolean isSalesL3Plus = csService.getAuthUserIds(
|
| - |
|
1023 |
com.spice.profitmandi.common.model.ProfitMandiConstants.TICKET_CATEGORY_SALES,
|
| - |
|
1024 |
Arrays.asList(EscalationType.L3, EscalationType.L4))
|
| - |
|
1025 |
.stream().anyMatch(u -> u.getId() == me.getId());
|
| - |
|
1026 |
|
| - |
|
1027 |
if (!isSalesL3Plus && !isWhitelisted) {
|
| - |
|
1028 |
return responseSender.forbidden("You are not authorized for this action. Only Sales L3 and above can update base location.");
|
| - |
|
1029 |
}
|
| - |
|
1030 |
|
| - |
|
1031 |
AuthUserLocation loc = new AuthUserLocation();
|
| - |
|
1032 |
loc.setAuthUserId(authUserId);
|
| - |
|
1033 |
loc.setLocationType("BASE");
|
| - |
|
1034 |
loc.setLocationName(locationName);
|
| - |
|
1035 |
loc.setLatitude(latitude);
|
| - |
|
1036 |
loc.setLongitude(longitude);
|
| - |
|
1037 |
loc.setAddress(address);
|
| - |
|
1038 |
loc.setCreatedTimestamp(LocalDateTime.now());
|
| - |
|
1039 |
authUserLocationRepository.persist(loc);
|
| - |
|
1040 |
|
| - |
|
1041 |
Map<String, Object> result = new HashMap<>();
|
| - |
|
1042 |
result.put("status", true);
|
| - |
|
1043 |
result.put("id", loc.getId());
|
| - |
|
1044 |
result.put("message", "Base location updated");
|
| - |
|
1045 |
return responseSender.ok(result);
|
| - |
|
1046 |
}
|
| - |
|
1047 |
|
| 1016 |
@GetMapping(value = "/beatPlan/downloadTemplate")
|
1048 |
@GetMapping(value = "/beatPlan/downloadTemplate")
|
| 1017 |
public ResponseEntity<?> downloadTemplate() {
|
1049 |
public ResponseEntity<?> downloadTemplate() throws java.io.IOException {
|
| - |
|
1050 |
org.apache.poi.xssf.usermodel.XSSFWorkbook wb = new org.apache.poi.xssf.usermodel.XSSFWorkbook();
|
| - |
|
1051 |
org.apache.poi.xssf.usermodel.XSSFSheet sheet = wb.createSheet("beat-plan");
|
| - |
|
1052 |
|
| 1018 |
String csv = "beat_name,auth_user_id,start_date,day_number,partner_codes\n";
|
1053 |
String[] cols = {"beat_name", "auth_user_id", "start_date", "day_number", "sequence_order", "partner_code"};
|
| - |
|
1054 |
|
| - |
|
1055 |
// Header style
|
| - |
|
1056 |
org.apache.poi.xssf.usermodel.XSSFCellStyle headerStyle = wb.createCellStyle();
|
| - |
|
1057 |
org.apache.poi.xssf.usermodel.XSSFFont headerFont = wb.createFont();
|
| - |
|
1058 |
headerFont.setBold(true);
|
| - |
|
1059 |
headerStyle.setFont(headerFont);
|
| - |
|
1060 |
headerStyle.setFillForegroundColor(new org.apache.poi.xssf.usermodel.XSSFColor(new java.awt.Color(230, 230, 230)));
|
| - |
|
1061 |
headerStyle.setFillPattern(org.apache.poi.ss.usermodel.FillPatternType.SOLID_FOREGROUND);
|
| - |
|
1062 |
|
| - |
|
1063 |
org.apache.poi.xssf.usermodel.XSSFRow header = sheet.createRow(0);
|
| - |
|
1064 |
for (int i = 0; i < cols.length; i++) {
|
| - |
|
1065 |
org.apache.poi.xssf.usermodel.XSSFCell c = header.createCell(i);
|
| - |
|
1066 |
c.setCellValue(cols[i]);
|
| - |
|
1067 |
c.setCellStyle(headerStyle);
|
| - |
|
1068 |
}
|
| - |
|
1069 |
|
| - |
|
1070 |
// Example rows — one partner per row. Inheritable columns blank after first row of a beat.
|
| - |
|
1071 |
Object[][] sample = {
|
| 1019 |
csv += "Jaipur East Route,280,2026-06-02,1,\"RJKAI1478,RJBUN1449,RJDEG1443\"\n";
|
1072 |
{"Jaipur East Route", "280", "2026-06-02", "1", "1", "RJKAI1478"},
|
| - |
|
1073 |
{"", "", "", "1", "2", "RJBUN1449"},
|
| - |
|
1074 |
{"", "", "", "1", "3", "RJDEG1443"},
|
| - |
|
1075 |
{"", "", "", "2", "1", "RJALR1362"},
|
| 1020 |
csv += ",280,,2,\"RJALR1362,RJBTR1388\"\n";
|
1076 |
{"", "", "", "2", "2", "RJBTR1388"},
|
| - |
|
1077 |
{"", "", "", "3", "1", "RJRSD1518"},
|
| 1021 |
csv += ",280,,3,\"RJRSD1518,RJSML356\"\n";
|
1078 |
{"", "", "", "3", "2", "RJSML356"},
|
| 1022 |
csv += "Agra Circuit,145,2026-06-05,1,\"UPAGR101,UPAGR102\"\n";
|
1079 |
{"Agra Circuit", "145", "2026-06-05", "1", "1", "UPAGR101"},
|
| - |
|
1080 |
{"", "", "", "1", "2", "UPAGR102"},
|
| - |
|
1081 |
};
|
| - |
|
1082 |
for (int r = 0; r < sample.length; r++) {
|
| - |
|
1083 |
org.apache.poi.xssf.usermodel.XSSFRow row = sheet.createRow(r + 1);
|
| - |
|
1084 |
for (int c = 0; c < cols.length; c++) {
|
| - |
|
1085 |
row.createCell(c).setCellValue(sample[r][c].toString());
|
| - |
|
1086 |
}
|
| - |
|
1087 |
}
|
| - |
|
1088 |
for (int i = 0; i < cols.length; i++) sheet.autoSizeColumn(i);
|
| - |
|
1089 |
|
| - |
|
1090 |
java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
|
| - |
|
1091 |
wb.write(out);
|
| - |
|
1092 |
wb.close();
|
| 1023 |
|
1093 |
|
| 1024 |
org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders();
|
1094 |
org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders();
|
| 1025 |
headers.add("Content-Disposition", "attachment; filename=beat_plan_template.csv");
|
1095 |
headers.add("Content-Disposition", "attachment; filename=beat_plan_template.xlsx");
|
| 1026 |
headers.add("Content-Type", "text/csv");
|
1096 |
headers.add("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
| 1027 |
return new ResponseEntity<>(csv, headers, org.springframework.http.HttpStatus.OK);
|
1097 |
return new ResponseEntity<>(out.toByteArray(), headers, org.springframework.http.HttpStatus.OK);
|
| 1028 |
}
|
1098 |
}
|
| 1029 |
|
1099 |
|
| 1030 |
@PostMapping(value = "/beatPlan/bulkUploadProcess")
|
1100 |
@PostMapping(value = "/beatPlan/bulkUploadProcess")
|
| 1031 |
public ResponseEntity<?> bulkUploadProcess(
|
1101 |
public ResponseEntity<?> bulkUploadProcess(
|
| 1032 |
HttpServletRequest request,
|
1102 |
HttpServletRequest request,
|
| Line 1034... |
Line 1104... |
| 1034 |
@RequestParam(value = "includeSundays", defaultValue = "false") boolean includeSundays) throws Exception {
|
1104 |
@RequestParam(value = "includeSundays", defaultValue = "false") boolean includeSundays) throws Exception {
|
| 1035 |
|
1105 |
|
| 1036 |
LoginDetails loginDetails = cookiesProcessor.getCookiesObject(request);
|
1106 |
LoginDetails loginDetails = cookiesProcessor.getCookiesObject(request);
|
| 1037 |
AuthUser currentUser = authRepository.selectByEmailOrMobile(loginDetails.getEmailId());
|
1107 |
AuthUser currentUser = authRepository.selectByEmailOrMobile(loginDetails.getEmailId());
|
| 1038 |
|
1108 |
|
| 1039 |
java.io.Reader reader = new java.io.InputStreamReader(file.getInputStream());
|
1109 |
// Read .xlsx — one partner per row. beat_name / auth_user_id / start_date
|
| 1040 |
org.apache.commons.csv.CSVParser parser = new org.apache.commons.csv.CSVParser(reader,
|
1110 |
// appear ONLY on the first row of a beat; subsequent rows inherit them.
|
| 1041 |
org.apache.commons.csv.CSVFormat.DEFAULT.withFirstRecordAsHeader().withTrim());
|
1111 |
org.apache.poi.ss.usermodel.Workbook workbook =
|
| 1042 |
List<org.apache.commons.csv.CSVRecord> allRecords = parser.getRecords();
|
1112 |
new org.apache.poi.xssf.usermodel.XSSFWorkbook(file.getInputStream());
|
| 1043 |
parser.close();
|
1113 |
org.apache.poi.ss.usermodel.Sheet sheet = workbook.getSheetAt(0);
|
| 1044 |
|
1114 |
|
| 1045 |
Map<String, String> lastBeatNameByUser = new HashMap<>();
|
1115 |
// Header → column index map
|
| 1046 |
Map<String, List<org.apache.commons.csv.CSVRecord>> beatGroups = new LinkedHashMap<>();
|
1116 |
org.apache.poi.ss.usermodel.Row headerRow = sheet.getRow(0);
|
| - |
|
1117 |
if (headerRow == null) {
|
| - |
|
1118 |
workbook.close();
|
| 1047 |
Map<Long, String> resolvedBeatNames = new HashMap<>();
|
1119 |
return responseSender.badRequest("Empty file");
|
| 1048 |
|
1120 |
}
|
| - |
|
1121 |
Map<String, Integer> colIdx = new HashMap<>();
|
| 1049 |
for (org.apache.commons.csv.CSVRecord record : allRecords) {
|
1122 |
for (int i = 0; i < headerRow.getLastCellNum(); i++) {
|
| 1050 |
String authId = record.get("auth_user_id").trim();
|
1123 |
String h = readCell(headerRow.getCell(i));
|
| 1051 |
String rawName = record.get("beat_name").trim().replaceAll("\\s+", " ");
|
1124 |
if (h != null) colIdx.put(h.trim().toLowerCase(), i);
|
| - |
|
1125 |
}
|
| 1052 |
if (rawName.isEmpty()) rawName = lastBeatNameByUser.getOrDefault(authId, "Beat");
|
1126 |
for (String required : new String[]{"beat_name", "auth_user_id", "day_number", "partner_code"}) {
|
| 1053 |
else lastBeatNameByUser.put(authId, rawName);
|
1127 |
if (!colIdx.containsKey(required)) {
|
| 1054 |
resolvedBeatNames.put(record.getRecordNumber(), rawName);
|
1128 |
workbook.close();
|
| 1055 |
beatGroups.computeIfAbsent(rawName + "|" + authId, k -> new ArrayList<>()).add(record);
|
1129 |
return responseSender.badRequest("Missing required column: " + required);
|
| - |
|
1130 |
}
|
| 1056 |
}
|
1131 |
}
|
| 1057 |
|
1132 |
|
| - |
|
1133 |
// Walk rows, group partners by (beat_name + auth_user_id) → day_number → sequence_order
|
| - |
|
1134 |
Map<String, BulkBeatGroup> beatGroups = new LinkedHashMap<>();
|
| - |
|
1135 |
String currentKey = null;
|
| - |
|
1136 |
String currentBeatName = null;
|
| - |
|
1137 |
String currentAuthId = null;
|
| - |
|
1138 |
String currentStartDate = null;
|
| - |
|
1139 |
|
| - |
|
1140 |
for (int r = 1; r <= sheet.getLastRowNum(); r++) {
|
| - |
|
1141 |
org.apache.poi.ss.usermodel.Row row = sheet.getRow(r);
|
| - |
|
1142 |
if (row == null) continue;
|
| - |
|
1143 |
|
| - |
|
1144 |
String beatName = readCell(row.getCell(colIdx.get("beat_name")));
|
| - |
|
1145 |
String authId = readCell(row.getCell(colIdx.get("auth_user_id")));
|
| - |
|
1146 |
String startDate = colIdx.containsKey("start_date") ? readCell(row.getCell(colIdx.get("start_date"))) : null;
|
| - |
|
1147 |
String dayNumber = readCell(row.getCell(colIdx.get("day_number")));
|
| - |
|
1148 |
String seqOrder = colIdx.containsKey("sequence_order") ? readCell(row.getCell(colIdx.get("sequence_order"))) : null;
|
| - |
|
1149 |
String code = readCell(row.getCell(colIdx.get("partner_code")));
|
| - |
|
1150 |
|
| - |
|
1151 |
if (beatName != null && !beatName.trim().isEmpty()) {
|
| - |
|
1152 |
// Start of a new beat — capture inheritable fields
|
| - |
|
1153 |
currentBeatName = beatName.trim().replaceAll("\\s+", " ");
|
| - |
|
1154 |
currentAuthId = authId != null ? authId.trim() : null;
|
| - |
|
1155 |
currentStartDate = (startDate != null && !startDate.trim().isEmpty()) ? startDate.trim() : null;
|
| - |
|
1156 |
currentKey = currentBeatName + "|" + currentAuthId;
|
| - |
|
1157 |
}
|
| - |
|
1158 |
if (currentKey == null) continue; // partner row before any beat header — skip
|
| - |
|
1159 |
if (code == null || code.trim().isEmpty()) continue;
|
| - |
|
1160 |
|
| - |
|
1161 |
final String beatNameF = currentBeatName;
|
| - |
|
1162 |
final String authIdF = currentAuthId;
|
| - |
|
1163 |
final String startDateF = currentStartDate;
|
| - |
|
1164 |
BulkBeatGroup g = beatGroups.computeIfAbsent(currentKey, k -> new BulkBeatGroup(beatNameF, authIdF, startDateF));
|
| - |
|
1165 |
|
| - |
|
1166 |
int day;
|
| - |
|
1167 |
try {
|
| - |
|
1168 |
day = Integer.parseInt(dayNumber.trim());
|
| - |
|
1169 |
} catch (Exception e) {
|
| - |
|
1170 |
continue;
|
| - |
|
1171 |
} // bad day → skip row
|
| - |
|
1172 |
|
| - |
|
1173 |
int seq = -1;
|
| - |
|
1174 |
if (seqOrder != null && !seqOrder.trim().isEmpty()) {
|
| - |
|
1175 |
try {
|
| - |
|
1176 |
seq = Integer.parseInt(seqOrder.trim());
|
| - |
|
1177 |
} catch (Exception ignore) {
|
| - |
|
1178 |
}
|
| - |
|
1179 |
}
|
| - |
|
1180 |
g.addPartner(day, seq, code.trim(), r + 1);
|
| - |
|
1181 |
}
|
| - |
|
1182 |
workbook.close();
|
| - |
|
1183 |
|
| 1058 |
List<FofoStore> allStores = fofoStoreRepository.selectAll();
|
1184 |
List<FofoStore> allStores = fofoStoreRepository.selectAll();
|
| 1059 |
Map<String, Integer> codeToId = new HashMap<>();
|
1185 |
Map<String, Integer> codeToId = new HashMap<>();
|
| 1060 |
for (FofoStore store : allStores) codeToId.put(store.getCode(), store.getId());
|
1186 |
for (FofoStore store : allStores) codeToId.put(store.getCode(), store.getId());
|
| 1061 |
|
1187 |
|
| 1062 |
LocalDate holidayStart = LocalDate.now();
|
1188 |
LocalDate holidayStart = LocalDate.now();
|
| Line 1064... |
Line 1190... |
| 1064 |
Set<LocalDate> holidayDates = holidays.stream().map(PublicHolidays::getDate).collect(Collectors.toSet());
|
1190 |
Set<LocalDate> holidayDates = holidays.stream().map(PublicHolidays::getDate).collect(Collectors.toSet());
|
| 1065 |
|
1191 |
|
| 1066 |
int beatsCreated = 0, errors = 0;
|
1192 |
int beatsCreated = 0, errors = 0;
|
| 1067 |
List<String> errorMessages = new ArrayList<>();
|
1193 |
List<String> errorMessages = new ArrayList<>();
|
| 1068 |
|
1194 |
|
| 1069 |
for (Map.Entry<String, List<org.apache.commons.csv.CSVRecord>> entry : beatGroups.entrySet()) {
|
1195 |
for (BulkBeatGroup g : beatGroups.values()) {
|
| 1070 |
try {
|
1196 |
try {
|
| 1071 |
String[] keyParts = entry.getKey().split("\\|");
|
1197 |
String beatName = g.beatName;
|
| 1072 |
String beatName = keyParts[0];
|
1198 |
int authUserId;
|
| - |
|
1199 |
try {
|
| 1073 |
int authUserId = Integer.parseInt(keyParts[1]);
|
1200 |
authUserId = Integer.parseInt(g.authUserId);
|
| 1074 |
List<org.apache.commons.csv.CSVRecord> rows = entry.getValue();
|
1201 |
} catch (Exception e) {
|
| 1075 |
rows.sort((a, b) -> Integer.parseInt(a.get("day_number").trim()) - Integer.parseInt(b.get("day_number").trim()));
|
1202 |
errorMessages.add("Beat '" + beatName + "': invalid auth_user_id '" + g.authUserId + "'. Skipped.");
|
| - |
|
1203 |
errors++;
|
| - |
|
1204 |
continue;
|
| 1076 |
|
1205 |
}
|
| 1077 |
String startDateStr = rows.get(0).get("start_date").trim();
|
- |
|
| 1078 |
LocalDate startDate = startDateStr.isEmpty() ? null : LocalDate.parse(startDateStr, DateTimeFormatter.ISO_DATE);
|
- |
|
| 1079 |
|
1206 |
|
| - |
|
1207 |
LocalDate startDate = (g.startDate == null) ? null : LocalDate.parse(g.startDate, DateTimeFormatter.ISO_DATE);
|
| 1080 |
if (startDate != null && startDate.isBefore(LocalDate.now())) {
|
1208 |
if (startDate != null && startDate.isBefore(LocalDate.now())) {
|
| 1081 |
errorMessages.add("Beat '" + beatName + "': start_date in past. Skipped.");
|
1209 |
errorMessages.add("Beat '" + beatName + "': start_date in past. Skipped.");
|
| 1082 |
errors++;
|
1210 |
errors++;
|
| 1083 |
continue;
|
1211 |
continue;
|
| 1084 |
}
|
1212 |
}
|
| 1085 |
|
1213 |
|
| - |
|
1214 |
List<Integer> sortedDays = new ArrayList<>(g.dayToPartners.keySet());
|
| - |
|
1215 |
Collections.sort(sortedDays);
|
| - |
|
1216 |
|
| 1086 |
List<LocalDate> scheduleDates = new ArrayList<>();
|
1217 |
List<LocalDate> scheduleDates = new ArrayList<>();
|
| 1087 |
if (startDate != null) {
|
1218 |
if (startDate != null) {
|
| 1088 |
LocalDate d = startDate;
|
1219 |
LocalDate d = startDate;
|
| 1089 |
while (scheduleDates.size() < rows.size()) {
|
1220 |
while (scheduleDates.size() < sortedDays.size()) {
|
| 1090 |
if (holidayDates.contains(d)) {
|
- |
|
| 1091 |
d = d.plusDays(1);
|
- |
|
| 1092 |
continue;
|
- |
|
| 1093 |
}
|
- |
|
| 1094 |
if (d.getDayOfWeek() == DayOfWeek.SUNDAY && !includeSundays) {
|
1221 |
if (holidayDates.contains(d) || (d.getDayOfWeek() == DayOfWeek.SUNDAY && !includeSundays)) {
|
| 1095 |
d = d.plusDays(1);
|
1222 |
d = d.plusDays(1);
|
| 1096 |
continue;
|
1223 |
continue;
|
| 1097 |
}
|
1224 |
}
|
| 1098 |
scheduleDates.add(d);
|
1225 |
scheduleDates.add(d);
|
| 1099 |
d = d.plusDays(1);
|
1226 |
d = d.plusDays(1);
|
| 1100 |
}
|
1227 |
}
|
| 1101 |
}
|
1228 |
}
|
| 1102 |
|
1229 |
|
| 1103 |
// Duplicate check — skip if a beat with same name already exists for this user
|
1230 |
// Duplicate check
|
| 1104 |
boolean isDuplicate = beatRepository.selectByAuthUserId(authUserId).stream()
|
1231 |
boolean isDuplicate = beatRepository.selectByAuthUserId(authUserId).stream()
|
| 1105 |
.anyMatch(b -> b.getName() != null && beatName.equalsIgnoreCase(b.getName().trim()));
|
1232 |
.anyMatch(b -> b.getName() != null && beatName.equalsIgnoreCase(b.getName().trim()));
|
| 1106 |
if (isDuplicate) {
|
1233 |
if (isDuplicate) {
|
| 1107 |
errorMessages.add("Beat '" + beatName + "' already exists for user " + authUserId + ". Skipped.");
|
1234 |
errorMessages.add("Beat '" + beatName + "' already exists for user " + authUserId + ". Skipped.");
|
| 1108 |
errors++;
|
1235 |
errors++;
|
| Line 1110... |
Line 1237... |
| 1110 |
}
|
1237 |
}
|
| 1111 |
|
1238 |
|
| 1112 |
String beatColor = BEAT_COLORS[Math.abs(beatName.hashCode()) % BEAT_COLORS.length];
|
1239 |
String beatColor = BEAT_COLORS[Math.abs(beatName.hashCode()) % BEAT_COLORS.length];
|
| 1113 |
AuthUserLocation homeLoc = authUserLocationRepository.selectLatestByAuthUserIdAndType(authUserId, "BASE");
|
1240 |
AuthUserLocation homeLoc = authUserLocationRepository.selectLatestByAuthUserIdAndType(authUserId, "BASE");
|
| 1114 |
|
1241 |
|
| 1115 |
// Create Beat master
|
- |
|
| 1116 |
Beat beat = new Beat();
|
1242 |
Beat beat = new Beat();
|
| 1117 |
beat.setName(beatName);
|
1243 |
beat.setName(beatName);
|
| 1118 |
beat.setAuthUserId(authUserId);
|
1244 |
beat.setAuthUserId(authUserId);
|
| 1119 |
beat.setBeatColor(beatColor);
|
1245 |
beat.setBeatColor(beatColor);
|
| 1120 |
beat.setTotalDays(rows.size());
|
1246 |
beat.setTotalDays(sortedDays.size());
|
| 1121 |
beat.setStartLocationName(homeLoc != null ? homeLoc.getLocationName() : "Home");
|
1247 |
beat.setStartLocationName(homeLoc != null ? homeLoc.getLocationName() : "Home");
|
| 1122 |
beat.setStartLatitude(homeLoc != null ? homeLoc.getLatitude() : null);
|
1248 |
beat.setStartLatitude(homeLoc != null ? homeLoc.getLatitude() : null);
|
| 1123 |
beat.setStartLongitude(homeLoc != null ? homeLoc.getLongitude() : null);
|
1249 |
beat.setStartLongitude(homeLoc != null ? homeLoc.getLongitude() : null);
|
| 1124 |
beat.setActive(true);
|
1250 |
beat.setActive(true);
|
| 1125 |
beat.setCreatedBy(currentUser.getId());
|
1251 |
beat.setCreatedBy(currentUser.getId());
|
| 1126 |
beat.setCreatedTimestamp(LocalDateTime.now());
|
1252 |
beat.setCreatedTimestamp(LocalDateTime.now());
|
| 1127 |
beatRepository.persist(beat);
|
1253 |
beatRepository.persist(beat);
|
| 1128 |
|
1254 |
|
| 1129 |
for (int rowIdx = 0; rowIdx < rows.size(); rowIdx++) {
|
1255 |
LocalDate bulkEndDate = scheduleDates.isEmpty() ? null : scheduleDates.get(scheduleDates.size() - 1);
|
| - |
|
1256 |
|
| 1130 |
org.apache.commons.csv.CSVRecord row = rows.get(rowIdx);
|
1257 |
for (int dayIdx = 0; dayIdx < sortedDays.size(); dayIdx++) {
|
| 1131 |
int dayNumber = Integer.parseInt(row.get("day_number").trim());
|
1258 |
int dayNumber = sortedDays.get(dayIdx);
|
| 1132 |
LocalDate planDate = (rowIdx < scheduleDates.size()) ? scheduleDates.get(rowIdx) : null;
|
1259 |
LocalDate planDate = (dayIdx < scheduleDates.size()) ? scheduleDates.get(dayIdx) : null;
|
| 1133 |
LocalDate bulkEndDate = scheduleDates.isEmpty() ? null : scheduleDates.get(scheduleDates.size() - 1);
|
- |
|
| 1134 |
|
1260 |
|
| 1135 |
// Always create schedule — placeholder date (9999-12-31) when unscheduled
|
- |
|
| 1136 |
BeatSchedule schedule = new BeatSchedule();
|
1261 |
BeatSchedule schedule = new BeatSchedule();
|
| 1137 |
schedule.setBeatId(beat.getId());
|
1262 |
schedule.setBeatId(beat.getId());
|
| 1138 |
schedule.setStartDate(planDate != null ? planDate : LocalDate.of(9999, 12, 31));
|
1263 |
schedule.setStartDate(planDate != null ? planDate : LocalDate.of(9999, 12, 31));
|
| 1139 |
schedule.setEndDate(bulkEndDate);
|
1264 |
schedule.setEndDate(bulkEndDate);
|
| 1140 |
schedule.setDayNumber(dayNumber);
|
1265 |
schedule.setDayNumber(dayNumber);
|
| 1141 |
schedule.setEndAction(rowIdx == rows.size() - 1 ? "HOME" : "DAYBREAK");
|
1266 |
schedule.setEndAction(dayIdx == sortedDays.size() - 1 ? "HOME" : "DAYBREAK");
|
| 1142 |
schedule.setCreatedTimestamp(LocalDateTime.now());
|
1267 |
schedule.setCreatedTimestamp(LocalDateTime.now());
|
| 1143 |
beatScheduleRepository.persist(schedule);
|
1268 |
beatScheduleRepository.persist(schedule);
|
| 1144 |
|
1269 |
|
| 1145 |
String[] partnerCodes = row.get("partner_codes").trim().split(",");
|
1270 |
List<BulkPartner> partners = g.dayToPartners.get(dayNumber);
|
| - |
|
1271 |
// Sort by explicit sequence_order when present, else by row order
|
| - |
|
1272 |
partners.sort((a, b) -> {
|
| 1146 |
for (int i = 0; i < partnerCodes.length; i++) {
|
1273 |
if (a.seq >= 0 && b.seq >= 0) return Integer.compare(a.seq, b.seq);
|
| 1147 |
String code = partnerCodes[i].trim();
|
1274 |
if (a.seq >= 0) return -1;
|
| 1148 |
if (code.isEmpty()) continue;
|
1275 |
if (b.seq >= 0) return 1;
|
| - |
|
1276 |
return Integer.compare(a.rowNum, b.rowNum);
|
| - |
|
1277 |
});
|
| - |
|
1278 |
|
| - |
|
1279 |
int autoSeq = 0;
|
| - |
|
1280 |
for (BulkPartner p : partners) {
|
| 1149 |
Integer fofoId = codeToId.get(code);
|
1281 |
Integer fofoId = codeToId.get(p.code);
|
| 1150 |
if (fofoId == null) {
|
1282 |
if (fofoId == null) {
|
| 1151 |
errorMessages.add("Code not found: " + code);
|
1283 |
errorMessages.add("Row " + p.rowNum + ": code not found '" + p.code + "'");
|
| 1152 |
errors++;
|
1284 |
errors++;
|
| 1153 |
continue;
|
1285 |
continue;
|
| 1154 |
}
|
1286 |
}
|
| 1155 |
|
- |
|
| 1156 |
BeatRoute route = new BeatRoute();
|
1287 |
BeatRoute route = new BeatRoute();
|
| 1157 |
route.setBeatId(beat.getId());
|
1288 |
route.setBeatId(beat.getId());
|
| 1158 |
route.setFofoId(fofoId);
|
1289 |
route.setFofoId(fofoId);
|
| 1159 |
route.setSequenceOrder(i);
|
1290 |
route.setSequenceOrder(p.seq >= 0 ? p.seq : autoSeq);
|
| 1160 |
route.setDayNumber(dayNumber);
|
1291 |
route.setDayNumber(dayNumber);
|
| 1161 |
route.setActive(true);
|
1292 |
route.setActive(true);
|
| 1162 |
beatRouteRepository.persist(route);
|
1293 |
beatRouteRepository.persist(route);
|
| - |
|
1294 |
autoSeq++;
|
| 1163 |
}
|
1295 |
}
|
| 1164 |
}
|
1296 |
}
|
| 1165 |
beatsCreated++;
|
1297 |
beatsCreated++;
|
| 1166 |
} catch (Exception e) {
|
1298 |
} catch (Exception e) {
|
| 1167 |
errors++;
|
1299 |
errors++;
|
| 1168 |
errorMessages.add("Error: " + entry.getKey() + " - " + e.getMessage());
|
1300 |
errorMessages.add("Error: " + g.beatName + " - " + e.getMessage());
|
| 1169 |
}
|
1301 |
}
|
| 1170 |
}
|
1302 |
}
|
| 1171 |
|
1303 |
|
| 1172 |
Map<String, Object> response = new HashMap<>();
|
1304 |
Map<String, Object> response = new HashMap<>();
|
| 1173 |
response.put("status", true);
|
1305 |
response.put("status", true);
|
| Line 1175... |
Line 1307... |
| 1175 |
response.put("errors", errors);
|
1307 |
response.put("errors", errors);
|
| 1176 |
response.put("errorMessages", errorMessages);
|
1308 |
response.put("errorMessages", errorMessages);
|
| 1177 |
return responseSender.ok(response);
|
1309 |
return responseSender.ok(response);
|
| 1178 |
}
|
1310 |
}
|
| 1179 |
|
1311 |
|
| - |
|
1312 |
private static class BulkBeatGroup {
|
| - |
|
1313 |
final String beatName;
|
| - |
|
1314 |
final String authUserId;
|
| - |
|
1315 |
final String startDate;
|
| - |
|
1316 |
final Map<Integer, List<BulkPartner>> dayToPartners = new LinkedHashMap<>();
|
| - |
|
1317 |
|
| - |
|
1318 |
BulkBeatGroup(String beatName, String authUserId, String startDate) {
|
| - |
|
1319 |
this.beatName = beatName;
|
| - |
|
1320 |
this.authUserId = authUserId;
|
| - |
|
1321 |
this.startDate = startDate;
|
| - |
|
1322 |
}
|
| - |
|
1323 |
|
| - |
|
1324 |
void addPartner(int day, int seq, String code, int rowNum) {
|
| - |
|
1325 |
dayToPartners.computeIfAbsent(day, k -> new ArrayList<>()).add(new BulkPartner(seq, code, rowNum));
|
| - |
|
1326 |
}
|
| - |
|
1327 |
}
|
| - |
|
1328 |
|
| - |
|
1329 |
private static class BulkPartner {
|
| - |
|
1330 |
final int seq;
|
| - |
|
1331 |
final String code;
|
| - |
|
1332 |
final int rowNum;
|
| - |
|
1333 |
|
| - |
|
1334 |
BulkPartner(int seq, String code, int rowNum) {
|
| - |
|
1335 |
this.seq = seq;
|
| - |
|
1336 |
this.code = code;
|
| - |
|
1337 |
this.rowNum = rowNum;
|
| - |
|
1338 |
}
|
| - |
|
1339 |
}
|
| - |
|
1340 |
|
| 1180 |
// ============ CALENDAR ============
|
1341 |
// ============ CALENDAR ============
|
| 1181 |
|
1342 |
|
| 1182 |
@PostMapping(value = "/beatPlan/delete")
|
1343 |
@PostMapping(value = "/beatPlan/delete")
|
| 1183 |
public ResponseEntity<?> deleteBeat(@RequestParam String planGroupId) {
|
1344 |
public ResponseEntity<?> deleteBeat(@RequestParam String planGroupId) {
|
| 1184 |
int beatId = Integer.parseInt(planGroupId);
|
1345 |
int beatId = Integer.parseInt(planGroupId);
|