Subversion Repositories SmartDukaan

Rev

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

Rev 36711 Rev 36716
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 {