Subversion Repositories SmartDukaan

Rev

Rev 36663 | Rev 36670 | Go to most recent revision | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 36663 Rev 36668
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);