Subversion Repositories SmartDukaan

Rev

Rev 37060 | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 37060 Rev 37120
Line 841... Line 841...
841
		ok.put("status", true);
841
		ok.put("status", true);
842
		ok.put("message", (isLead ? "Lead" : "Partner") + " added to beat '" + beat.getName() + "' on " + dateStr);
842
		ok.put("message", (isLead ? "Lead" : "Partner") + " added to beat '" + beat.getName() + "' on " + dateStr);
843
		return responseSender.ok(ok);
843
		return responseSender.ok(ok);
844
	}
844
	}
845
 
845
 
-
 
846
	// ==================== ASSIGN LEADS ONTO BEATS ====================
-
 
847
	// A rep's assigned leads can be routed onto a beat from two surfaces:
-
 
848
	//   • Route Planner "Assigned Leads" list → single lead, calendar-chip picks beat+date
-
 
849
	//   • Day View "Add Lead" modal           → bulk write onto a known beat+date
-
 
850
	// Both converge on the same dated, APPROVED lead_route write (writeLeadRoute),
-
 
851
	// mirroring VisitRequestController.approveSchedule and deferredAssignToBeat's
-
 
852
	// lead branch. Nothing downstream changes — getBeatVisits and the
-
 
853
	// /visit-approvals/beat-route feed already render lead_route rows for the run.
-
 
854
 
-
 
855
	// Active/open lead colours a manager can still route (matches getOpenLead's
-
 
856
	// All → yellow+green expansion). Red/other are dropped as not-worth-visiting.
-
 
857
	private static final Set<String> ROUTABLE_LEAD_COLORS =
-
 
858
			new HashSet<>(Arrays.asList("yellow", "green"));
-
 
859
 
-
 
860
	// Can the caller see the selected user's leads? Super-admin sees everyone;
-
 
861
	// otherwise the user must be the caller or in their downline.
-
 
862
	private boolean canViewUserLeads(AuthUser me, int authUserId) {
-
 
863
		if (me == null) return false;
-
 
864
		if (isSuperAdmin(me)) return true;
-
 
865
		if (me.getId() == authUserId) return true;
-
 
866
		return new HashSet<>(authService.getAllReportees(me.getId())).contains(authUserId);
-
 
867
	}
-
 
868
 
-
 
869
	// GET /beatPlan/assignedLeads?authUserId=&beatId=&date=
-
 
870
	// Lists the selected user's active/open leads (pending + follow-up, yellow/green).
-
 
871
	// When beatId+date are supplied (Day View), each lead is tagged alreadyOnBeat if
-
 
872
	// it already has a non-cancelled lead_route on that beat+date.
-
 
873
	@GetMapping(value = "/beatPlan/assignedLeads")
-
 
874
	public ResponseEntity<?> assignedLeads(
-
 
875
			HttpServletRequest request,
-
 
876
			@RequestParam int authUserId,
-
 
877
			@RequestParam(required = false) Integer beatId,
-
 
878
			@RequestParam(required = false) String date) throws ProfitMandiBusinessException {
-
 
879
 
-
 
880
		AuthUser me = currentUser(request);
-
 
881
		if (me == null) return responseSender.unauthorized("Not logged in");
-
 
882
		if (!canViewUserLeads(me, authUserId)) {
-
 
883
			// Not visible to this manager → empty (mirrors VisitRequestController.list).
-
 
884
			return responseSender.ok(Collections.singletonMap("rows", new ArrayList<>()));
-
 
885
		}
-
 
886
 
-
 
887
		// Active/open only — pending + follow-up.
-
 
888
		List<Lead> leads = new ArrayList<>();
-
 
889
		leads.addAll(leadRepository.selectByAssignAuthIdAndStatus(
-
 
890
				authUserId, com.spice.profitmandi.dao.enumuration.dtr.LeadStatus.pending));
-
 
891
		leads.addAll(leadRepository.selectByAssignAuthIdAndStatus(
-
 
892
				authUserId, com.spice.profitmandi.dao.enumuration.dtr.LeadStatus.followUp));
-
 
893
 
-
 
894
		// Leads already on this beat+date (Day View only), to flag dupes in the picker.
-
 
895
		LocalDate onDate = (date != null && !date.isEmpty()) ? LocalDate.parse(date) : null;
-
 
896
		Set<Integer> alreadyOnBeat = new HashSet<>();
-
 
897
		if (beatId != null && onDate != null) {
-
 
898
			for (LeadRoute lr : leadRouteRepository.selectByBeatId(beatId)) {
-
 
899
				if (onDate.equals(lr.getScheduleDate()) && !"CANCELLED".equals(lr.getStatus())) {
-
 
900
					alreadyOnBeat.add(lr.getLeadId());
-
 
901
				}
-
 
902
			}
-
 
903
		}
-
 
904
 
-
 
905
		// Colour-filter + dedupe, then batch-resolve approved geo (avoids N+1).
-
 
906
		List<Lead> routable = new ArrayList<>();
-
 
907
		Set<Integer> seen = new HashSet<>();
-
 
908
		for (Lead lead : leads) {
-
 
909
			if (!seen.add(lead.getId())) continue;
-
 
910
			String color = lead.getColor() != null ? lead.getColor().toLowerCase() : "";
-
 
911
			if (!ROUTABLE_LEAD_COLORS.contains(color)) continue;
-
 
912
			routable.add(lead);
-
 
913
		}
-
 
914
		Set<Integer> withGeo = new HashSet<>();
-
 
915
		if (!routable.isEmpty()) {
-
 
916
			List<Integer> ids = routable.stream().map(Lead::getId).collect(Collectors.toList());
-
 
917
			for (com.spice.profitmandi.dao.entity.user.LeadLiveLocation loc :
-
 
918
					leadLiveLocationRepositoryAuto.selectByLeadIds(ids)) {
-
 
919
				if (loc.getStatus() != null && "APPROVED".equals(loc.getStatus().name())) {
-
 
920
					withGeo.add(loc.getLeadId());
-
 
921
				}
-
 
922
			}
-
 
923
		}
-
 
924
 
-
 
925
		List<Map<String, Object>> rows = new ArrayList<>();
-
 
926
		for (Lead lead : routable) {
-
 
927
			Map<String, Object> m = new HashMap<>();
-
 
928
			m.put("leadId", lead.getId());
-
 
929
			String name = ((lead.getFirstName() != null ? lead.getFirstName() : "") + " "
-
 
930
					+ (lead.getLastName() != null ? lead.getLastName() : "")).trim();
-
 
931
			m.put("leadName", name.isEmpty() ? ("Lead #" + lead.getId()) : name);
-
 
932
			m.put("outletName", lead.getOutLetName());
-
 
933
			m.put("mobile", lead.getLeadMobile());
-
 
934
			m.put("city", lead.getCity());
-
 
935
			m.put("state", lead.getState());
-
 
936
			m.put("stage", lead.getEffectiveStage() != null ? lead.getEffectiveStage().name() : "");
-
 
937
			m.put("color", lead.getColor());
-
 
938
			m.put("hasGeo", withGeo.contains(lead.getId()));
-
 
939
			m.put("alreadyOnBeat", alreadyOnBeat.contains(lead.getId()));
-
 
940
			rows.add(m);
-
 
941
		}
-
 
942
 
-
 
943
		return responseSender.ok(Collections.singletonMap("rows", rows));
-
 
944
	}
-
 
945
 
-
 
946
	// Shared, idempotent lead_route write. Returns true when a new row is created,
-
 
947
	// false when the lead is already on that (beat, date). Best-effort nudges the
-
 
948
	// lead forward to BEAT_PLANNED. Callers are responsible for the beat-runs-on-date
-
 
949
	// and future-date guards. Mirrors deferredAssignToBeat's lead branch.
-
 
950
	private boolean writeLeadRoute(int beatId, int leadId, LocalDate date, int approverAuthId) {
-
 
951
		boolean exists = leadRouteRepository.selectByBeatId(beatId).stream()
-
 
952
				.anyMatch(lr -> lr.getLeadId() == leadId
-
 
953
						&& date.equals(lr.getScheduleDate())
-
 
954
						&& !"CANCELLED".equals(lr.getStatus()));
-
 
955
		if (exists) return false;
-
 
956
 
-
 
957
		LocalDateTime now = LocalDateTime.now();
-
 
958
		LeadRoute lr = new LeadRoute();
-
 
959
		lr.setBeatId(beatId);
-
 
960
		lr.setLeadId(leadId);
-
 
961
		lr.setScheduleDate(date);
-
 
962
		lr.setSequenceOrder(9999); // append; planner can reorder
-
 
963
		lr.setStatus("APPROVED");
-
 
964
		lr.setApprovedBy(approverAuthId);
-
 
965
		lr.setApprovedTimestamp(now);
-
 
966
		lr.setCreatedTimestamp(now);
-
 
967
		lr.setUpdatedTimestamp(now);
-
 
968
		leadRouteRepository.persist(lr);
-
 
969
 
-
 
970
		advanceLeadToBeatPlanned(leadId);
-
 
971
		return true;
-
 
972
	}
-
 
973
 
-
 
974
	// Best-effort: move a lead forward to BEAT_PLANNED when it's still in an earlier
-
 
975
	// happy-path stage. Never regresses a VISITED+ lead and never aborts the caller's
-
 
976
	// write on failure (the legacy status is kept in sync via toLegacyStatus()).
-
 
977
	private void advanceLeadToBeatPlanned(int leadId) {
-
 
978
		try {
-
 
979
			Lead lead = leadRepository.selectById(leadId);
-
 
980
			if (lead == null) return;
-
 
981
			com.spice.profitmandi.dao.enumuration.dtr.LeadStage cur = lead.getEffectiveStage();
-
 
982
			boolean beforeBeatPlanned =
-
 
983
					cur == com.spice.profitmandi.dao.enumuration.dtr.LeadStage.NEW
-
 
984
							|| cur == com.spice.profitmandi.dao.enumuration.dtr.LeadStage.ASSIGNED
-
 
985
							|| cur == com.spice.profitmandi.dao.enumuration.dtr.LeadStage.CONTACTED
-
 
986
							|| cur == com.spice.profitmandi.dao.enumuration.dtr.LeadStage.QUALIFIED;
-
 
987
			if (!beforeBeatPlanned) return;
-
 
988
			com.spice.profitmandi.dao.enumuration.dtr.LeadStage target =
-
 
989
					com.spice.profitmandi.dao.enumuration.dtr.LeadStage.BEAT_PLANNED;
-
 
990
			lead.setStage(target);
-
 
991
			lead.setStatus(target.toLegacyStatus());
-
 
992
			lead.setUpdatedTimestamp(LocalDateTime.now());
-
 
993
			leadRepository.persist(lead);
-
 
994
		} catch (Exception e) {
-
 
995
			LOGGER.warn("Could not advance lead {} to BEAT_PLANNED", leadId, e);
-
 
996
		}
-
 
997
	}
-
 
998
 
-
 
999
	// Confirm a beat actually runs on the given date.
-
 
1000
	private boolean beatRunsOnDate(int beatId, LocalDate date) {
-
 
1001
		return beatScheduleRepository.selectByBeatId(beatId).stream()
-
 
1002
				.anyMatch(s -> date.equals(s.getStartDate()));
-
 
1003
	}
-
 
1004
 
-
 
1005
	// POST /beatPlan/lead/scheduleOnBeat  {leadId, beatId, date}
-
 
1006
	// Single-lead write for the Route Planner calendar-chip handoff. Strictly-future
-
 
1007
	// date only (matches the visit-request / deferred chip flow).
-
 
1008
	@PostMapping(value = "/beatPlan/lead/scheduleOnBeat")
-
 
1009
	public ResponseEntity<?> leadScheduleOnBeat(
-
 
1010
			HttpServletRequest request,
-
 
1011
			@org.springframework.web.bind.annotation.RequestBody Map<String, Object> body) throws Exception {
-
 
1012
		AuthUser me = currentUser(request);
-
 
1013
		if (me == null) return responseSender.unauthorized("Not logged in");
-
 
1014
		if (!canEditBeat(me)) return responseSender.badRequest("Scheduling a lead is restricted to L2 and above.");
-
 
1015
 
-
 
1016
		Integer beatId = body.get("beatId") != null ? ((Number) body.get("beatId")).intValue() : null;
-
 
1017
		Integer leadId = body.get("leadId") != null ? ((Number) body.get("leadId")).intValue() : null;
-
 
1018
		String dateStr = (String) body.get("date");
-
 
1019
		if (beatId == null || leadId == null || dateStr == null)
-
 
1020
			return responseSender.badRequest("leadId, beatId and date are required");
-
 
1021
 
-
 
1022
		LocalDate date;
-
 
1023
		try {
-
 
1024
			date = LocalDate.parse(dateStr);
-
 
1025
		} catch (Exception e) {
-
 
1026
			return responseSender.badRequest("Invalid date (yyyy-MM-dd)");
-
 
1027
		}
-
 
1028
		if (!date.isAfter(LocalDate.now()))
-
 
1029
			return responseSender.badRequest("Pick a future date — today is already in progress.");
-
 
1030
 
-
 
1031
		Beat beat = beatRepository.selectById(beatId);
-
 
1032
		if (beat == null) return responseSender.badRequest("Beat not found");
-
 
1033
		if (!beatRunsOnDate(beatId, date))
-
 
1034
			return responseSender.badRequest("That beat is not scheduled on " + dateStr);
-
 
1035
		if (leadRepository.selectById(leadId) == null)
-
 
1036
			return responseSender.badRequest("Lead not found");
-
 
1037
 
-
 
1038
		boolean created = writeLeadRoute(beatId, leadId, date, me.getId());
-
 
1039
 
-
 
1040
		Map<String, Object> ok = new HashMap<>();
-
 
1041
		ok.put("status", true);
-
 
1042
		ok.put("created", created);
-
 
1043
		ok.put("beatId", beatId);
-
 
1044
		ok.put("scheduleDate", date.toString());
-
 
1045
		ok.put("message", created
-
 
1046
				? "Lead added to beat '" + beat.getName() + "' on " + dateStr
-
 
1047
				: "Lead is already on that beat for " + dateStr);
-
 
1048
		return responseSender.ok(ok);
-
 
1049
	}
-
 
1050
 
-
 
1051
	// POST /beatPlan/leadsForBeat/submit  {beatId, date, leadIds:[...]}
-
 
1052
	// Bulk write for the Day View "Add Lead" modal onto a known beat+date. Allows
-
 
1053
	// today or a future date (the row's scheduled run may be today).
-
 
1054
	@PostMapping(value = "/beatPlan/leadsForBeat/submit")
-
 
1055
	public ResponseEntity<?> leadsForBeatSubmit(
-
 
1056
			HttpServletRequest request,
-
 
1057
			@org.springframework.web.bind.annotation.RequestBody Map<String, Object> body) throws Exception {
-
 
1058
		AuthUser me = currentUser(request);
-
 
1059
		if (me == null) return responseSender.unauthorized("Not logged in");
-
 
1060
		if (!canEditBeat(me)) return responseSender.badRequest("Adding a lead is restricted to L2 and above.");
-
 
1061
 
-
 
1062
		Integer beatId = body.get("beatId") != null ? ((Number) body.get("beatId")).intValue() : null;
-
 
1063
		String dateStr = (String) body.get("date");
-
 
1064
		List<?> leadIdsRaw = (List<?>) body.get("leadIds");
-
 
1065
		if (beatId == null || dateStr == null || leadIdsRaw == null || leadIdsRaw.isEmpty())
-
 
1066
			return responseSender.badRequest("beatId, date and leadIds are required");
-
 
1067
 
-
 
1068
		LocalDate date;
-
 
1069
		try {
-
 
1070
			date = LocalDate.parse(dateStr);
-
 
1071
		} catch (Exception e) {
-
 
1072
			return responseSender.badRequest("Invalid date (yyyy-MM-dd)");
-
 
1073
		}
-
 
1074
		if (date.isBefore(LocalDate.now()))
-
 
1075
			return responseSender.badRequest("Pick today or a future date.");
-
 
1076
 
-
 
1077
		Beat beat = beatRepository.selectById(beatId);
-
 
1078
		if (beat == null) return responseSender.badRequest("Beat not found");
-
 
1079
		if (!beatRunsOnDate(beatId, date))
-
 
1080
			return responseSender.badRequest("That beat is not scheduled on " + dateStr);
-
 
1081
 
-
 
1082
		int added = 0, skipped = 0;
-
 
1083
		for (Object idRaw : leadIdsRaw) {
-
 
1084
			int leadId = ((Number) idRaw).intValue();
-
 
1085
			if (leadRepository.selectById(leadId) == null) {
-
 
1086
				skipped++;
-
 
1087
				continue;
-
 
1088
			}
-
 
1089
			if (writeLeadRoute(beatId, leadId, date, me.getId())) added++;
-
 
1090
			else skipped++;
-
 
1091
		}
-
 
1092
 
-
 
1093
		Map<String, Object> ok = new HashMap<>();
-
 
1094
		ok.put("status", true);
-
 
1095
		ok.put("added", added);
-
 
1096
		ok.put("skipped", skipped);
-
 
1097
		ok.put("message", added + " lead(s) added to '" + beat.getName() + "' on " + dateStr
-
 
1098
				+ (skipped > 0 ? " (" + skipped + " already present or invalid)" : ""));
-
 
1099
		return responseSender.ok(ok);
-
 
1100
	}
-
 
1101
 
-
 
1102
	// POST /beatPlan/lead/createBeatAndSchedule  {authUserId, leadId, date, beatName?}
-
 
1103
	// The "+ New beat" action — a 1-day beat with this lead as the only Day-1 stop,
-
 
1104
	// for a rep who has no beat to land on. Mirrors VisitRequestController.createBeatAndSchedule.
-
 
1105
	@PostMapping(value = "/beatPlan/lead/createBeatAndSchedule")
-
 
1106
	public ResponseEntity<?> leadCreateBeatAndSchedule(
-
 
1107
			HttpServletRequest request,
-
 
1108
			@org.springframework.web.bind.annotation.RequestBody Map<String, Object> body) throws Exception {
-
 
1109
		AuthUser me = currentUser(request);
-
 
1110
		if (me == null) return responseSender.unauthorized("Not logged in");
-
 
1111
		if (!canEditBeat(me)) return responseSender.badRequest("Creating a beat is restricted to L2 and above.");
-
 
1112
 
-
 
1113
		Integer authUserId = body.get("authUserId") != null ? ((Number) body.get("authUserId")).intValue() : null;
-
 
1114
		Integer leadId = body.get("leadId") != null ? ((Number) body.get("leadId")).intValue() : null;
-
 
1115
		String dateStr = (String) body.get("date");
-
 
1116
		if (authUserId == null || leadId == null || dateStr == null)
-
 
1117
			return responseSender.badRequest("authUserId, leadId and date are required");
-
 
1118
 
-
 
1119
		LocalDate date;
-
 
1120
		try {
-
 
1121
			date = LocalDate.parse(dateStr);
-
 
1122
		} catch (Exception e) {
-
 
1123
			return responseSender.badRequest("Invalid date (yyyy-MM-dd)");
-
 
1124
		}
-
 
1125
		if (!date.isAfter(LocalDate.now()))
-
 
1126
			return responseSender.badRequest("Pick a future date — today is already in progress.");
-
 
1127
 
-
 
1128
		Lead lead = leadRepository.selectById(leadId);
-
 
1129
		if (lead == null) return responseSender.badRequest("Lead not found");
-
 
1130
 
-
 
1131
		// One-beat-per-day guard for the rep.
-
 
1132
		for (Beat existing : beatRepository.selectActiveByAuthUserId(authUserId)) {
-
 
1133
			for (BeatSchedule s : beatScheduleRepository.selectByBeatId(existing.getId())) {
-
 
1134
				if (date.equals(s.getStartDate())) {
-
 
1135
					return responseSender.badRequest("This user already has '"
-
 
1136
							+ (existing.getName() != null ? existing.getName() : "Beat #" + existing.getId())
-
 
1137
							+ "' scheduled on " + date + " — use Schedule on a beat instead.");
-
 
1138
				}
-
 
1139
			}
-
 
1140
		}
-
 
1141
 
-
 
1142
		String leadLabel = ((lead.getFirstName() != null ? lead.getFirstName() : "") + " "
-
 
1143
				+ (lead.getLastName() != null ? lead.getLastName() : "")).trim();
-
 
1144
		if (leadLabel.isEmpty()) leadLabel = "Lead #" + leadId;
-
 
1145
		Object nameRaw = body.get("beatName");
-
 
1146
		String beatName = (nameRaw != null && !nameRaw.toString().trim().isEmpty())
-
 
1147
				? nameRaw.toString().trim()
-
 
1148
				: ("Lead - " + leadLabel + " - " + date);
-
 
1149
		if (beatName.length() > 100) beatName = beatName.substring(0, 100);
-
 
1150
 
-
 
1151
		LocalDateTime now = LocalDateTime.now();
-
 
1152
		Beat beat = new Beat();
-
 
1153
		beat.setName(beatName);
-
 
1154
		beat.setAuthUserId(authUserId);
-
 
1155
		beat.setBeatColor(BEAT_COLORS[Math.abs(beatName.hashCode()) % BEAT_COLORS.length]);
-
 
1156
		beat.setTotalDays(1);
-
 
1157
		beat.setActive(true);
-
 
1158
		beat.setCreatedBy(me.getId());
-
 
1159
		beat.setCreatedTimestamp(now);
-
 
1160
		beatRepository.persist(beat);
-
 
1161
 
-
 
1162
		BeatSchedule sched = new BeatSchedule();
-
 
1163
		sched.setBeatId(beat.getId());
-
 
1164
		sched.setStartDate(date);
-
 
1165
		sched.setEndDate(date);
-
 
1166
		sched.setDayNumber(1);
-
 
1167
		sched.setEndAction("HOME");
-
 
1168
		sched.setCreatedTimestamp(now);
-
 
1169
		beatScheduleRepository.persist(sched);
-
 
1170
 
-
 
1171
		writeLeadRoute(beat.getId(), leadId, date, me.getId());
-
 
1172
 
-
 
1173
		Map<String, Object> ok = new HashMap<>();
-
 
1174
		ok.put("status", true);
-
 
1175
		ok.put("beatId", beat.getId());
-
 
1176
		ok.put("beatName", beatName);
-
 
1177
		ok.put("scheduleDate", date.toString());
-
 
1178
		ok.put("message", "Beat '" + beatName + "' created on " + dateStr);
-
 
1179
		return responseSender.ok(ok);
-
 
1180
	}
-
 
1181
 
846
	// authUserId -> dtr.users id (via shared email), memoized in the passed cache.
1182
	// authUserId -> dtr.users id (via shared email), memoized in the passed cache.
847
	// Used by the reschedule action to create the new PENDING location_tracking row.
1183
	// Used by the reschedule action to create the new PENDING location_tracking row.
848
	private Integer resolveDtrId(int authUserId, Map<Integer, Integer> cache) {
1184
	private Integer resolveDtrId(int authUserId, Map<Integer, Integer> cache) {
849
		if (cache.containsKey(authUserId)) return cache.get(authUserId);
1185
		if (cache.containsKey(authUserId)) return cache.get(authUserId);
850
		Integer dtrId = null;
1186
		Integer dtrId = null;