Subversion Repositories SmartDukaan

Rev

Rev 36660 | Rev 36668 | 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;
36663 ranu 98
	@Autowired
99
	private com.spice.profitmandi.dao.repository.dtr.UserRepository userRepositoryAuto;
100
	@Autowired
101
	private com.spice.profitmandi.common.web.client.RestClient restClientAuto;
102
	@Autowired
103
	private com.spice.profitmandi.dao.repository.auth.LocationTrackingRepository locationTrackingRepositoryAuto;
36650 ranu 104
 
36655 ranu 105
	private static Double parseDoubleOrNull(String s) {
106
		if (s == null || s.trim().isEmpty()) return null;
107
		try {
108
			return Double.parseDouble(s.trim());
109
		} catch (NumberFormatException e) {
110
			return null;
111
		}
112
	}
113
 
114
	private static double haversineKm(double lat1, double lng1, double lat2, double lng2) {
115
		double R = 6371;
116
		double dLat = Math.toRadians(lat2 - lat1);
117
		double dLng = Math.toRadians(lng2 - lng1);
118
		double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
119
				+ Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
120
				* Math.sin(dLng / 2) * Math.sin(dLng / 2);
121
		double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
122
		return R * c;
123
	}
124
 
36663 ranu 125
	// ====================== ASSIGN VISIT ======================
126
	// Day View "Assign Visit" — lets an admin pick parties (stores) for a specific
127
	// auth user on a specific date and pushes them as visit tasks to the v2
128
	// /profitmandi-web/v2/beat-tracking/batch endpoint.
129
 
130
	// List of parties (stores) assigned to this auth user + their dtr.users.id
131
	@GetMapping(value = "/beatPlan/assignVisit/parties")
132
	public ResponseEntity<?> assignVisitParties(@RequestParam int authUserId) throws Exception {
133
		AuthUser au = authRepository.selectById(authUserId);
134
		if (au == null) return responseSender.badRequest("Auth user not found");
135
 
136
		// Map auth_user → dtr.users via email
137
		Integer dtrUserId = null;
138
		try {
139
			com.spice.profitmandi.dao.entity.dtr.User dtrUser =
140
					userRepositoryAuto.selectByEmailId(au.getEmailId());
141
			if (dtrUser != null) dtrUserId = dtrUser.getId();
142
		} catch (Exception ignored) {
143
		}
144
 
145
		Map<Integer, List<Integer>> mapping = csService.getAuthUserIdPartnerIdMapping();
146
		List<Integer> fofoIds = mapping.get(authUserId);
147
 
148
		List<Map<String, Object>> parties = new ArrayList<>();
149
		if (fofoIds != null && !fofoIds.isEmpty()) {
150
			List<FofoStore> stores = fofoStoreRepository.selectByRetailerIds(fofoIds);
151
			Map<Integer, CustomRetailer> retailerMap = retailerService.getFofoRetailers(fofoIds);
152
			for (FofoStore store : stores) {
153
				if (!store.isActive() || store.isClosed()) continue;
154
				CustomRetailer retailer = retailerMap.get(store.getId());
155
				Map<String, Object> p = new HashMap<>();
156
				p.put("fofoStoreId", store.getId());
157
				p.put("code", store.getCode());
158
				p.put("outletName", store.getOutletName() != null ? store.getOutletName()
159
						: (retailer != null ? retailer.getBusinessName() : "Store #" + store.getId()));
160
				p.put("latitude", store.getLatitude());
161
				p.put("longitude", store.getLongitude());
162
				p.put("city", retailer != null && retailer.getAddress() != null ? retailer.getAddress().getCity() : null);
163
				parties.add(p);
164
			}
165
			// Sort by code for stable display
166
			parties.sort((a, b) -> String.valueOf(a.get("code")).compareToIgnoreCase(String.valueOf(b.get("code"))));
167
		}
168
 
169
		Map<String, Object> result = new HashMap<>();
170
		result.put("dtrUserId", dtrUserId);
171
		result.put("authUserId", authUserId);
172
		result.put("userName", au.getFirstName() + " " + au.getLastName());
173
		result.put("parties", parties);
174
		return responseSender.ok(result);
175
	}
176
 
177
	// Submit assignment — accepts a JSON body, builds the v2 payload, posts it
178
	@PostMapping(value = "/beatPlan/assignVisit/submit")
179
	public ResponseEntity<?> assignVisitSubmit(
180
			HttpServletRequest request,
181
			@org.springframework.web.bind.annotation.RequestBody Map<String, Object> body) throws Exception {
182
 
183
		Integer authUserId = body.get("authUserId") != null ? ((Number) body.get("authUserId")).intValue() : null;
184
		String planDate = (String) body.get("planDate");
185
		List<Map<String, Object>> selected = (List<Map<String, Object>>) body.get("parties");
186
		if (authUserId == null || planDate == null || selected == null || selected.isEmpty()) {
187
			return responseSender.badRequest("authUserId, planDate and parties are required");
188
		}
189
 
190
		AuthUser au = authRepository.selectById(authUserId);
191
		if (au == null) return responseSender.badRequest("Auth user not found");
192
 
193
		// Map auth → dtr.users.id (this is the userId the v2 endpoint expects)
194
		com.spice.profitmandi.dao.entity.dtr.User dtrUser;
195
		try {
196
			dtrUser = userRepositoryAuto.selectByEmailId(au.getEmailId());
197
		} catch (Exception e) {
198
			return responseSender.badRequest("Failed to look up dtr.users for this auth user");
199
		}
200
		if (dtrUser == null) {
201
			return responseSender.badRequest("No dtr.users record found for auth user " + authUserId);
202
		}
203
		int dtrUserId = dtrUser.getId();
204
 
205
		// Persist directly via the shared DAO — mirrors BeatTrackingController.createBatch
206
		// in profitmandi-web. We can't autowire a controller across WARs, but
207
		// LocationTrackingRepository lives in profitmandi-dao and is shared.
208
		LocalDate taskDate;
209
		try {
210
			taskDate = LocalDate.parse(planDate);
211
		} catch (Exception e) {
212
			return responseSender.badRequest("Invalid planDate (expected yyyy-MM-dd): " + planDate);
213
		}
214
 
215
		LocalDateTime now = LocalDateTime.now();
216
		List<com.spice.profitmandi.dao.entity.auth.LocationTracking> created = new ArrayList<>();
217
 
218
		for (Map<String, Object> p : selected) {
219
			Integer fofoStoreId = ((Number) p.get("fofoStoreId")).intValue();
220
			String outletName = (String) p.get("outletName");
221
			String lat = (String) p.get("latitude");
222
			String lng = (String) p.get("longitude");
223
			String agenda = (String) p.get("agenda");
224
			if (agenda != null) agenda = agenda.trim();
225
			if (agenda == null || agenda.isEmpty()) agenda = "Visit";
226
 
227
			String visitLocation = (lat != null && lng != null && !lat.isEmpty() && !lng.isEmpty())
228
					? (lat + "," + lng) : "0.0000,0.0000";
229
 
230
			String displayName = (outletName != null && !outletName.isEmpty()) ? outletName : ("Store #" + fofoStoreId);
231
 
232
			com.spice.profitmandi.dao.entity.auth.LocationTracking row =
233
					new com.spice.profitmandi.dao.entity.auth.LocationTracking();
234
			row.setUserId(dtrUserId);
235
			row.setDeviceId("0");
236
			row.setTaskId(fofoStoreId);
237
			row.setTaskDate(taskDate);
238
			row.setTaskName(agenda + " | " + displayName);
239
			row.setTaskType("franchisee-visit");
240
			row.setMarkType("PENDING");
241
			row.setAddress("");
242
			row.setVisitLocation(visitLocation);
243
			row.setCheckInLatLng("0.0000,0.0000");
244
			row.setCheckOutLatLng("0.0000,0.0000");
245
			row.setCheckInTime(java.time.LocalTime.MIDNIGHT);
246
			row.setCheckOutTime(java.time.LocalTime.MIDNIGHT);
247
			row.setTransitTime(java.time.LocalTime.MIDNIGHT);
248
			row.setTimeSpent(java.time.LocalTime.MIDNIGHT);
249
			row.setEstimatedTime(java.time.LocalTime.MIDNIGHT);
250
			row.setSessionStartTime(java.time.LocalTime.MIDNIGHT);
251
			row.setSessionEndTime(java.time.LocalTime.MIDNIGHT);
252
			row.setTotalDistance("0.0");
253
			row.setStatus(false);
254
			row.setCreatedTimestamp(now);
255
			row.setUpdatedTimestamp(now);
256
 
257
			// Do NOT try/catch this — if persist throws, let it propagate so
258
			// @Transactional(rollbackFor = Throwable.class) rolls back cleanly.
259
			// Catching here lets Hibernate try to commit a broken session, which
260
			// then surfaces the misleading "null id in entry" flush error and hides
261
			// the real root cause (constraint violation, bad value, etc.).
262
			locationTrackingRepositoryAuto.persist(row);
263
			created.add(row);
264
		}
265
		LOGGER.info("assignVisit persisted {} location_tracking rows for dtrUserId={}", created.size(), dtrUserId);
266
 
267
		Map<String, Object> result = new HashMap<>();
268
		result.put("status", true);
269
		result.put("assignedCount", created.size());
270
		result.put("dtrUserId", dtrUserId);
271
		result.put("message", created.size() + " visit tasks assigned to " + au.getFirstName() + " " + au.getLastName());
272
		return responseSender.ok(result);
273
	}
274
 
36655 ranu 275
	// ====================== BASE LOCATION MANAGEMENT ======================
276
	// Inline page that lets Sales L3+ pick a user and set their base (home)
277
	// location via map. Reads use the existing /beatPlan/getBaseLocation, writes
278
	// go through the L3+-guarded endpoint below.
279
	@GetMapping(value = "/beatPlan/baseLocationPage")
280
	public String baseLocationPage(HttpServletRequest request, Model model) {
281
		EscalationType[] escalationTypes = EscalationType.values();
282
		model.addAttribute("escalationTypes", escalationTypes);
283
		return "beat-plan-base-location";
284
	}
285
 
286
	@PostMapping(value = "/beatPlan/updateBaseLocation")
287
	public ResponseEntity<?> updateBaseLocation(
288
			HttpServletRequest request,
289
			@RequestParam int authUserId,
290
			@RequestParam String locationName,
291
			@RequestParam String latitude,
292
			@RequestParam String longitude,
293
			@RequestParam(required = false) String address) throws Exception {
294
 
295
		LoginDetails ld = cookiesProcessor.getCookiesObject(request);
296
		AuthUser me = authRepository.selectByEmailOrMobile(ld.getEmailId());
297
		if (me == null) return responseSender.badRequest("Not logged in");
298
 
299
		// Permission gate: only Sales L3 and above
300
		boolean isSalesL3Plus = csService.getAuthUserIds(
301
						com.spice.profitmandi.common.model.ProfitMandiConstants.TICKET_CATEGORY_SALES,
302
						Arrays.asList(EscalationType.L3, EscalationType.L4))
303
				.stream().anyMatch(u -> u.getId() == me.getId());
304
		if (!isSalesL3Plus) {
305
			return responseSender.badRequest("Only Sales L3 and above can update base location");
306
		}
307
 
308
		AuthUserLocation loc = new AuthUserLocation();
309
		loc.setAuthUserId(authUserId);
310
		loc.setLocationType("BASE");
311
		loc.setLocationName(locationName);
312
		loc.setLatitude(latitude);
313
		loc.setLongitude(longitude);
314
		loc.setAddress(address);
315
		loc.setCreatedTimestamp(LocalDateTime.now());
316
		authUserLocationRepository.persist(loc);
317
 
318
		Map<String, Object> result = new HashMap<>();
319
		result.put("status", true);
320
		result.put("id", loc.getId());
321
		result.put("message", "Base location updated");
322
		return responseSender.ok(result);
323
	}
324
 
325
	// ====================== ONE-TIME LAT/LNG MIGRATION ======================
326
	// For each active fofo_store, compare its stored lat/lng with the geocoded
327
	// address lat/lng (cached in Redis). If the gap is > thresholdKm (default 5)
328
	// OR the store has no lat/lng yet, update the store with the geocoded
329
	// coordinates. Otherwise keep the existing values.
330
	//
331
	// Usage:
332
	//   GET /beatPlan/migrateStoreLatLng              -> dry run, default 5km, all
333
	//   GET /beatPlan/migrateStoreLatLng?apply=true   -> actually update
334
	//   ?thresholdKm=3      -> use a different threshold
335
	//   ?limit=100          -> process only N stores (for staged runs)
336
	@GetMapping(value = "/beatPlan/migrateStoreLatLng")
337
	public ResponseEntity<?> migrateStoreLatLng(
338
			@RequestParam(required = false, defaultValue = "false") boolean apply,
339
			@RequestParam(required = false, defaultValue = "5") double thresholdKm,
36660 ranu 340
			@RequestParam(required = false, defaultValue = "0") int limit,
341
			@RequestParam(required = false, defaultValue = "0") int offset) throws ProfitMandiBusinessException {
36655 ranu 342
 
36660 ranu 343
		List<FofoStore> all = fofoStoreRepository.selectActiveStores();
344
		int totalAvailable = all.size();
345
		// offset + limit so this can be run in batches against large datasets
346
		int from = Math.max(0, Math.min(offset, totalAvailable));
347
		int to = limit > 0 ? Math.min(from + limit, totalAvailable) : totalAvailable;
348
		List<FofoStore> stores = all.subList(from, to);
36655 ranu 349
 
350
		List<Integer> ids = stores.stream().map(FofoStore::getId).collect(Collectors.toList());
351
		Map<Integer, CustomRetailer> retailerMap = retailerService.getFofoRetailers(ids);
352
 
353
		int total = stores.size();
354
		int updated = 0, kept = 0, noAddress = 0, noGeocode = 0, errored = 0;
355
		List<Map<String, Object>> changes = new ArrayList<>();
356
 
357
		for (FofoStore store : stores) {
358
			try {
359
				CustomRetailer retailer = retailerMap.get(store.getId());
360
				if (retailer == null || retailer.getAddress() == null) {
361
					noAddress++;
362
					continue;
363
				}
364
 
365
				String geoAddr = com.spice.profitmandi.service.GeocodingService.buildGeoAddress(
366
						retailer.getAddress().getLine1(), retailer.getAddress().getCity(),
367
						retailer.getAddress().getState(), retailer.getAddress().getPinCode());
368
				if (geoAddr == null || geoAddr.isEmpty()) {
369
					noAddress++;
370
					continue;
371
				}
372
 
373
				double[] coords = geocodingService.geocodeAddress(geoAddr);
374
				if (coords == null) {
375
					noGeocode++;
376
					continue;
377
				}
378
 
379
				Double existingLat = parseDoubleOrNull(store.getLatitude());
380
				Double existingLng = parseDoubleOrNull(store.getLongitude());
381
 
382
				boolean shouldUpdate;
383
				double distKm = -1;
384
				String reason;
385
				if (existingLat == null || existingLng == null) {
386
					shouldUpdate = true;
387
					reason = "missing existing lat/lng";
388
				} else {
389
					distKm = haversineKm(existingLat, existingLng, coords[0], coords[1]);
390
					shouldUpdate = distKm > thresholdKm;
391
					reason = shouldUpdate
392
							? "gap " + Math.round(distKm * 10.0) / 10.0 + "km > " + thresholdKm + "km"
393
							: "gap " + Math.round(distKm * 10.0) / 10.0 + "km within " + thresholdKm + "km";
394
				}
395
 
396
				if (shouldUpdate) {
397
					if (apply) {
398
						store.setLatitude(String.valueOf(coords[0]));
399
						store.setLongitude(String.valueOf(coords[1]));
400
						fofoStoreRepository.persist(store);
401
					}
402
					updated++;
403
					Map<String, Object> ch = new HashMap<>();
404
					ch.put("storeId", store.getId());
405
					ch.put("code", store.getCode());
406
					ch.put("oldLat", existingLat);
407
					ch.put("oldLng", existingLng);
408
					ch.put("newLat", coords[0]);
409
					ch.put("newLng", coords[1]);
410
					ch.put("distKm", distKm >= 0 ? Math.round(distKm * 10.0) / 10.0 : null);
411
					ch.put("reason", reason);
412
					changes.add(ch);
413
				} else {
414
					kept++;
415
				}
416
			} catch (Exception e) {
417
				errored++;
418
				LOGGER.warn("Geocode/migrate failed for fofoId={}: {}", store.getId(), e.getMessage());
419
			}
420
		}
421
 
422
		Map<String, Object> result = new HashMap<>();
423
		result.put("mode", apply ? "APPLIED" : "DRY RUN — pass &apply=true to actually update");
424
		result.put("thresholdKm", thresholdKm);
36660 ranu 425
		result.put("totalAvailable", totalAvailable);  // total active stores in DB
426
		result.put("offset", from);
427
		result.put("processed", total);                 // stores processed this run
428
		result.put("nextOffset", to);                   // next offset to resume (or = totalAvailable when done)
429
		result.put("done", to >= totalAvailable);
36655 ranu 430
		result.put("updated", updated);
431
		result.put("kept", kept);
432
		result.put("noAddress", noAddress);
433
		result.put("noGeocode", noGeocode);
434
		result.put("errored", errored);
435
		// Limit changes preview to avoid huge responses
436
		result.put("changes", changes.size() > 200 ? changes.subList(0, 200) : changes);
437
		result.put("changesShownCount", Math.min(changes.size(), 200));
438
		return responseSender.ok(result);
439
	}
440
 
36651 ranu 441
	// ====================== EDIT BEAT ======================
442
	// Update an existing beat — name + partner stops (routes).
443
	// Schedules are NOT touched here; manage them via calendar drag-drop.
444
	@PostMapping(value = "/beatPlan/updateBeat")
445
	public ResponseEntity<?> updateBeat(
446
			HttpServletRequest request,
447
			@RequestParam int beatId,
448
			@RequestParam String planData) throws Exception {
449
 
450
		Beat beat = beatRepository.selectById(beatId);
451
		if (beat == null) return responseSender.badRequest("Beat not found");
452
 
453
		Gson gson = new Gson();
454
		Type type = new TypeToken<Map<String, Object>>() {
455
		}.getType();
456
		Map<String, Object> plan = gson.fromJson(planData, type);
457
 
458
		List<Map<String, Object>> days = (List<Map<String, Object>>) plan.get("days");
459
		if (days == null || days.isEmpty()) return responseSender.badRequest("No days provided");
460
 
461
		// Update name if changed (and not colliding with another beat)
462
		String newName = plan.get("beatName") != null ? ((String) plan.get("beatName")).trim() : beat.getName();
463
		if (newName != null && !newName.equalsIgnoreCase(beat.getName())) {
464
			// Make sure no other beat for this user already uses this name
465
			boolean collides = beatRepository.selectByAuthUserId(beat.getAuthUserId()).stream()
466
					.anyMatch(b -> b.getId() != beat.getId()
467
							&& b.getName() != null
468
							&& newName.equalsIgnoreCase(b.getName().trim()));
469
			if (collides) return responseSender.badRequest("Another beat with this name already exists");
470
			beat.setName(newName);
471
		}
472
 
473
		// Update start location from first day if present
474
		Map<String, Object> firstDay = days.get(0);
475
		if (firstDay.get("startLocationName") != null)
476
			beat.setStartLocationName((String) firstDay.get("startLocationName"));
477
		if (firstDay.get("startLatitude") != null) beat.setStartLatitude((String) firstDay.get("startLatitude"));
478
		if (firstDay.get("startLongitude") != null) beat.setStartLongitude((String) firstDay.get("startLongitude"));
479
		beat.setTotalDays(days.size());
480
 
481
		// Replace routes (partner stops). Schedules stay intact.
482
		beatRouteRepository.deleteByBeatId(beatId);
483
		// Collect lead IDs the user kept on the plan (for date-aware edit below)
484
		Set<Integer> keptLeadIds = new HashSet<>();
485
		for (int d = 0; d < days.size(); d++) {
486
			Map<String, Object> day = days.get(d);
487
			int dayNumber = d + 1;
488
			List<Map<String, Object>> visits = (List<Map<String, Object>>) day.get("visits");
489
			if (visits == null) continue;
490
			int partnerSeq = 0;
491
			for (int i = 0; i < visits.size(); i++) {
492
				Map<String, Object> v = visits.get(i);
493
				if ("lead".equals(v.get("type"))) {
494
					keptLeadIds.add(((Number) v.get("id")).intValue());
495
					continue; // leads live in lead_route, handled below
496
				}
497
				BeatRoute route = new BeatRoute();
498
				route.setBeatId(beatId);
499
				route.setFofoId(((Number) v.get("id")).intValue());
500
				route.setSequenceOrder(partnerSeq++);
501
				route.setDayNumber(dayNumber);
502
				route.setActive(true);
503
				beatRouteRepository.persist(route);
504
			}
505
		}
506
 
507
		// Date-aware lead handling: if planDate is provided, remove lead_route rows
508
		// for that (beat, date) that the user removed in the editor.
509
		String planDateStr = (String) plan.get("planDate");
510
		if (planDateStr != null && !planDateStr.isEmpty()) {
511
			try {
512
				LocalDate planDate = LocalDate.parse(planDateStr);
513
				List<LeadRoute> existing = leadRouteRepository.selectByBeatId(beatId);
514
				for (LeadRoute lr : existing) {
515
					if (!"APPROVED".equals(lr.getStatus())) continue;
516
					if (lr.getScheduleDate() == null || !lr.getScheduleDate().equals(planDate)) continue;
517
					if (keptLeadIds.contains(lr.getLeadId())) continue;
518
					// User removed this lead from the date — mark cancelled
519
					lr.setStatus("CANCELLED");
520
					lr.setUpdatedTimestamp(LocalDateTime.now());
521
					// activity log
522
					LeadActivity a = new LeadActivity();
523
					a.setLeadId(lr.getLeadId());
524
					a.setRemark("Removed from beat '" + beat.getName() + "' on " + planDate);
525
					a.setAuthId(0);
526
					a.setCreatedTimestamp(LocalDateTime.now());
527
					leadActivityRepositoryAuto.persist(a);
528
				}
529
			} catch (Exception e) {
530
				LOGGER.warn("Could not parse planDate '{}' — leads not adjusted", planDateStr);
531
			}
532
		}
533
 
534
		Map<String, Object> response = new HashMap<>();
535
		response.put("status", true);
536
		response.put("planGroupId", String.valueOf(beat.getId()));
537
		response.put("message", "Beat updated successfully");
538
		return responseSender.ok(response);
539
	}
540
 
36650 ranu 541
	// ====================== DAY VIEW ======================
542
	// Inline page (loaded into dashboard #main-content): tabular list of all beats
543
	// scheduled in a date range across all users. Each row has a View button that
544
	// opens that user's calendar in a modal.
545
	@GetMapping(value = "/beatPlan/dayView")
546
	public String beatPlanDayView(HttpServletRequest request, Model model) {
547
		EscalationType[] escalationTypes = EscalationType.values();
548
		model.addAttribute("escalationTypes", escalationTypes);
549
		return "beat-plan-day-view";
550
	}
551
 
552
	// Tabular JSON: one row per (beat, scheduled date) in [startDate, endDate].
553
	@GetMapping(value = "/beatPlan/scheduledList")
554
	public ResponseEntity<?> scheduledList(
555
			@RequestParam(required = false) String startDate,
556
			@RequestParam(required = false) String endDate) {
557
 
558
		LocalDate start, end;
559
		try {
560
			start = (startDate == null || startDate.isEmpty()) ? LocalDate.now() : LocalDate.parse(startDate);
561
			end = (endDate == null || endDate.isEmpty()) ? start.plusDays(7) : LocalDate.parse(endDate);
562
		} catch (Exception e) {
563
			return responseSender.badRequest("Invalid date — expected yyyy-MM-dd");
564
		}
565
 
566
		List<com.spice.profitmandi.dao.model.BeatDayDetails> beats =
567
				beatPlanQueryService.getAllScheduledBeats(start, end);
568
 
569
		// Resolve user names in bulk
570
		Set<Integer> userIds = beats.stream()
571
				.map(com.spice.profitmandi.dao.model.BeatDayDetails::getAuthUserId)
572
				.collect(java.util.stream.Collectors.toSet());
573
		Map<Integer, AuthUser> userMap = new HashMap<>();
574
		if (!userIds.isEmpty()) {
575
			authRepository.selectByIds(new ArrayList<>(userIds))
576
					.forEach(u -> userMap.put(u.getId(), u));
577
		}
578
 
579
		List<Map<String, Object>> rows = new ArrayList<>();
580
		for (com.spice.profitmandi.dao.model.BeatDayDetails b : beats) {
581
			AuthUser u = userMap.get(b.getAuthUserId());
582
			Map<String, Object> row = new HashMap<>();
583
			row.put("authUserId", b.getAuthUserId());
584
			row.put("userName", u != null ? (u.getFirstName() + " " + u.getLastName()) : "User #" + b.getAuthUserId());
585
			row.put("scheduleDate", b.getScheduleDate().toString());
586
			row.put("dayNumber", b.getDayNumber());
587
			row.put("beatId", b.getBeatId());
588
			row.put("beatName", b.getBeatName());
589
			row.put("beatColor", b.getBeatColor());
590
			row.put("partnerCount", b.getPartnerStops().size());
591
			row.put("leadCount", b.getLeadStops().size());
592
			row.put("visitCount", b.getPartnerStops().size() + b.getLeadStops().size());
593
			rows.add(row);
594
		}
595
 
596
		Map<String, Object> result = new HashMap<>();
597
		result.put("rows", rows);
598
		result.put("startDate", start.toString());
599
		result.put("endDate", end.toString());
600
		return responseSender.ok(result);
601
	}
602
 
603
	// JSON: beats running for (authUserId, date) — enriched with partner/lead names & coords
604
	@GetMapping(value = "/beatPlan/dayViewData")
605
	public ResponseEntity<?> beatPlanDayViewData(
606
			@RequestParam int authUserId,
607
			@RequestParam String date) throws ProfitMandiBusinessException {
608
 
609
		LocalDate localDate;
610
		try {
611
			localDate = LocalDate.parse(date);
612
		} catch (Exception e) {
613
			return responseSender.badRequest("Invalid date — expected yyyy-MM-dd");
614
		}
615
 
616
		List<com.spice.profitmandi.dao.model.BeatDayDetails> beats =
617
				beatPlanQueryService.getBeatsForUserOnDate(authUserId, localDate);
618
 
619
		// Collect all partner & lead IDs to fetch metadata in bulk
620
		Set<Integer> partnerIds = new HashSet<>();
621
		Set<Integer> leadIds = new HashSet<>();
622
		for (com.spice.profitmandi.dao.model.BeatDayDetails b : beats) {
623
			b.getPartnerStops().forEach(s -> partnerIds.add((Integer) s.get("fofoId")));
624
			b.getLeadStops().forEach(s -> leadIds.add((Integer) s.get("leadId")));
625
		}
626
 
627
		// Partners: name + geocoded lat/lng (geocoder is cached in Redis)
628
		Map<Integer, CustomRetailer> retailerMap = partnerIds.isEmpty()
629
				? new HashMap<>()
630
				: retailerService.getFofoRetailers(new ArrayList<>(partnerIds));
631
		Map<Integer, FofoStore> storeMap = new HashMap<>();
632
		if (!partnerIds.isEmpty()) {
633
			fofoStoreRepository.selectByRetailerIds(new ArrayList<>(partnerIds))
634
					.forEach(fs -> storeMap.put(fs.getId(), fs));
635
		}
636
 
637
		// Leads: name + geo
638
		Map<Integer, com.spice.profitmandi.dao.entity.user.Lead> leadMap = new HashMap<>();
639
		Map<Integer, com.spice.profitmandi.dao.entity.user.LeadLiveLocation> leadGeoMap = new HashMap<>();
640
		for (int leadId : leadIds) {
641
			com.spice.profitmandi.dao.entity.user.Lead l = leadRepository.selectById(leadId);
642
			if (l != null) leadMap.put(leadId, l);
643
			com.spice.profitmandi.dao.entity.user.LeadLiveLocation lg =
644
					leadLiveLocationRepositoryAuto.selectApprovedByLeadId(leadId);
645
			if (lg != null) leadGeoMap.put(leadId, lg);
646
		}
647
 
648
		// Enrich each stop
649
		List<Map<String, Object>> out = new ArrayList<>();
650
		for (com.spice.profitmandi.dao.model.BeatDayDetails b : beats) {
651
			Map<String, Object> beatJson = new HashMap<>();
652
			beatJson.put("beatId", b.getBeatId());
653
			beatJson.put("beatName", b.getBeatName());
654
			beatJson.put("beatColor", b.getBeatColor());
655
			beatJson.put("dayNumber", b.getDayNumber());
656
			beatJson.put("scheduleDate", b.getScheduleDate().toString());
657
			beatJson.put("endAction", b.getEndAction());
658
			beatJson.put("totalDistanceKm", b.getTotalDistanceKm());
659
			beatJson.put("totalTimeMins", b.getTotalTimeMins());
660
			beatJson.put("startLocationName", b.getStartLocationName());
661
			beatJson.put("startLatitude", b.getStartLatitude());
662
			beatJson.put("startLongitude", b.getStartLongitude());
663
 
664
			List<Map<String, Object>> stops = new ArrayList<>();
665
			// Partners
666
			for (Map<String, Object> ps : b.getPartnerStops()) {
667
				int fofoId = (Integer) ps.get("fofoId");
668
				Map<String, Object> stop = new HashMap<>();
669
				stop.put("type", "partner");
670
				stop.put("id", fofoId);
671
				stop.put("sequenceOrder", ps.get("sequenceOrder"));
672
				FofoStore fs = storeMap.get(fofoId);
673
				CustomRetailer cr = retailerMap.get(fofoId);
674
				stop.put("code", fs != null ? fs.getCode() : null);
675
				stop.put("name", fs != null && fs.getOutletName() != null ? fs.getOutletName()
676
						: (cr != null ? cr.getBusinessName() : "Store #" + fofoId));
36655 ranu 677
				// Use FofoStore lat/lng directly (no geocoding needed after migration)
678
				if (fs != null && fs.getLatitude() != null && fs.getLongitude() != null
679
						&& !fs.getLatitude().isEmpty() && !fs.getLongitude().isEmpty()) {
36650 ranu 680
					try {
36655 ranu 681
						stop.put("lat", Double.parseDouble(fs.getLatitude()));
682
						stop.put("lng", Double.parseDouble(fs.getLongitude()));
683
					} catch (NumberFormatException ignored) {
36650 ranu 684
					}
685
				}
36655 ranu 686
				if (cr != null && cr.getAddress() != null) {
687
					stop.put("address", cr.getAddress().getAddressString());
688
				}
36650 ranu 689
				stops.add(stop);
690
			}
691
			// Leads
692
			for (Map<String, Object> ls : b.getLeadStops()) {
693
				int leadId = (Integer) ls.get("leadId");
694
				Map<String, Object> stop = new HashMap<>();
695
				stop.put("type", "lead");
696
				stop.put("id", leadId);
697
				stop.put("sequenceOrder", ls.get("sequenceOrder"));
698
				stop.put("nearestStoreId", ls.get("nearestStoreId"));
699
				com.spice.profitmandi.dao.entity.user.Lead l = leadMap.get(leadId);
700
				stop.put("name", l != null ? l.getFirstName() + " " + l.getLastName() : "Lead #" + leadId);
701
				stop.put("mobile", l != null ? l.getLeadMobile() : null);
702
				stop.put("city", l != null ? l.getCity() : null);
703
				com.spice.profitmandi.dao.entity.user.LeadLiveLocation lg = leadGeoMap.get(leadId);
704
				if (lg != null) {
705
					stop.put("lat", lg.getLatitude());
706
					stop.put("lng", lg.getLongitude());
707
				}
708
				stops.add(stop);
709
			}
710
			beatJson.put("stops", stops);
711
			beatJson.put("partnerCount", b.getPartnerStops().size());
712
			beatJson.put("leadCount", b.getLeadStops().size());
713
			out.add(beatJson);
714
		}
715
 
716
		Map<String, Object> result = new HashMap<>();
717
		result.put("beats", out);
718
		return responseSender.ok(result);
719
	}
720
 
36618 ranu 721
	@GetMapping(value = "/beatPlan/getAuthUsers")
722
	public ResponseEntity<?> getAuthUsers(
723
			@RequestParam int categoryId,
724
			@RequestParam EscalationType escalationType) {
725
		List<AuthUser> authUsers = csService.getAuthUserByCategoryId(categoryId, escalationType);
726
		List<Map<String, Object>> result = authUsers.stream()
727
				.filter(au -> au.getActive())
728
				.map(au -> {
729
					Map<String, Object> map = new HashMap<>();
730
					map.put("id", au.getId());
731
					map.put("name", au.getFirstName() + " " + au.getLastName());
732
					return map;
733
				})
734
				.collect(Collectors.toList());
735
		return responseSender.ok(result);
736
	}
737
 
36644 ranu 738
	// Returns visits for a beat.
739
	// - Partner stops (beat_route) belong to the beat template — always returned.
740
	// - Lead stops (lead_route) belong to a specific run — returned ONLY when planDate
741
	//   is given and matches the lead's schedule_date. (No planDate = template view.)
36632 ranu 742
	@GetMapping(value = "/beatPlan/getBeatVisits")
36644 ranu 743
	public ResponseEntity<?> getBeatVisits(
744
			@RequestParam String planGroupId,
745
			@RequestParam(required = false) String planDate) {
746
 
747
		int beatId;
748
		try {
749
			beatId = Integer.parseInt(planGroupId);
750
		} catch (NumberFormatException e) {
751
			return responseSender.ok(new ArrayList<>());
752
		}
753
 
754
		List<BeatRoute> routes = beatRouteRepository.selectByBeatId(beatId);
755
		List<Map<String, Object>> result = new ArrayList<>();
756
 
757
		// Partner stops — always (they belong to the beat template)
758
		for (BeatRoute r : routes) {
36632 ranu 759
			Map<String, Object> map = new HashMap<>();
36644 ranu 760
			map.put("fofoId", r.getFofoId());
761
			map.put("dayNumber", r.getDayNumber());
762
			map.put("sequenceOrder", r.getSequenceOrder());
763
			map.put("visitType", "partner");
764
			result.add(map);
765
		}
766
 
767
		// Lead stops — only for the requested run date
768
		if (planDate != null && !planDate.isEmpty()) {
769
			LocalDate date = LocalDate.parse(planDate);
770
			List<LeadRoute> leads = leadRouteRepository.selectByBeatId(beatId);
771
			for (LeadRoute lr : leads) {
772
				if ("APPROVED".equals(lr.getStatus())
773
						&& lr.getScheduleDate() != null
774
						&& lr.getScheduleDate().equals(date)) {
775
					Map<String, Object> map = new HashMap<>();
776
					map.put("fofoId", lr.getLeadId());
777
					map.put("dayNumber", 1);
778
					map.put("sequenceOrder", lr.getSequenceOrder() != null ? lr.getSequenceOrder() : 999);
779
					map.put("visitType", "lead");
780
					result.add(map);
781
				}
782
			}
783
		}
784
 
785
		// Sort by dayNumber then sequenceOrder
786
		result.sort((a, b) -> {
787
			int cmp = Integer.compare((int) a.get("dayNumber"), (int) b.get("dayNumber"));
788
			return cmp != 0 ? cmp : Integer.compare((int) a.get("sequenceOrder"), (int) b.get("sequenceOrder"));
789
		});
790
 
36632 ranu 791
		return responseSender.ok(result);
792
	}
793
 
36618 ranu 794
	@GetMapping(value = "/beatPlan/getBaseLocation")
795
	public ResponseEntity<?> getBaseLocation(@RequestParam int authUserId) {
796
		AuthUserLocation baseLoc = authUserLocationRepository.selectLatestByAuthUserIdAndType(authUserId, "BASE");
797
		if (baseLoc == null) {
798
			return responseSender.ok(new HashMap<>());
799
		}
800
		Map<String, Object> result = new HashMap<>();
801
		result.put("id", baseLoc.getId());
802
		result.put("locationName", baseLoc.getLocationName());
803
		result.put("latitude", baseLoc.getLatitude());
804
		result.put("longitude", baseLoc.getLongitude());
805
		result.put("address", baseLoc.getAddress());
806
		return responseSender.ok(result);
807
	}
808
 
809
	@PostMapping(value = "/beatPlan/saveBaseLocation")
810
	public ResponseEntity<?> saveBaseLocation(
811
			@RequestParam int authUserId,
812
			@RequestParam String locationName,
813
			@RequestParam String latitude,
814
			@RequestParam String longitude,
815
			@RequestParam(required = false) String address) {
816
		AuthUserLocation loc = new AuthUserLocation();
817
		loc.setAuthUserId(authUserId);
818
		loc.setLocationType("BASE");
819
		loc.setLocationName(locationName);
820
		loc.setLatitude(latitude);
821
		loc.setLongitude(longitude);
822
		loc.setAddress(address);
823
		loc.setCreatedTimestamp(LocalDateTime.now());
824
		authUserLocationRepository.persist(loc);
825
 
826
		Map<String, Object> result = new HashMap<>();
827
		result.put("status", true);
828
		result.put("id", loc.getId());
829
		return responseSender.ok(result);
830
	}
831
 
832
	@GetMapping(value = "/beatPlan/getPartners")
833
	public ResponseEntity<?> getPartners(
834
			@RequestParam int authUserId,
835
			@RequestParam int categoryId,
836
			@RequestParam(required = false) String startLat,
837
			@RequestParam(required = false) String startLng) throws ProfitMandiBusinessException {
838
 
839
		Map<Integer, List<Integer>> pp = csService.getAuthUserIdPartnerIdMapping();
840
		List<Integer> fofoIds = pp.get(authUserId);
841
 
36644 ranu 842
		if (fofoIds == null || fofoIds.isEmpty()) {
36618 ranu 843
			Map<String, Object> empty = new HashMap<>();
844
			empty.put("partners", new ArrayList<>());
845
			return responseSender.ok(empty);
846
		}
847
 
848
		List<FofoStore> fofoStores = fofoStoreRepository.selectByRetailerIds(fofoIds);
849
		Map<Integer, CustomRetailer> retailerMap = retailerService.getFofoRetailers(fofoIds);
850
 
851
		List<Map<String, Object>> partners = new ArrayList<>();
852
 
853
		for (FofoStore store : fofoStores) {
854
			if (!store.isActive() || store.isClosed()) continue;
855
			CustomRetailer retailer = retailerMap.get(store.getId());
856
 
857
			Map<String, Object> partnerData = new HashMap<>();
858
			partnerData.put("fofoId", store.getId());
859
			partnerData.put("code", store.getCode());
860
			partnerData.put("outletName", store.getOutletName());
861
			partnerData.put("type", "partner");
862
 
36655 ranu 863
			// Use FofoStore lat/lng directly (migrated from address geocode)
864
			if (store.getLatitude() != null && !store.getLatitude().isEmpty()
865
					&& store.getLongitude() != null && !store.getLongitude().isEmpty()) {
866
				partnerData.put("latitude", store.getLatitude());
867
				partnerData.put("longitude", store.getLongitude());
868
			}
869
 
36618 ranu 870
			if (retailer != null) {
871
				partnerData.put("businessName", retailer.getBusinessName());
872
				if (retailer.getAddress() != null) {
36644 ranu 873
					partnerData.put("address", retailer.getAddress().getAddressString());
36618 ranu 874
				}
875
			}
876
			partners.add(partnerData);
877
		}
878
 
879
		if (startLat != null && startLng != null && !startLat.isEmpty() && !startLng.isEmpty()) {
880
			partners = sortByNearestNeighborFromStart(partners, Double.parseDouble(startLat), Double.parseDouble(startLng));
881
		} else {
882
			partners = sortByNearestNeighbor(partners);
883
		}
884
 
885
		Map<String, Object> response = new HashMap<>();
886
		response.put("partners", partners);
887
		return responseSender.ok(response);
888
	}
889
 
890
	@PostMapping(value = "/beatPlan/submitPlan")
891
	public ResponseEntity<?> submitPlan(
892
			HttpServletRequest request,
893
			@RequestParam int authUserId,
894
			@RequestParam String planData) throws Exception {
895
 
896
		LoginDetails loginDetails = cookiesProcessor.getCookiesObject(request);
897
		AuthUser currentUser = authRepository.selectByEmailOrMobile(loginDetails.getEmailId());
898
 
899
		Gson gson = new Gson();
900
		Type type = new TypeToken<Map<String, Object>>() {
901
		}.getType();
902
		Map<String, Object> plan = gson.fromJson(planData, type);
903
 
904
		List<Map<String, Object>> days = (List<Map<String, Object>>) plan.get("days");
905
		List<String> dates = (List<String>) plan.get("dates");
906
 
36644 ranu 907
		String beatName = (plan.get("beatName") != null ? (String) plan.get("beatName") : "Beat").trim();
36618 ranu 908
 
36644 ranu 909
		// Duplicate check — same name + same authUserId = duplicate
910
		List<Beat> existingBeats = beatRepository.selectByAuthUserId(authUserId);
911
		for (Beat existing : existingBeats) {
912
			if (existing.getName() != null && beatName.equalsIgnoreCase(existing.getName().trim())) {
913
				LOGGER.info("Duplicate beat blocked: name='{}' authUserId={} existingId={}", beatName, authUserId, existing.getId());
36618 ranu 914
				Map<String, Object> response = new HashMap<>();
915
				response.put("status", true);
36644 ranu 916
				response.put("planGroupId", String.valueOf(existing.getId()));
36618 ranu 917
				response.put("duplicate", true);
36644 ranu 918
				response.put("message", "Beat '" + beatName + "' already exists");
36618 ranu 919
				return responseSender.ok(response);
920
			}
921
		}
922
 
36644 ranu 923
		String beatColor = BEAT_COLORS[Math.abs(beatName.hashCode()) % BEAT_COLORS.length];
924
		int totalDays = days.size();
36618 ranu 925
 
36644 ranu 926
		// Create Beat master
927
		Beat beat = new Beat();
928
		beat.setName(beatName);
929
		beat.setAuthUserId(authUserId);
930
		beat.setBeatColor(beatColor);
931
		beat.setTotalDays(totalDays);
932
		beat.setActive(true);
933
		beat.setCreatedBy(currentUser.getId());
934
		beat.setCreatedTimestamp(LocalDateTime.now());
935
 
936
		// Set start location from first day
937
		if (!days.isEmpty()) {
938
			Map<String, Object> firstDay = days.get(0);
939
			beat.setStartLocationName((String) firstDay.get("startLocationName"));
940
			beat.setStartLatitude((String) firstDay.get("startLatitude"));
941
			beat.setStartLongitude((String) firstDay.get("startLongitude"));
942
		}
943
		beatRepository.persist(beat);
944
 
945
		// End date of the whole beat = last scheduled day's date
946
		LocalDate beatEndDate = null;
947
		if (dates != null) {
948
			for (int d = dates.size() - 1; d >= 0; d--) {
949
				if (dates.get(d) != null) {
950
					beatEndDate = LocalDate.parse(dates.get(d), DateTimeFormatter.ISO_DATE);
951
					break;
952
				}
953
			}
954
		}
955
 
956
		// Create routes and schedules for each day
36618 ranu 957
		for (int d = 0; d < days.size(); d++) {
958
			Map<String, Object> day = days.get(d);
959
			int dayNumber = d + 1;
960
			LocalDate planDate = (dates != null && d < dates.size() && dates.get(d) != null)
36644 ranu 961
					? LocalDate.parse(dates.get(d), DateTimeFormatter.ISO_DATE) : null;
36618 ranu 962
 
36644 ranu 963
			// Auto-determine end action: last day = HOME, others = DAYBREAK
964
			String endAction = (String) day.get("endAction");
965
			if (endAction == null || endAction.isEmpty()) {
966
				endAction = (dayNumber == totalDays) ? "HOME" : "DAYBREAK";
36618 ranu 967
			}
968
 
36644 ranu 969
			// Always create schedule (even if planDate is null — unscheduled beat)
970
			BeatSchedule schedule = new BeatSchedule();
971
			schedule.setBeatId(beat.getId());
972
			schedule.setStartDate(planDate != null ? planDate : LocalDate.of(9999, 12, 31)); // placeholder for unscheduled
973
			schedule.setEndDate(beatEndDate);
974
			schedule.setDayNumber(dayNumber);
975
			schedule.setEndAction(endAction);
976
			schedule.setStayLocationName((String) day.get("stayLocationName"));
977
			schedule.setStayLatitude((String) day.get("stayLatitude"));
978
			schedule.setStayLongitude((String) day.get("stayLongitude"));
979
			if (day.get("totalDistanceKm") != null)
980
				schedule.setTotalDistanceKm(((Number) day.get("totalDistanceKm")).doubleValue());
981
			if (day.get("totalTimeMins") != null)
982
				schedule.setTotalTimeMins(((Number) day.get("totalTimeMins")).intValue());
983
			schedule.setCreatedTimestamp(LocalDateTime.now());
984
			beatScheduleRepository.persist(schedule);
985
 
986
			// Routes (stops)
36618 ranu 987
			List<Map<String, Object>> visits = (List<Map<String, Object>>) day.get("visits");
988
			if (visits != null) {
989
				for (int i = 0; i < visits.size(); i++) {
990
					Map<String, Object> visit = visits.get(i);
36644 ranu 991
					BeatRoute route = new BeatRoute();
992
					route.setBeatId(beat.getId());
993
					route.setFofoId(((Number) visit.get("id")).intValue());
994
					route.setSequenceOrder(i);
995
					route.setDayNumber(dayNumber);
996
					route.setActive(true);
997
					beatRouteRepository.persist(route);
36618 ranu 998
				}
999
			}
1000
		}
1001
 
1002
		Map<String, Object> response = new HashMap<>();
1003
		response.put("status", true);
36644 ranu 1004
		response.put("planGroupId", String.valueOf(beat.getId()));
36618 ranu 1005
		response.put("message", "Beat plan submitted successfully");
1006
		return responseSender.ok(response);
1007
	}
1008
 
36632 ranu 1009
	// ============ BULK UPLOAD ============
1010
 
1011
	@GetMapping(value = "/beatPlan/bulkUpload")
1012
	public String bulkUploadPage(HttpServletRequest request, Model model) {
1013
		return "beat-plan-bulk";
1014
	}
1015
 
1016
	@GetMapping(value = "/beatPlan/downloadTemplate")
1017
	public ResponseEntity<?> downloadTemplate() {
1018
		String csv = "beat_name,auth_user_id,start_date,day_number,partner_codes\n";
1019
		csv += "Jaipur East Route,280,2026-06-02,1,\"RJKAI1478,RJBUN1449,RJDEG1443\"\n";
1020
		csv += ",280,,2,\"RJALR1362,RJBTR1388\"\n";
1021
		csv += ",280,,3,\"RJRSD1518,RJSML356\"\n";
1022
		csv += "Agra Circuit,145,2026-06-05,1,\"UPAGR101,UPAGR102\"\n";
1023
 
1024
		org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders();
1025
		headers.add("Content-Disposition", "attachment; filename=beat_plan_template.csv");
1026
		headers.add("Content-Type", "text/csv");
1027
		return new ResponseEntity<>(csv, headers, org.springframework.http.HttpStatus.OK);
1028
	}
1029
 
1030
	@PostMapping(value = "/beatPlan/bulkUploadProcess")
1031
	public ResponseEntity<?> bulkUploadProcess(
1032
			HttpServletRequest request,
1033
			@RequestParam("file") org.springframework.web.multipart.MultipartFile file,
1034
			@RequestParam(value = "includeSundays", defaultValue = "false") boolean includeSundays) throws Exception {
1035
 
1036
		LoginDetails loginDetails = cookiesProcessor.getCookiesObject(request);
1037
		AuthUser currentUser = authRepository.selectByEmailOrMobile(loginDetails.getEmailId());
1038
 
1039
		java.io.Reader reader = new java.io.InputStreamReader(file.getInputStream());
1040
		org.apache.commons.csv.CSVParser parser = new org.apache.commons.csv.CSVParser(reader,
1041
				org.apache.commons.csv.CSVFormat.DEFAULT.withFirstRecordAsHeader().withTrim());
1042
		List<org.apache.commons.csv.CSVRecord> allRecords = parser.getRecords();
1043
		parser.close();
1044
 
1045
		Map<String, String> lastBeatNameByUser = new HashMap<>();
1046
		Map<String, List<org.apache.commons.csv.CSVRecord>> beatGroups = new LinkedHashMap<>();
1047
		Map<Long, String> resolvedBeatNames = new HashMap<>();
1048
 
1049
		for (org.apache.commons.csv.CSVRecord record : allRecords) {
1050
			String authId = record.get("auth_user_id").trim();
36644 ranu 1051
			String rawName = record.get("beat_name").trim().replaceAll("\\s+", " ");
1052
			if (rawName.isEmpty()) rawName = lastBeatNameByUser.getOrDefault(authId, "Beat");
1053
			else lastBeatNameByUser.put(authId, rawName);
1054
			resolvedBeatNames.put(record.getRecordNumber(), rawName);
1055
			beatGroups.computeIfAbsent(rawName + "|" + authId, k -> new ArrayList<>()).add(record);
36632 ranu 1056
		}
1057
 
1058
		List<FofoStore> allStores = fofoStoreRepository.selectAll();
1059
		Map<String, Integer> codeToId = new HashMap<>();
36644 ranu 1060
		for (FofoStore store : allStores) codeToId.put(store.getCode(), store.getId());
36632 ranu 1061
 
1062
		LocalDate holidayStart = LocalDate.now();
36644 ranu 1063
		List<PublicHolidays> holidays = publicHolidaysRepository.selectAllBetweenDates(holidayStart, holidayStart.plusMonths(6));
36632 ranu 1064
		Set<LocalDate> holidayDates = holidays.stream().map(PublicHolidays::getDate).collect(Collectors.toSet());
1065
 
36644 ranu 1066
		int beatsCreated = 0, errors = 0;
36632 ranu 1067
		List<String> errorMessages = new ArrayList<>();
1068
 
1069
		for (Map.Entry<String, List<org.apache.commons.csv.CSVRecord>> entry : beatGroups.entrySet()) {
1070
			try {
1071
				String[] keyParts = entry.getKey().split("\\|");
1072
				String beatName = keyParts[0];
1073
				int authUserId = Integer.parseInt(keyParts[1]);
1074
				List<org.apache.commons.csv.CSVRecord> rows = entry.getValue();
1075
				rows.sort((a, b) -> Integer.parseInt(a.get("day_number").trim()) - Integer.parseInt(b.get("day_number").trim()));
1076
 
1077
				String startDateStr = rows.get(0).get("start_date").trim();
36644 ranu 1078
				LocalDate startDate = startDateStr.isEmpty() ? null : LocalDate.parse(startDateStr, DateTimeFormatter.ISO_DATE);
36632 ranu 1079
 
1080
				if (startDate != null && startDate.isBefore(LocalDate.now())) {
36644 ranu 1081
					errorMessages.add("Beat '" + beatName + "': start_date in past. Skipped.");
36632 ranu 1082
					errors++;
1083
					continue;
1084
				}
1085
 
1086
				List<LocalDate> scheduleDates = new ArrayList<>();
1087
				if (startDate != null) {
1088
					LocalDate d = startDate;
36644 ranu 1089
					while (scheduleDates.size() < rows.size()) {
1090
						if (holidayDates.contains(d)) {
1091
							d = d.plusDays(1);
1092
							continue;
36632 ranu 1093
						}
36644 ranu 1094
						if (d.getDayOfWeek() == DayOfWeek.SUNDAY && !includeSundays) {
1095
							d = d.plusDays(1);
1096
							continue;
1097
						}
1098
						scheduleDates.add(d);
36632 ranu 1099
						d = d.plusDays(1);
1100
					}
1101
				}
1102
 
36644 ranu 1103
				// Duplicate check — skip if a beat with same name already exists for this user
1104
				boolean isDuplicate = beatRepository.selectByAuthUserId(authUserId).stream()
1105
						.anyMatch(b -> b.getName() != null && beatName.equalsIgnoreCase(b.getName().trim()));
1106
				if (isDuplicate) {
1107
					errorMessages.add("Beat '" + beatName + "' already exists for user " + authUserId + ". Skipped.");
1108
					errors++;
1109
					continue;
1110
				}
36632 ranu 1111
 
36644 ranu 1112
				String beatColor = BEAT_COLORS[Math.abs(beatName.hashCode()) % BEAT_COLORS.length];
36632 ranu 1113
				AuthUserLocation homeLoc = authUserLocationRepository.selectLatestByAuthUserIdAndType(authUserId, "BASE");
1114
 
36644 ranu 1115
				// Create Beat master
1116
				Beat beat = new Beat();
1117
				beat.setName(beatName);
1118
				beat.setAuthUserId(authUserId);
1119
				beat.setBeatColor(beatColor);
1120
				beat.setTotalDays(rows.size());
1121
				beat.setStartLocationName(homeLoc != null ? homeLoc.getLocationName() : "Home");
1122
				beat.setStartLatitude(homeLoc != null ? homeLoc.getLatitude() : null);
1123
				beat.setStartLongitude(homeLoc != null ? homeLoc.getLongitude() : null);
1124
				beat.setActive(true);
1125
				beat.setCreatedBy(currentUser.getId());
1126
				beat.setCreatedTimestamp(LocalDateTime.now());
1127
				beatRepository.persist(beat);
36632 ranu 1128
 
36644 ranu 1129
				for (int rowIdx = 0; rowIdx < rows.size(); rowIdx++) {
36632 ranu 1130
					org.apache.commons.csv.CSVRecord row = rows.get(rowIdx);
1131
					int dayNumber = Integer.parseInt(row.get("day_number").trim());
1132
					LocalDate planDate = (rowIdx < scheduleDates.size()) ? scheduleDates.get(rowIdx) : null;
36644 ranu 1133
					LocalDate bulkEndDate = scheduleDates.isEmpty() ? null : scheduleDates.get(scheduleDates.size() - 1);
36632 ranu 1134
 
36644 ranu 1135
					// Always create schedule — placeholder date (9999-12-31) when unscheduled
1136
					BeatSchedule schedule = new BeatSchedule();
1137
					schedule.setBeatId(beat.getId());
1138
					schedule.setStartDate(planDate != null ? planDate : LocalDate.of(9999, 12, 31));
1139
					schedule.setEndDate(bulkEndDate);
1140
					schedule.setDayNumber(dayNumber);
1141
					schedule.setEndAction(rowIdx == rows.size() - 1 ? "HOME" : "DAYBREAK");
1142
					schedule.setCreatedTimestamp(LocalDateTime.now());
1143
					beatScheduleRepository.persist(schedule);
36632 ranu 1144
 
36644 ranu 1145
					String[] partnerCodes = row.get("partner_codes").trim().split(",");
36632 ranu 1146
					for (int i = 0; i < partnerCodes.length; i++) {
36644 ranu 1147
						String code = partnerCodes[i].trim();
1148
						if (code.isEmpty()) continue;
1149
						Integer fofoId = codeToId.get(code);
36632 ranu 1150
						if (fofoId == null) {
36644 ranu 1151
							errorMessages.add("Code not found: " + code);
36632 ranu 1152
							errors++;
1153
							continue;
1154
						}
1155
 
36644 ranu 1156
						BeatRoute route = new BeatRoute();
1157
						route.setBeatId(beat.getId());
1158
						route.setFofoId(fofoId);
1159
						route.setSequenceOrder(i);
1160
						route.setDayNumber(dayNumber);
1161
						route.setActive(true);
1162
						beatRouteRepository.persist(route);
36632 ranu 1163
					}
1164
				}
1165
				beatsCreated++;
1166
			} catch (Exception e) {
1167
				errors++;
36644 ranu 1168
				errorMessages.add("Error: " + entry.getKey() + " - " + e.getMessage());
36632 ranu 1169
			}
1170
		}
1171
 
1172
		Map<String, Object> response = new HashMap<>();
1173
		response.put("status", true);
1174
		response.put("beatsCreated", beatsCreated);
1175
		response.put("errors", errors);
1176
		response.put("errorMessages", errorMessages);
1177
		return responseSender.ok(response);
1178
	}
1179
 
36644 ranu 1180
	// ============ CALENDAR ============
36618 ranu 1181
 
1182
	@PostMapping(value = "/beatPlan/delete")
1183
	public ResponseEntity<?> deleteBeat(@RequestParam String planGroupId) {
36644 ranu 1184
		int beatId = Integer.parseInt(planGroupId);
1185
		beatRouteRepository.deleteByBeatId(beatId);
1186
		beatScheduleRepository.deleteByBeatId(beatId);
1187
		Beat beat = beatRepository.selectById(beatId);
1188
		if (beat != null) {
1189
			beat.setActive(false);
1190
		}
36618 ranu 1191
 
1192
		Map<String, Object> response = new HashMap<>();
1193
		response.put("status", true);
1194
		response.put("message", "Beat deleted");
1195
		return responseSender.ok(response);
1196
	}
1197
 
1198
	@GetMapping(value = "/beatPlan/calendar")
1199
	public ResponseEntity<?> getCalendar(
1200
			@RequestParam int authUserId,
1201
			@RequestParam String month) {
1202
 
1203
		YearMonth ym = YearMonth.parse(month);
1204
		LocalDate startDate = ym.atDay(1);
1205
		LocalDate endDate = ym.atEndOfMonth();
1206
 
1207
		List<PublicHolidays> holidays = publicHolidaysRepository.selectAllBetweenDates(startDate, endDate);
1208
		List<Map<String, String>> holidayList = holidays.stream().map(h -> {
1209
			Map<String, String> m = new HashMap<>();
1210
			m.put("date", h.getDate().toString());
1211
			m.put("occasion", h.getOccasion());
1212
			return m;
1213
		}).collect(Collectors.toList());
1214
 
36644 ranu 1215
		List<Beat> allBeats = beatRepository.selectActiveByAuthUserId(authUserId);
36618 ranu 1216
		LocalDate today = LocalDate.now();
1217
		List<Map<String, Object>> scheduledBeats = new ArrayList<>();
1218
 
36644 ranu 1219
		for (Beat beat : allBeats) {
1220
			List<BeatSchedule> schedules = beatScheduleRepository.selectByBeatId(beat.getId());
1221
			List<BeatRoute> routes = beatRouteRepository.selectByBeatId(beat.getId());
36618 ranu 1222
 
36644 ranu 1223
			boolean allNullDates = schedules.isEmpty() || schedules.stream().allMatch(s -> s.getStartDate().getYear() == 9999);
1224
			boolean hasToday = !allNullDates && schedules.stream().anyMatch(s -> s.getStartDate().equals(today));
1225
			boolean allPast = !allNullDates && schedules.stream().filter(s -> s.getStartDate().getYear() != 9999).allMatch(s -> s.getStartDate().isBefore(today));
1226
			boolean allFuture = !allNullDates && schedules.stream().filter(s -> s.getStartDate().getYear() != 9999).allMatch(s -> s.getStartDate().isAfter(today));
36618 ranu 1227
 
1228
			String status;
1229
			if (allNullDates) status = "unscheduled";
1230
			else if (hasToday) status = "running";
1231
			else if (allPast) status = "completed";
36644 ranu 1232
			else status = "scheduled";
36618 ranu 1233
 
36644 ranu 1234
			Map<String, Object> beatInfo = new HashMap<>();
1235
			beatInfo.put("planGroupId", String.valueOf(beat.getId()));
1236
			beatInfo.put("beatName", beat.getName() != null ? beat.getName() : "Beat");
1237
			beatInfo.put("beatColor", beat.getBeatColor() != null ? beat.getBeatColor() : "#3498DB");
1238
			beatInfo.put("status", status);
36618 ranu 1239
 
1240
			List<Map<String, Object>> dayInfoList = new ArrayList<>();
36644 ranu 1241
			for (BeatSchedule s : schedules) {
36618 ranu 1242
				Map<String, Object> dayInfo = new HashMap<>();
36644 ranu 1243
				dayInfo.put("dayNumber", s.getDayNumber());
1244
				boolean isUnscheduled = s.getStartDate().getYear() == 9999;
1245
				dayInfo.put("planDate", isUnscheduled ? null : s.getStartDate().toString());
1246
				dayInfo.put("totalKm", s.getTotalDistanceKm());
1247
				dayInfo.put("totalMins", s.getTotalTimeMins());
1248
				long visitCount = routes.stream().filter(r -> r.getDayNumber() == s.getDayNumber()).count();
1249
				dayInfo.put("visitCount", (int) visitCount);
36618 ranu 1250
				dayInfoList.add(dayInfo);
1251
			}
36644 ranu 1252
			if (schedules.isEmpty()) {
1253
				// No schedule at all — show from routes
1254
				Map<Integer, Long> dayCounts = routes.stream()
1255
						.collect(Collectors.groupingBy(BeatRoute::getDayNumber, Collectors.counting()));
1256
				for (int d = 1; d <= beat.getTotalDays(); d++) {
1257
					Map<String, Object> dayInfo = new HashMap<>();
1258
					dayInfo.put("dayNumber", d);
1259
					dayInfo.put("planDate", null);
1260
					dayInfo.put("totalKm", null);
1261
					dayInfo.put("totalMins", null);
1262
					dayInfo.put("visitCount", dayCounts.getOrDefault(d, 0L).intValue());
1263
					dayInfoList.add(dayInfo);
1264
				}
1265
			}
1266
			beatInfo.put("days", dayInfoList);
1267
			scheduledBeats.add(beatInfo);
36618 ranu 1268
		}
1269
 
1270
		Set<String> blockedDates = new HashSet<>();
1271
		for (LocalDate d = startDate; !d.isAfter(endDate); d = d.plusDays(1)) {
36644 ranu 1272
			if (d.getDayOfWeek() == DayOfWeek.SUNDAY) blockedDates.add(d.toString());
36618 ranu 1273
		}
36644 ranu 1274
		for (PublicHolidays h : holidays) blockedDates.add(h.getDate().toString());
36618 ranu 1275
 
1276
		Map<String, Object> response = new HashMap<>();
1277
		response.put("holidays", holidayList);
1278
		response.put("scheduledBeats", scheduledBeats);
1279
		response.put("blockedDates", blockedDates);
1280
		return responseSender.ok(response);
1281
	}
1282
 
1283
	@PostMapping(value = "/beatPlan/scheduleOnCalendar")
1284
	public ResponseEntity<?> scheduleOnCalendar(
1285
			HttpServletRequest request,
1286
			@RequestParam String planGroupId,
1287
			@RequestParam String dates,
1288
			@RequestParam(required = false) String beatName,
1289
			@RequestParam(required = false) String beatColor) throws Exception {
1290
 
36644 ranu 1291
		int beatId = Integer.parseInt(planGroupId);
36618 ranu 1292
		Gson gson = new Gson();
1293
		List<String> dateList = gson.fromJson(dates, new TypeToken<List<String>>() {
1294
		}.getType());
1295
 
36644 ranu 1296
		Beat beat = beatRepository.selectById(beatId);
1297
		if (beat == null) return responseSender.badRequest("Beat not found");
36618 ranu 1298
 
36644 ranu 1299
		if (beatName != null) beat.setName(beatName);
1300
		if (beatColor != null && !beatColor.isEmpty()) beat.setBeatColor(beatColor);
36618 ranu 1301
 
36644 ranu 1302
		// Delete old schedules and create new
1303
		beatScheduleRepository.deleteByBeatId(beatId);
1304
		LocalDate schEndDate = dateList.isEmpty() ? null : LocalDate.parse(dateList.get(dateList.size() - 1));
1305
		for (int i = 0; i < dateList.size() && i < beat.getTotalDays(); i++) {
1306
			BeatSchedule schedule = new BeatSchedule();
1307
			schedule.setBeatId(beatId);
1308
			schedule.setStartDate(LocalDate.parse(dateList.get(i)));
1309
			schedule.setEndDate(schEndDate);
1310
			schedule.setDayNumber(i + 1);
1311
			schedule.setEndAction(i == dateList.size() - 1 ? "HOME" : "DAYBREAK");
1312
			schedule.setCreatedTimestamp(LocalDateTime.now());
1313
			beatScheduleRepository.persist(schedule);
36618 ranu 1314
		}
1315
 
1316
		Map<String, Object> response = new HashMap<>();
1317
		response.put("status", true);
1318
		response.put("message", "Beat scheduled successfully");
1319
		return responseSender.ok(response);
1320
	}
1321
 
36644 ranu 1322
	// Drag-drop scheduling — adds schedule dates to the EXISTING beat (no new beat created)
36618 ranu 1323
	@PostMapping(value = "/beatPlan/repeatBeat")
1324
	public ResponseEntity<?> repeatBeat(
1325
			HttpServletRequest request,
1326
			@RequestParam String sourcePlanGroupId,
1327
			@RequestParam int authUserId,
1328
			@RequestParam String dates) throws Exception {
1329
 
36644 ranu 1330
		int beatId = Integer.parseInt(sourcePlanGroupId);
36618 ranu 1331
		Gson gson = new Gson();
1332
		List<String> dateList = gson.fromJson(dates, new TypeToken<List<String>>() {
1333
		}.getType());
1334
 
36644 ranu 1335
		Beat beat = beatRepository.selectById(beatId);
1336
		if (beat == null) return responseSender.badRequest("Beat not found");
36618 ranu 1337
 
36644 ranu 1338
		// Remove placeholder (unscheduled) schedule rows
1339
		List<BeatSchedule> existing = beatScheduleRepository.selectByBeatId(beatId);
1340
		for (BeatSchedule s : existing) {
1341
			if (s.getStartDate() != null && s.getStartDate().getYear() == 9999) {
1342
				beatScheduleRepository.delete(s);
1343
			}
36618 ranu 1344
		}
1345
 
36644 ranu 1346
		// Add new real-date schedule rows for the existing beat
1347
		LocalDate repeatEndDate = dateList.isEmpty() ? null : LocalDate.parse(dateList.get(dateList.size() - 1));
1348
		for (int i = 0; i < dateList.size(); i++) {
1349
			BeatSchedule schedule = new BeatSchedule();
1350
			schedule.setBeatId(beatId);
1351
			schedule.setStartDate(LocalDate.parse(dateList.get(i)));
1352
			schedule.setEndDate(repeatEndDate);
1353
			schedule.setDayNumber(i + 1);
1354
			schedule.setEndAction(i == dateList.size() - 1 ? "HOME" : "DAYBREAK");
1355
			schedule.setCreatedTimestamp(LocalDateTime.now());
1356
			beatScheduleRepository.persist(schedule);
36618 ranu 1357
		}
1358
 
1359
		Map<String, Object> response = new HashMap<>();
1360
		response.put("status", true);
36644 ranu 1361
		response.put("planGroupId", String.valueOf(beatId));
1362
		response.put("message", "Beat scheduled successfully");
36618 ranu 1363
		return responseSender.ok(response);
1364
	}
1365
 
1366
	@GetMapping(value = "/beatPlan/availableSlots")
1367
	public ResponseEntity<?> getAvailableSlots(
1368
			@RequestParam int authUserId,
1369
			@RequestParam String month,
1370
			@RequestParam int daysNeeded) {
1371
 
1372
		YearMonth ym = YearMonth.parse(month);
1373
		LocalDate startDate = ym.atDay(1);
1374
		LocalDate endDate = ym.atEndOfMonth();
1375
		LocalDate today = LocalDate.now();
1376
 
1377
		Set<LocalDate> blocked = new HashSet<>();
1378
		for (LocalDate d = startDate; !d.isAfter(endDate); d = d.plusDays(1)) {
1379
			if (d.getDayOfWeek() == DayOfWeek.SUNDAY) blocked.add(d);
36644 ranu 1380
			if (!d.isAfter(today)) blocked.add(d);
36618 ranu 1381
		}
1382
 
1383
		List<PublicHolidays> holidays = publicHolidaysRepository.selectAllBetweenDates(startDate, endDate);
1384
		for (PublicHolidays h : holidays) blocked.add(h.getDate());
1385
 
36644 ranu 1386
		// Get all scheduled dates for this user
1387
		List<Beat> userBeats = beatRepository.selectActiveByAuthUserId(authUserId);
1388
		for (Beat b : userBeats) {
1389
			List<BeatSchedule> schedules = beatScheduleRepository.selectByBeatId(b.getId());
1390
			for (BeatSchedule s : schedules) blocked.add(s.getStartDate());
36618 ranu 1391
		}
1392
 
1393
		List<String> available = new ArrayList<>();
1394
		for (LocalDate d = startDate.isAfter(today) ? startDate : today.plusDays(1);
1395
			 !d.isAfter(endDate) && available.size() < daysNeeded;
1396
			 d = d.plusDays(1)) {
36644 ranu 1397
			if (!blocked.contains(d)) available.add(d.toString());
36618 ranu 1398
		}
1399
 
1400
		Map<String, Object> response = new HashMap<>();
1401
		response.put("suggestedDates", available);
1402
		response.put("totalAvailable", available.size());
1403
		return responseSender.ok(response);
1404
	}
1405
 
1406
	// --- Sorting helpers ---
1407
 
1408
	private List<Map<String, Object>> sortByNearestNeighborFromStart(
1409
			List<Map<String, Object>> partners, double startLat, double startLng) {
1410
		List<Map<String, Object>> withCoords = new ArrayList<>();
1411
		List<Map<String, Object>> withoutCoords = new ArrayList<>();
1412
		for (Map<String, Object> p : partners) {
36644 ranu 1413
			if (hasValidCoords(p)) withCoords.add(p);
1414
			else withoutCoords.add(p);
36618 ranu 1415
		}
1416
		List<Map<String, Object>> sorted = new ArrayList<>();
36644 ranu 1417
		double currentLat = startLat, currentLng = startLng;
36618 ranu 1418
		while (!withCoords.isEmpty()) {
1419
			int nearestIdx = 0;
1420
			double nearestDist = Double.MAX_VALUE;
1421
			for (int i = 0; i < withCoords.size(); i++) {
36644 ranu 1422
				double dist = haversine(currentLat, currentLng,
1423
						Double.parseDouble(withCoords.get(i).get("latitude").toString()),
1424
						Double.parseDouble(withCoords.get(i).get("longitude").toString()));
36618 ranu 1425
				if (dist < nearestDist) {
1426
					nearestDist = dist;
1427
					nearestIdx = i;
1428
				}
1429
			}
1430
			Map<String, Object> nearest = withCoords.remove(nearestIdx);
1431
			sorted.add(nearest);
1432
			currentLat = Double.parseDouble(nearest.get("latitude").toString());
1433
			currentLng = Double.parseDouble(nearest.get("longitude").toString());
1434
		}
1435
		sorted.addAll(withoutCoords);
1436
		return sorted;
1437
	}
1438
 
1439
	private List<Map<String, Object>> sortByNearestNeighbor(List<Map<String, Object>> partners) {
1440
		List<Map<String, Object>> withCoords = new ArrayList<>();
1441
		List<Map<String, Object>> withoutCoords = new ArrayList<>();
1442
		for (Map<String, Object> p : partners) {
36644 ranu 1443
			if (hasValidCoords(p)) withCoords.add(p);
1444
			else withoutCoords.add(p);
36618 ranu 1445
		}
1446
		List<Map<String, Object>> sorted = new ArrayList<>();
1447
		if (!withCoords.isEmpty()) {
1448
			sorted.add(withCoords.remove(0));
1449
			while (!withCoords.isEmpty()) {
1450
				Map<String, Object> last = sorted.get(sorted.size() - 1);
1451
				double lastLat = Double.parseDouble(last.get("latitude").toString());
1452
				double lastLng = Double.parseDouble(last.get("longitude").toString());
1453
				int nearestIdx = 0;
1454
				double nearestDist = Double.MAX_VALUE;
1455
				for (int i = 0; i < withCoords.size(); i++) {
36644 ranu 1456
					double dist = haversine(lastLat, lastLng,
1457
							Double.parseDouble(withCoords.get(i).get("latitude").toString()),
1458
							Double.parseDouble(withCoords.get(i).get("longitude").toString()));
36618 ranu 1459
					if (dist < nearestDist) {
1460
						nearestDist = dist;
1461
						nearestIdx = i;
1462
					}
1463
				}
1464
				sorted.add(withCoords.remove(nearestIdx));
1465
			}
1466
		}
1467
		sorted.addAll(withoutCoords);
1468
		return sorted;
1469
	}
1470
 
1471
	private boolean hasValidCoords(Map<String, Object> p) {
1472
		Object lat = p.get("latitude");
1473
		Object lng = p.get("longitude");
36644 ranu 1474
		return lat != null && lng != null && !lat.toString().isEmpty() && !lng.toString().isEmpty();
36618 ranu 1475
	}
1476
 
1477
	private double haversine(double lat1, double lng1, double lat2, double lng2) {
1478
		double R = 6371;
1479
		double dLat = Math.toRadians(lat2 - lat1);
1480
		double dLng = Math.toRadians(lng2 - lng1);
1481
		double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
1482
				+ Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
1483
				* Math.sin(dLng / 2) * Math.sin(dLng / 2);
1484
		double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
1485
		return R * c;
1486
	}
1487
}