Subversion Repositories SmartDukaan

Rev

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

Rev 36728 Rev 36740
Line 99... Line 99...
99
	private com.spice.profitmandi.dao.repository.dtr.UserRepository userRepositoryAuto;
99
	private com.spice.profitmandi.dao.repository.dtr.UserRepository userRepositoryAuto;
100
	@Autowired
100
	@Autowired
101
	private com.spice.profitmandi.common.web.client.RestClient restClientAuto;
101
	private com.spice.profitmandi.common.web.client.RestClient restClientAuto;
102
	@Autowired
102
	@Autowired
103
	private com.spice.profitmandi.dao.repository.auth.LocationTrackingRepository locationTrackingRepositoryAuto;
103
	private com.spice.profitmandi.dao.repository.auth.LocationTrackingRepository locationTrackingRepositoryAuto;
-
 
104
	@Autowired
-
 
105
	private com.spice.profitmandi.dao.repository.dtr.BeatDeferredVisitRepository beatDeferredVisitRepository;
104
 
106
 
105
	private static Double parseDoubleOrNull(String s) {
107
	private static Double parseDoubleOrNull(String s) {
106
		if (s == null || s.trim().isEmpty()) return null;
108
		if (s == null || s.trim().isEmpty()) return null;
107
		try {
109
		try {
108
			return Double.parseDouble(s.trim());
110
			return Double.parseDouble(s.trim());
Line 453... Line 455...
453
		msg.append(" for ").append(au.getFirstName()).append(" ").append(au.getLastName());
455
		msg.append(" for ").append(au.getFirstName()).append(" ").append(au.getLastName());
454
		result.put("message", msg.toString());
456
		result.put("message", msg.toString());
455
		return responseSender.ok(result);
457
		return responseSender.ok(result);
456
	}
458
	}
457
 
459
 
-
 
460
	// ====================== DEFERRED PARTNERS ======================
-
 
461
	// Heads review partners that weren't visited on their planned day and act on
-
 
462
	// them. The deferral lifecycle lives in user.beat_deferred_visit (separate
-
 
463
	// from the raw location_tracking event log). Detection = explicit
-
 
464
	// (mark_type='DEFERRED') + derived (planned beat_route minus completed visits).
-
 
465
 
-
 
466
	// Page
-
 
467
	@GetMapping(value = "/beatPlan/deferredView")
-
 
468
	public String deferredView(HttpServletRequest request, Model model) throws ProfitMandiBusinessException {
-
 
469
		model.addAttribute("escalationTypes", visibleLevelsFor(request));
-
 
470
		return "beat-plan-deferred";
-
 
471
	}
-
 
472
 
-
 
473
	// List (syncs the table first, then returns the head's downline deferrals).
-
 
474
	@GetMapping(value = "/beatPlan/deferred")
-
 
475
	public ResponseEntity<?> deferredList(
-
 
476
			HttpServletRequest request,
-
 
477
			@RequestParam(required = false) String startDate,
-
 
478
			@RequestParam(required = false) String endDate) throws Exception {
-
 
479
 
-
 
480
		LocalDate start, end;
-
 
481
		try {
-
 
482
			start = (startDate == null || startDate.isEmpty()) ? LocalDate.now().minusDays(7) : LocalDate.parse(startDate);
-
 
483
			end = (endDate == null || endDate.isEmpty()) ? LocalDate.now() : LocalDate.parse(endDate);
-
 
484
		} catch (Exception e) {
-
 
485
			return responseSender.badRequest("Invalid date — expected yyyy-MM-dd");
-
 
486
		}
-
 
487
 
-
 
488
		LoginDetails ld = cookiesProcessor.getCookiesObject(request);
-
 
489
		AuthUser me = (ld != null) ? authRepository.selectByEmailOrMobile(ld.getEmailId()) : null;
-
 
490
		if (me == null) return responseSender.unauthorized("Not logged in");
-
 
491
 
-
 
492
		// PURE READ. Deferrals are persisted at the write point (BeatTrackingController,
-
 
493
		// when mark_type='DEFERRED' is recorded) — this endpoint never writes.
-
 
494
		List<BeatDeferredVisit> source;
-
 
495
		if (isSuperAdmin(me)) {
-
 
496
			source = beatDeferredVisitRepository.selectByDateRange(start, end);
-
 
497
		} else {
-
 
498
			Set<Integer> downline = new HashSet<>(authService.getAllReportees(me.getId()));
-
 
499
			downline.add(me.getId());
-
 
500
			source = beatDeferredVisitRepository.selectByAuthUserIdsAndDateRange(new ArrayList<>(downline), start, end);
-
 
501
		}
-
 
502
		List<BeatDeferredVisit> rows = source.stream()
-
 
503
				.filter(r -> "DEFERRED".equals(r.getStatus()))
-
 
504
				.collect(Collectors.toList());
-
 
505
 
-
 
506
		// ---- nextScheduledDate (info only: does a future run already cover it?) ----
-
 
507
		// Only meaningful for partner visits (leads aren't on beat_route). Purely a
-
 
508
		// hint — the row stays actionable even when auto-covered, since the next run
-
 
509
		// could be far off.
-
 
510
		Map<Integer, Map<Integer, LocalDate>> coverCache = new HashMap<>();
-
 
511
		Map<Integer, LocalDate> nextByRowId = new HashMap<>();
-
 
512
		for (BeatDeferredVisit r : rows) {
-
 
513
			if (!"franchisee-visit".equals(r.getTaskType())) continue;
-
 
514
			Map<Integer, LocalDate> cover = coverCache.computeIfAbsent(r.getAuthUserId(), this::computeFutureCover);
-
 
515
			LocalDate next = cover.get(r.getFofoId());
-
 
516
			if (next != null) nextByRowId.put(r.getId(), next);
-
 
517
		}
-
 
518
 
-
 
519
		// ---- resolve user names (display name + type already denormalized on the row) ----
-
 
520
		Set<Integer> authIds = rows.stream().map(BeatDeferredVisit::getAuthUserId).collect(Collectors.toSet());
-
 
521
		Map<Integer, AuthUser> userMap = new HashMap<>();
-
 
522
		if (!authIds.isEmpty())
-
 
523
			authRepository.selectByIds(new ArrayList<>(authIds)).forEach(u -> userMap.put(u.getId(), u));
-
 
524
 
-
 
525
		List<Map<String, Object>> out = new ArrayList<>();
-
 
526
		for (BeatDeferredVisit r : rows) {
-
 
527
			AuthUser u = userMap.get(r.getAuthUserId());
-
 
528
			boolean isLead = "lead".equalsIgnoreCase(r.getTaskType());
-
 
529
			Map<String, Object> row = new HashMap<>();
-
 
530
			row.put("id", r.getId());
-
 
531
			row.put("authUserId", r.getAuthUserId());
-
 
532
			row.put("userName", u != null ? (u.getFirstName() + " " + u.getLastName()) : ("User #" + r.getAuthUserId()));
-
 
533
			row.put("fofoStoreId", r.getFofoId());
-
 
534
			row.put("name", r.getDisplayName() != null ? r.getDisplayName() : ("#" + r.getFofoId()));
-
 
535
			row.put("type", isLead ? "Lead" : "Visit");
-
 
536
			row.put("deferredDate", r.getDeferredDate() != null ? r.getDeferredDate().toString() : null);
-
 
537
			row.put("reason", r.getReason());
-
 
538
			row.put("status", r.getStatus());
-
 
539
			LocalDate next = nextByRowId.get(r.getId());
-
 
540
			row.put("nextScheduledDate", next != null ? next.toString() : null);
-
 
541
			out.add(row);
-
 
542
		}
-
 
543
		out.sort((a, c) -> String.valueOf(a.get("deferredDate")).compareTo(String.valueOf(c.get("deferredDate"))));
-
 
544
 
-
 
545
		Map<String, Object> result = new HashMap<>();
-
 
546
		result.put("rows", out);
-
 
547
		result.put("startDate", start.toString());
-
 
548
		result.put("endDate", end.toString());
-
 
549
		return responseSender.ok(result);
-
 
550
	}
-
 
551
 
-
 
552
	// Head action on a deferral: reschedule (one-off visit, or into an existing
-
 
553
	// beat-day) or cancel. Never edits the beat template.
-
 
554
	@PostMapping(value = "/beatPlan/deferred/action")
-
 
555
	public ResponseEntity<?> deferredAction(
-
 
556
			HttpServletRequest request,
-
 
557
			@org.springframework.web.bind.annotation.RequestBody Map<String, Object> body) throws Exception {
-
 
558
 
-
 
559
		Integer deferredId = body.get("deferredId") != null ? ((Number) body.get("deferredId")).intValue() : null;
-
 
560
		String action = (String) body.get("action");
-
 
561
		String toDateStr = (String) body.get("toDate");
-
 
562
		if (deferredId == null || action == null)
-
 
563
			return responseSender.badRequest("deferredId and action are required");
-
 
564
 
-
 
565
		LoginDetails ld = cookiesProcessor.getCookiesObject(request);
-
 
566
		AuthUser me = (ld != null) ? authRepository.selectByEmailOrMobile(ld.getEmailId()) : null;
-
 
567
		if (me == null) return responseSender.unauthorized("Not logged in");
-
 
568
 
-
 
569
		BeatDeferredVisit d = beatDeferredVisitRepository.selectById(deferredId);
-
 
570
		if (d == null) return responseSender.badRequest("Deferred record not found");
-
 
571
 
-
 
572
		LocalDateTime now = LocalDateTime.now();
-
 
573
 
-
 
574
		if ("cancel".equalsIgnoreCase(action)) {
-
 
575
			d.setStatus("CANCELLED");
-
 
576
			d.setActionBy(me.getId());
-
 
577
			d.setUpdatedTimestamp(now);
-
 
578
			beatDeferredVisitRepository.persist(d);
-
 
579
			Map<String, Object> ok = new HashMap<>();
-
 
580
			ok.put("status", true);
-
 
581
			ok.put("message", "Deferred visit cancelled");
-
 
582
			return responseSender.ok(ok);
-
 
583
		}
-
 
584
 
-
 
585
		// reschedule_oneoff | reschedule_beat
-
 
586
		if (toDateStr == null || toDateStr.isEmpty())
-
 
587
			return responseSender.badRequest("toDate is required to reschedule");
-
 
588
		LocalDate toDate;
-
 
589
		try {
-
 
590
			toDate = LocalDate.parse(toDateStr);
-
 
591
		} catch (Exception e) {
-
 
592
			return responseSender.badRequest("Invalid toDate (yyyy-MM-dd)");
-
 
593
		}
-
 
594
		if (toDate.isBefore(LocalDate.now())) return responseSender.badRequest("Reschedule date cannot be in the past");
-
 
595
 
-
 
596
		if ("reschedule_beat".equalsIgnoreCase(action)) {
-
 
597
			boolean hasBeat = beatRepository.selectActiveByAuthUserId(d.getAuthUserId()).stream()
-
 
598
					.flatMap(b -> beatScheduleRepository.selectByBeatId(b.getId()).stream())
-
 
599
					.anyMatch(s -> s.getStartDate() != null && s.getStartDate().equals(toDate));
-
 
600
			if (!hasBeat)
-
 
601
				return responseSender.badRequest("No beat is scheduled for this user on " + toDateStr + ". Pick another date or use a one-off visit.");
-
 
602
		}
-
 
603
 
-
 
604
		// Resolve dtr user, then create a PENDING task on toDate. Reuse the
-
 
605
		// denormalized name + type (works for both partner visits and leads — for
-
 
606
		// leads, looking up fofo_store would be the wrong id space). For visits we
-
 
607
		// still try to pull lat/lng for the visit location.
-
 
608
		Integer dtrId = resolveDtrId(d.getAuthUserId(), new HashMap<>());
-
 
609
		if (dtrId == null) return responseSender.badRequest("No dtr.users record for this sales person");
-
 
610
		boolean isLead = "lead".equalsIgnoreCase(d.getTaskType());
-
 
611
		String visitLocation = "0.0000,0.0000";
-
 
612
		if (!isLead) {
-
 
613
			try {
-
 
614
				List<FofoStore> ss = fofoStoreRepository.selectByRetailerIds(java.util.Collections.singletonList(d.getFofoId()));
-
 
615
				if (!ss.isEmpty()) {
-
 
616
					FofoStore fs = ss.get(0);
-
 
617
					if (fs.getLatitude() != null && fs.getLongitude() != null
-
 
618
							&& !fs.getLatitude().isEmpty() && !fs.getLongitude().isEmpty()) {
-
 
619
						visitLocation = fs.getLatitude() + "," + fs.getLongitude();
-
 
620
					}
-
 
621
				}
-
 
622
			} catch (Exception ignored) {
-
 
623
			}
-
 
624
		}
-
 
625
		String taskName = d.getDisplayName() != null ? d.getDisplayName()
-
 
626
				: ((d.getReason() != null ? d.getReason() : "Rescheduled") + " | #" + d.getFofoId());
-
 
627
 
-
 
628
		com.spice.profitmandi.dao.entity.auth.LocationTracking row = new com.spice.profitmandi.dao.entity.auth.LocationTracking();
-
 
629
		row.setUserId(dtrId);
-
 
630
		row.setDeviceId("0");
-
 
631
		row.setTaskId(d.getFofoId());
-
 
632
		row.setTaskDate(toDate);
-
 
633
		row.setTaskName(taskName);
-
 
634
		row.setTaskDescription("Rescheduled from " + d.getDeferredDate());
-
 
635
		row.setTaskType(d.getTaskType() != null ? d.getTaskType() : "franchisee-visit");
-
 
636
		row.setMarkType("PENDING");
-
 
637
		row.setAddress("");
-
 
638
		row.setVisitLocation(visitLocation);
-
 
639
		row.setCheckInLatLng("0.0000,0.0000");
-
 
640
		row.setCheckOutLatLng("0.0000,0.0000");
-
 
641
		row.setCheckInTime(java.time.LocalTime.MIDNIGHT);
-
 
642
		row.setCheckOutTime(java.time.LocalTime.MIDNIGHT);
-
 
643
		row.setTransitTime(java.time.LocalTime.MIDNIGHT);
-
 
644
		row.setTimeSpent(java.time.LocalTime.MIDNIGHT);
-
 
645
		row.setEstimatedTime(java.time.LocalTime.MIDNIGHT);
-
 
646
		row.setSessionStartTime(java.time.LocalTime.MIDNIGHT);
-
 
647
		row.setSessionEndTime(java.time.LocalTime.MIDNIGHT);
-
 
648
		row.setTotalDistance("0.0");
-
 
649
		row.setStatus(false);
-
 
650
		row.setCreatedTimestamp(now);
-
 
651
		row.setUpdatedTimestamp(now);
-
 
652
		locationTrackingRepositoryAuto.persist(row);
-
 
653
 
-
 
654
		d.setStatus("RESCHEDULED");
-
 
655
		d.setRescheduledToDate(toDate);
-
 
656
		d.setActionBy(me.getId());
-
 
657
		d.setUpdatedTimestamp(now);
-
 
658
		beatDeferredVisitRepository.persist(d);
-
 
659
 
-
 
660
		Map<String, Object> ok = new HashMap<>();
-
 
661
		ok.put("status", true);
-
 
662
		ok.put("message", "Visit rescheduled to " + toDateStr);
-
 
663
		return responseSender.ok(ok);
-
 
664
	}
-
 
665
 
-
 
666
	// Drop a deferred item into a specific upcoming BEAT run (chosen from the beat
-
 
667
	// calendar). Lead → a lead_route row on that beat/date (renders as a lead stop).
-
 
668
	// Partner visit → appended to that beat's route for the date's day_number.
-
 
669
	// The beat plan calendar then shows it. Marks the deferral RESCHEDULED.
-
 
670
	@PostMapping(value = "/beatPlan/deferred/assignToBeat")
-
 
671
	public ResponseEntity<?> deferredAssignToBeat(
-
 
672
			HttpServletRequest request,
-
 
673
			@org.springframework.web.bind.annotation.RequestBody Map<String, Object> body) throws Exception {
-
 
674
 
-
 
675
		Integer deferredId = body.get("deferredId") != null ? ((Number) body.get("deferredId")).intValue() : null;
-
 
676
		Integer beatId = body.get("beatId") != null ? ((Number) body.get("beatId")).intValue() : null;
-
 
677
		String dateStr = (String) body.get("date");
-
 
678
		if (deferredId == null || beatId == null || dateStr == null)
-
 
679
			return responseSender.badRequest("deferredId, beatId and date are required");
-
 
680
 
-
 
681
		LoginDetails ld = cookiesProcessor.getCookiesObject(request);
-
 
682
		AuthUser me = (ld != null) ? authRepository.selectByEmailOrMobile(ld.getEmailId()) : null;
-
 
683
		if (me == null) return responseSender.unauthorized("Not logged in");
-
 
684
 
-
 
685
		LocalDate date;
-
 
686
		try {
-
 
687
			date = LocalDate.parse(dateStr);
-
 
688
		} catch (Exception e) {
-
 
689
			return responseSender.badRequest("Invalid date (yyyy-MM-dd)");
-
 
690
		}
-
 
691
		if (date.isBefore(LocalDate.now())) return responseSender.badRequest("Pick an upcoming date");
-
 
692
 
-
 
693
		BeatDeferredVisit d = beatDeferredVisitRepository.selectById(deferredId);
-
 
694
		if (d == null) return responseSender.badRequest("Deferred record not found");
-
 
695
 
-
 
696
		// A deferral can only move FORWARD — never onto the day it was deferred or earlier.
-
 
697
		if (d.getDeferredDate() != null && !date.isAfter(d.getDeferredDate())) {
-
 
698
			return responseSender.badRequest("A deferred item can only be moved to a date after "
-
 
699
					+ d.getDeferredDate() + " (it was deferred that day).");
-
 
700
		}
-
 
701
 
-
 
702
		Beat beat = beatRepository.selectById(beatId);
-
 
703
		if (beat == null) return responseSender.badRequest("Beat not found");
-
 
704
 
-
 
705
		// The beat must actually run on the chosen date — get that run's day number.
-
 
706
		BeatSchedule sched = beatScheduleRepository.selectByBeatId(beatId).stream()
-
 
707
				.filter(s -> s.getStartDate() != null && s.getStartDate().equals(date))
-
 
708
				.findFirst().orElse(null);
-
 
709
		if (sched == null) return responseSender.badRequest("That beat is not scheduled on " + dateStr);
-
 
710
 
-
 
711
		LocalDateTime now = LocalDateTime.now();
-
 
712
		boolean isLead = "lead".equalsIgnoreCase(d.getTaskType());
-
 
713
 
-
 
714
		if (isLead) {
-
 
715
			// Avoid duplicating the same lead on the same beat/date
-
 
716
			boolean exists = leadRouteRepository.selectByBeatId(beatId).stream()
-
 
717
					.anyMatch(lr -> lr.getLeadId() == d.getFofoId()
-
 
718
							&& date.equals(lr.getScheduleDate())
-
 
719
							&& !"CANCELLED".equals(lr.getStatus()));
-
 
720
			if (!exists) {
-
 
721
				LeadRoute lr = new LeadRoute();
-
 
722
				lr.setBeatId(beatId);
-
 
723
				lr.setLeadId(d.getFofoId());
-
 
724
				lr.setScheduleDate(date);
-
 
725
				lr.setSequenceOrder(9999); // append; planner can reorder
-
 
726
				lr.setStatus("APPROVED");
-
 
727
				lr.setApprovedBy(me.getId());
-
 
728
				lr.setApprovedTimestamp(now);
-
 
729
				lr.setCreatedTimestamp(now);
-
 
730
				lr.setUpdatedTimestamp(now);
-
 
731
				leadRouteRepository.persist(lr);
-
 
732
			}
-
 
733
		} else {
-
 
734
			// Partner visit → append to that beat's route for the date's day number,
-
 
735
			// if not already present on that day.
-
 
736
			boolean exists = beatRouteRepository.selectByBeatId(beatId).stream()
-
 
737
					.anyMatch(r -> r.getFofoId() == d.getFofoId() && r.getDayNumber() == sched.getDayNumber() && r.isActive());
-
 
738
			if (!exists) {
-
 
739
				int nextSeq = beatRouteRepository.selectByBeatId(beatId).stream()
-
 
740
						.filter(r -> r.getDayNumber() == sched.getDayNumber())
-
 
741
						.mapToInt(BeatRoute::getSequenceOrder).max().orElse(-1) + 1;
-
 
742
				BeatRoute br = new BeatRoute();
-
 
743
				br.setBeatId(beatId);
-
 
744
				br.setFofoId(d.getFofoId());
-
 
745
				br.setDayNumber(sched.getDayNumber());
-
 
746
				br.setSequenceOrder(nextSeq);
-
 
747
				br.setActive(true);
-
 
748
				beatRouteRepository.persist(br);
-
 
749
			}
-
 
750
		}
-
 
751
 
-
 
752
		d.setStatus("RESCHEDULED");
-
 
753
		d.setRescheduledToDate(date);
-
 
754
		d.setActionBy(me.getId());
-
 
755
		d.setUpdatedTimestamp(now);
-
 
756
		beatDeferredVisitRepository.persist(d);
-
 
757
 
-
 
758
		Map<String, Object> ok = new HashMap<>();
-
 
759
		ok.put("status", true);
-
 
760
		ok.put("message", (isLead ? "Lead" : "Partner") + " added to beat '" + beat.getName() + "' on " + dateStr);
-
 
761
		return responseSender.ok(ok);
-
 
762
	}
-
 
763
 
-
 
764
	// authUserId -> dtr.users id (via shared email), memoized in the passed cache.
-
 
765
	// Used by the reschedule action to create the new PENDING location_tracking row.
-
 
766
	private Integer resolveDtrId(int authUserId, Map<Integer, Integer> cache) {
-
 
767
		if (cache.containsKey(authUserId)) return cache.get(authUserId);
-
 
768
		Integer dtrId = null;
-
 
769
		try {
-
 
770
			AuthUser au = authRepository.selectById(authUserId);
-
 
771
			if (au != null && au.getEmailId() != null) {
-
 
772
				com.spice.profitmandi.dao.entity.dtr.User u = userRepositoryAuto.selectByEmailId(au.getEmailId());
-
 
773
				if (u != null) dtrId = u.getId();
-
 
774
			}
-
 
775
		} catch (Exception ignored) {
-
 
776
		}
-
 
777
		cache.put(authUserId, dtrId);
-
 
778
		return dtrId;
-
 
779
	}
-
 
780
 
-
 
781
	// For an auth user: fofoId -> earliest upcoming (>= today) scheduled date where
-
 
782
	// an active beat's route still includes that partner (the "Next Scheduled" hint).
-
 
783
	private Map<Integer, LocalDate> computeFutureCover(int authUserId) {
-
 
784
		LocalDate today = LocalDate.now();
-
 
785
		Map<Integer, LocalDate> cover = new HashMap<>();
-
 
786
		for (Beat b : beatRepository.selectActiveByAuthUserId(authUserId)) {
-
 
787
			LocalDate earliest = null;
-
 
788
			for (BeatSchedule s : beatScheduleRepository.selectByBeatId(b.getId())) {
-
 
789
				LocalDate dt = s.getStartDate();
-
 
790
				if (dt != null && dt.getYear() != 9999 && !dt.isBefore(today)) {
-
 
791
					if (earliest == null || dt.isBefore(earliest)) earliest = dt;
-
 
792
				}
-
 
793
			}
-
 
794
			if (earliest == null) continue;
-
 
795
			for (BeatRoute rt : beatRouteRepository.selectByBeatId(b.getId())) {
-
 
796
				if (!rt.isActive()) continue;
-
 
797
				LocalDate cur = cover.get(rt.getFofoId());
-
 
798
				if (cur == null || earliest.isBefore(cur)) cover.put(rt.getFofoId(), earliest);
-
 
799
			}
-
 
800
		}
-
 
801
		return cover;
-
 
802
	}
-
 
803
 
458
	@GetMapping(value = "/beatPlanWindow")
804
	@GetMapping(value = "/beatPlanWindow")
459
	public String beatPlanWindow(HttpServletRequest request, Model model) throws ProfitMandiBusinessException {
805
	public String beatPlanWindow(HttpServletRequest request, Model model) throws ProfitMandiBusinessException {
460
		model.addAttribute("escalationTypes", visibleLevelsFor(request));
806
		model.addAttribute("escalationTypes", visibleLevelsFor(request));
461
		return "beat-plan-window";
807
		return "beat-plan-window";
462
	}
808
	}