Subversion Repositories SmartDukaan

Rev

Rev 36644 | Rev 36651 | 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
36650 ranu 66
	private com.spice.profitmandi.dao.service.BeatPlanQueryService beatPlanQueryService;
67
	@Autowired
36618 ranu 68
	private AuthUserLocationRepository authUserLocationRepository;
69
	@Autowired
70
	private LeadRepository leadRepository;
71
	@Autowired
72
	private PublicHolidaysRepository publicHolidaysRepository;
73
	@Autowired
74
	private com.spice.profitmandi.service.GeocodingService geocodingService;
75
	@Autowired
76
	private CookiesProcessor cookiesProcessor;
77
	@Autowired
78
	private ResponseSender responseSender;
79
 
80
	@GetMapping(value = "/beatPlan")
81
	public String beatPlan(HttpServletRequest request, Model model) {
82
		EscalationType[] escalationTypes = EscalationType.values();
83
		model.addAttribute("escalationTypes", escalationTypes);
84
		return "beat-plan";
85
	}
86
 
87
	@GetMapping(value = "/beatPlanWindow")
88
	public String beatPlanWindow(HttpServletRequest request, Model model) {
89
		EscalationType[] escalationTypes = EscalationType.values();
90
		model.addAttribute("escalationTypes", escalationTypes);
91
		return "beat-plan-window";
92
	}
93
 
36650 ranu 94
	@Autowired
95
	private com.spice.profitmandi.dao.repository.dtr.LeadLiveLocationRepository leadLiveLocationRepositoryAuto;
96
 
97
	// ====================== DAY VIEW ======================
98
	// Inline page (loaded into dashboard #main-content): tabular list of all beats
99
	// scheduled in a date range across all users. Each row has a View button that
100
	// opens that user's calendar in a modal.
101
	@GetMapping(value = "/beatPlan/dayView")
102
	public String beatPlanDayView(HttpServletRequest request, Model model) {
103
		EscalationType[] escalationTypes = EscalationType.values();
104
		model.addAttribute("escalationTypes", escalationTypes);
105
		return "beat-plan-day-view";
106
	}
107
 
108
	// Tabular JSON: one row per (beat, scheduled date) in [startDate, endDate].
109
	@GetMapping(value = "/beatPlan/scheduledList")
110
	public ResponseEntity<?> scheduledList(
111
			@RequestParam(required = false) String startDate,
112
			@RequestParam(required = false) String endDate) {
113
 
114
		LocalDate start, end;
115
		try {
116
			start = (startDate == null || startDate.isEmpty()) ? LocalDate.now() : LocalDate.parse(startDate);
117
			end = (endDate == null || endDate.isEmpty()) ? start.plusDays(7) : LocalDate.parse(endDate);
118
		} catch (Exception e) {
119
			return responseSender.badRequest("Invalid date — expected yyyy-MM-dd");
120
		}
121
 
122
		List<com.spice.profitmandi.dao.model.BeatDayDetails> beats =
123
				beatPlanQueryService.getAllScheduledBeats(start, end);
124
 
125
		// Resolve user names in bulk
126
		Set<Integer> userIds = beats.stream()
127
				.map(com.spice.profitmandi.dao.model.BeatDayDetails::getAuthUserId)
128
				.collect(java.util.stream.Collectors.toSet());
129
		Map<Integer, AuthUser> userMap = new HashMap<>();
130
		if (!userIds.isEmpty()) {
131
			authRepository.selectByIds(new ArrayList<>(userIds))
132
					.forEach(u -> userMap.put(u.getId(), u));
133
		}
134
 
135
		List<Map<String, Object>> rows = new ArrayList<>();
136
		for (com.spice.profitmandi.dao.model.BeatDayDetails b : beats) {
137
			AuthUser u = userMap.get(b.getAuthUserId());
138
			Map<String, Object> row = new HashMap<>();
139
			row.put("authUserId", b.getAuthUserId());
140
			row.put("userName", u != null ? (u.getFirstName() + " " + u.getLastName()) : "User #" + b.getAuthUserId());
141
			row.put("scheduleDate", b.getScheduleDate().toString());
142
			row.put("dayNumber", b.getDayNumber());
143
			row.put("beatId", b.getBeatId());
144
			row.put("beatName", b.getBeatName());
145
			row.put("beatColor", b.getBeatColor());
146
			row.put("partnerCount", b.getPartnerStops().size());
147
			row.put("leadCount", b.getLeadStops().size());
148
			row.put("visitCount", b.getPartnerStops().size() + b.getLeadStops().size());
149
			rows.add(row);
150
		}
151
 
152
		Map<String, Object> result = new HashMap<>();
153
		result.put("rows", rows);
154
		result.put("startDate", start.toString());
155
		result.put("endDate", end.toString());
156
		return responseSender.ok(result);
157
	}
158
 
159
	// JSON: beats running for (authUserId, date) — enriched with partner/lead names & coords
160
	@GetMapping(value = "/beatPlan/dayViewData")
161
	public ResponseEntity<?> beatPlanDayViewData(
162
			@RequestParam int authUserId,
163
			@RequestParam String date) throws ProfitMandiBusinessException {
164
 
165
		LocalDate localDate;
166
		try {
167
			localDate = LocalDate.parse(date);
168
		} catch (Exception e) {
169
			return responseSender.badRequest("Invalid date — expected yyyy-MM-dd");
170
		}
171
 
172
		List<com.spice.profitmandi.dao.model.BeatDayDetails> beats =
173
				beatPlanQueryService.getBeatsForUserOnDate(authUserId, localDate);
174
 
175
		// Collect all partner & lead IDs to fetch metadata in bulk
176
		Set<Integer> partnerIds = new HashSet<>();
177
		Set<Integer> leadIds = new HashSet<>();
178
		for (com.spice.profitmandi.dao.model.BeatDayDetails b : beats) {
179
			b.getPartnerStops().forEach(s -> partnerIds.add((Integer) s.get("fofoId")));
180
			b.getLeadStops().forEach(s -> leadIds.add((Integer) s.get("leadId")));
181
		}
182
 
183
		// Partners: name + geocoded lat/lng (geocoder is cached in Redis)
184
		Map<Integer, CustomRetailer> retailerMap = partnerIds.isEmpty()
185
				? new HashMap<>()
186
				: retailerService.getFofoRetailers(new ArrayList<>(partnerIds));
187
		Map<Integer, FofoStore> storeMap = new HashMap<>();
188
		if (!partnerIds.isEmpty()) {
189
			fofoStoreRepository.selectByRetailerIds(new ArrayList<>(partnerIds))
190
					.forEach(fs -> storeMap.put(fs.getId(), fs));
191
		}
192
 
193
		// Leads: name + geo
194
		Map<Integer, com.spice.profitmandi.dao.entity.user.Lead> leadMap = new HashMap<>();
195
		Map<Integer, com.spice.profitmandi.dao.entity.user.LeadLiveLocation> leadGeoMap = new HashMap<>();
196
		for (int leadId : leadIds) {
197
			com.spice.profitmandi.dao.entity.user.Lead l = leadRepository.selectById(leadId);
198
			if (l != null) leadMap.put(leadId, l);
199
			com.spice.profitmandi.dao.entity.user.LeadLiveLocation lg =
200
					leadLiveLocationRepositoryAuto.selectApprovedByLeadId(leadId);
201
			if (lg != null) leadGeoMap.put(leadId, lg);
202
		}
203
 
204
		// Enrich each stop
205
		List<Map<String, Object>> out = new ArrayList<>();
206
		for (com.spice.profitmandi.dao.model.BeatDayDetails b : beats) {
207
			Map<String, Object> beatJson = new HashMap<>();
208
			beatJson.put("beatId", b.getBeatId());
209
			beatJson.put("beatName", b.getBeatName());
210
			beatJson.put("beatColor", b.getBeatColor());
211
			beatJson.put("dayNumber", b.getDayNumber());
212
			beatJson.put("scheduleDate", b.getScheduleDate().toString());
213
			beatJson.put("endAction", b.getEndAction());
214
			beatJson.put("totalDistanceKm", b.getTotalDistanceKm());
215
			beatJson.put("totalTimeMins", b.getTotalTimeMins());
216
			beatJson.put("startLocationName", b.getStartLocationName());
217
			beatJson.put("startLatitude", b.getStartLatitude());
218
			beatJson.put("startLongitude", b.getStartLongitude());
219
 
220
			List<Map<String, Object>> stops = new ArrayList<>();
221
			// Partners
222
			for (Map<String, Object> ps : b.getPartnerStops()) {
223
				int fofoId = (Integer) ps.get("fofoId");
224
				Map<String, Object> stop = new HashMap<>();
225
				stop.put("type", "partner");
226
				stop.put("id", fofoId);
227
				stop.put("sequenceOrder", ps.get("sequenceOrder"));
228
				FofoStore fs = storeMap.get(fofoId);
229
				CustomRetailer cr = retailerMap.get(fofoId);
230
				stop.put("code", fs != null ? fs.getCode() : null);
231
				stop.put("name", fs != null && fs.getOutletName() != null ? fs.getOutletName()
232
						: (cr != null ? cr.getBusinessName() : "Store #" + fofoId));
233
				if (cr != null && cr.getAddress() != null) {
234
					stop.put("address", cr.getAddress().getAddressString());
235
					try {
236
						String geoAddr = com.spice.profitmandi.service.GeocodingService.buildGeoAddress(
237
								cr.getAddress().getLine1(), cr.getAddress().getCity(),
238
								cr.getAddress().getState(), cr.getAddress().getPinCode());
239
						double[] coords = geocodingService.geocodeAddress(geoAddr);
240
						if (coords != null) {
241
							stop.put("lat", coords[0]);
242
							stop.put("lng", coords[1]);
243
						}
244
					} catch (Exception ignored) {
245
					}
246
				}
247
				stops.add(stop);
248
			}
249
			// Leads
250
			for (Map<String, Object> ls : b.getLeadStops()) {
251
				int leadId = (Integer) ls.get("leadId");
252
				Map<String, Object> stop = new HashMap<>();
253
				stop.put("type", "lead");
254
				stop.put("id", leadId);
255
				stop.put("sequenceOrder", ls.get("sequenceOrder"));
256
				stop.put("nearestStoreId", ls.get("nearestStoreId"));
257
				com.spice.profitmandi.dao.entity.user.Lead l = leadMap.get(leadId);
258
				stop.put("name", l != null ? l.getFirstName() + " " + l.getLastName() : "Lead #" + leadId);
259
				stop.put("mobile", l != null ? l.getLeadMobile() : null);
260
				stop.put("city", l != null ? l.getCity() : null);
261
				com.spice.profitmandi.dao.entity.user.LeadLiveLocation lg = leadGeoMap.get(leadId);
262
				if (lg != null) {
263
					stop.put("lat", lg.getLatitude());
264
					stop.put("lng", lg.getLongitude());
265
				}
266
				stops.add(stop);
267
			}
268
			beatJson.put("stops", stops);
269
			beatJson.put("partnerCount", b.getPartnerStops().size());
270
			beatJson.put("leadCount", b.getLeadStops().size());
271
			out.add(beatJson);
272
		}
273
 
274
		Map<String, Object> result = new HashMap<>();
275
		result.put("beats", out);
276
		return responseSender.ok(result);
277
	}
278
 
36618 ranu 279
	@GetMapping(value = "/beatPlan/getAuthUsers")
280
	public ResponseEntity<?> getAuthUsers(
281
			@RequestParam int categoryId,
282
			@RequestParam EscalationType escalationType) {
283
		List<AuthUser> authUsers = csService.getAuthUserByCategoryId(categoryId, escalationType);
284
		List<Map<String, Object>> result = authUsers.stream()
285
				.filter(au -> au.getActive())
286
				.map(au -> {
287
					Map<String, Object> map = new HashMap<>();
288
					map.put("id", au.getId());
289
					map.put("name", au.getFirstName() + " " + au.getLastName());
290
					return map;
291
				})
292
				.collect(Collectors.toList());
293
		return responseSender.ok(result);
294
	}
295
 
36644 ranu 296
	// Returns visits for a beat.
297
	// - Partner stops (beat_route) belong to the beat template — always returned.
298
	// - Lead stops (lead_route) belong to a specific run — returned ONLY when planDate
299
	//   is given and matches the lead's schedule_date. (No planDate = template view.)
36632 ranu 300
	@GetMapping(value = "/beatPlan/getBeatVisits")
36644 ranu 301
	public ResponseEntity<?> getBeatVisits(
302
			@RequestParam String planGroupId,
303
			@RequestParam(required = false) String planDate) {
304
 
305
		int beatId;
306
		try {
307
			beatId = Integer.parseInt(planGroupId);
308
		} catch (NumberFormatException e) {
309
			return responseSender.ok(new ArrayList<>());
310
		}
311
 
312
		List<BeatRoute> routes = beatRouteRepository.selectByBeatId(beatId);
313
		List<Map<String, Object>> result = new ArrayList<>();
314
 
315
		// Partner stops — always (they belong to the beat template)
316
		for (BeatRoute r : routes) {
36632 ranu 317
			Map<String, Object> map = new HashMap<>();
36644 ranu 318
			map.put("fofoId", r.getFofoId());
319
			map.put("dayNumber", r.getDayNumber());
320
			map.put("sequenceOrder", r.getSequenceOrder());
321
			map.put("visitType", "partner");
322
			result.add(map);
323
		}
324
 
325
		// Lead stops — only for the requested run date
326
		if (planDate != null && !planDate.isEmpty()) {
327
			LocalDate date = LocalDate.parse(planDate);
328
			List<LeadRoute> leads = leadRouteRepository.selectByBeatId(beatId);
329
			for (LeadRoute lr : leads) {
330
				if ("APPROVED".equals(lr.getStatus())
331
						&& lr.getScheduleDate() != null
332
						&& lr.getScheduleDate().equals(date)) {
333
					Map<String, Object> map = new HashMap<>();
334
					map.put("fofoId", lr.getLeadId());
335
					map.put("dayNumber", 1);
336
					map.put("sequenceOrder", lr.getSequenceOrder() != null ? lr.getSequenceOrder() : 999);
337
					map.put("visitType", "lead");
338
					result.add(map);
339
				}
340
			}
341
		}
342
 
343
		// Sort by dayNumber then sequenceOrder
344
		result.sort((a, b) -> {
345
			int cmp = Integer.compare((int) a.get("dayNumber"), (int) b.get("dayNumber"));
346
			return cmp != 0 ? cmp : Integer.compare((int) a.get("sequenceOrder"), (int) b.get("sequenceOrder"));
347
		});
348
 
36632 ranu 349
		return responseSender.ok(result);
350
	}
351
 
36618 ranu 352
	@GetMapping(value = "/beatPlan/getBaseLocation")
353
	public ResponseEntity<?> getBaseLocation(@RequestParam int authUserId) {
354
		AuthUserLocation baseLoc = authUserLocationRepository.selectLatestByAuthUserIdAndType(authUserId, "BASE");
355
		if (baseLoc == null) {
356
			return responseSender.ok(new HashMap<>());
357
		}
358
		Map<String, Object> result = new HashMap<>();
359
		result.put("id", baseLoc.getId());
360
		result.put("locationName", baseLoc.getLocationName());
361
		result.put("latitude", baseLoc.getLatitude());
362
		result.put("longitude", baseLoc.getLongitude());
363
		result.put("address", baseLoc.getAddress());
364
		return responseSender.ok(result);
365
	}
366
 
367
	@PostMapping(value = "/beatPlan/saveBaseLocation")
368
	public ResponseEntity<?> saveBaseLocation(
369
			@RequestParam int authUserId,
370
			@RequestParam String locationName,
371
			@RequestParam String latitude,
372
			@RequestParam String longitude,
373
			@RequestParam(required = false) String address) {
374
		AuthUserLocation loc = new AuthUserLocation();
375
		loc.setAuthUserId(authUserId);
376
		loc.setLocationType("BASE");
377
		loc.setLocationName(locationName);
378
		loc.setLatitude(latitude);
379
		loc.setLongitude(longitude);
380
		loc.setAddress(address);
381
		loc.setCreatedTimestamp(LocalDateTime.now());
382
		authUserLocationRepository.persist(loc);
383
 
384
		Map<String, Object> result = new HashMap<>();
385
		result.put("status", true);
386
		result.put("id", loc.getId());
387
		return responseSender.ok(result);
388
	}
389
 
390
	@GetMapping(value = "/beatPlan/getPartners")
391
	public ResponseEntity<?> getPartners(
392
			@RequestParam int authUserId,
393
			@RequestParam int categoryId,
394
			@RequestParam(required = false) String startLat,
395
			@RequestParam(required = false) String startLng) throws ProfitMandiBusinessException {
396
 
397
		Map<Integer, List<Integer>> pp = csService.getAuthUserIdPartnerIdMapping();
398
		List<Integer> fofoIds = pp.get(authUserId);
399
 
36644 ranu 400
		if (fofoIds == null || fofoIds.isEmpty()) {
36618 ranu 401
			Map<String, Object> empty = new HashMap<>();
402
			empty.put("partners", new ArrayList<>());
403
			return responseSender.ok(empty);
404
		}
405
 
406
		List<FofoStore> fofoStores = fofoStoreRepository.selectByRetailerIds(fofoIds);
407
		Map<Integer, CustomRetailer> retailerMap = retailerService.getFofoRetailers(fofoIds);
408
 
409
		List<Map<String, Object>> partners = new ArrayList<>();
410
		List<String> addressesToGeocode = new ArrayList<>();
411
 
412
		for (FofoStore store : fofoStores) {
413
			if (!store.isActive() || store.isClosed()) continue;
414
			CustomRetailer retailer = retailerMap.get(store.getId());
415
 
416
			Map<String, Object> partnerData = new HashMap<>();
417
			partnerData.put("fofoId", store.getId());
418
			partnerData.put("code", store.getCode());
419
			partnerData.put("outletName", store.getOutletName());
420
			partnerData.put("type", "partner");
421
 
36632 ranu 422
			String geoAddress = null;
36618 ranu 423
			if (retailer != null) {
424
				partnerData.put("businessName", retailer.getBusinessName());
425
				if (retailer.getAddress() != null) {
36644 ranu 426
					partnerData.put("address", retailer.getAddress().getAddressString());
36632 ranu 427
					geoAddress = com.spice.profitmandi.service.GeocodingService.buildGeoAddress(
428
							retailer.getAddress().getLine1(),
429
							retailer.getAddress().getCity(),
430
							retailer.getAddress().getState(),
431
							retailer.getAddress().getPinCode());
36618 ranu 432
				}
433
			}
36632 ranu 434
			addressesToGeocode.add(geoAddress);
36618 ranu 435
			partners.add(partnerData);
436
		}
437
 
36644 ranu 438
		// Geocode in parallel
36618 ranu 439
		java.util.concurrent.ExecutorService executor = java.util.concurrent.Executors.newFixedThreadPool(
36644 ranu 440
				Math.min(10, Math.max(1, partners.size())));
36618 ranu 441
		List<java.util.concurrent.Future<double[]>> futures = new ArrayList<>();
442
		for (String addr : addressesToGeocode) {
443
			futures.add(executor.submit(() -> {
444
				if (addr != null && !addr.isEmpty()) {
445
					try {
446
						return geocodingService.geocodeAddress(addr);
447
					} catch (Exception e) {
448
						return null;
449
					}
450
				}
451
				return null;
452
			}));
453
		}
454
		for (int i = 0; i < partners.size(); i++) {
455
			try {
456
				double[] coords = futures.get(i).get(10, java.util.concurrent.TimeUnit.SECONDS);
457
				if (coords != null) {
36644 ranu 458
					partners.get(i).put("latitude", String.valueOf(coords[0]));
459
					partners.get(i).put("longitude", String.valueOf(coords[1]));
36618 ranu 460
				}
461
			} catch (Exception e) {
36644 ranu 462
				LOGGER.warn("Geocoding timeout/error for partner {}", partners.get(i).get("code"));
36618 ranu 463
			}
464
		}
465
		executor.shutdown();
466
 
467
		if (startLat != null && startLng != null && !startLat.isEmpty() && !startLng.isEmpty()) {
468
			partners = sortByNearestNeighborFromStart(partners, Double.parseDouble(startLat), Double.parseDouble(startLng));
469
		} else {
470
			partners = sortByNearestNeighbor(partners);
471
		}
472
 
473
		Map<String, Object> response = new HashMap<>();
474
		response.put("partners", partners);
475
		return responseSender.ok(response);
476
	}
477
 
478
	@PostMapping(value = "/beatPlan/submitPlan")
479
	public ResponseEntity<?> submitPlan(
480
			HttpServletRequest request,
481
			@RequestParam int authUserId,
482
			@RequestParam String planData) throws Exception {
483
 
484
		LoginDetails loginDetails = cookiesProcessor.getCookiesObject(request);
485
		AuthUser currentUser = authRepository.selectByEmailOrMobile(loginDetails.getEmailId());
486
 
487
		Gson gson = new Gson();
488
		Type type = new TypeToken<Map<String, Object>>() {
489
		}.getType();
490
		Map<String, Object> plan = gson.fromJson(planData, type);
491
 
492
		List<Map<String, Object>> days = (List<Map<String, Object>>) plan.get("days");
493
		List<String> dates = (List<String>) plan.get("dates");
494
 
36644 ranu 495
		String beatName = (plan.get("beatName") != null ? (String) plan.get("beatName") : "Beat").trim();
36618 ranu 496
 
36644 ranu 497
		// Duplicate check — same name + same authUserId = duplicate
498
		List<Beat> existingBeats = beatRepository.selectByAuthUserId(authUserId);
499
		for (Beat existing : existingBeats) {
500
			if (existing.getName() != null && beatName.equalsIgnoreCase(existing.getName().trim())) {
501
				LOGGER.info("Duplicate beat blocked: name='{}' authUserId={} existingId={}", beatName, authUserId, existing.getId());
36618 ranu 502
				Map<String, Object> response = new HashMap<>();
503
				response.put("status", true);
36644 ranu 504
				response.put("planGroupId", String.valueOf(existing.getId()));
36618 ranu 505
				response.put("duplicate", true);
36644 ranu 506
				response.put("message", "Beat '" + beatName + "' already exists");
36618 ranu 507
				return responseSender.ok(response);
508
			}
509
		}
510
 
36644 ranu 511
		String beatColor = BEAT_COLORS[Math.abs(beatName.hashCode()) % BEAT_COLORS.length];
512
		int totalDays = days.size();
36618 ranu 513
 
36644 ranu 514
		// Create Beat master
515
		Beat beat = new Beat();
516
		beat.setName(beatName);
517
		beat.setAuthUserId(authUserId);
518
		beat.setBeatColor(beatColor);
519
		beat.setTotalDays(totalDays);
520
		beat.setActive(true);
521
		beat.setCreatedBy(currentUser.getId());
522
		beat.setCreatedTimestamp(LocalDateTime.now());
523
 
524
		// Set start location from first day
525
		if (!days.isEmpty()) {
526
			Map<String, Object> firstDay = days.get(0);
527
			beat.setStartLocationName((String) firstDay.get("startLocationName"));
528
			beat.setStartLatitude((String) firstDay.get("startLatitude"));
529
			beat.setStartLongitude((String) firstDay.get("startLongitude"));
530
		}
531
		beatRepository.persist(beat);
532
 
533
		// End date of the whole beat = last scheduled day's date
534
		LocalDate beatEndDate = null;
535
		if (dates != null) {
536
			for (int d = dates.size() - 1; d >= 0; d--) {
537
				if (dates.get(d) != null) {
538
					beatEndDate = LocalDate.parse(dates.get(d), DateTimeFormatter.ISO_DATE);
539
					break;
540
				}
541
			}
542
		}
543
 
544
		// Create routes and schedules for each day
36618 ranu 545
		for (int d = 0; d < days.size(); d++) {
546
			Map<String, Object> day = days.get(d);
547
			int dayNumber = d + 1;
548
			LocalDate planDate = (dates != null && d < dates.size() && dates.get(d) != null)
36644 ranu 549
					? LocalDate.parse(dates.get(d), DateTimeFormatter.ISO_DATE) : null;
36618 ranu 550
 
36644 ranu 551
			// Auto-determine end action: last day = HOME, others = DAYBREAK
552
			String endAction = (String) day.get("endAction");
553
			if (endAction == null || endAction.isEmpty()) {
554
				endAction = (dayNumber == totalDays) ? "HOME" : "DAYBREAK";
36618 ranu 555
			}
556
 
36644 ranu 557
			// Always create schedule (even if planDate is null — unscheduled beat)
558
			BeatSchedule schedule = new BeatSchedule();
559
			schedule.setBeatId(beat.getId());
560
			schedule.setStartDate(planDate != null ? planDate : LocalDate.of(9999, 12, 31)); // placeholder for unscheduled
561
			schedule.setEndDate(beatEndDate);
562
			schedule.setDayNumber(dayNumber);
563
			schedule.setEndAction(endAction);
564
			schedule.setStayLocationName((String) day.get("stayLocationName"));
565
			schedule.setStayLatitude((String) day.get("stayLatitude"));
566
			schedule.setStayLongitude((String) day.get("stayLongitude"));
567
			if (day.get("totalDistanceKm") != null)
568
				schedule.setTotalDistanceKm(((Number) day.get("totalDistanceKm")).doubleValue());
569
			if (day.get("totalTimeMins") != null)
570
				schedule.setTotalTimeMins(((Number) day.get("totalTimeMins")).intValue());
571
			schedule.setCreatedTimestamp(LocalDateTime.now());
572
			beatScheduleRepository.persist(schedule);
573
 
574
			// Routes (stops)
36618 ranu 575
			List<Map<String, Object>> visits = (List<Map<String, Object>>) day.get("visits");
576
			if (visits != null) {
577
				for (int i = 0; i < visits.size(); i++) {
578
					Map<String, Object> visit = visits.get(i);
36644 ranu 579
					BeatRoute route = new BeatRoute();
580
					route.setBeatId(beat.getId());
581
					route.setFofoId(((Number) visit.get("id")).intValue());
582
					route.setSequenceOrder(i);
583
					route.setDayNumber(dayNumber);
584
					route.setActive(true);
585
					beatRouteRepository.persist(route);
36618 ranu 586
				}
587
			}
588
		}
589
 
590
		Map<String, Object> response = new HashMap<>();
591
		response.put("status", true);
36644 ranu 592
		response.put("planGroupId", String.valueOf(beat.getId()));
36618 ranu 593
		response.put("message", "Beat plan submitted successfully");
594
		return responseSender.ok(response);
595
	}
596
 
36632 ranu 597
	// ============ BULK UPLOAD ============
598
 
599
	@GetMapping(value = "/beatPlan/bulkUpload")
600
	public String bulkUploadPage(HttpServletRequest request, Model model) {
601
		return "beat-plan-bulk";
602
	}
603
 
604
	@GetMapping(value = "/beatPlan/downloadTemplate")
605
	public ResponseEntity<?> downloadTemplate() {
606
		String csv = "beat_name,auth_user_id,start_date,day_number,partner_codes\n";
607
		csv += "Jaipur East Route,280,2026-06-02,1,\"RJKAI1478,RJBUN1449,RJDEG1443\"\n";
608
		csv += ",280,,2,\"RJALR1362,RJBTR1388\"\n";
609
		csv += ",280,,3,\"RJRSD1518,RJSML356\"\n";
610
		csv += "Agra Circuit,145,2026-06-05,1,\"UPAGR101,UPAGR102\"\n";
611
 
612
		org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders();
613
		headers.add("Content-Disposition", "attachment; filename=beat_plan_template.csv");
614
		headers.add("Content-Type", "text/csv");
615
		return new ResponseEntity<>(csv, headers, org.springframework.http.HttpStatus.OK);
616
	}
617
 
618
	@PostMapping(value = "/beatPlan/bulkUploadProcess")
619
	public ResponseEntity<?> bulkUploadProcess(
620
			HttpServletRequest request,
621
			@RequestParam("file") org.springframework.web.multipart.MultipartFile file,
622
			@RequestParam(value = "includeSundays", defaultValue = "false") boolean includeSundays) throws Exception {
623
 
624
		LoginDetails loginDetails = cookiesProcessor.getCookiesObject(request);
625
		AuthUser currentUser = authRepository.selectByEmailOrMobile(loginDetails.getEmailId());
626
 
627
		java.io.Reader reader = new java.io.InputStreamReader(file.getInputStream());
628
		org.apache.commons.csv.CSVParser parser = new org.apache.commons.csv.CSVParser(reader,
629
				org.apache.commons.csv.CSVFormat.DEFAULT.withFirstRecordAsHeader().withTrim());
630
		List<org.apache.commons.csv.CSVRecord> allRecords = parser.getRecords();
631
		parser.close();
632
 
633
		Map<String, String> lastBeatNameByUser = new HashMap<>();
634
		Map<String, List<org.apache.commons.csv.CSVRecord>> beatGroups = new LinkedHashMap<>();
635
		Map<Long, String> resolvedBeatNames = new HashMap<>();
636
 
637
		for (org.apache.commons.csv.CSVRecord record : allRecords) {
638
			String authId = record.get("auth_user_id").trim();
36644 ranu 639
			String rawName = record.get("beat_name").trim().replaceAll("\\s+", " ");
640
			if (rawName.isEmpty()) rawName = lastBeatNameByUser.getOrDefault(authId, "Beat");
641
			else lastBeatNameByUser.put(authId, rawName);
642
			resolvedBeatNames.put(record.getRecordNumber(), rawName);
643
			beatGroups.computeIfAbsent(rawName + "|" + authId, k -> new ArrayList<>()).add(record);
36632 ranu 644
		}
645
 
646
		List<FofoStore> allStores = fofoStoreRepository.selectAll();
647
		Map<String, Integer> codeToId = new HashMap<>();
36644 ranu 648
		for (FofoStore store : allStores) codeToId.put(store.getCode(), store.getId());
36632 ranu 649
 
650
		LocalDate holidayStart = LocalDate.now();
36644 ranu 651
		List<PublicHolidays> holidays = publicHolidaysRepository.selectAllBetweenDates(holidayStart, holidayStart.plusMonths(6));
36632 ranu 652
		Set<LocalDate> holidayDates = holidays.stream().map(PublicHolidays::getDate).collect(Collectors.toSet());
653
 
36644 ranu 654
		int beatsCreated = 0, errors = 0;
36632 ranu 655
		List<String> errorMessages = new ArrayList<>();
656
 
657
		for (Map.Entry<String, List<org.apache.commons.csv.CSVRecord>> entry : beatGroups.entrySet()) {
658
			try {
659
				String[] keyParts = entry.getKey().split("\\|");
660
				String beatName = keyParts[0];
661
				int authUserId = Integer.parseInt(keyParts[1]);
662
				List<org.apache.commons.csv.CSVRecord> rows = entry.getValue();
663
				rows.sort((a, b) -> Integer.parseInt(a.get("day_number").trim()) - Integer.parseInt(b.get("day_number").trim()));
664
 
665
				String startDateStr = rows.get(0).get("start_date").trim();
36644 ranu 666
				LocalDate startDate = startDateStr.isEmpty() ? null : LocalDate.parse(startDateStr, DateTimeFormatter.ISO_DATE);
36632 ranu 667
 
668
				if (startDate != null && startDate.isBefore(LocalDate.now())) {
36644 ranu 669
					errorMessages.add("Beat '" + beatName + "': start_date in past. Skipped.");
36632 ranu 670
					errors++;
671
					continue;
672
				}
673
 
674
				List<LocalDate> scheduleDates = new ArrayList<>();
675
				if (startDate != null) {
676
					LocalDate d = startDate;
36644 ranu 677
					while (scheduleDates.size() < rows.size()) {
678
						if (holidayDates.contains(d)) {
679
							d = d.plusDays(1);
680
							continue;
36632 ranu 681
						}
36644 ranu 682
						if (d.getDayOfWeek() == DayOfWeek.SUNDAY && !includeSundays) {
683
							d = d.plusDays(1);
684
							continue;
685
						}
686
						scheduleDates.add(d);
36632 ranu 687
						d = d.plusDays(1);
688
					}
689
				}
690
 
36644 ranu 691
				// Duplicate check — skip if a beat with same name already exists for this user
692
				boolean isDuplicate = beatRepository.selectByAuthUserId(authUserId).stream()
693
						.anyMatch(b -> b.getName() != null && beatName.equalsIgnoreCase(b.getName().trim()));
694
				if (isDuplicate) {
695
					errorMessages.add("Beat '" + beatName + "' already exists for user " + authUserId + ". Skipped.");
696
					errors++;
697
					continue;
698
				}
36632 ranu 699
 
36644 ranu 700
				String beatColor = BEAT_COLORS[Math.abs(beatName.hashCode()) % BEAT_COLORS.length];
36632 ranu 701
				AuthUserLocation homeLoc = authUserLocationRepository.selectLatestByAuthUserIdAndType(authUserId, "BASE");
702
 
36644 ranu 703
				// Create Beat master
704
				Beat beat = new Beat();
705
				beat.setName(beatName);
706
				beat.setAuthUserId(authUserId);
707
				beat.setBeatColor(beatColor);
708
				beat.setTotalDays(rows.size());
709
				beat.setStartLocationName(homeLoc != null ? homeLoc.getLocationName() : "Home");
710
				beat.setStartLatitude(homeLoc != null ? homeLoc.getLatitude() : null);
711
				beat.setStartLongitude(homeLoc != null ? homeLoc.getLongitude() : null);
712
				beat.setActive(true);
713
				beat.setCreatedBy(currentUser.getId());
714
				beat.setCreatedTimestamp(LocalDateTime.now());
715
				beatRepository.persist(beat);
36632 ranu 716
 
36644 ranu 717
				for (int rowIdx = 0; rowIdx < rows.size(); rowIdx++) {
36632 ranu 718
					org.apache.commons.csv.CSVRecord row = rows.get(rowIdx);
719
					int dayNumber = Integer.parseInt(row.get("day_number").trim());
720
					LocalDate planDate = (rowIdx < scheduleDates.size()) ? scheduleDates.get(rowIdx) : null;
36644 ranu 721
					LocalDate bulkEndDate = scheduleDates.isEmpty() ? null : scheduleDates.get(scheduleDates.size() - 1);
36632 ranu 722
 
36644 ranu 723
					// Always create schedule — placeholder date (9999-12-31) when unscheduled
724
					BeatSchedule schedule = new BeatSchedule();
725
					schedule.setBeatId(beat.getId());
726
					schedule.setStartDate(planDate != null ? planDate : LocalDate.of(9999, 12, 31));
727
					schedule.setEndDate(bulkEndDate);
728
					schedule.setDayNumber(dayNumber);
729
					schedule.setEndAction(rowIdx == rows.size() - 1 ? "HOME" : "DAYBREAK");
730
					schedule.setCreatedTimestamp(LocalDateTime.now());
731
					beatScheduleRepository.persist(schedule);
36632 ranu 732
 
36644 ranu 733
					String[] partnerCodes = row.get("partner_codes").trim().split(",");
36632 ranu 734
					for (int i = 0; i < partnerCodes.length; i++) {
36644 ranu 735
						String code = partnerCodes[i].trim();
736
						if (code.isEmpty()) continue;
737
						Integer fofoId = codeToId.get(code);
36632 ranu 738
						if (fofoId == null) {
36644 ranu 739
							errorMessages.add("Code not found: " + code);
36632 ranu 740
							errors++;
741
							continue;
742
						}
743
 
36644 ranu 744
						BeatRoute route = new BeatRoute();
745
						route.setBeatId(beat.getId());
746
						route.setFofoId(fofoId);
747
						route.setSequenceOrder(i);
748
						route.setDayNumber(dayNumber);
749
						route.setActive(true);
750
						beatRouteRepository.persist(route);
36632 ranu 751
					}
752
				}
753
				beatsCreated++;
754
			} catch (Exception e) {
755
				errors++;
36644 ranu 756
				errorMessages.add("Error: " + entry.getKey() + " - " + e.getMessage());
36632 ranu 757
			}
758
		}
759
 
760
		Map<String, Object> response = new HashMap<>();
761
		response.put("status", true);
762
		response.put("beatsCreated", beatsCreated);
763
		response.put("errors", errors);
764
		response.put("errorMessages", errorMessages);
765
		return responseSender.ok(response);
766
	}
767
 
36644 ranu 768
	// ============ CALENDAR ============
36618 ranu 769
 
770
	@PostMapping(value = "/beatPlan/delete")
771
	public ResponseEntity<?> deleteBeat(@RequestParam String planGroupId) {
36644 ranu 772
		int beatId = Integer.parseInt(planGroupId);
773
		beatRouteRepository.deleteByBeatId(beatId);
774
		beatScheduleRepository.deleteByBeatId(beatId);
775
		Beat beat = beatRepository.selectById(beatId);
776
		if (beat != null) {
777
			beat.setActive(false);
778
		}
36618 ranu 779
 
780
		Map<String, Object> response = new HashMap<>();
781
		response.put("status", true);
782
		response.put("message", "Beat deleted");
783
		return responseSender.ok(response);
784
	}
785
 
786
	@GetMapping(value = "/beatPlan/calendar")
787
	public ResponseEntity<?> getCalendar(
788
			@RequestParam int authUserId,
789
			@RequestParam String month) {
790
 
791
		YearMonth ym = YearMonth.parse(month);
792
		LocalDate startDate = ym.atDay(1);
793
		LocalDate endDate = ym.atEndOfMonth();
794
 
795
		List<PublicHolidays> holidays = publicHolidaysRepository.selectAllBetweenDates(startDate, endDate);
796
		List<Map<String, String>> holidayList = holidays.stream().map(h -> {
797
			Map<String, String> m = new HashMap<>();
798
			m.put("date", h.getDate().toString());
799
			m.put("occasion", h.getOccasion());
800
			return m;
801
		}).collect(Collectors.toList());
802
 
36644 ranu 803
		List<Beat> allBeats = beatRepository.selectActiveByAuthUserId(authUserId);
36618 ranu 804
		LocalDate today = LocalDate.now();
805
		List<Map<String, Object>> scheduledBeats = new ArrayList<>();
806
 
36644 ranu 807
		for (Beat beat : allBeats) {
808
			List<BeatSchedule> schedules = beatScheduleRepository.selectByBeatId(beat.getId());
809
			List<BeatRoute> routes = beatRouteRepository.selectByBeatId(beat.getId());
36618 ranu 810
 
36644 ranu 811
			boolean allNullDates = schedules.isEmpty() || schedules.stream().allMatch(s -> s.getStartDate().getYear() == 9999);
812
			boolean hasToday = !allNullDates && schedules.stream().anyMatch(s -> s.getStartDate().equals(today));
813
			boolean allPast = !allNullDates && schedules.stream().filter(s -> s.getStartDate().getYear() != 9999).allMatch(s -> s.getStartDate().isBefore(today));
814
			boolean allFuture = !allNullDates && schedules.stream().filter(s -> s.getStartDate().getYear() != 9999).allMatch(s -> s.getStartDate().isAfter(today));
36618 ranu 815
 
816
			String status;
817
			if (allNullDates) status = "unscheduled";
818
			else if (hasToday) status = "running";
819
			else if (allPast) status = "completed";
36644 ranu 820
			else status = "scheduled";
36618 ranu 821
 
36644 ranu 822
			Map<String, Object> beatInfo = new HashMap<>();
823
			beatInfo.put("planGroupId", String.valueOf(beat.getId()));
824
			beatInfo.put("beatName", beat.getName() != null ? beat.getName() : "Beat");
825
			beatInfo.put("beatColor", beat.getBeatColor() != null ? beat.getBeatColor() : "#3498DB");
826
			beatInfo.put("status", status);
36618 ranu 827
 
828
			List<Map<String, Object>> dayInfoList = new ArrayList<>();
36644 ranu 829
			for (BeatSchedule s : schedules) {
36618 ranu 830
				Map<String, Object> dayInfo = new HashMap<>();
36644 ranu 831
				dayInfo.put("dayNumber", s.getDayNumber());
832
				boolean isUnscheduled = s.getStartDate().getYear() == 9999;
833
				dayInfo.put("planDate", isUnscheduled ? null : s.getStartDate().toString());
834
				dayInfo.put("totalKm", s.getTotalDistanceKm());
835
				dayInfo.put("totalMins", s.getTotalTimeMins());
836
				long visitCount = routes.stream().filter(r -> r.getDayNumber() == s.getDayNumber()).count();
837
				dayInfo.put("visitCount", (int) visitCount);
36618 ranu 838
				dayInfoList.add(dayInfo);
839
			}
36644 ranu 840
			if (schedules.isEmpty()) {
841
				// No schedule at all — show from routes
842
				Map<Integer, Long> dayCounts = routes.stream()
843
						.collect(Collectors.groupingBy(BeatRoute::getDayNumber, Collectors.counting()));
844
				for (int d = 1; d <= beat.getTotalDays(); d++) {
845
					Map<String, Object> dayInfo = new HashMap<>();
846
					dayInfo.put("dayNumber", d);
847
					dayInfo.put("planDate", null);
848
					dayInfo.put("totalKm", null);
849
					dayInfo.put("totalMins", null);
850
					dayInfo.put("visitCount", dayCounts.getOrDefault(d, 0L).intValue());
851
					dayInfoList.add(dayInfo);
852
				}
853
			}
854
			beatInfo.put("days", dayInfoList);
855
			scheduledBeats.add(beatInfo);
36618 ranu 856
		}
857
 
858
		Set<String> blockedDates = new HashSet<>();
859
		for (LocalDate d = startDate; !d.isAfter(endDate); d = d.plusDays(1)) {
36644 ranu 860
			if (d.getDayOfWeek() == DayOfWeek.SUNDAY) blockedDates.add(d.toString());
36618 ranu 861
		}
36644 ranu 862
		for (PublicHolidays h : holidays) blockedDates.add(h.getDate().toString());
36618 ranu 863
 
864
		Map<String, Object> response = new HashMap<>();
865
		response.put("holidays", holidayList);
866
		response.put("scheduledBeats", scheduledBeats);
867
		response.put("blockedDates", blockedDates);
868
		return responseSender.ok(response);
869
	}
870
 
871
	@PostMapping(value = "/beatPlan/scheduleOnCalendar")
872
	public ResponseEntity<?> scheduleOnCalendar(
873
			HttpServletRequest request,
874
			@RequestParam String planGroupId,
875
			@RequestParam String dates,
876
			@RequestParam(required = false) String beatName,
877
			@RequestParam(required = false) String beatColor) throws Exception {
878
 
36644 ranu 879
		int beatId = Integer.parseInt(planGroupId);
36618 ranu 880
		Gson gson = new Gson();
881
		List<String> dateList = gson.fromJson(dates, new TypeToken<List<String>>() {
882
		}.getType());
883
 
36644 ranu 884
		Beat beat = beatRepository.selectById(beatId);
885
		if (beat == null) return responseSender.badRequest("Beat not found");
36618 ranu 886
 
36644 ranu 887
		if (beatName != null) beat.setName(beatName);
888
		if (beatColor != null && !beatColor.isEmpty()) beat.setBeatColor(beatColor);
36618 ranu 889
 
36644 ranu 890
		// Delete old schedules and create new
891
		beatScheduleRepository.deleteByBeatId(beatId);
892
		LocalDate schEndDate = dateList.isEmpty() ? null : LocalDate.parse(dateList.get(dateList.size() - 1));
893
		for (int i = 0; i < dateList.size() && i < beat.getTotalDays(); i++) {
894
			BeatSchedule schedule = new BeatSchedule();
895
			schedule.setBeatId(beatId);
896
			schedule.setStartDate(LocalDate.parse(dateList.get(i)));
897
			schedule.setEndDate(schEndDate);
898
			schedule.setDayNumber(i + 1);
899
			schedule.setEndAction(i == dateList.size() - 1 ? "HOME" : "DAYBREAK");
900
			schedule.setCreatedTimestamp(LocalDateTime.now());
901
			beatScheduleRepository.persist(schedule);
36618 ranu 902
		}
903
 
904
		Map<String, Object> response = new HashMap<>();
905
		response.put("status", true);
906
		response.put("message", "Beat scheduled successfully");
907
		return responseSender.ok(response);
908
	}
909
 
36644 ranu 910
	// Drag-drop scheduling — adds schedule dates to the EXISTING beat (no new beat created)
36618 ranu 911
	@PostMapping(value = "/beatPlan/repeatBeat")
912
	public ResponseEntity<?> repeatBeat(
913
			HttpServletRequest request,
914
			@RequestParam String sourcePlanGroupId,
915
			@RequestParam int authUserId,
916
			@RequestParam String dates) throws Exception {
917
 
36644 ranu 918
		int beatId = Integer.parseInt(sourcePlanGroupId);
36618 ranu 919
		Gson gson = new Gson();
920
		List<String> dateList = gson.fromJson(dates, new TypeToken<List<String>>() {
921
		}.getType());
922
 
36644 ranu 923
		Beat beat = beatRepository.selectById(beatId);
924
		if (beat == null) return responseSender.badRequest("Beat not found");
36618 ranu 925
 
36644 ranu 926
		// Remove placeholder (unscheduled) schedule rows
927
		List<BeatSchedule> existing = beatScheduleRepository.selectByBeatId(beatId);
928
		for (BeatSchedule s : existing) {
929
			if (s.getStartDate() != null && s.getStartDate().getYear() == 9999) {
930
				beatScheduleRepository.delete(s);
931
			}
36618 ranu 932
		}
933
 
36644 ranu 934
		// Add new real-date schedule rows for the existing beat
935
		LocalDate repeatEndDate = dateList.isEmpty() ? null : LocalDate.parse(dateList.get(dateList.size() - 1));
936
		for (int i = 0; i < dateList.size(); i++) {
937
			BeatSchedule schedule = new BeatSchedule();
938
			schedule.setBeatId(beatId);
939
			schedule.setStartDate(LocalDate.parse(dateList.get(i)));
940
			schedule.setEndDate(repeatEndDate);
941
			schedule.setDayNumber(i + 1);
942
			schedule.setEndAction(i == dateList.size() - 1 ? "HOME" : "DAYBREAK");
943
			schedule.setCreatedTimestamp(LocalDateTime.now());
944
			beatScheduleRepository.persist(schedule);
36618 ranu 945
		}
946
 
947
		Map<String, Object> response = new HashMap<>();
948
		response.put("status", true);
36644 ranu 949
		response.put("planGroupId", String.valueOf(beatId));
950
		response.put("message", "Beat scheduled successfully");
36618 ranu 951
		return responseSender.ok(response);
952
	}
953
 
954
	@GetMapping(value = "/beatPlan/availableSlots")
955
	public ResponseEntity<?> getAvailableSlots(
956
			@RequestParam int authUserId,
957
			@RequestParam String month,
958
			@RequestParam int daysNeeded) {
959
 
960
		YearMonth ym = YearMonth.parse(month);
961
		LocalDate startDate = ym.atDay(1);
962
		LocalDate endDate = ym.atEndOfMonth();
963
		LocalDate today = LocalDate.now();
964
 
965
		Set<LocalDate> blocked = new HashSet<>();
966
		for (LocalDate d = startDate; !d.isAfter(endDate); d = d.plusDays(1)) {
967
			if (d.getDayOfWeek() == DayOfWeek.SUNDAY) blocked.add(d);
36644 ranu 968
			if (!d.isAfter(today)) blocked.add(d);
36618 ranu 969
		}
970
 
971
		List<PublicHolidays> holidays = publicHolidaysRepository.selectAllBetweenDates(startDate, endDate);
972
		for (PublicHolidays h : holidays) blocked.add(h.getDate());
973
 
36644 ranu 974
		// Get all scheduled dates for this user
975
		List<Beat> userBeats = beatRepository.selectActiveByAuthUserId(authUserId);
976
		for (Beat b : userBeats) {
977
			List<BeatSchedule> schedules = beatScheduleRepository.selectByBeatId(b.getId());
978
			for (BeatSchedule s : schedules) blocked.add(s.getStartDate());
36618 ranu 979
		}
980
 
981
		List<String> available = new ArrayList<>();
982
		for (LocalDate d = startDate.isAfter(today) ? startDate : today.plusDays(1);
983
			 !d.isAfter(endDate) && available.size() < daysNeeded;
984
			 d = d.plusDays(1)) {
36644 ranu 985
			if (!blocked.contains(d)) available.add(d.toString());
36618 ranu 986
		}
987
 
988
		Map<String, Object> response = new HashMap<>();
989
		response.put("suggestedDates", available);
990
		response.put("totalAvailable", available.size());
991
		return responseSender.ok(response);
992
	}
993
 
994
	// --- Sorting helpers ---
995
 
996
	private List<Map<String, Object>> sortByNearestNeighborFromStart(
997
			List<Map<String, Object>> partners, double startLat, double startLng) {
998
		List<Map<String, Object>> withCoords = new ArrayList<>();
999
		List<Map<String, Object>> withoutCoords = new ArrayList<>();
1000
		for (Map<String, Object> p : partners) {
36644 ranu 1001
			if (hasValidCoords(p)) withCoords.add(p);
1002
			else withoutCoords.add(p);
36618 ranu 1003
		}
1004
		List<Map<String, Object>> sorted = new ArrayList<>();
36644 ranu 1005
		double currentLat = startLat, currentLng = startLng;
36618 ranu 1006
		while (!withCoords.isEmpty()) {
1007
			int nearestIdx = 0;
1008
			double nearestDist = Double.MAX_VALUE;
1009
			for (int i = 0; i < withCoords.size(); i++) {
36644 ranu 1010
				double dist = haversine(currentLat, currentLng,
1011
						Double.parseDouble(withCoords.get(i).get("latitude").toString()),
1012
						Double.parseDouble(withCoords.get(i).get("longitude").toString()));
36618 ranu 1013
				if (dist < nearestDist) {
1014
					nearestDist = dist;
1015
					nearestIdx = i;
1016
				}
1017
			}
1018
			Map<String, Object> nearest = withCoords.remove(nearestIdx);
1019
			sorted.add(nearest);
1020
			currentLat = Double.parseDouble(nearest.get("latitude").toString());
1021
			currentLng = Double.parseDouble(nearest.get("longitude").toString());
1022
		}
1023
		sorted.addAll(withoutCoords);
1024
		return sorted;
1025
	}
1026
 
1027
	private List<Map<String, Object>> sortByNearestNeighbor(List<Map<String, Object>> partners) {
1028
		List<Map<String, Object>> withCoords = new ArrayList<>();
1029
		List<Map<String, Object>> withoutCoords = new ArrayList<>();
1030
		for (Map<String, Object> p : partners) {
36644 ranu 1031
			if (hasValidCoords(p)) withCoords.add(p);
1032
			else withoutCoords.add(p);
36618 ranu 1033
		}
1034
		List<Map<String, Object>> sorted = new ArrayList<>();
1035
		if (!withCoords.isEmpty()) {
1036
			sorted.add(withCoords.remove(0));
1037
			while (!withCoords.isEmpty()) {
1038
				Map<String, Object> last = sorted.get(sorted.size() - 1);
1039
				double lastLat = Double.parseDouble(last.get("latitude").toString());
1040
				double lastLng = Double.parseDouble(last.get("longitude").toString());
1041
				int nearestIdx = 0;
1042
				double nearestDist = Double.MAX_VALUE;
1043
				for (int i = 0; i < withCoords.size(); i++) {
36644 ranu 1044
					double dist = haversine(lastLat, lastLng,
1045
							Double.parseDouble(withCoords.get(i).get("latitude").toString()),
1046
							Double.parseDouble(withCoords.get(i).get("longitude").toString()));
36618 ranu 1047
					if (dist < nearestDist) {
1048
						nearestDist = dist;
1049
						nearestIdx = i;
1050
					}
1051
				}
1052
				sorted.add(withCoords.remove(nearestIdx));
1053
			}
1054
		}
1055
		sorted.addAll(withoutCoords);
1056
		return sorted;
1057
	}
1058
 
1059
	private boolean hasValidCoords(Map<String, Object> p) {
1060
		Object lat = p.get("latitude");
1061
		Object lng = p.get("longitude");
36644 ranu 1062
		return lat != null && lng != null && !lat.toString().isEmpty() && !lng.toString().isEmpty();
36618 ranu 1063
	}
1064
 
1065
	private double haversine(double lat1, double lng1, double lat2, double lng2) {
1066
		double R = 6371;
1067
		double dLat = Math.toRadians(lat2 - lat1);
1068
		double dLng = Math.toRadians(lng2 - lng1);
1069
		double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
1070
				+ Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
1071
				* Math.sin(dLng / 2) * Math.sin(dLng / 2);
1072
		double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
1073
		return R * c;
1074
	}
1075
}