Subversion Repositories SmartDukaan

Rev

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