Subversion Repositories SmartDukaan

Rev

Rev 36651 | Rev 36660 | Go to most recent revision | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 36651 Rev 36655
Line 94... Line 94...
94
	@Autowired
94
	@Autowired
95
	private com.spice.profitmandi.dao.repository.dtr.LeadLiveLocationRepository leadLiveLocationRepositoryAuto;
95
	private com.spice.profitmandi.dao.repository.dtr.LeadLiveLocationRepository leadLiveLocationRepositoryAuto;
96
	@Autowired
96
	@Autowired
97
	private com.spice.profitmandi.dao.repository.dtr.LeadActivityRepository leadActivityRepositoryAuto;
97
	private com.spice.profitmandi.dao.repository.dtr.LeadActivityRepository leadActivityRepositoryAuto;
98
 
98
 
-
 
99
	private static Double parseDoubleOrNull(String s) {
-
 
100
		if (s == null || s.trim().isEmpty()) return null;
-
 
101
		try {
-
 
102
			return Double.parseDouble(s.trim());
-
 
103
		} catch (NumberFormatException e) {
-
 
104
			return null;
-
 
105
		}
-
 
106
	}
-
 
107
 
-
 
108
	private static double haversineKm(double lat1, double lng1, double lat2, double lng2) {
-
 
109
		double R = 6371;
-
 
110
		double dLat = Math.toRadians(lat2 - lat1);
-
 
111
		double dLng = Math.toRadians(lng2 - lng1);
-
 
112
		double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
-
 
113
				+ Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
-
 
114
				* Math.sin(dLng / 2) * Math.sin(dLng / 2);
-
 
115
		double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
-
 
116
		return R * c;
-
 
117
	}
-
 
118
 
-
 
119
	// ====================== BASE LOCATION MANAGEMENT ======================
-
 
120
	// Inline page that lets Sales L3+ pick a user and set their base (home)
-
 
121
	// location via map. Reads use the existing /beatPlan/getBaseLocation, writes
-
 
122
	// go through the L3+-guarded endpoint below.
-
 
123
	@GetMapping(value = "/beatPlan/baseLocationPage")
-
 
124
	public String baseLocationPage(HttpServletRequest request, Model model) {
-
 
125
		EscalationType[] escalationTypes = EscalationType.values();
-
 
126
		model.addAttribute("escalationTypes", escalationTypes);
-
 
127
		return "beat-plan-base-location";
-
 
128
	}
-
 
129
 
-
 
130
	@PostMapping(value = "/beatPlan/updateBaseLocation")
-
 
131
	public ResponseEntity<?> updateBaseLocation(
-
 
132
			HttpServletRequest request,
-
 
133
			@RequestParam int authUserId,
-
 
134
			@RequestParam String locationName,
-
 
135
			@RequestParam String latitude,
-
 
136
			@RequestParam String longitude,
-
 
137
			@RequestParam(required = false) String address) throws Exception {
-
 
138
 
-
 
139
		LoginDetails ld = cookiesProcessor.getCookiesObject(request);
-
 
140
		AuthUser me = authRepository.selectByEmailOrMobile(ld.getEmailId());
-
 
141
		if (me == null) return responseSender.badRequest("Not logged in");
-
 
142
 
-
 
143
		// Permission gate: only Sales L3 and above
-
 
144
		boolean isSalesL3Plus = csService.getAuthUserIds(
-
 
145
						com.spice.profitmandi.common.model.ProfitMandiConstants.TICKET_CATEGORY_SALES,
-
 
146
						Arrays.asList(EscalationType.L3, EscalationType.L4))
-
 
147
				.stream().anyMatch(u -> u.getId() == me.getId());
-
 
148
		if (!isSalesL3Plus) {
-
 
149
			return responseSender.badRequest("Only Sales L3 and above can update base location");
-
 
150
		}
-
 
151
 
-
 
152
		AuthUserLocation loc = new AuthUserLocation();
-
 
153
		loc.setAuthUserId(authUserId);
-
 
154
		loc.setLocationType("BASE");
-
 
155
		loc.setLocationName(locationName);
-
 
156
		loc.setLatitude(latitude);
-
 
157
		loc.setLongitude(longitude);
-
 
158
		loc.setAddress(address);
-
 
159
		loc.setCreatedTimestamp(LocalDateTime.now());
-
 
160
		authUserLocationRepository.persist(loc);
-
 
161
 
-
 
162
		Map<String, Object> result = new HashMap<>();
-
 
163
		result.put("status", true);
-
 
164
		result.put("id", loc.getId());
-
 
165
		result.put("message", "Base location updated");
-
 
166
		return responseSender.ok(result);
-
 
167
	}
-
 
168
 
-
 
169
	// ====================== ONE-TIME LAT/LNG MIGRATION ======================
-
 
170
	// For each active fofo_store, compare its stored lat/lng with the geocoded
-
 
171
	// address lat/lng (cached in Redis). If the gap is > thresholdKm (default 5)
-
 
172
	// OR the store has no lat/lng yet, update the store with the geocoded
-
 
173
	// coordinates. Otherwise keep the existing values.
-
 
174
	//
-
 
175
	// Usage:
-
 
176
	//   GET /beatPlan/migrateStoreLatLng              -> dry run, default 5km, all
-
 
177
	//   GET /beatPlan/migrateStoreLatLng?apply=true   -> actually update
-
 
178
	//   ?thresholdKm=3      -> use a different threshold
-
 
179
	//   ?limit=100          -> process only N stores (for staged runs)
-
 
180
	@GetMapping(value = "/beatPlan/migrateStoreLatLng")
-
 
181
	public ResponseEntity<?> migrateStoreLatLng(
-
 
182
			@RequestParam(required = false, defaultValue = "false") boolean apply,
-
 
183
			@RequestParam(required = false, defaultValue = "5") double thresholdKm,
-
 
184
			@RequestParam(required = false, defaultValue = "0") int limit) throws ProfitMandiBusinessException {
-
 
185
 
-
 
186
		List<FofoStore> stores = fofoStoreRepository.selectActiveStores();
-
 
187
		if (limit > 0 && stores.size() > limit) stores = stores.subList(0, limit);
-
 
188
 
-
 
189
		List<Integer> ids = stores.stream().map(FofoStore::getId).collect(Collectors.toList());
-
 
190
		Map<Integer, CustomRetailer> retailerMap = retailerService.getFofoRetailers(ids);
-
 
191
 
-
 
192
		int total = stores.size();
-
 
193
		int updated = 0, kept = 0, noAddress = 0, noGeocode = 0, errored = 0;
-
 
194
		List<Map<String, Object>> changes = new ArrayList<>();
-
 
195
 
-
 
196
		for (FofoStore store : stores) {
-
 
197
			try {
-
 
198
				CustomRetailer retailer = retailerMap.get(store.getId());
-
 
199
				if (retailer == null || retailer.getAddress() == null) {
-
 
200
					noAddress++;
-
 
201
					continue;
-
 
202
				}
-
 
203
 
-
 
204
				String geoAddr = com.spice.profitmandi.service.GeocodingService.buildGeoAddress(
-
 
205
						retailer.getAddress().getLine1(), retailer.getAddress().getCity(),
-
 
206
						retailer.getAddress().getState(), retailer.getAddress().getPinCode());
-
 
207
				if (geoAddr == null || geoAddr.isEmpty()) {
-
 
208
					noAddress++;
-
 
209
					continue;
-
 
210
				}
-
 
211
 
-
 
212
				double[] coords = geocodingService.geocodeAddress(geoAddr);
-
 
213
				if (coords == null) {
-
 
214
					noGeocode++;
-
 
215
					continue;
-
 
216
				}
-
 
217
 
-
 
218
				Double existingLat = parseDoubleOrNull(store.getLatitude());
-
 
219
				Double existingLng = parseDoubleOrNull(store.getLongitude());
-
 
220
 
-
 
221
				boolean shouldUpdate;
-
 
222
				double distKm = -1;
-
 
223
				String reason;
-
 
224
				if (existingLat == null || existingLng == null) {
-
 
225
					shouldUpdate = true;
-
 
226
					reason = "missing existing lat/lng";
-
 
227
				} else {
-
 
228
					distKm = haversineKm(existingLat, existingLng, coords[0], coords[1]);
-
 
229
					shouldUpdate = distKm > thresholdKm;
-
 
230
					reason = shouldUpdate
-
 
231
							? "gap " + Math.round(distKm * 10.0) / 10.0 + "km > " + thresholdKm + "km"
-
 
232
							: "gap " + Math.round(distKm * 10.0) / 10.0 + "km within " + thresholdKm + "km";
-
 
233
				}
-
 
234
 
-
 
235
				if (shouldUpdate) {
-
 
236
					if (apply) {
-
 
237
						store.setLatitude(String.valueOf(coords[0]));
-
 
238
						store.setLongitude(String.valueOf(coords[1]));
-
 
239
						fofoStoreRepository.persist(store);
-
 
240
					}
-
 
241
					updated++;
-
 
242
					Map<String, Object> ch = new HashMap<>();
-
 
243
					ch.put("storeId", store.getId());
-
 
244
					ch.put("code", store.getCode());
-
 
245
					ch.put("oldLat", existingLat);
-
 
246
					ch.put("oldLng", existingLng);
-
 
247
					ch.put("newLat", coords[0]);
-
 
248
					ch.put("newLng", coords[1]);
-
 
249
					ch.put("distKm", distKm >= 0 ? Math.round(distKm * 10.0) / 10.0 : null);
-
 
250
					ch.put("reason", reason);
-
 
251
					changes.add(ch);
-
 
252
				} else {
-
 
253
					kept++;
-
 
254
				}
-
 
255
			} catch (Exception e) {
-
 
256
				errored++;
-
 
257
				LOGGER.warn("Geocode/migrate failed for fofoId={}: {}", store.getId(), e.getMessage());
-
 
258
			}
-
 
259
		}
-
 
260
 
-
 
261
		Map<String, Object> result = new HashMap<>();
-
 
262
		result.put("mode", apply ? "APPLIED" : "DRY RUN — pass &apply=true to actually update");
-
 
263
		result.put("thresholdKm", thresholdKm);
-
 
264
		result.put("total", total);
-
 
265
		result.put("updated", updated);
-
 
266
		result.put("kept", kept);
-
 
267
		result.put("noAddress", noAddress);
-
 
268
		result.put("noGeocode", noGeocode);
-
 
269
		result.put("errored", errored);
-
 
270
		// Limit changes preview to avoid huge responses
-
 
271
		result.put("changes", changes.size() > 200 ? changes.subList(0, 200) : changes);
-
 
272
		result.put("changesShownCount", Math.min(changes.size(), 200));
-
 
273
		return responseSender.ok(result);
-
 
274
	}
-
 
275
 
99
	// ====================== EDIT BEAT ======================
276
	// ====================== EDIT BEAT ======================
100
	// Update an existing beat — name + partner stops (routes).
277
	// Update an existing beat — name + partner stops (routes).
101
	// Schedules are NOT touched here; manage them via calendar drag-drop.
278
	// Schedules are NOT touched here; manage them via calendar drag-drop.
102
	@PostMapping(value = "/beatPlan/updateBeat")
279
	@PostMapping(value = "/beatPlan/updateBeat")
103
	public ResponseEntity<?> updateBeat(
280
	public ResponseEntity<?> updateBeat(
Line 330... Line 507...
330
				FofoStore fs = storeMap.get(fofoId);
507
				FofoStore fs = storeMap.get(fofoId);
331
				CustomRetailer cr = retailerMap.get(fofoId);
508
				CustomRetailer cr = retailerMap.get(fofoId);
332
				stop.put("code", fs != null ? fs.getCode() : null);
509
				stop.put("code", fs != null ? fs.getCode() : null);
333
				stop.put("name", fs != null && fs.getOutletName() != null ? fs.getOutletName()
510
				stop.put("name", fs != null && fs.getOutletName() != null ? fs.getOutletName()
334
						: (cr != null ? cr.getBusinessName() : "Store #" + fofoId));
511
						: (cr != null ? cr.getBusinessName() : "Store #" + fofoId));
-
 
512
				// Use FofoStore lat/lng directly (no geocoding needed after migration)
335
				if (cr != null && cr.getAddress() != null) {
513
				if (fs != null && fs.getLatitude() != null && fs.getLongitude() != null
336
					stop.put("address", cr.getAddress().getAddressString());
514
						&& !fs.getLatitude().isEmpty() && !fs.getLongitude().isEmpty()) {
337
					try {
515
					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]);
516
						stop.put("lat", Double.parseDouble(fs.getLatitude()));
344
							stop.put("lng", coords[1]);
517
						stop.put("lng", Double.parseDouble(fs.getLongitude()));
345
						}
-
 
346
					} catch (Exception ignored) {
518
					} catch (NumberFormatException ignored) {
347
					}
519
					}
348
				}
520
				}
-
 
521
				if (cr != null && cr.getAddress() != null) {
-
 
522
					stop.put("address", cr.getAddress().getAddressString());
-
 
523
				}
349
				stops.add(stop);
524
				stops.add(stop);
350
			}
525
			}
351
			// Leads
526
			// Leads
352
			for (Map<String, Object> ls : b.getLeadStops()) {
527
			for (Map<String, Object> ls : b.getLeadStops()) {
353
				int leadId = (Integer) ls.get("leadId");
528
				int leadId = (Integer) ls.get("leadId");
Line 507... Line 682...
507
 
682
 
508
		List<FofoStore> fofoStores = fofoStoreRepository.selectByRetailerIds(fofoIds);
683
		List<FofoStore> fofoStores = fofoStoreRepository.selectByRetailerIds(fofoIds);
509
		Map<Integer, CustomRetailer> retailerMap = retailerService.getFofoRetailers(fofoIds);
684
		Map<Integer, CustomRetailer> retailerMap = retailerService.getFofoRetailers(fofoIds);
510
 
685
 
511
		List<Map<String, Object>> partners = new ArrayList<>();
686
		List<Map<String, Object>> partners = new ArrayList<>();
512
		List<String> addressesToGeocode = new ArrayList<>();
-
 
513
 
687
 
514
		for (FofoStore store : fofoStores) {
688
		for (FofoStore store : fofoStores) {
515
			if (!store.isActive() || store.isClosed()) continue;
689
			if (!store.isActive() || store.isClosed()) continue;
516
			CustomRetailer retailer = retailerMap.get(store.getId());
690
			CustomRetailer retailer = retailerMap.get(store.getId());
517
 
691
 
Line 519... Line 693...
519
			partnerData.put("fofoId", store.getId());
693
			partnerData.put("fofoId", store.getId());
520
			partnerData.put("code", store.getCode());
694
			partnerData.put("code", store.getCode());
521
			partnerData.put("outletName", store.getOutletName());
695
			partnerData.put("outletName", store.getOutletName());
522
			partnerData.put("type", "partner");
696
			partnerData.put("type", "partner");
523
 
697
 
-
 
698
			// Use FofoStore lat/lng directly (migrated from address geocode)
-
 
699
			if (store.getLatitude() != null && !store.getLatitude().isEmpty()
-
 
700
					&& store.getLongitude() != null && !store.getLongitude().isEmpty()) {
-
 
701
				partnerData.put("latitude", store.getLatitude());
524
			String geoAddress = null;
702
				partnerData.put("longitude", store.getLongitude());
-
 
703
			}
-
 
704
 
525
			if (retailer != null) {
705
			if (retailer != null) {
526
				partnerData.put("businessName", retailer.getBusinessName());
706
				partnerData.put("businessName", retailer.getBusinessName());
527
				if (retailer.getAddress() != null) {
707
				if (retailer.getAddress() != null) {
528
					partnerData.put("address", retailer.getAddress().getAddressString());
708
					partnerData.put("address", retailer.getAddress().getAddressString());
529
					geoAddress = com.spice.profitmandi.service.GeocodingService.buildGeoAddress(
-
 
530
							retailer.getAddress().getLine1(),
-
 
531
							retailer.getAddress().getCity(),
-
 
532
							retailer.getAddress().getState(),
-
 
533
							retailer.getAddress().getPinCode());
-
 
534
				}
709
				}
535
			}
710
			}
536
			addressesToGeocode.add(geoAddress);
-
 
537
			partners.add(partnerData);
711
			partners.add(partnerData);
538
		}
712
		}
539
 
713
 
540
		// Geocode in parallel
-
 
541
		java.util.concurrent.ExecutorService executor = java.util.concurrent.Executors.newFixedThreadPool(
-
 
542
				Math.min(10, Math.max(1, partners.size())));
-
 
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) {
-
 
560
					partners.get(i).put("latitude", String.valueOf(coords[0]));
-
 
561
					partners.get(i).put("longitude", String.valueOf(coords[1]));
-
 
562
				}
-
 
563
			} catch (Exception e) {
-
 
564
				LOGGER.warn("Geocoding timeout/error for partner {}", partners.get(i).get("code"));
-
 
565
			}
-
 
566
		}
-
 
567
		executor.shutdown();
-
 
568
 
-
 
569
		if (startLat != null && startLng != null && !startLat.isEmpty() && !startLng.isEmpty()) {
714
		if (startLat != null && startLng != null && !startLat.isEmpty() && !startLng.isEmpty()) {
570
			partners = sortByNearestNeighborFromStart(partners, Double.parseDouble(startLat), Double.parseDouble(startLng));
715
			partners = sortByNearestNeighborFromStart(partners, Double.parseDouble(startLat), Double.parseDouble(startLng));
571
		} else {
716
		} else {
572
			partners = sortByNearestNeighbor(partners);
717
			partners = sortByNearestNeighbor(partners);
573
		}
718
		}