Subversion Repositories SmartDukaan

Rev

Rev 36632 | Go to most recent revision | Details | 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;
11
import com.spice.profitmandi.dao.entity.user.AuthUserLocation;
12
import com.spice.profitmandi.dao.entity.user.BeatPlan;
13
import com.spice.profitmandi.dao.entity.user.BeatPlanDay;
14
import com.spice.profitmandi.dao.enumuration.cs.EscalationType;
15
import com.spice.profitmandi.dao.repository.auth.AuthRepository;
16
import com.spice.profitmandi.dao.repository.cs.CsService;
17
import com.spice.profitmandi.dao.repository.dtr.*;
18
import com.spice.profitmandi.dao.repository.logistics.PublicHolidaysRepository;
19
import com.spice.profitmandi.service.user.RetailerService;
20
import com.spice.profitmandi.web.model.LoginDetails;
21
import com.spice.profitmandi.web.util.CookiesProcessor;
22
import org.apache.logging.log4j.LogManager;
23
import org.apache.logging.log4j.Logger;
24
import org.springframework.beans.factory.annotation.Autowired;
25
import org.springframework.http.ResponseEntity;
26
import org.springframework.stereotype.Controller;
27
import org.springframework.transaction.annotation.Transactional;
28
import org.springframework.ui.Model;
29
import org.springframework.web.bind.annotation.GetMapping;
30
import org.springframework.web.bind.annotation.PostMapping;
31
import org.springframework.web.bind.annotation.RequestParam;
32
 
33
import javax.servlet.http.HttpServletRequest;
34
import java.lang.reflect.Type;
35
import java.time.DayOfWeek;
36
import java.time.LocalDate;
37
import java.time.LocalDateTime;
38
import java.time.YearMonth;
39
import java.time.format.DateTimeFormatter;
40
import java.util.*;
41
import java.util.stream.Collectors;
42
 
43
@Controller
44
@Transactional(rollbackFor = Throwable.class)
45
public class BeatPlanController {
46
	private static final Logger LOGGER = LogManager.getLogger(BeatPlanController.class);
47
	private static final String[] BEAT_COLORS = {
48
			"#3498DB", "#E74C3C", "#2ECC71", "#9B59B6", "#F39C12",
49
			"#1ABC9C", "#E67E22", "#34495E", "#16A085", "#C0392B"
50
	};
51
	@Autowired
52
	private CsService csService;
53
	@Autowired
54
	private AuthRepository authRepository;
55
	@Autowired
56
	private FofoStoreRepository fofoStoreRepository;
57
	@Autowired
58
	private RetailerService retailerService;
59
	@Autowired
60
	private BeatPlanRepository beatPlanRepository;
61
	@Autowired
62
	private BeatPlanDayRepository beatPlanDayRepository;
63
	@Autowired
64
	private AuthUserLocationRepository authUserLocationRepository;
65
	@Autowired
66
	private LeadRepository leadRepository;
67
	@Autowired
68
	private PublicHolidaysRepository publicHolidaysRepository;
69
	@Autowired
70
	private com.spice.profitmandi.service.GeocodingService geocodingService;
71
	@Autowired
72
	private CookiesProcessor cookiesProcessor;
73
	@Autowired
74
	private ResponseSender responseSender;
75
 
76
	@GetMapping(value = "/beatPlan")
77
	public String beatPlan(HttpServletRequest request, Model model) {
78
		EscalationType[] escalationTypes = EscalationType.values();
79
		model.addAttribute("escalationTypes", escalationTypes);
80
		return "beat-plan";
81
	}
82
 
83
	@GetMapping(value = "/beatPlanWindow")
84
	public String beatPlanWindow(HttpServletRequest request, Model model) {
85
		EscalationType[] escalationTypes = EscalationType.values();
86
		model.addAttribute("escalationTypes", escalationTypes);
87
		return "beat-plan-window";
88
	}
89
 
90
	@GetMapping(value = "/beatPlan/getAuthUsers")
91
	public ResponseEntity<?> getAuthUsers(
92
			@RequestParam int categoryId,
93
			@RequestParam EscalationType escalationType) {
94
 
95
		List<AuthUser> authUsers = csService.getAuthUserByCategoryId(categoryId, escalationType);
96
		List<Map<String, Object>> result = authUsers.stream()
97
				.filter(au -> au.getActive())
98
				.map(au -> {
99
					Map<String, Object> map = new HashMap<>();
100
					map.put("id", au.getId());
101
					map.put("name", au.getFirstName() + " " + au.getLastName());
102
					return map;
103
				})
104
				.collect(Collectors.toList());
105
 
106
		return responseSender.ok(result);
107
	}
108
 
109
	@GetMapping(value = "/beatPlan/getBaseLocation")
110
	public ResponseEntity<?> getBaseLocation(@RequestParam int authUserId) {
111
		AuthUserLocation baseLoc = authUserLocationRepository.selectLatestByAuthUserIdAndType(authUserId, "BASE");
112
		if (baseLoc == null) {
113
			return responseSender.ok(new HashMap<>());
114
		}
115
		Map<String, Object> result = new HashMap<>();
116
		result.put("id", baseLoc.getId());
117
		result.put("locationName", baseLoc.getLocationName());
118
		result.put("latitude", baseLoc.getLatitude());
119
		result.put("longitude", baseLoc.getLongitude());
120
		result.put("address", baseLoc.getAddress());
121
		return responseSender.ok(result);
122
	}
123
 
124
	@PostMapping(value = "/beatPlan/saveBaseLocation")
125
	public ResponseEntity<?> saveBaseLocation(
126
			@RequestParam int authUserId,
127
			@RequestParam String locationName,
128
			@RequestParam String latitude,
129
			@RequestParam String longitude,
130
			@RequestParam(required = false) String address) {
131
 
132
		AuthUserLocation loc = new AuthUserLocation();
133
		loc.setAuthUserId(authUserId);
134
		loc.setLocationType("BASE");
135
		loc.setLocationName(locationName);
136
		loc.setLatitude(latitude);
137
		loc.setLongitude(longitude);
138
		loc.setAddress(address);
139
		loc.setCreatedTimestamp(LocalDateTime.now());
140
		authUserLocationRepository.persist(loc);
141
 
142
		Map<String, Object> result = new HashMap<>();
143
		result.put("status", true);
144
		result.put("id", loc.getId());
145
		return responseSender.ok(result);
146
	}
147
 
148
	@GetMapping(value = "/beatPlan/getPartners")
149
	public ResponseEntity<?> getPartners(
150
			@RequestParam int authUserId,
151
			@RequestParam int categoryId,
152
			@RequestParam(required = false) String startLat,
153
			@RequestParam(required = false) String startLng) throws ProfitMandiBusinessException {
154
 
155
		Map<Integer, List<Integer>> pp = csService.getAuthUserIdPartnerIdMapping();
156
		List<Integer> fofoIds = pp.get(authUserId);
157
 
158
 
159
		if (fofoIds.isEmpty()) {
160
			Map<String, Object> empty = new HashMap<>();
161
			empty.put("partners", new ArrayList<>());
162
			return responseSender.ok(empty);
163
		}
164
 
165
		List<FofoStore> fofoStores = fofoStoreRepository.selectByRetailerIds(fofoIds);
166
		Map<Integer, CustomRetailer> retailerMap = retailerService.getFofoRetailers(fofoIds);
167
 
168
		// Build partner data list with addresses
169
		List<Map<String, Object>> partners = new ArrayList<>();
170
		List<String> addressesToGeocode = new ArrayList<>();
171
		List<FofoStore> activeStores = new ArrayList<>();
172
 
173
		for (FofoStore store : fofoStores) {
174
			if (!store.isActive() || store.isClosed()) continue;
175
			activeStores.add(store);
176
 
177
			CustomRetailer retailer = retailerMap.get(store.getId());
178
 
179
			Map<String, Object> partnerData = new HashMap<>();
180
			partnerData.put("fofoId", store.getId());
181
			partnerData.put("code", store.getCode());
182
			partnerData.put("outletName", store.getOutletName());
183
			partnerData.put("type", "partner");
184
 
185
			String addressStr = null;
186
			if (retailer != null) {
187
				partnerData.put("businessName", retailer.getBusinessName());
188
				if (retailer.getAddress() != null) {
189
					addressStr = retailer.getAddress().getAddressString();
190
					partnerData.put("address", addressStr);
191
				}
192
			}
193
 
194
			addressesToGeocode.add(addressStr);
195
			partners.add(partnerData);
196
		}
197
 
198
		// Geocode all addresses in parallel (cached eternally in Redis, so only first call hits Google)
199
		java.util.concurrent.ExecutorService executor = java.util.concurrent.Executors.newFixedThreadPool(
200
				Math.min(10, partners.size()));
201
		List<java.util.concurrent.Future<double[]>> futures = new ArrayList<>();
202
 
203
		for (String addr : addressesToGeocode) {
204
			futures.add(executor.submit(() -> {
205
				if (addr != null && !addr.isEmpty()) {
206
					try {
207
						return geocodingService.geocodeAddress(addr);
208
					} catch (Exception e) {
209
						return null;
210
					}
211
				}
212
				return null;
213
			}));
214
		}
215
 
216
		// Collect results
217
		for (int i = 0; i < partners.size(); i++) {
218
			Map<String, Object> partnerData = partners.get(i);
219
			String lat = null, lng = null;
220
 
221
			try {
222
				double[] coords = futures.get(i).get(10, java.util.concurrent.TimeUnit.SECONDS);
223
				if (coords != null) {
224
					lat = String.valueOf(coords[0]);
225
					lng = String.valueOf(coords[1]);
226
				}
227
			} catch (Exception e) {
228
				LOGGER.warn("Geocoding timeout/error for partner {}", partnerData.get("code"));
229
			}
230
 
231
			partnerData.put("latitude", lat);
232
			partnerData.put("longitude", lng);
233
		}
234
 
235
		executor.shutdown();
236
 
237
		// Sort by nearest neighbor from start location if provided, otherwise from first partner
238
		if (startLat != null && startLng != null && !startLat.isEmpty() && !startLng.isEmpty()) {
239
			partners = sortByNearestNeighborFromStart(partners, Double.parseDouble(startLat), Double.parseDouble(startLng));
240
		} else {
241
			partners = sortByNearestNeighbor(partners);
242
		}
243
 
244
		Map<String, Object> response = new HashMap<>();
245
		response.put("partners", partners);
246
		return responseSender.ok(response);
247
	}
248
 
249
	@PostMapping(value = "/beatPlan/submitPlan")
250
	public ResponseEntity<?> submitPlan(
251
			HttpServletRequest request,
252
			@RequestParam int authUserId,
253
			@RequestParam String planData) throws Exception {
254
 
255
		LoginDetails loginDetails = cookiesProcessor.getCookiesObject(request);
256
		AuthUser currentUser = authRepository.selectByEmailOrMobile(loginDetails.getEmailId());
257
 
258
		Gson gson = new Gson();
259
		Type type = new TypeToken<Map<String, Object>>() {
260
		}.getType();
261
		Map<String, Object> plan = gson.fromJson(planData, type);
262
 
263
		List<Map<String, Object>> days = (List<Map<String, Object>>) plan.get("days");
264
		List<String> dates = (List<String>) plan.get("dates");
265
 
266
		// Build visit fingerprint to detect duplicates
267
		List<Integer> newVisitIds = new ArrayList<>();
268
		for (Map<String, Object> day : days) {
269
			List<Map<String, Object>> visits = (List<Map<String, Object>>) day.get("visits");
270
			if (visits != null) {
271
				for (Map<String, Object> v : visits) {
272
					newVisitIds.add(((Number) v.get("id")).intValue());
273
				}
274
			}
275
		}
276
		Collections.sort(newVisitIds);
277
 
278
		// Check existing beats for this auth user
279
		List<BeatPlanDay> existingBeatDays = beatPlanDayRepository.selectByAuthUserIdAndDateRange(
280
				authUserId, LocalDate.of(2000, 1, 1), LocalDate.of(2099, 12, 31));
281
		Set<String> existingGroups = existingBeatDays.stream()
282
				.map(BeatPlanDay::getPlanGroupId).filter(g -> g != null).collect(Collectors.toSet());
283
 
284
		for (String existingGroup : existingGroups) {
285
			List<BeatPlan> existingVisits = beatPlanRepository.selectByPlanGroupId(existingGroup);
286
			List<Integer> existingIds = existingVisits.stream()
287
					.map(BeatPlan::getFofoId).sorted().collect(Collectors.toList());
288
			if (existingIds.equals(newVisitIds)) {
289
				// Duplicate found — return existing planGroupId
290
				Map<String, Object> response = new HashMap<>();
291
				response.put("status", true);
292
				response.put("planGroupId", existingGroup);
293
				response.put("duplicate", true);
294
				response.put("message", "Beat already exists with same visits");
295
				return responseSender.ok(response);
296
			}
297
		}
298
 
299
		String planGroupId = UUID.randomUUID().toString();
300
		String beatName = plan.get("beatName") != null ? (String) plan.get("beatName") : "Beat";
301
		String beatColor = BEAT_COLORS[Math.abs(planGroupId.hashCode()) % BEAT_COLORS.length];
302
 
303
		for (int d = 0; d < days.size(); d++) {
304
			Map<String, Object> day = days.get(d);
305
			int dayNumber = d + 1;
306
			LocalDate planDate = (dates != null && d < dates.size() && dates.get(d) != null)
307
					? LocalDate.parse(dates.get(d), DateTimeFormatter.ISO_DATE)
308
					: null;
309
 
310
			// Save day-level info
311
			BeatPlanDay bpDay = new BeatPlanDay();
312
			bpDay.setPlanGroupId(planGroupId);
313
			bpDay.setAuthUserId(authUserId);
314
			bpDay.setDayNumber(dayNumber);
315
			bpDay.setPlanDate(planDate);
316
			bpDay.setBeatName(beatName);
317
			bpDay.setBeatColor(beatColor);
318
			bpDay.setStartLocationName((String) day.get("startLocationName"));
319
			bpDay.setStartLatitude((String) day.get("startLatitude"));
320
			bpDay.setStartLongitude((String) day.get("startLongitude"));
321
			bpDay.setEndAction((String) day.get("endAction"));
322
			bpDay.setStayLocationName((String) day.get("stayLocationName"));
323
			bpDay.setStayLatitude((String) day.get("stayLatitude"));
324
			bpDay.setStayLongitude((String) day.get("stayLongitude"));
325
			if (day.get("totalDistanceKm") != null) {
326
				bpDay.setTotalDistanceKm(((Number) day.get("totalDistanceKm")).doubleValue());
327
			}
328
			if (day.get("totalTimeMins") != null) {
329
				bpDay.setTotalTimeMins(((Number) day.get("totalTimeMins")).intValue());
330
			}
331
			bpDay.setCreatedBy(currentUser.getId());
332
			bpDay.setCreatedTimestamp(LocalDateTime.now());
333
			bpDay.setActive(true);
334
			beatPlanDayRepository.persist(bpDay);
335
 
336
			// Save visits for this day
337
			List<Map<String, Object>> visits = (List<Map<String, Object>>) day.get("visits");
338
			if (visits != null) {
339
				for (int i = 0; i < visits.size(); i++) {
340
					Map<String, Object> visit = visits.get(i);
341
					BeatPlan bp = new BeatPlan();
342
					bp.setAuthUserId(authUserId);
343
					bp.setFofoId(((Number) visit.get("id")).intValue());
344
					bp.setVisitType((String) visit.get("type"));
345
					bp.setDayNumber(dayNumber);
346
					bp.setPlanGroupId(planGroupId);
347
					bp.setPlanDate(planDate);
348
					bp.setSequenceOrder(i);
349
					bp.setCreatedBy(currentUser.getId());
350
					bp.setCreatedTimestamp(LocalDateTime.now());
351
					bp.setUpdatedTimestamp(LocalDateTime.now());
352
					bp.setActive(true);
353
					beatPlanRepository.persist(bp);
354
				}
355
			}
356
		}
357
 
358
		Map<String, Object> response = new HashMap<>();
359
		response.put("status", true);
360
		response.put("planGroupId", planGroupId);
361
		response.put("message", "Beat plan submitted successfully");
362
		return responseSender.ok(response);
363
	}
364
 
365
	// ============ CALENDAR ENDPOINTS ============
366
 
367
	@PostMapping(value = "/beatPlan/delete")
368
	public ResponseEntity<?> deleteBeat(@RequestParam String planGroupId) {
369
		beatPlanDayRepository.deleteByPlanGroupId(planGroupId);
370
		beatPlanRepository.deleteByPlanGroupId(planGroupId);
371
 
372
		Map<String, Object> response = new HashMap<>();
373
		response.put("status", true);
374
		response.put("message", "Beat deleted");
375
		return responseSender.ok(response);
376
	}
377
 
378
	@GetMapping(value = "/beatPlan/calendar")
379
	public ResponseEntity<?> getCalendar(
380
			@RequestParam int authUserId,
381
			@RequestParam String month) {
382
 
383
		YearMonth ym = YearMonth.parse(month);
384
		LocalDate startDate = ym.atDay(1);
385
		LocalDate endDate = ym.atEndOfMonth();
386
 
387
		// Get holidays
388
		List<PublicHolidays> holidays = publicHolidaysRepository.selectAllBetweenDates(startDate, endDate);
389
		List<Map<String, String>> holidayList = holidays.stream().map(h -> {
390
			Map<String, String> m = new HashMap<>();
391
			m.put("date", h.getDate().toString());
392
			m.put("occasion", h.getOccasion());
393
			return m;
394
		}).collect(Collectors.toList());
395
 
396
		// Get beats for this month (includes null-date/unscheduled ones)
397
		List<BeatPlanDay> beatDays = beatPlanDayRepository.selectByAuthUserIdAndDateRange(authUserId, startDate, endDate);
398
 
399
		// Group by planGroupId
400
		Map<String, List<BeatPlanDay>> grouped = beatDays.stream()
401
				.filter(d -> d.getPlanGroupId() != null)
402
				.collect(Collectors.groupingBy(BeatPlanDay::getPlanGroupId));
403
 
404
		LocalDate today = LocalDate.now();
405
		List<Map<String, Object>> scheduledBeats = new ArrayList<>();
406
 
407
		for (Map.Entry<String, List<BeatPlanDay>> entry : grouped.entrySet()) {
408
			List<BeatPlanDay> days = entry.getValue();
409
			BeatPlanDay first = days.get(0);
410
 
411
			// Determine status
412
			boolean allNullDates = days.stream().allMatch(d -> d.getPlanDate() == null);
413
			boolean hasToday = days.stream().anyMatch(d -> d.getPlanDate() != null && d.getPlanDate().equals(today));
414
			boolean allPast = !allNullDates && days.stream().filter(d -> d.getPlanDate() != null).allMatch(d -> d.getPlanDate().isBefore(today));
415
			boolean allFuture = !allNullDates && days.stream().filter(d -> d.getPlanDate() != null).allMatch(d -> d.getPlanDate().isAfter(today));
416
 
417
			String status;
418
			if (allNullDates) status = "unscheduled";
419
			else if (hasToday) status = "running";
420
			else if (allPast) status = "completed";
421
			else if (allFuture) status = "scheduled";
422
			else status = "scheduled"; // mixed past/future
423
 
424
			// Get visit counts per day
425
			Map<String, Object> beat = new HashMap<>();
426
			beat.put("planGroupId", entry.getKey());
427
			beat.put("beatName", first.getBeatName() != null ? first.getBeatName() : "Beat");
428
			beat.put("beatColor", first.getBeatColor() != null ? first.getBeatColor() : "#3498DB");
429
			beat.put("status", status);
430
 
431
			List<Map<String, Object>> dayInfoList = new ArrayList<>();
432
			for (BeatPlanDay d : days) {
433
				Map<String, Object> dayInfo = new HashMap<>();
434
				dayInfo.put("dayNumber", d.getDayNumber());
435
				dayInfo.put("planDate", d.getPlanDate() != null ? d.getPlanDate().toString() : null);
436
				dayInfo.put("totalKm", d.getTotalDistanceKm());
437
				dayInfo.put("totalMins", d.getTotalTimeMins());
438
 
439
				// Count visits for this day
440
				List<BeatPlan> visits = beatPlanRepository.selectByPlanGroupId(entry.getKey()).stream()
441
						.filter(bp -> bp.getDayNumber() == d.getDayNumber())
442
						.collect(Collectors.toList());
443
				dayInfo.put("visitCount", visits.size());
444
				dayInfoList.add(dayInfo);
445
			}
446
			beat.put("days", dayInfoList);
447
			scheduledBeats.add(beat);
448
		}
449
 
450
		// Build blocked dates (Sundays + holidays)
451
		Set<String> blockedDates = new HashSet<>();
452
		for (LocalDate d = startDate; !d.isAfter(endDate); d = d.plusDays(1)) {
453
			if (d.getDayOfWeek() == DayOfWeek.SUNDAY) {
454
				blockedDates.add(d.toString());
455
			}
456
		}
457
		for (PublicHolidays h : holidays) {
458
			blockedDates.add(h.getDate().toString());
459
		}
460
 
461
		Map<String, Object> response = new HashMap<>();
462
		response.put("holidays", holidayList);
463
		response.put("scheduledBeats", scheduledBeats);
464
		response.put("blockedDates", blockedDates);
465
 
466
		return responseSender.ok(response);
467
	}
468
 
469
	@PostMapping(value = "/beatPlan/scheduleOnCalendar")
470
	public ResponseEntity<?> scheduleOnCalendar(
471
			HttpServletRequest request,
472
			@RequestParam String planGroupId,
473
			@RequestParam String dates,
474
			@RequestParam(required = false) String beatName,
475
			@RequestParam(required = false) String beatColor) throws Exception {
476
 
477
		Gson gson = new Gson();
478
		List<String> dateList = gson.fromJson(dates, new TypeToken<List<String>>() {
479
		}.getType());
480
 
481
		List<BeatPlanDay> beatDays = beatPlanDayRepository.selectByPlanGroupId(planGroupId);
482
		if (beatDays.isEmpty()) {
483
			return responseSender.badRequest("No beat found for this plan group");
484
		}
485
 
486
		// Check running beat — can't reschedule
487
		LocalDate today = LocalDate.now();
488
		boolean isRunning = beatDays.stream().anyMatch(d -> d.getPlanDate() != null && d.getPlanDate().equals(today));
489
		if (isRunning) {
490
			return responseSender.badRequest("Cannot reschedule a running beat");
491
		}
492
 
493
		// Auto-assign color if not provided
494
		if (beatColor == null || beatColor.isEmpty()) {
495
			beatColor = BEAT_COLORS[Math.abs(planGroupId.hashCode()) % BEAT_COLORS.length];
496
		}
497
 
498
		// Update dates on beat_plan_day
499
		beatDays.sort(Comparator.comparingInt(BeatPlanDay::getDayNumber));
500
		for (int i = 0; i < beatDays.size() && i < dateList.size(); i++) {
501
			BeatPlanDay day = beatDays.get(i);
502
			day.setPlanDate(LocalDate.parse(dateList.get(i)));
503
			if (beatName != null) day.setBeatName(beatName);
504
			day.setBeatColor(beatColor);
505
			beatPlanDayRepository.persist(day);
506
		}
507
 
508
		// Update dates on beat_plan records too
509
		List<BeatPlan> beatPlans = beatPlanRepository.selectByPlanGroupId(planGroupId);
510
		for (BeatPlan bp : beatPlans) {
511
			for (int i = 0; i < beatDays.size() && i < dateList.size(); i++) {
512
				if (bp.getDayNumber() == beatDays.get(i).getDayNumber()) {
513
					bp.setPlanDate(LocalDate.parse(dateList.get(i)));
514
					beatPlanRepository.persist(bp);
515
				}
516
			}
517
		}
518
 
519
		Map<String, Object> response = new HashMap<>();
520
		response.put("status", true);
521
		response.put("message", "Beat scheduled successfully");
522
		return responseSender.ok(response);
523
	}
524
 
525
	@PostMapping(value = "/beatPlan/repeatBeat")
526
	public ResponseEntity<?> repeatBeat(
527
			HttpServletRequest request,
528
			@RequestParam String sourcePlanGroupId,
529
			@RequestParam int authUserId,
530
			@RequestParam String dates) throws Exception {
531
 
532
		LoginDetails loginDetails = cookiesProcessor.getCookiesObject(request);
533
		AuthUser currentUser = authRepository.selectByEmailOrMobile(loginDetails.getEmailId());
534
 
535
		Gson gson = new Gson();
536
		List<String> dateList = gson.fromJson(dates, new TypeToken<List<String>>() {
537
		}.getType());
538
 
539
		// Get source beat days and visits
540
		List<BeatPlanDay> sourceDays = beatPlanDayRepository.selectByPlanGroupId(sourcePlanGroupId);
541
		List<BeatPlan> sourceVisits = beatPlanRepository.selectByPlanGroupId(sourcePlanGroupId);
542
 
543
		if (sourceDays.isEmpty()) {
544
			return responseSender.badRequest("Source beat not found");
545
		}
546
 
547
		String newPlanGroupId = UUID.randomUUID().toString();
548
		String beatName = sourceDays.get(0).getBeatName();
549
		String beatColor = sourceDays.get(0).getBeatColor();
550
 
551
		// Copy days with new dates
552
		sourceDays.sort(Comparator.comparingInt(BeatPlanDay::getDayNumber));
553
		for (int i = 0; i < sourceDays.size() && i < dateList.size(); i++) {
554
			BeatPlanDay src = sourceDays.get(i);
555
			BeatPlanDay newDay = new BeatPlanDay();
556
			newDay.setPlanGroupId(newPlanGroupId);
557
			newDay.setAuthUserId(authUserId);
558
			newDay.setDayNumber(src.getDayNumber());
559
			newDay.setPlanDate(LocalDate.parse(dateList.get(i)));
560
			newDay.setBeatName(beatName);
561
			newDay.setBeatColor(beatColor);
562
			newDay.setStartLocationName(src.getStartLocationName());
563
			newDay.setStartLatitude(src.getStartLatitude());
564
			newDay.setStartLongitude(src.getStartLongitude());
565
			newDay.setEndAction(src.getEndAction());
566
			newDay.setStayLocationName(src.getStayLocationName());
567
			newDay.setStayLatitude(src.getStayLatitude());
568
			newDay.setStayLongitude(src.getStayLongitude());
569
			newDay.setTotalDistanceKm(src.getTotalDistanceKm());
570
			newDay.setTotalTimeMins(src.getTotalTimeMins());
571
			newDay.setCreatedBy(currentUser.getId());
572
			newDay.setCreatedTimestamp(LocalDateTime.now());
573
			newDay.setActive(true);
574
			beatPlanDayRepository.persist(newDay);
575
		}
576
 
577
		// Copy visits with new plan group and dates
578
		for (BeatPlan srcVisit : sourceVisits) {
579
			BeatPlan newVisit = new BeatPlan();
580
			newVisit.setAuthUserId(authUserId);
581
			newVisit.setFofoId(srcVisit.getFofoId());
582
			newVisit.setVisitType(srcVisit.getVisitType());
583
			newVisit.setDayNumber(srcVisit.getDayNumber());
584
			newVisit.setPlanGroupId(newPlanGroupId);
585
			newVisit.setSequenceOrder(srcVisit.getSequenceOrder());
586
			newVisit.setCreatedBy(currentUser.getId());
587
			newVisit.setCreatedTimestamp(LocalDateTime.now());
588
			newVisit.setUpdatedTimestamp(LocalDateTime.now());
589
			newVisit.setActive(true);
590
 
591
			// Set date from new dateList
592
			int dayIdx = srcVisit.getDayNumber() - 1;
593
			if (dayIdx >= 0 && dayIdx < dateList.size()) {
594
				newVisit.setPlanDate(LocalDate.parse(dateList.get(dayIdx)));
595
			}
596
			beatPlanRepository.persist(newVisit);
597
		}
598
 
599
		Map<String, Object> response = new HashMap<>();
600
		response.put("status", true);
601
		response.put("planGroupId", newPlanGroupId);
602
		response.put("message", "Beat repeated successfully");
603
		return responseSender.ok(response);
604
	}
605
 
606
	@GetMapping(value = "/beatPlan/availableSlots")
607
	public ResponseEntity<?> getAvailableSlots(
608
			@RequestParam int authUserId,
609
			@RequestParam String month,
610
			@RequestParam int daysNeeded) {
611
 
612
		YearMonth ym = YearMonth.parse(month);
613
		LocalDate startDate = ym.atDay(1);
614
		LocalDate endDate = ym.atEndOfMonth();
615
		LocalDate today = LocalDate.now();
616
 
617
		// Blocked: Sundays + holidays + already scheduled dates
618
		Set<LocalDate> blocked = new HashSet<>();
619
 
620
		for (LocalDate d = startDate; !d.isAfter(endDate); d = d.plusDays(1)) {
621
			if (d.getDayOfWeek() == DayOfWeek.SUNDAY) blocked.add(d);
622
			if (!d.isAfter(today)) blocked.add(d); // past dates blocked
623
		}
624
 
625
		List<PublicHolidays> holidays = publicHolidaysRepository.selectAllBetweenDates(startDate, endDate);
626
		for (PublicHolidays h : holidays) blocked.add(h.getDate());
627
 
628
		List<BeatPlanDay> existingBeats = beatPlanDayRepository.selectByAuthUserIdAndDateRange(authUserId, startDate, endDate);
629
		for (BeatPlanDay bd : existingBeats) {
630
			if (bd.getPlanDate() != null) blocked.add(bd.getPlanDate());
631
		}
632
 
633
		// Find earliest available slots
634
		List<String> available = new ArrayList<>();
635
		for (LocalDate d = startDate.isAfter(today) ? startDate : today.plusDays(1);
636
			 !d.isAfter(endDate) && available.size() < daysNeeded;
637
			 d = d.plusDays(1)) {
638
			if (!blocked.contains(d)) {
639
				available.add(d.toString());
640
			}
641
		}
642
 
643
		Map<String, Object> response = new HashMap<>();
644
		response.put("suggestedDates", available);
645
		response.put("totalAvailable", available.size());
646
		return responseSender.ok(response);
647
	}
648
 
649
	// --- Sorting helpers ---
650
 
651
	private List<Map<String, Object>> sortByNearestNeighborFromStart(
652
			List<Map<String, Object>> partners, double startLat, double startLng) {
653
 
654
		List<Map<String, Object>> withCoords = new ArrayList<>();
655
		List<Map<String, Object>> withoutCoords = new ArrayList<>();
656
 
657
		for (Map<String, Object> p : partners) {
658
			if (hasValidCoords(p)) {
659
				withCoords.add(p);
660
			} else {
661
				withoutCoords.add(p);
662
			}
663
		}
664
 
665
		List<Map<String, Object>> sorted = new ArrayList<>();
666
		double currentLat = startLat;
667
		double currentLng = startLng;
668
 
669
		while (!withCoords.isEmpty()) {
670
			int nearestIdx = 0;
671
			double nearestDist = Double.MAX_VALUE;
672
			for (int i = 0; i < withCoords.size(); i++) {
673
				double lat = Double.parseDouble(withCoords.get(i).get("latitude").toString());
674
				double lng = Double.parseDouble(withCoords.get(i).get("longitude").toString());
675
				double dist = haversine(currentLat, currentLng, lat, lng);
676
				if (dist < nearestDist) {
677
					nearestDist = dist;
678
					nearestIdx = i;
679
				}
680
			}
681
			Map<String, Object> nearest = withCoords.remove(nearestIdx);
682
			sorted.add(nearest);
683
			currentLat = Double.parseDouble(nearest.get("latitude").toString());
684
			currentLng = Double.parseDouble(nearest.get("longitude").toString());
685
		}
686
 
687
		sorted.addAll(withoutCoords);
688
		return sorted;
689
	}
690
 
691
	private List<Map<String, Object>> sortByNearestNeighbor(List<Map<String, Object>> partners) {
692
		List<Map<String, Object>> withCoords = new ArrayList<>();
693
		List<Map<String, Object>> withoutCoords = new ArrayList<>();
694
 
695
		for (Map<String, Object> p : partners) {
696
			if (hasValidCoords(p)) {
697
				withCoords.add(p);
698
			} else {
699
				withoutCoords.add(p);
700
			}
701
		}
702
 
703
		List<Map<String, Object>> sorted = new ArrayList<>();
704
		if (!withCoords.isEmpty()) {
705
			sorted.add(withCoords.remove(0));
706
			while (!withCoords.isEmpty()) {
707
				Map<String, Object> last = sorted.get(sorted.size() - 1);
708
				double lastLat = Double.parseDouble(last.get("latitude").toString());
709
				double lastLng = Double.parseDouble(last.get("longitude").toString());
710
 
711
				int nearestIdx = 0;
712
				double nearestDist = Double.MAX_VALUE;
713
				for (int i = 0; i < withCoords.size(); i++) {
714
					double lat = Double.parseDouble(withCoords.get(i).get("latitude").toString());
715
					double lng = Double.parseDouble(withCoords.get(i).get("longitude").toString());
716
					double dist = haversine(lastLat, lastLng, lat, lng);
717
					if (dist < nearestDist) {
718
						nearestDist = dist;
719
						nearestIdx = i;
720
					}
721
				}
722
				sorted.add(withCoords.remove(nearestIdx));
723
			}
724
		}
725
		sorted.addAll(withoutCoords);
726
		return sorted;
727
	}
728
 
729
	private boolean hasValidCoords(Map<String, Object> p) {
730
		Object lat = p.get("latitude");
731
		Object lng = p.get("longitude");
732
		return lat != null && lng != null
733
				&& !lat.toString().isEmpty() && !lng.toString().isEmpty();
734
	}
735
 
736
	private double haversine(double lat1, double lng1, double lat2, double lng2) {
737
		double R = 6371;
738
		double dLat = Math.toRadians(lat2 - lat1);
739
		double dLng = Math.toRadians(lng2 - lng1);
740
		double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
741
				+ Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
742
				* Math.sin(dLng / 2) * Math.sin(dLng / 2);
743
		double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
744
		return R * c;
745
	}
746
}