| Line 182... |
Line 182... |
| 182 |
// ====================== ASSIGN VISIT ======================
|
182 |
// ====================== ASSIGN VISIT ======================
|
| 183 |
// Day View "Assign Visit" — lets an admin pick parties (stores) for a specific
|
183 |
// Day View "Assign Visit" — lets an admin pick parties (stores) for a specific
|
| 184 |
// auth user on a specific date and pushes them as visit tasks to the v2
|
184 |
// auth user on a specific date and pushes them as visit tasks to the v2
|
| 185 |
// /profitmandi-web/v2/beat-tracking/batch endpoint.
|
185 |
// /profitmandi-web/v2/beat-tracking/batch endpoint.
|
| 186 |
|
186 |
|
| 187 |
// List of parties (stores) assigned to this auth user + their dtr.users.id
|
187 |
// List of parties (stores) assigned to this auth user + their dtr.users.id.
|
| - |
|
188 |
// When date+beatId are passed, each party is also tagged with:
|
| - |
|
189 |
// inBeat = is this store part of the scheduled beat's route on that date
|
| - |
|
190 |
// existingAgendas[] = agendas already saved for this store on that date (so the
|
| - |
|
191 |
// modal can pre-fill them and let the user refill rather than re-assign)
|
| 188 |
@GetMapping(value = "/beatPlan/assignVisit/parties")
|
192 |
@GetMapping(value = "/beatPlan/assignVisit/parties")
|
| 189 |
public ResponseEntity<?> assignVisitParties(@RequestParam int authUserId) throws Exception {
|
193 |
public ResponseEntity<?> assignVisitParties(
|
| - |
|
194 |
@RequestParam int authUserId,
|
| - |
|
195 |
@RequestParam(required = false) String date,
|
| - |
|
196 |
@RequestParam(required = false) Integer beatId) throws Exception {
|
| 190 |
AuthUser au = authRepository.selectById(authUserId);
|
197 |
AuthUser au = authRepository.selectById(authUserId);
|
| 191 |
if (au == null) return responseSender.badRequest("Auth user not found");
|
198 |
if (au == null) return responseSender.badRequest("Auth user not found");
|
| 192 |
|
199 |
|
| 193 |
// Map auth_user → dtr.users via email
|
200 |
// Map auth_user → dtr.users via email
|
| 194 |
Integer dtrUserId = null;
|
201 |
Integer dtrUserId = null;
|
| Line 197... |
Line 204... |
| 197 |
userRepositoryAuto.selectByEmailId(au.getEmailId());
|
204 |
userRepositoryAuto.selectByEmailId(au.getEmailId());
|
| 198 |
if (dtrUser != null) dtrUserId = dtrUser.getId();
|
205 |
if (dtrUser != null) dtrUserId = dtrUser.getId();
|
| 199 |
} catch (Exception ignored) {
|
206 |
} catch (Exception ignored) {
|
| 200 |
}
|
207 |
}
|
| 201 |
|
208 |
|
| - |
|
209 |
// Parse optional date
|
| - |
|
210 |
LocalDate parsedDate = null;
|
| - |
|
211 |
if (date != null && !date.isEmpty()) {
|
| - |
|
212 |
try {
|
| - |
|
213 |
parsedDate = LocalDate.parse(date);
|
| - |
|
214 |
} catch (Exception ignored) {
|
| - |
|
215 |
}
|
| - |
|
216 |
}
|
| - |
|
217 |
|
| - |
|
218 |
// Build (fofoId → dayNumber) of partners already in the beat's scheduled route for this date
|
| - |
|
219 |
Set<Integer> inBeatFofoIds = new HashSet<>();
|
| - |
|
220 |
if (beatId != null && parsedDate != null) {
|
| - |
|
221 |
final LocalDate dateF = parsedDate; // capture for lambda (parsedDate is reassigned earlier so not effectively final)
|
| - |
|
222 |
List<BeatSchedule> schedules = beatScheduleRepository.selectByBeatId(beatId);
|
| - |
|
223 |
BeatSchedule match = schedules.stream()
|
| - |
|
224 |
.filter(s -> s.getStartDate() != null && s.getStartDate().equals(dateF))
|
| - |
|
225 |
.findFirst().orElse(null);
|
| - |
|
226 |
if (match != null) {
|
| - |
|
227 |
List<BeatRoute> routes = beatRouteRepository.selectByBeatId(beatId);
|
| - |
|
228 |
routes.stream()
|
| - |
|
229 |
.filter(r -> r.getDayNumber() == match.getDayNumber() && r.isActive())
|
| - |
|
230 |
.forEach(r -> inBeatFofoIds.add(r.getFofoId()));
|
| - |
|
231 |
}
|
| - |
|
232 |
}
|
| - |
|
233 |
|
| - |
|
234 |
// Build (fofoId → existingAgendas) and (fofoId → existingDescription) from
|
| - |
|
235 |
// any already-saved location_tracking rows for this user on this date.
|
| - |
|
236 |
// Agenda is stored as task_name = "agenda1, agenda2 | OutletName"
|
| - |
|
237 |
// so we split on " | " to peel the outlet suffix off, then split agendas by ", ".
|
| - |
|
238 |
// Description is stored on task_description (free text).
|
| - |
|
239 |
Map<Integer, List<String>> existingAgendaByFofo = new HashMap<>();
|
| - |
|
240 |
Map<Integer, String> existingDescByFofo = new HashMap<>();
|
| - |
|
241 |
Map<Integer, Integer> existingTrackingIdByFofo = new HashMap<>();
|
| - |
|
242 |
if (dtrUserId != null && parsedDate != null) {
|
| - |
|
243 |
List<com.spice.profitmandi.dao.entity.auth.LocationTracking> existing =
|
| - |
|
244 |
locationTrackingRepositoryAuto.findByUserAndDate(dtrUserId, parsedDate);
|
| - |
|
245 |
for (com.spice.profitmandi.dao.entity.auth.LocationTracking lt : existing) {
|
| - |
|
246 |
if (!"franchisee-visit".equals(lt.getTaskType())) continue;
|
| - |
|
247 |
if (existingAgendaByFofo.containsKey(lt.getTaskId())) continue; // first wins
|
| - |
|
248 |
String taskName = lt.getTaskName() == null ? "" : lt.getTaskName();
|
| - |
|
249 |
String agendaPart = taskName;
|
| - |
|
250 |
int pipeIdx = taskName.lastIndexOf(" | ");
|
| - |
|
251 |
if (pipeIdx > 0) agendaPart = taskName.substring(0, pipeIdx);
|
| - |
|
252 |
List<String> agendas = new ArrayList<>();
|
| - |
|
253 |
for (String a : agendaPart.split(",")) {
|
| - |
|
254 |
String trimmed = a.trim();
|
| - |
|
255 |
if (!trimmed.isEmpty()) agendas.add(trimmed);
|
| - |
|
256 |
}
|
| - |
|
257 |
existingAgendaByFofo.put(lt.getTaskId(), agendas);
|
| - |
|
258 |
existingDescByFofo.put(lt.getTaskId(), lt.getTaskDescription() != null ? lt.getTaskDescription() : "");
|
| - |
|
259 |
existingTrackingIdByFofo.put(lt.getTaskId(), lt.getId());
|
| - |
|
260 |
}
|
| - |
|
261 |
}
|
| - |
|
262 |
|
| 202 |
Map<Integer, List<Integer>> mapping = csService.getAuthUserIdPartnerIdMapping();
|
263 |
Map<Integer, List<Integer>> mapping = csService.getAuthUserIdPartnerIdMapping();
|
| 203 |
List<Integer> fofoIds = mapping.get(authUserId);
|
264 |
List<Integer> fofoIds = mapping.get(authUserId);
|
| 204 |
|
265 |
|
| 205 |
List<Map<String, Object>> parties = new ArrayList<>();
|
266 |
List<Map<String, Object>> parties = new ArrayList<>();
|
| 206 |
if (fofoIds != null && !fofoIds.isEmpty()) {
|
267 |
if (fofoIds != null && !fofoIds.isEmpty()) {
|
| Line 215... |
Line 276... |
| 215 |
p.put("outletName", store.getOutletName() != null ? store.getOutletName()
|
276 |
p.put("outletName", store.getOutletName() != null ? store.getOutletName()
|
| 216 |
: (retailer != null ? retailer.getBusinessName() : "Store #" + store.getId()));
|
277 |
: (retailer != null ? retailer.getBusinessName() : "Store #" + store.getId()));
|
| 217 |
p.put("latitude", store.getLatitude());
|
278 |
p.put("latitude", store.getLatitude());
|
| 218 |
p.put("longitude", store.getLongitude());
|
279 |
p.put("longitude", store.getLongitude());
|
| 219 |
p.put("city", retailer != null && retailer.getAddress() != null ? retailer.getAddress().getCity() : null);
|
280 |
p.put("city", retailer != null && retailer.getAddress() != null ? retailer.getAddress().getCity() : null);
|
| - |
|
281 |
p.put("inBeat", inBeatFofoIds.contains(store.getId()));
|
| - |
|
282 |
p.put("existingAgendas", existingAgendaByFofo.getOrDefault(store.getId(), new ArrayList<>()));
|
| - |
|
283 |
p.put("existingDescription", existingDescByFofo.getOrDefault(store.getId(), ""));
|
| - |
|
284 |
p.put("existingTrackingId", existingTrackingIdByFofo.get(store.getId()));
|
| 220 |
parties.add(p);
|
285 |
parties.add(p);
|
| 221 |
}
|
286 |
}
|
| 222 |
// Sort by code for stable display
|
287 |
// In-beat first, then by code
|
| - |
|
288 |
parties.sort((a, b) -> {
|
| - |
|
289 |
boolean ai = Boolean.TRUE.equals(a.get("inBeat"));
|
| - |
|
290 |
boolean bi = Boolean.TRUE.equals(b.get("inBeat"));
|
| - |
|
291 |
if (ai != bi) return ai ? -1 : 1;
|
| 223 |
parties.sort((a, b) -> String.valueOf(a.get("code")).compareToIgnoreCase(String.valueOf(b.get("code"))));
|
292 |
return String.valueOf(a.get("code")).compareToIgnoreCase(String.valueOf(b.get("code")));
|
| - |
|
293 |
});
|
| 224 |
}
|
294 |
}
|
| 225 |
|
295 |
|
| 226 |
Map<String, Object> result = new HashMap<>();
|
296 |
Map<String, Object> result = new HashMap<>();
|
| 227 |
result.put("dtrUserId", dtrUserId);
|
297 |
result.put("dtrUserId", dtrUserId);
|
| 228 |
result.put("authUserId", authUserId);
|
298 |
result.put("authUserId", authUserId);
|
| 229 |
result.put("userName", au.getFirstName() + " " + au.getLastName());
|
299 |
result.put("userName", au.getFirstName() + " " + au.getLastName());
|
| 230 |
result.put("parties", parties);
|
300 |
result.put("parties", parties);
|
| - |
|
301 |
result.put("agendaOptions", com.spice.profitmandi.dao.enumuration.dtr.VisitAgenda.labels());
|
| 231 |
return responseSender.ok(result);
|
302 |
return responseSender.ok(result);
|
| 232 |
}
|
303 |
}
|
| 233 |
|
304 |
|
| 234 |
// Submit assignment — accepts a JSON body, builds the v2 payload, posts it
|
305 |
// Submit assignment — accepts a JSON body, builds the v2 payload, posts it
|
| 235 |
@PostMapping(value = "/beatPlan/assignVisit/submit")
|
306 |
@PostMapping(value = "/beatPlan/assignVisit/submit")
|
| Line 267... |
Line 338... |
| 267 |
taskDate = LocalDate.parse(planDate);
|
338 |
taskDate = LocalDate.parse(planDate);
|
| 268 |
} catch (Exception e) {
|
339 |
} catch (Exception e) {
|
| 269 |
return responseSender.badRequest("Invalid planDate (expected yyyy-MM-dd): " + planDate);
|
340 |
return responseSender.badRequest("Invalid planDate (expected yyyy-MM-dd): " + planDate);
|
| 270 |
}
|
341 |
}
|
| 271 |
|
342 |
|
| - |
|
343 |
// Existing rows for this user on this date — keyed by fofoStoreId.
|
| - |
|
344 |
// If a row already exists for a party, we UPDATE its agenda instead of
|
| - |
|
345 |
// creating a duplicate (this is the "refill agenda" path for already-
|
| - |
|
346 |
// assigned parties).
|
| - |
|
347 |
Map<Integer, com.spice.profitmandi.dao.entity.auth.LocationTracking> existingByFofo = new HashMap<>();
|
| - |
|
348 |
for (com.spice.profitmandi.dao.entity.auth.LocationTracking lt :
|
| - |
|
349 |
locationTrackingRepositoryAuto.findByUserAndDate(dtrUserId, taskDate)) {
|
| - |
|
350 |
if (!"franchisee-visit".equals(lt.getTaskType())) continue;
|
| - |
|
351 |
existingByFofo.putIfAbsent(lt.getTaskId(), lt);
|
| - |
|
352 |
}
|
| - |
|
353 |
|
| - |
|
354 |
// Defence-in-depth: Assign Visit is only valid for today's run. Hiding the
|
| - |
|
355 |
// button on the UI isn't enough — block at the API too.
|
| - |
|
356 |
if (!taskDate.equals(LocalDate.now())) {
|
| - |
|
357 |
return responseSender.badRequest("Visits can only be assigned for today's date (" + LocalDate.now() + ")");
|
| - |
|
358 |
}
|
| - |
|
359 |
|
| 272 |
LocalDateTime now = LocalDateTime.now();
|
360 |
LocalDateTime now = LocalDateTime.now();
|
| 273 |
List<com.spice.profitmandi.dao.entity.auth.LocationTracking> created = new ArrayList<>();
|
361 |
int createdCount = 0, updatedCount = 0;
|
| 274 |
|
362 |
|
| 275 |
for (Map<String, Object> p : selected) {
|
363 |
for (Map<String, Object> p : selected) {
|
| 276 |
Integer fofoStoreId = ((Number) p.get("fofoStoreId")).intValue();
|
364 |
Integer fofoStoreId = ((Number) p.get("fofoStoreId")).intValue();
|
| 277 |
String outletName = (String) p.get("outletName");
|
365 |
String outletName = (String) p.get("outletName");
|
| 278 |
String lat = (String) p.get("latitude");
|
366 |
String lat = (String) p.get("latitude");
|
| 279 |
String lng = (String) p.get("longitude");
|
367 |
String lng = (String) p.get("longitude");
|
| - |
|
368 |
String description = (String) p.get("description");
|
| - |
|
369 |
if (description != null) description = description.trim();
|
| - |
|
370 |
if (description == null) description = "";
|
| - |
|
371 |
|
| - |
|
372 |
// Multi-agenda: accept agendas[] (new format) or fall back to agenda (legacy single)
|
| - |
|
373 |
List<String> agendas = new ArrayList<>();
|
| - |
|
374 |
Object rawAgendas = p.get("agendas");
|
| - |
|
375 |
if (rawAgendas instanceof List) {
|
| - |
|
376 |
for (Object o : (List<?>) rawAgendas) {
|
| - |
|
377 |
if (o != null) {
|
| - |
|
378 |
String s = String.valueOf(o).trim();
|
| - |
|
379 |
if (!s.isEmpty()) agendas.add(s);
|
| - |
|
380 |
}
|
| - |
|
381 |
}
|
| - |
|
382 |
}
|
| - |
|
383 |
if (agendas.isEmpty()) {
|
| 280 |
String agenda = (String) p.get("agenda");
|
384 |
String single = (String) p.get("agenda");
|
| 281 |
if (agenda != null) agenda = agenda.trim();
|
385 |
if (single != null && !single.trim().isEmpty()) agendas.add(single.trim());
|
| - |
|
386 |
}
|
| 282 |
if (agenda == null || agenda.isEmpty()) agenda = "Visit";
|
387 |
if (agendas.isEmpty()) agendas.add("Visit");
|
| - |
|
388 |
String agendaJoined = String.join(", ", agendas);
|
| 283 |
|
389 |
|
| 284 |
String visitLocation = (lat != null && lng != null && !lat.isEmpty() && !lng.isEmpty())
|
390 |
String visitLocation = (lat != null && lng != null && !lat.isEmpty() && !lng.isEmpty())
|
| 285 |
? (lat + "," + lng) : "0.0000,0.0000";
|
391 |
? (lat + "," + lng) : "0.0000,0.0000";
|
| 286 |
|
392 |
|
| 287 |
String displayName = (outletName != null && !outletName.isEmpty()) ? outletName : ("Store #" + fofoStoreId);
|
393 |
String displayName = (outletName != null && !outletName.isEmpty()) ? outletName : ("Store #" + fofoStoreId);
|
| - |
|
394 |
String newTaskName = agendaJoined + " | " + displayName;
|
| - |
|
395 |
|
| - |
|
396 |
com.spice.profitmandi.dao.entity.auth.LocationTracking existing = existingByFofo.get(fofoStoreId);
|
| - |
|
397 |
if (existing != null) {
|
| - |
|
398 |
// Refill — agenda (task_name), description, and visit location change
|
| - |
|
399 |
existing.setTaskName(newTaskName);
|
| - |
|
400 |
existing.setTaskDescription(description);
|
| - |
|
401 |
existing.setVisitLocation(visitLocation);
|
| - |
|
402 |
existing.setUpdatedTimestamp(now);
|
| - |
|
403 |
locationTrackingRepositoryAuto.persist(existing);
|
| - |
|
404 |
updatedCount++;
|
| - |
|
405 |
continue;
|
| - |
|
406 |
}
|
| 288 |
|
407 |
|
| 289 |
com.spice.profitmandi.dao.entity.auth.LocationTracking row =
|
408 |
com.spice.profitmandi.dao.entity.auth.LocationTracking row =
|
| 290 |
new com.spice.profitmandi.dao.entity.auth.LocationTracking();
|
409 |
new com.spice.profitmandi.dao.entity.auth.LocationTracking();
|
| 291 |
row.setUserId(dtrUserId);
|
410 |
row.setUserId(dtrUserId);
|
| 292 |
row.setDeviceId("0");
|
411 |
row.setDeviceId("0");
|
| 293 |
row.setTaskId(fofoStoreId);
|
412 |
row.setTaskId(fofoStoreId);
|
| 294 |
row.setTaskDate(taskDate);
|
413 |
row.setTaskDate(taskDate);
|
| 295 |
row.setTaskName(agenda + " | " + displayName);
|
414 |
row.setTaskName(newTaskName);
|
| - |
|
415 |
row.setTaskDescription(description);
|
| 296 |
row.setTaskType("franchisee-visit");
|
416 |
row.setTaskType("franchisee-visit");
|
| 297 |
row.setMarkType("PENDING");
|
417 |
row.setMarkType("PENDING");
|
| 298 |
row.setAddress("");
|
418 |
row.setAddress("");
|
| 299 |
row.setVisitLocation(visitLocation);
|
419 |
row.setVisitLocation(visitLocation);
|
| 300 |
row.setCheckInLatLng("0.0000,0.0000");
|
420 |
row.setCheckInLatLng("0.0000,0.0000");
|
| Line 311... |
Line 431... |
| 311 |
row.setCreatedTimestamp(now);
|
431 |
row.setCreatedTimestamp(now);
|
| 312 |
row.setUpdatedTimestamp(now);
|
432 |
row.setUpdatedTimestamp(now);
|
| 313 |
|
433 |
|
| 314 |
// Do NOT try/catch this — if persist throws, let it propagate so
|
434 |
// Do NOT try/catch this — if persist throws, let it propagate so
|
| 315 |
// @Transactional(rollbackFor = Throwable.class) rolls back cleanly.
|
435 |
// @Transactional(rollbackFor = Throwable.class) rolls back cleanly.
|
| 316 |
// Catching here lets Hibernate try to commit a broken session, which
|
- |
|
| 317 |
// then surfaces the misleading "null id in entry" flush error and hides
|
- |
|
| 318 |
// the real root cause (constraint violation, bad value, etc.).
|
- |
|
| 319 |
locationTrackingRepositoryAuto.persist(row);
|
436 |
locationTrackingRepositoryAuto.persist(row);
|
| 320 |
created.add(row);
|
437 |
createdCount++;
|
| 321 |
}
|
438 |
}
|
| 322 |
LOGGER.info("assignVisit persisted {} location_tracking rows for dtrUserId={}", created.size(), dtrUserId);
|
439 |
LOGGER.info("assignVisit dtrUserId={} created={} updated={}", dtrUserId, createdCount, updatedCount);
|
| 323 |
|
440 |
|
| 324 |
Map<String, Object> result = new HashMap<>();
|
441 |
Map<String, Object> result = new HashMap<>();
|
| 325 |
result.put("status", true);
|
442 |
result.put("status", true);
|
| 326 |
result.put("assignedCount", created.size());
|
443 |
result.put("createdCount", createdCount);
|
| - |
|
444 |
result.put("updatedCount", updatedCount);
|
| 327 |
result.put("dtrUserId", dtrUserId);
|
445 |
result.put("dtrUserId", dtrUserId);
|
| - |
|
446 |
StringBuilder msg = new StringBuilder();
|
| - |
|
447 |
if (createdCount > 0)
|
| - |
|
448 |
msg.append(createdCount).append(" new visit").append(createdCount == 1 ? "" : "s").append(" assigned");
|
| - |
|
449 |
if (updatedCount > 0) {
|
| - |
|
450 |
if (msg.length() > 0) msg.append(", ");
|
| - |
|
451 |
msg.append(updatedCount).append(" existing agenda").append(updatedCount == 1 ? "" : "s").append(" refilled");
|
| - |
|
452 |
}
|
| 328 |
result.put("message", created.size() + " visit tasks assigned to " + au.getFirstName() + " " + au.getLastName());
|
453 |
msg.append(" for ").append(au.getFirstName()).append(" ").append(au.getLastName());
|
| - |
|
454 |
result.put("message", msg.toString());
|
| 329 |
return responseSender.ok(result);
|
455 |
return responseSender.ok(result);
|
| 330 |
}
|
456 |
}
|
| 331 |
|
457 |
|
| 332 |
@GetMapping(value = "/beatPlanWindow")
|
458 |
@GetMapping(value = "/beatPlanWindow")
|
| 333 |
public String beatPlanWindow(HttpServletRequest request, Model model) throws ProfitMandiBusinessException {
|
459 |
public String beatPlanWindow(HttpServletRequest request, Model model) throws ProfitMandiBusinessException {
|