Subversion Repositories SmartDukaan

Rev

Rev 36632 | Rev 36650 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
36618 ranu 1
package com.spice.profitmandi.web.controller;
2
 
3
import com.google.gson.Gson;
4
import com.google.gson.reflect.TypeToken;
5
import com.spice.profitmandi.common.exception.ProfitMandiBusinessException;
6
import com.spice.profitmandi.common.model.CustomRetailer;
7
import com.spice.profitmandi.common.web.util.ResponseSender;
8
import com.spice.profitmandi.dao.entity.auth.AuthUser;
9
import com.spice.profitmandi.dao.entity.fofo.FofoStore;
10
import com.spice.profitmandi.dao.entity.logistics.PublicHolidays;
36644 ranu 11
import com.spice.profitmandi.dao.entity.user.*;
36618 ranu 12
import com.spice.profitmandi.dao.enumuration.cs.EscalationType;
13
import com.spice.profitmandi.dao.repository.auth.AuthRepository;
14
import com.spice.profitmandi.dao.repository.cs.CsService;
15
import com.spice.profitmandi.dao.repository.dtr.*;
16
import com.spice.profitmandi.dao.repository.logistics.PublicHolidaysRepository;
17
import com.spice.profitmandi.service.user.RetailerService;
18
import com.spice.profitmandi.web.model.LoginDetails;
19
import com.spice.profitmandi.web.util.CookiesProcessor;
20
import org.apache.logging.log4j.LogManager;
21
import org.apache.logging.log4j.Logger;
22
import org.springframework.beans.factory.annotation.Autowired;
23
import org.springframework.http.ResponseEntity;
24
import org.springframework.stereotype.Controller;
25
import org.springframework.transaction.annotation.Transactional;
26
import org.springframework.ui.Model;
27
import org.springframework.web.bind.annotation.GetMapping;
28
import org.springframework.web.bind.annotation.PostMapping;
29
import org.springframework.web.bind.annotation.RequestParam;
30
 
31
import javax.servlet.http.HttpServletRequest;
32
import java.lang.reflect.Type;
33
import java.time.DayOfWeek;
34
import java.time.LocalDate;
35
import java.time.LocalDateTime;
36
import java.time.YearMonth;
37
import java.time.format.DateTimeFormatter;
38
import java.util.*;
39
import java.util.stream.Collectors;
40
 
41
@Controller
42
@Transactional(rollbackFor = Throwable.class)
43
public class BeatPlanController {
44
	private static final Logger LOGGER = LogManager.getLogger(BeatPlanController.class);
45
	private static final String[] BEAT_COLORS = {
46
			"#3498DB", "#E74C3C", "#2ECC71", "#9B59B6", "#F39C12",
47
			"#1ABC9C", "#E67E22", "#34495E", "#16A085", "#C0392B"
48
	};
49
	@Autowired
50
	private CsService csService;
51
	@Autowired
52
	private AuthRepository authRepository;
53
	@Autowired
54
	private FofoStoreRepository fofoStoreRepository;
55
	@Autowired
56
	private RetailerService retailerService;
57
	@Autowired
36644 ranu 58
	private BeatRepository beatRepository;
36618 ranu 59
	@Autowired
36644 ranu 60
	private BeatRouteRepository beatRouteRepository;
36618 ranu 61
	@Autowired
36644 ranu 62
	private BeatScheduleRepository beatScheduleRepository;
63
	@Autowired
64
	private LeadRouteRepository leadRouteRepository;
65
	@Autowired
36618 ranu 66
	private AuthUserLocationRepository authUserLocationRepository;
67
	@Autowired
68
	private LeadRepository leadRepository;
69
	@Autowired
70
	private PublicHolidaysRepository publicHolidaysRepository;
71
	@Autowired
72
	private com.spice.profitmandi.service.GeocodingService geocodingService;
73
	@Autowired
74
	private CookiesProcessor cookiesProcessor;
75
	@Autowired
76
	private ResponseSender responseSender;
77
 
78
	@GetMapping(value = "/beatPlan")
79
	public String beatPlan(HttpServletRequest request, Model model) {
80
		EscalationType[] escalationTypes = EscalationType.values();
81
		model.addAttribute("escalationTypes", escalationTypes);
82
		return "beat-plan";
83
	}
84
 
85
	@GetMapping(value = "/beatPlanWindow")
86
	public String beatPlanWindow(HttpServletRequest request, Model model) {
87
		EscalationType[] escalationTypes = EscalationType.values();
88
		model.addAttribute("escalationTypes", escalationTypes);
89
		return "beat-plan-window";
90
	}
91
 
92
	@GetMapping(value = "/beatPlan/getAuthUsers")
93
	public ResponseEntity<?> getAuthUsers(
94
			@RequestParam int categoryId,
95
			@RequestParam EscalationType escalationType) {
96
		List<AuthUser> authUsers = csService.getAuthUserByCategoryId(categoryId, escalationType);
97
		List<Map<String, Object>> result = authUsers.stream()
98
				.filter(au -> au.getActive())
99
				.map(au -> {
100
					Map<String, Object> map = new HashMap<>();
101
					map.put("id", au.getId());
102
					map.put("name", au.getFirstName() + " " + au.getLastName());
103
					return map;
104
				})
105
				.collect(Collectors.toList());
106
		return responseSender.ok(result);
107
	}
108
 
36644 ranu 109
	// Returns visits for a beat.
110
	// - Partner stops (beat_route) belong to the beat template — always returned.
111
	// - Lead stops (lead_route) belong to a specific run — returned ONLY when planDate
112
	//   is given and matches the lead's schedule_date. (No planDate = template view.)
36632 ranu 113
	@GetMapping(value = "/beatPlan/getBeatVisits")
36644 ranu 114
	public ResponseEntity<?> getBeatVisits(
115
			@RequestParam String planGroupId,
116
			@RequestParam(required = false) String planDate) {
117
 
118
		int beatId;
119
		try {
120
			beatId = Integer.parseInt(planGroupId);
121
		} catch (NumberFormatException e) {
122
			return responseSender.ok(new ArrayList<>());
123
		}
124
 
125
		List<BeatRoute> routes = beatRouteRepository.selectByBeatId(beatId);
126
		List<Map<String, Object>> result = new ArrayList<>();
127
 
128
		// Partner stops — always (they belong to the beat template)
129
		for (BeatRoute r : routes) {
36632 ranu 130
			Map<String, Object> map = new HashMap<>();
36644 ranu 131
			map.put("fofoId", r.getFofoId());
132
			map.put("dayNumber", r.getDayNumber());
133
			map.put("sequenceOrder", r.getSequenceOrder());
134
			map.put("visitType", "partner");
135
			result.add(map);
136
		}
137
 
138
		// Lead stops — only for the requested run date
139
		if (planDate != null && !planDate.isEmpty()) {
140
			LocalDate date = LocalDate.parse(planDate);
141
			List<LeadRoute> leads = leadRouteRepository.selectByBeatId(beatId);
142
			for (LeadRoute lr : leads) {
143
				if ("APPROVED".equals(lr.getStatus())
144
						&& lr.getScheduleDate() != null
145
						&& lr.getScheduleDate().equals(date)) {
146
					Map<String, Object> map = new HashMap<>();
147
					map.put("fofoId", lr.getLeadId());
148
					map.put("dayNumber", 1);
149
					map.put("sequenceOrder", lr.getSequenceOrder() != null ? lr.getSequenceOrder() : 999);
150
					map.put("visitType", "lead");
151
					result.add(map);
152
				}
153
			}
154
		}
155
 
156
		// Sort by dayNumber then sequenceOrder
157
		result.sort((a, b) -> {
158
			int cmp = Integer.compare((int) a.get("dayNumber"), (int) b.get("dayNumber"));
159
			return cmp != 0 ? cmp : Integer.compare((int) a.get("sequenceOrder"), (int) b.get("sequenceOrder"));
160
		});
161
 
36632 ranu 162
		return responseSender.ok(result);
163
	}
164
 
36618 ranu 165
	@GetMapping(value = "/beatPlan/getBaseLocation")
166
	public ResponseEntity<?> getBaseLocation(@RequestParam int authUserId) {
167
		AuthUserLocation baseLoc = authUserLocationRepository.selectLatestByAuthUserIdAndType(authUserId, "BASE");
168
		if (baseLoc == null) {
169
			return responseSender.ok(new HashMap<>());
170
		}
171
		Map<String, Object> result = new HashMap<>();
172
		result.put("id", baseLoc.getId());
173
		result.put("locationName", baseLoc.getLocationName());
174
		result.put("latitude", baseLoc.getLatitude());
175
		result.put("longitude", baseLoc.getLongitude());
176
		result.put("address", baseLoc.getAddress());
177
		return responseSender.ok(result);
178
	}
179
 
180
	@PostMapping(value = "/beatPlan/saveBaseLocation")
181
	public ResponseEntity<?> saveBaseLocation(
182
			@RequestParam int authUserId,
183
			@RequestParam String locationName,
184
			@RequestParam String latitude,
185
			@RequestParam String longitude,
186
			@RequestParam(required = false) String address) {
187
		AuthUserLocation loc = new AuthUserLocation();
188
		loc.setAuthUserId(authUserId);
189
		loc.setLocationType("BASE");
190
		loc.setLocationName(locationName);
191
		loc.setLatitude(latitude);
192
		loc.setLongitude(longitude);
193
		loc.setAddress(address);
194
		loc.setCreatedTimestamp(LocalDateTime.now());
195
		authUserLocationRepository.persist(loc);
196
 
197
		Map<String, Object> result = new HashMap<>();
198
		result.put("status", true);
199
		result.put("id", loc.getId());
200
		return responseSender.ok(result);
201
	}
202
 
203
	@GetMapping(value = "/beatPlan/getPartners")
204
	public ResponseEntity<?> getPartners(
205
			@RequestParam int authUserId,
206
			@RequestParam int categoryId,
207
			@RequestParam(required = false) String startLat,
208
			@RequestParam(required = false) String startLng) throws ProfitMandiBusinessException {
209
 
210
		Map<Integer, List<Integer>> pp = csService.getAuthUserIdPartnerIdMapping();
211
		List<Integer> fofoIds = pp.get(authUserId);
212
 
36644 ranu 213
		if (fofoIds == null || fofoIds.isEmpty()) {
36618 ranu 214
			Map<String, Object> empty = new HashMap<>();
215
			empty.put("partners", new ArrayList<>());
216
			return responseSender.ok(empty);
217
		}
218
 
219
		List<FofoStore> fofoStores = fofoStoreRepository.selectByRetailerIds(fofoIds);
220
		Map<Integer, CustomRetailer> retailerMap = retailerService.getFofoRetailers(fofoIds);
221
 
222
		List<Map<String, Object>> partners = new ArrayList<>();
223
		List<String> addressesToGeocode = new ArrayList<>();
224
 
225
		for (FofoStore store : fofoStores) {
226
			if (!store.isActive() || store.isClosed()) continue;
227
			CustomRetailer retailer = retailerMap.get(store.getId());
228
 
229
			Map<String, Object> partnerData = new HashMap<>();
230
			partnerData.put("fofoId", store.getId());
231
			partnerData.put("code", store.getCode());
232
			partnerData.put("outletName", store.getOutletName());
233
			partnerData.put("type", "partner");
234
 
36632 ranu 235
			String geoAddress = null;
36618 ranu 236
			if (retailer != null) {
237
				partnerData.put("businessName", retailer.getBusinessName());
238
				if (retailer.getAddress() != null) {
36644 ranu 239
					partnerData.put("address", retailer.getAddress().getAddressString());
36632 ranu 240
					geoAddress = com.spice.profitmandi.service.GeocodingService.buildGeoAddress(
241
							retailer.getAddress().getLine1(),
242
							retailer.getAddress().getCity(),
243
							retailer.getAddress().getState(),
244
							retailer.getAddress().getPinCode());
36618 ranu 245
				}
246
			}
36632 ranu 247
			addressesToGeocode.add(geoAddress);
36618 ranu 248
			partners.add(partnerData);
249
		}
250
 
36644 ranu 251
		// Geocode in parallel
36618 ranu 252
		java.util.concurrent.ExecutorService executor = java.util.concurrent.Executors.newFixedThreadPool(
36644 ranu 253
				Math.min(10, Math.max(1, partners.size())));
36618 ranu 254
		List<java.util.concurrent.Future<double[]>> futures = new ArrayList<>();
255
		for (String addr : addressesToGeocode) {
256
			futures.add(executor.submit(() -> {
257
				if (addr != null && !addr.isEmpty()) {
258
					try {
259
						return geocodingService.geocodeAddress(addr);
260
					} catch (Exception e) {
261
						return null;
262
					}
263
				}
264
				return null;
265
			}));
266
		}
267
		for (int i = 0; i < partners.size(); i++) {
268
			try {
269
				double[] coords = futures.get(i).get(10, java.util.concurrent.TimeUnit.SECONDS);
270
				if (coords != null) {
36644 ranu 271
					partners.get(i).put("latitude", String.valueOf(coords[0]));
272
					partners.get(i).put("longitude", String.valueOf(coords[1]));
36618 ranu 273
				}
274
			} catch (Exception e) {
36644 ranu 275
				LOGGER.warn("Geocoding timeout/error for partner {}", partners.get(i).get("code"));
36618 ranu 276
			}
277
		}
278
		executor.shutdown();
279
 
280
		if (startLat != null && startLng != null && !startLat.isEmpty() && !startLng.isEmpty()) {
281
			partners = sortByNearestNeighborFromStart(partners, Double.parseDouble(startLat), Double.parseDouble(startLng));
282
		} else {
283
			partners = sortByNearestNeighbor(partners);
284
		}
285
 
286
		Map<String, Object> response = new HashMap<>();
287
		response.put("partners", partners);
288
		return responseSender.ok(response);
289
	}
290
 
291
	@PostMapping(value = "/beatPlan/submitPlan")
292
	public ResponseEntity<?> submitPlan(
293
			HttpServletRequest request,
294
			@RequestParam int authUserId,
295
			@RequestParam String planData) throws Exception {
296
 
297
		LoginDetails loginDetails = cookiesProcessor.getCookiesObject(request);
298
		AuthUser currentUser = authRepository.selectByEmailOrMobile(loginDetails.getEmailId());
299
 
300
		Gson gson = new Gson();
301
		Type type = new TypeToken<Map<String, Object>>() {
302
		}.getType();
303
		Map<String, Object> plan = gson.fromJson(planData, type);
304
 
305
		List<Map<String, Object>> days = (List<Map<String, Object>>) plan.get("days");
306
		List<String> dates = (List<String>) plan.get("dates");
307
 
36644 ranu 308
		String beatName = (plan.get("beatName") != null ? (String) plan.get("beatName") : "Beat").trim();
36618 ranu 309
 
36644 ranu 310
		// Duplicate check — same name + same authUserId = duplicate
311
		List<Beat> existingBeats = beatRepository.selectByAuthUserId(authUserId);
312
		for (Beat existing : existingBeats) {
313
			if (existing.getName() != null && beatName.equalsIgnoreCase(existing.getName().trim())) {
314
				LOGGER.info("Duplicate beat blocked: name='{}' authUserId={} existingId={}", beatName, authUserId, existing.getId());
36618 ranu 315
				Map<String, Object> response = new HashMap<>();
316
				response.put("status", true);
36644 ranu 317
				response.put("planGroupId", String.valueOf(existing.getId()));
36618 ranu 318
				response.put("duplicate", true);
36644 ranu 319
				response.put("message", "Beat '" + beatName + "' already exists");
36618 ranu 320
				return responseSender.ok(response);
321
			}
322
		}
323
 
36644 ranu 324
		String beatColor = BEAT_COLORS[Math.abs(beatName.hashCode()) % BEAT_COLORS.length];
325
		int totalDays = days.size();
36618 ranu 326
 
36644 ranu 327
		// Create Beat master
328
		Beat beat = new Beat();
329
		beat.setName(beatName);
330
		beat.setAuthUserId(authUserId);
331
		beat.setBeatColor(beatColor);
332
		beat.setTotalDays(totalDays);
333
		beat.setActive(true);
334
		beat.setCreatedBy(currentUser.getId());
335
		beat.setCreatedTimestamp(LocalDateTime.now());
336
 
337
		// Set start location from first day
338
		if (!days.isEmpty()) {
339
			Map<String, Object> firstDay = days.get(0);
340
			beat.setStartLocationName((String) firstDay.get("startLocationName"));
341
			beat.setStartLatitude((String) firstDay.get("startLatitude"));
342
			beat.setStartLongitude((String) firstDay.get("startLongitude"));
343
		}
344
		beatRepository.persist(beat);
345
 
346
		// End date of the whole beat = last scheduled day's date
347
		LocalDate beatEndDate = null;
348
		if (dates != null) {
349
			for (int d = dates.size() - 1; d >= 0; d--) {
350
				if (dates.get(d) != null) {
351
					beatEndDate = LocalDate.parse(dates.get(d), DateTimeFormatter.ISO_DATE);
352
					break;
353
				}
354
			}
355
		}
356
 
357
		// Create routes and schedules for each day
36618 ranu 358
		for (int d = 0; d < days.size(); d++) {
359
			Map<String, Object> day = days.get(d);
360
			int dayNumber = d + 1;
361
			LocalDate planDate = (dates != null && d < dates.size() && dates.get(d) != null)
36644 ranu 362
					? LocalDate.parse(dates.get(d), DateTimeFormatter.ISO_DATE) : null;
36618 ranu 363
 
36644 ranu 364
			// Auto-determine end action: last day = HOME, others = DAYBREAK
365
			String endAction = (String) day.get("endAction");
366
			if (endAction == null || endAction.isEmpty()) {
367
				endAction = (dayNumber == totalDays) ? "HOME" : "DAYBREAK";
36618 ranu 368
			}
369
 
36644 ranu 370
			// Always create schedule (even if planDate is null — unscheduled beat)
371
			BeatSchedule schedule = new BeatSchedule();
372
			schedule.setBeatId(beat.getId());
373
			schedule.setStartDate(planDate != null ? planDate : LocalDate.of(9999, 12, 31)); // placeholder for unscheduled
374
			schedule.setEndDate(beatEndDate);
375
			schedule.setDayNumber(dayNumber);
376
			schedule.setEndAction(endAction);
377
			schedule.setStayLocationName((String) day.get("stayLocationName"));
378
			schedule.setStayLatitude((String) day.get("stayLatitude"));
379
			schedule.setStayLongitude((String) day.get("stayLongitude"));
380
			if (day.get("totalDistanceKm") != null)
381
				schedule.setTotalDistanceKm(((Number) day.get("totalDistanceKm")).doubleValue());
382
			if (day.get("totalTimeMins") != null)
383
				schedule.setTotalTimeMins(((Number) day.get("totalTimeMins")).intValue());
384
			schedule.setCreatedTimestamp(LocalDateTime.now());
385
			beatScheduleRepository.persist(schedule);
386
 
387
			// Routes (stops)
36618 ranu 388
			List<Map<String, Object>> visits = (List<Map<String, Object>>) day.get("visits");
389
			if (visits != null) {
390
				for (int i = 0; i < visits.size(); i++) {
391
					Map<String, Object> visit = visits.get(i);
36644 ranu 392
					BeatRoute route = new BeatRoute();
393
					route.setBeatId(beat.getId());
394
					route.setFofoId(((Number) visit.get("id")).intValue());
395
					route.setSequenceOrder(i);
396
					route.setDayNumber(dayNumber);
397
					route.setActive(true);
398
					beatRouteRepository.persist(route);
36618 ranu 399
				}
400
			}
401
		}
402
 
403
		Map<String, Object> response = new HashMap<>();
404
		response.put("status", true);
36644 ranu 405
		response.put("planGroupId", String.valueOf(beat.getId()));
36618 ranu 406
		response.put("message", "Beat plan submitted successfully");
407
		return responseSender.ok(response);
408
	}
409
 
36632 ranu 410
	// ============ BULK UPLOAD ============
411
 
412
	@GetMapping(value = "/beatPlan/bulkUpload")
413
	public String bulkUploadPage(HttpServletRequest request, Model model) {
414
		return "beat-plan-bulk";
415
	}
416
 
417
	@GetMapping(value = "/beatPlan/downloadTemplate")
418
	public ResponseEntity<?> downloadTemplate() {
419
		String csv = "beat_name,auth_user_id,start_date,day_number,partner_codes\n";
420
		csv += "Jaipur East Route,280,2026-06-02,1,\"RJKAI1478,RJBUN1449,RJDEG1443\"\n";
421
		csv += ",280,,2,\"RJALR1362,RJBTR1388\"\n";
422
		csv += ",280,,3,\"RJRSD1518,RJSML356\"\n";
423
		csv += "Agra Circuit,145,2026-06-05,1,\"UPAGR101,UPAGR102\"\n";
424
 
425
		org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders();
426
		headers.add("Content-Disposition", "attachment; filename=beat_plan_template.csv");
427
		headers.add("Content-Type", "text/csv");
428
		return new ResponseEntity<>(csv, headers, org.springframework.http.HttpStatus.OK);
429
	}
430
 
431
	@PostMapping(value = "/beatPlan/bulkUploadProcess")
432
	public ResponseEntity<?> bulkUploadProcess(
433
			HttpServletRequest request,
434
			@RequestParam("file") org.springframework.web.multipart.MultipartFile file,
435
			@RequestParam(value = "includeSundays", defaultValue = "false") boolean includeSundays) throws Exception {
436
 
437
		LoginDetails loginDetails = cookiesProcessor.getCookiesObject(request);
438
		AuthUser currentUser = authRepository.selectByEmailOrMobile(loginDetails.getEmailId());
439
 
440
		java.io.Reader reader = new java.io.InputStreamReader(file.getInputStream());
441
		org.apache.commons.csv.CSVParser parser = new org.apache.commons.csv.CSVParser(reader,
442
				org.apache.commons.csv.CSVFormat.DEFAULT.withFirstRecordAsHeader().withTrim());
443
		List<org.apache.commons.csv.CSVRecord> allRecords = parser.getRecords();
444
		parser.close();
445
 
446
		Map<String, String> lastBeatNameByUser = new HashMap<>();
447
		Map<String, List<org.apache.commons.csv.CSVRecord>> beatGroups = new LinkedHashMap<>();
448
		Map<Long, String> resolvedBeatNames = new HashMap<>();
449
 
450
		for (org.apache.commons.csv.CSVRecord record : allRecords) {
451
			String authId = record.get("auth_user_id").trim();
36644 ranu 452
			String rawName = record.get("beat_name").trim().replaceAll("\\s+", " ");
453
			if (rawName.isEmpty()) rawName = lastBeatNameByUser.getOrDefault(authId, "Beat");
454
			else lastBeatNameByUser.put(authId, rawName);
455
			resolvedBeatNames.put(record.getRecordNumber(), rawName);
456
			beatGroups.computeIfAbsent(rawName + "|" + authId, k -> new ArrayList<>()).add(record);
36632 ranu 457
		}
458
 
459
		List<FofoStore> allStores = fofoStoreRepository.selectAll();
460
		Map<String, Integer> codeToId = new HashMap<>();
36644 ranu 461
		for (FofoStore store : allStores) codeToId.put(store.getCode(), store.getId());
36632 ranu 462
 
463
		LocalDate holidayStart = LocalDate.now();
36644 ranu 464
		List<PublicHolidays> holidays = publicHolidaysRepository.selectAllBetweenDates(holidayStart, holidayStart.plusMonths(6));
36632 ranu 465
		Set<LocalDate> holidayDates = holidays.stream().map(PublicHolidays::getDate).collect(Collectors.toSet());
466
 
36644 ranu 467
		int beatsCreated = 0, errors = 0;
36632 ranu 468
		List<String> errorMessages = new ArrayList<>();
469
 
470
		for (Map.Entry<String, List<org.apache.commons.csv.CSVRecord>> entry : beatGroups.entrySet()) {
471
			try {
472
				String[] keyParts = entry.getKey().split("\\|");
473
				String beatName = keyParts[0];
474
				int authUserId = Integer.parseInt(keyParts[1]);
475
				List<org.apache.commons.csv.CSVRecord> rows = entry.getValue();
476
				rows.sort((a, b) -> Integer.parseInt(a.get("day_number").trim()) - Integer.parseInt(b.get("day_number").trim()));
477
 
478
				String startDateStr = rows.get(0).get("start_date").trim();
36644 ranu 479
				LocalDate startDate = startDateStr.isEmpty() ? null : LocalDate.parse(startDateStr, DateTimeFormatter.ISO_DATE);
36632 ranu 480
 
481
				if (startDate != null && startDate.isBefore(LocalDate.now())) {
36644 ranu 482
					errorMessages.add("Beat '" + beatName + "': start_date in past. Skipped.");
36632 ranu 483
					errors++;
484
					continue;
485
				}
486
 
487
				List<LocalDate> scheduleDates = new ArrayList<>();
488
				if (startDate != null) {
489
					LocalDate d = startDate;
36644 ranu 490
					while (scheduleDates.size() < rows.size()) {
491
						if (holidayDates.contains(d)) {
492
							d = d.plusDays(1);
493
							continue;
36632 ranu 494
						}
36644 ranu 495
						if (d.getDayOfWeek() == DayOfWeek.SUNDAY && !includeSundays) {
496
							d = d.plusDays(1);
497
							continue;
498
						}
499
						scheduleDates.add(d);
36632 ranu 500
						d = d.plusDays(1);
501
					}
502
				}
503
 
36644 ranu 504
				// Duplicate check — skip if a beat with same name already exists for this user
505
				boolean isDuplicate = beatRepository.selectByAuthUserId(authUserId).stream()
506
						.anyMatch(b -> b.getName() != null && beatName.equalsIgnoreCase(b.getName().trim()));
507
				if (isDuplicate) {
508
					errorMessages.add("Beat '" + beatName + "' already exists for user " + authUserId + ". Skipped.");
509
					errors++;
510
					continue;
511
				}
36632 ranu 512
 
36644 ranu 513
				String beatColor = BEAT_COLORS[Math.abs(beatName.hashCode()) % BEAT_COLORS.length];
36632 ranu 514
				AuthUserLocation homeLoc = authUserLocationRepository.selectLatestByAuthUserIdAndType(authUserId, "BASE");
515
 
36644 ranu 516
				// Create Beat master
517
				Beat beat = new Beat();
518
				beat.setName(beatName);
519
				beat.setAuthUserId(authUserId);
520
				beat.setBeatColor(beatColor);
521
				beat.setTotalDays(rows.size());
522
				beat.setStartLocationName(homeLoc != null ? homeLoc.getLocationName() : "Home");
523
				beat.setStartLatitude(homeLoc != null ? homeLoc.getLatitude() : null);
524
				beat.setStartLongitude(homeLoc != null ? homeLoc.getLongitude() : null);
525
				beat.setActive(true);
526
				beat.setCreatedBy(currentUser.getId());
527
				beat.setCreatedTimestamp(LocalDateTime.now());
528
				beatRepository.persist(beat);
36632 ranu 529
 
36644 ranu 530
				for (int rowIdx = 0; rowIdx < rows.size(); rowIdx++) {
36632 ranu 531
					org.apache.commons.csv.CSVRecord row = rows.get(rowIdx);
532
					int dayNumber = Integer.parseInt(row.get("day_number").trim());
533
					LocalDate planDate = (rowIdx < scheduleDates.size()) ? scheduleDates.get(rowIdx) : null;
36644 ranu 534
					LocalDate bulkEndDate = scheduleDates.isEmpty() ? null : scheduleDates.get(scheduleDates.size() - 1);
36632 ranu 535
 
36644 ranu 536
					// Always create schedule — placeholder date (9999-12-31) when unscheduled
537
					BeatSchedule schedule = new BeatSchedule();
538
					schedule.setBeatId(beat.getId());
539
					schedule.setStartDate(planDate != null ? planDate : LocalDate.of(9999, 12, 31));
540
					schedule.setEndDate(bulkEndDate);
541
					schedule.setDayNumber(dayNumber);
542
					schedule.setEndAction(rowIdx == rows.size() - 1 ? "HOME" : "DAYBREAK");
543
					schedule.setCreatedTimestamp(LocalDateTime.now());
544
					beatScheduleRepository.persist(schedule);
36632 ranu 545
 
36644 ranu 546
					String[] partnerCodes = row.get("partner_codes").trim().split(",");
36632 ranu 547
					for (int i = 0; i < partnerCodes.length; i++) {
36644 ranu 548
						String code = partnerCodes[i].trim();
549
						if (code.isEmpty()) continue;
550
						Integer fofoId = codeToId.get(code);
36632 ranu 551
						if (fofoId == null) {
36644 ranu 552
							errorMessages.add("Code not found: " + code);
36632 ranu 553
							errors++;
554
							continue;
555
						}
556
 
36644 ranu 557
						BeatRoute route = new BeatRoute();
558
						route.setBeatId(beat.getId());
559
						route.setFofoId(fofoId);
560
						route.setSequenceOrder(i);
561
						route.setDayNumber(dayNumber);
562
						route.setActive(true);
563
						beatRouteRepository.persist(route);
36632 ranu 564
					}
565
				}
566
				beatsCreated++;
567
			} catch (Exception e) {
568
				errors++;
36644 ranu 569
				errorMessages.add("Error: " + entry.getKey() + " - " + e.getMessage());
36632 ranu 570
			}
571
		}
572
 
573
		Map<String, Object> response = new HashMap<>();
574
		response.put("status", true);
575
		response.put("beatsCreated", beatsCreated);
576
		response.put("errors", errors);
577
		response.put("errorMessages", errorMessages);
578
		return responseSender.ok(response);
579
	}
580
 
36644 ranu 581
	// ============ CALENDAR ============
36618 ranu 582
 
583
	@PostMapping(value = "/beatPlan/delete")
584
	public ResponseEntity<?> deleteBeat(@RequestParam String planGroupId) {
36644 ranu 585
		int beatId = Integer.parseInt(planGroupId);
586
		beatRouteRepository.deleteByBeatId(beatId);
587
		beatScheduleRepository.deleteByBeatId(beatId);
588
		Beat beat = beatRepository.selectById(beatId);
589
		if (beat != null) {
590
			beat.setActive(false);
591
		}
36618 ranu 592
 
593
		Map<String, Object> response = new HashMap<>();
594
		response.put("status", true);
595
		response.put("message", "Beat deleted");
596
		return responseSender.ok(response);
597
	}
598
 
599
	@GetMapping(value = "/beatPlan/calendar")
600
	public ResponseEntity<?> getCalendar(
601
			@RequestParam int authUserId,
602
			@RequestParam String month) {
603
 
604
		YearMonth ym = YearMonth.parse(month);
605
		LocalDate startDate = ym.atDay(1);
606
		LocalDate endDate = ym.atEndOfMonth();
607
 
608
		List<PublicHolidays> holidays = publicHolidaysRepository.selectAllBetweenDates(startDate, endDate);
609
		List<Map<String, String>> holidayList = holidays.stream().map(h -> {
610
			Map<String, String> m = new HashMap<>();
611
			m.put("date", h.getDate().toString());
612
			m.put("occasion", h.getOccasion());
613
			return m;
614
		}).collect(Collectors.toList());
615
 
36644 ranu 616
		List<Beat> allBeats = beatRepository.selectActiveByAuthUserId(authUserId);
36618 ranu 617
		LocalDate today = LocalDate.now();
618
		List<Map<String, Object>> scheduledBeats = new ArrayList<>();
619
 
36644 ranu 620
		for (Beat beat : allBeats) {
621
			List<BeatSchedule> schedules = beatScheduleRepository.selectByBeatId(beat.getId());
622
			List<BeatRoute> routes = beatRouteRepository.selectByBeatId(beat.getId());
36618 ranu 623
 
36644 ranu 624
			boolean allNullDates = schedules.isEmpty() || schedules.stream().allMatch(s -> s.getStartDate().getYear() == 9999);
625
			boolean hasToday = !allNullDates && schedules.stream().anyMatch(s -> s.getStartDate().equals(today));
626
			boolean allPast = !allNullDates && schedules.stream().filter(s -> s.getStartDate().getYear() != 9999).allMatch(s -> s.getStartDate().isBefore(today));
627
			boolean allFuture = !allNullDates && schedules.stream().filter(s -> s.getStartDate().getYear() != 9999).allMatch(s -> s.getStartDate().isAfter(today));
36618 ranu 628
 
629
			String status;
630
			if (allNullDates) status = "unscheduled";
631
			else if (hasToday) status = "running";
632
			else if (allPast) status = "completed";
36644 ranu 633
			else status = "scheduled";
36618 ranu 634
 
36644 ranu 635
			Map<String, Object> beatInfo = new HashMap<>();
636
			beatInfo.put("planGroupId", String.valueOf(beat.getId()));
637
			beatInfo.put("beatName", beat.getName() != null ? beat.getName() : "Beat");
638
			beatInfo.put("beatColor", beat.getBeatColor() != null ? beat.getBeatColor() : "#3498DB");
639
			beatInfo.put("status", status);
36618 ranu 640
 
641
			List<Map<String, Object>> dayInfoList = new ArrayList<>();
36644 ranu 642
			for (BeatSchedule s : schedules) {
36618 ranu 643
				Map<String, Object> dayInfo = new HashMap<>();
36644 ranu 644
				dayInfo.put("dayNumber", s.getDayNumber());
645
				boolean isUnscheduled = s.getStartDate().getYear() == 9999;
646
				dayInfo.put("planDate", isUnscheduled ? null : s.getStartDate().toString());
647
				dayInfo.put("totalKm", s.getTotalDistanceKm());
648
				dayInfo.put("totalMins", s.getTotalTimeMins());
649
				long visitCount = routes.stream().filter(r -> r.getDayNumber() == s.getDayNumber()).count();
650
				dayInfo.put("visitCount", (int) visitCount);
36618 ranu 651
				dayInfoList.add(dayInfo);
652
			}
36644 ranu 653
			if (schedules.isEmpty()) {
654
				// No schedule at all — show from routes
655
				Map<Integer, Long> dayCounts = routes.stream()
656
						.collect(Collectors.groupingBy(BeatRoute::getDayNumber, Collectors.counting()));
657
				for (int d = 1; d <= beat.getTotalDays(); d++) {
658
					Map<String, Object> dayInfo = new HashMap<>();
659
					dayInfo.put("dayNumber", d);
660
					dayInfo.put("planDate", null);
661
					dayInfo.put("totalKm", null);
662
					dayInfo.put("totalMins", null);
663
					dayInfo.put("visitCount", dayCounts.getOrDefault(d, 0L).intValue());
664
					dayInfoList.add(dayInfo);
665
				}
666
			}
667
			beatInfo.put("days", dayInfoList);
668
			scheduledBeats.add(beatInfo);
36618 ranu 669
		}
670
 
671
		Set<String> blockedDates = new HashSet<>();
672
		for (LocalDate d = startDate; !d.isAfter(endDate); d = d.plusDays(1)) {
36644 ranu 673
			if (d.getDayOfWeek() == DayOfWeek.SUNDAY) blockedDates.add(d.toString());
36618 ranu 674
		}
36644 ranu 675
		for (PublicHolidays h : holidays) blockedDates.add(h.getDate().toString());
36618 ranu 676
 
677
		Map<String, Object> response = new HashMap<>();
678
		response.put("holidays", holidayList);
679
		response.put("scheduledBeats", scheduledBeats);
680
		response.put("blockedDates", blockedDates);
681
		return responseSender.ok(response);
682
	}
683
 
684
	@PostMapping(value = "/beatPlan/scheduleOnCalendar")
685
	public ResponseEntity<?> scheduleOnCalendar(
686
			HttpServletRequest request,
687
			@RequestParam String planGroupId,
688
			@RequestParam String dates,
689
			@RequestParam(required = false) String beatName,
690
			@RequestParam(required = false) String beatColor) throws Exception {
691
 
36644 ranu 692
		int beatId = Integer.parseInt(planGroupId);
36618 ranu 693
		Gson gson = new Gson();
694
		List<String> dateList = gson.fromJson(dates, new TypeToken<List<String>>() {
695
		}.getType());
696
 
36644 ranu 697
		Beat beat = beatRepository.selectById(beatId);
698
		if (beat == null) return responseSender.badRequest("Beat not found");
36618 ranu 699
 
36644 ranu 700
		if (beatName != null) beat.setName(beatName);
701
		if (beatColor != null && !beatColor.isEmpty()) beat.setBeatColor(beatColor);
36618 ranu 702
 
36644 ranu 703
		// Delete old schedules and create new
704
		beatScheduleRepository.deleteByBeatId(beatId);
705
		LocalDate schEndDate = dateList.isEmpty() ? null : LocalDate.parse(dateList.get(dateList.size() - 1));
706
		for (int i = 0; i < dateList.size() && i < beat.getTotalDays(); i++) {
707
			BeatSchedule schedule = new BeatSchedule();
708
			schedule.setBeatId(beatId);
709
			schedule.setStartDate(LocalDate.parse(dateList.get(i)));
710
			schedule.setEndDate(schEndDate);
711
			schedule.setDayNumber(i + 1);
712
			schedule.setEndAction(i == dateList.size() - 1 ? "HOME" : "DAYBREAK");
713
			schedule.setCreatedTimestamp(LocalDateTime.now());
714
			beatScheduleRepository.persist(schedule);
36618 ranu 715
		}
716
 
717
		Map<String, Object> response = new HashMap<>();
718
		response.put("status", true);
719
		response.put("message", "Beat scheduled successfully");
720
		return responseSender.ok(response);
721
	}
722
 
36644 ranu 723
	// Drag-drop scheduling — adds schedule dates to the EXISTING beat (no new beat created)
36618 ranu 724
	@PostMapping(value = "/beatPlan/repeatBeat")
725
	public ResponseEntity<?> repeatBeat(
726
			HttpServletRequest request,
727
			@RequestParam String sourcePlanGroupId,
728
			@RequestParam int authUserId,
729
			@RequestParam String dates) throws Exception {
730
 
36644 ranu 731
		int beatId = Integer.parseInt(sourcePlanGroupId);
36618 ranu 732
		Gson gson = new Gson();
733
		List<String> dateList = gson.fromJson(dates, new TypeToken<List<String>>() {
734
		}.getType());
735
 
36644 ranu 736
		Beat beat = beatRepository.selectById(beatId);
737
		if (beat == null) return responseSender.badRequest("Beat not found");
36618 ranu 738
 
36644 ranu 739
		// Remove placeholder (unscheduled) schedule rows
740
		List<BeatSchedule> existing = beatScheduleRepository.selectByBeatId(beatId);
741
		for (BeatSchedule s : existing) {
742
			if (s.getStartDate() != null && s.getStartDate().getYear() == 9999) {
743
				beatScheduleRepository.delete(s);
744
			}
36618 ranu 745
		}
746
 
36644 ranu 747
		// Add new real-date schedule rows for the existing beat
748
		LocalDate repeatEndDate = dateList.isEmpty() ? null : LocalDate.parse(dateList.get(dateList.size() - 1));
749
		for (int i = 0; i < dateList.size(); i++) {
750
			BeatSchedule schedule = new BeatSchedule();
751
			schedule.setBeatId(beatId);
752
			schedule.setStartDate(LocalDate.parse(dateList.get(i)));
753
			schedule.setEndDate(repeatEndDate);
754
			schedule.setDayNumber(i + 1);
755
			schedule.setEndAction(i == dateList.size() - 1 ? "HOME" : "DAYBREAK");
756
			schedule.setCreatedTimestamp(LocalDateTime.now());
757
			beatScheduleRepository.persist(schedule);
36618 ranu 758
		}
759
 
760
		Map<String, Object> response = new HashMap<>();
761
		response.put("status", true);
36644 ranu 762
		response.put("planGroupId", String.valueOf(beatId));
763
		response.put("message", "Beat scheduled successfully");
36618 ranu 764
		return responseSender.ok(response);
765
	}
766
 
767
	@GetMapping(value = "/beatPlan/availableSlots")
768
	public ResponseEntity<?> getAvailableSlots(
769
			@RequestParam int authUserId,
770
			@RequestParam String month,
771
			@RequestParam int daysNeeded) {
772
 
773
		YearMonth ym = YearMonth.parse(month);
774
		LocalDate startDate = ym.atDay(1);
775
		LocalDate endDate = ym.atEndOfMonth();
776
		LocalDate today = LocalDate.now();
777
 
778
		Set<LocalDate> blocked = new HashSet<>();
779
		for (LocalDate d = startDate; !d.isAfter(endDate); d = d.plusDays(1)) {
780
			if (d.getDayOfWeek() == DayOfWeek.SUNDAY) blocked.add(d);
36644 ranu 781
			if (!d.isAfter(today)) blocked.add(d);
36618 ranu 782
		}
783
 
784
		List<PublicHolidays> holidays = publicHolidaysRepository.selectAllBetweenDates(startDate, endDate);
785
		for (PublicHolidays h : holidays) blocked.add(h.getDate());
786
 
36644 ranu 787
		// Get all scheduled dates for this user
788
		List<Beat> userBeats = beatRepository.selectActiveByAuthUserId(authUserId);
789
		for (Beat b : userBeats) {
790
			List<BeatSchedule> schedules = beatScheduleRepository.selectByBeatId(b.getId());
791
			for (BeatSchedule s : schedules) blocked.add(s.getStartDate());
36618 ranu 792
		}
793
 
794
		List<String> available = new ArrayList<>();
795
		for (LocalDate d = startDate.isAfter(today) ? startDate : today.plusDays(1);
796
			 !d.isAfter(endDate) && available.size() < daysNeeded;
797
			 d = d.plusDays(1)) {
36644 ranu 798
			if (!blocked.contains(d)) available.add(d.toString());
36618 ranu 799
		}
800
 
801
		Map<String, Object> response = new HashMap<>();
802
		response.put("suggestedDates", available);
803
		response.put("totalAvailable", available.size());
804
		return responseSender.ok(response);
805
	}
806
 
807
	// --- Sorting helpers ---
808
 
809
	private List<Map<String, Object>> sortByNearestNeighborFromStart(
810
			List<Map<String, Object>> partners, double startLat, double startLng) {
811
		List<Map<String, Object>> withCoords = new ArrayList<>();
812
		List<Map<String, Object>> withoutCoords = new ArrayList<>();
813
		for (Map<String, Object> p : partners) {
36644 ranu 814
			if (hasValidCoords(p)) withCoords.add(p);
815
			else withoutCoords.add(p);
36618 ranu 816
		}
817
		List<Map<String, Object>> sorted = new ArrayList<>();
36644 ranu 818
		double currentLat = startLat, currentLng = startLng;
36618 ranu 819
		while (!withCoords.isEmpty()) {
820
			int nearestIdx = 0;
821
			double nearestDist = Double.MAX_VALUE;
822
			for (int i = 0; i < withCoords.size(); i++) {
36644 ranu 823
				double dist = haversine(currentLat, currentLng,
824
						Double.parseDouble(withCoords.get(i).get("latitude").toString()),
825
						Double.parseDouble(withCoords.get(i).get("longitude").toString()));
36618 ranu 826
				if (dist < nearestDist) {
827
					nearestDist = dist;
828
					nearestIdx = i;
829
				}
830
			}
831
			Map<String, Object> nearest = withCoords.remove(nearestIdx);
832
			sorted.add(nearest);
833
			currentLat = Double.parseDouble(nearest.get("latitude").toString());
834
			currentLng = Double.parseDouble(nearest.get("longitude").toString());
835
		}
836
		sorted.addAll(withoutCoords);
837
		return sorted;
838
	}
839
 
840
	private List<Map<String, Object>> sortByNearestNeighbor(List<Map<String, Object>> partners) {
841
		List<Map<String, Object>> withCoords = new ArrayList<>();
842
		List<Map<String, Object>> withoutCoords = new ArrayList<>();
843
		for (Map<String, Object> p : partners) {
36644 ranu 844
			if (hasValidCoords(p)) withCoords.add(p);
845
			else withoutCoords.add(p);
36618 ranu 846
		}
847
		List<Map<String, Object>> sorted = new ArrayList<>();
848
		if (!withCoords.isEmpty()) {
849
			sorted.add(withCoords.remove(0));
850
			while (!withCoords.isEmpty()) {
851
				Map<String, Object> last = sorted.get(sorted.size() - 1);
852
				double lastLat = Double.parseDouble(last.get("latitude").toString());
853
				double lastLng = Double.parseDouble(last.get("longitude").toString());
854
				int nearestIdx = 0;
855
				double nearestDist = Double.MAX_VALUE;
856
				for (int i = 0; i < withCoords.size(); i++) {
36644 ranu 857
					double dist = haversine(lastLat, lastLng,
858
							Double.parseDouble(withCoords.get(i).get("latitude").toString()),
859
							Double.parseDouble(withCoords.get(i).get("longitude").toString()));
36618 ranu 860
					if (dist < nearestDist) {
861
						nearestDist = dist;
862
						nearestIdx = i;
863
					}
864
				}
865
				sorted.add(withCoords.remove(nearestIdx));
866
			}
867
		}
868
		sorted.addAll(withoutCoords);
869
		return sorted;
870
	}
871
 
872
	private boolean hasValidCoords(Map<String, Object> p) {
873
		Object lat = p.get("latitude");
874
		Object lng = p.get("longitude");
36644 ranu 875
		return lat != null && lng != null && !lat.toString().isEmpty() && !lng.toString().isEmpty();
36618 ranu 876
	}
877
 
878
	private double haversine(double lat1, double lng1, double lat2, double lng2) {
879
		double R = 6371;
880
		double dLat = Math.toRadians(lat2 - lat1);
881
		double dLng = Math.toRadians(lng2 - lng1);
882
		double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
883
				+ Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
884
				* Math.sin(dLng / 2) * Math.sin(dLng / 2);
885
		double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
886
		return R * c;
887
	}
888
}