| 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 |
}
|