| Line 84... |
Line 84... |
| 84 |
// AI leads are auto-assigned to a random active BGC L1 (the lead desk). Previously a hardcoded
|
84 |
// AI leads are auto-assigned to a random active BGC L1 (the lead desk). Previously a hardcoded
|
| 85 |
// round-robin between Khushbu (134) and Archana (126); before that a fixed 53 / Shankar Mushran.
|
85 |
// round-robin between Khushbu (134) and Archana (126); before that a fixed 53 / Shankar Mushran.
|
| 86 |
// 126/134 are themselves BGC L1, so they stay in the pool - it just widens to the whole desk.
|
86 |
// 126/134 are themselves BGC L1, so they stay in the pool - it just widens to the whole desk.
|
| 87 |
private static final int AI_ASSIGN_FALLBACK = 134; // Khushbu Saxena - used only if no active BGC L1 resolves
|
87 |
private static final int AI_ASSIGN_FALLBACK = 134; // Khushbu Saxena - used only if no active BGC L1 resolves
|
| 88 |
|
88 |
|
| - |
|
89 |
// Ira channels the AI side sends in AiLeadRequest.source, stored on lead.ai_channel.
|
| - |
|
90 |
private static final String IRA_PHONE = "IRA_PHONE";
|
| - |
|
91 |
private static final String IRA_WEB = "IRA_WEB";
|
| - |
|
92 |
|
| - |
|
93 |
// AI leads from the Ira toll-free number all go to Meenakshi Mehta, and she is left out of the
|
| - |
|
94 |
// random pool for every other AI lead.
|
| - |
|
95 |
private static final int IRA_PHONE_LEAD_ASSIGNEE = 84;
|
| - |
|
96 |
|
| 89 |
@Value("${ai.lead.intake.token:}")
|
97 |
@Value("${ai.lead.intake.token:}")
|
| 90 |
private String aiLeadIntakeToken;
|
98 |
private String aiLeadIntakeToken;
|
| 91 |
|
99 |
|
| 92 |
/**
|
100 |
/**
|
| 93 |
* AI-assistant lead intake, hosted in the fofo app so it deploys via {@code fofo_deploy}. Mirrors the
|
101 |
* AI-assistant lead intake, hosted in the fofo app so it deploys via {@code fofo_deploy}. Mirrors the
|
| Line 153... |
Line 161... |
| 153 |
String recordingUrl = aiLeadRequest.getRecordingUrl();
|
161 |
String recordingUrl = aiLeadRequest.getRecordingUrl();
|
| 154 |
if (recordingUrl != null && !recordingUrl.trim().isEmpty()) {
|
162 |
if (recordingUrl != null && !recordingUrl.trim().isEmpty()) {
|
| 155 |
lead.setRecordingUrl(recordingUrl.trim());
|
163 |
lead.setRecordingUrl(recordingUrl.trim());
|
| 156 |
}
|
164 |
}
|
| 157 |
|
165 |
|
| - |
|
166 |
// Ira channel: IRA_PHONE from the toll-free number, everything else (IRA_WEB, no source, or an
|
| - |
|
167 |
// unknown value) is a web lead. A missing source is still accepted, as before.
|
| - |
|
168 |
String channel = aiLeadRequest.getSource() == null ? "" : aiLeadRequest.getSource().trim().toUpperCase();
|
| - |
|
169 |
if (!channel.isEmpty() && !IRA_PHONE.equals(channel) && !IRA_WEB.equals(channel)) {
|
| - |
|
170 |
LOGGER.warn("AI lead intake: unknown source '{}' for mobile {}, treating as web", aiLeadRequest.getSource(), mobile);
|
| - |
|
171 |
}
|
| - |
|
172 |
lead.setAiChannel(IRA_PHONE.equals(channel) ? IRA_PHONE : IRA_WEB);
|
| - |
|
173 |
|
| 158 |
// Random active BGC L1 - only on a real create, dedup hits return before this point.
|
174 |
// Only on a real create, dedup hits return before this point.
|
| 159 |
int assignTo = resolveAiLeadAssignee();
|
175 |
int assignTo = resolveAiLeadAssignee(lead);
|
| 160 |
lead.setAssignTo(assignTo);
|
176 |
lead.setAssignTo(assignTo);
|
| 161 |
lead.setAuthId(lead.getAssignTo());
|
177 |
lead.setAuthId(lead.getAssignTo());
|
| 162 |
lead.setCreatedTimestamp(LocalDateTime.now());
|
178 |
lead.setCreatedTimestamp(LocalDateTime.now());
|
| 163 |
lead.setUpdatedTimestamp(LocalDateTime.now());
|
179 |
lead.setUpdatedTimestamp(LocalDateTime.now());
|
| 164 |
// Stamp before the insert: region, owner, stage and an SLA clock. Without it the lead is
|
180 |
// Stamp before the insert: region, owner, stage and an SLA clock. Without it the lead is
|
| Line 195... |
Line 211... |
| 195 |
ProfitMandiConstants.TICKET_CATEGORY_BGC, EscalationType.L1);
|
211 |
ProfitMandiConstants.TICKET_CATEGORY_BGC, EscalationType.L1);
|
| 196 |
|
212 |
|
| 197 |
List<AuthUser> eligible = bgcL1 == null ? Collections.<AuthUser>emptyList()
|
213 |
List<AuthUser> eligible = bgcL1 == null ? Collections.<AuthUser>emptyList()
|
| 198 |
: bgcL1.stream()
|
214 |
: bgcL1.stream()
|
| 199 |
.filter(x -> Boolean.TRUE.equals(x.isActive()))
|
215 |
.filter(x -> Boolean.TRUE.equals(x.isActive()))
|
| - |
|
216 |
.filter(x -> x.getId() != IRA_PHONE_LEAD_ASSIGNEE)
|
| 200 |
.collect(Collectors.toList());
|
217 |
.collect(Collectors.toList());
|
| 201 |
|
218 |
|
| 202 |
if (eligible.isEmpty()) {
|
219 |
if (eligible.isEmpty()) {
|
| 203 |
LOGGER.warn("AI lead intake: no active BGC L1 in cs.position, falling back to auth id {}",
|
220 |
LOGGER.warn("AI lead intake: no active BGC L1 in cs.position, falling back to auth id {}",
|
| 204 |
AI_ASSIGN_FALLBACK);
|
221 |
AI_ASSIGN_FALLBACK);
|
| Line 210... |
Line 227... |
| 210 |
LOGGER.info("AI lead intake: assigned to BGC L1 {} ({}) picked from {} eligible",
|
227 |
LOGGER.info("AI lead intake: assigned to BGC L1 {} ({}) picked from {} eligible",
|
| 211 |
assignee.getId(), assignee.getFullName(), eligible.size());
|
228 |
assignee.getId(), assignee.getFullName(), eligible.size());
|
| 212 |
return assignee.getId();
|
229 |
return assignee.getId();
|
| 213 |
}
|
230 |
}
|
| 214 |
|
231 |
|
| - |
|
232 |
/** IRA_PHONE / IRA_WEB for AI Assistant leads (any AI lead not from the phone line is web), else null. */
|
| - |
|
233 |
private String aiChannelOf(Lead lead) {
|
| - |
|
234 |
if (!AI_LEAD_SOURCE.equals(lead.getSource())) {
|
| - |
|
235 |
return null;
|
| - |
|
236 |
}
|
| - |
|
237 |
return IRA_PHONE.equals(lead.getAiChannel()) ? IRA_PHONE : IRA_WEB;
|
| - |
|
238 |
}
|
| - |
|
239 |
|
| - |
|
240 |
/** Source as the Lead Dashboard shows it: "AI Assistant (Phone)" / "AI Assistant (Web)" for AI leads. */
|
| - |
|
241 |
private String displaySource(Lead lead) {
|
| - |
|
242 |
String channel = aiChannelOf(lead);
|
| - |
|
243 |
String source = lead.getSource() == null ? "" : lead.getSource();
|
| - |
|
244 |
return IRA_PHONE.equals(channel) ? source + " (Phone)" : IRA_WEB.equals(channel) ? source + " (Web)" : source;
|
| - |
|
245 |
}
|
| - |
|
246 |
|
| - |
|
247 |
/** Keeps only AI leads from one channel (IRA_PHONE / IRA_WEB); a blank channel keeps every lead. */
|
| - |
|
248 |
private List<Lead> filterByAiChannel(List<Lead> leads, String aiChannel) {
|
| - |
|
249 |
if (aiChannel == null || aiChannel.trim().isEmpty()) {
|
| - |
|
250 |
return leads;
|
| - |
|
251 |
}
|
| - |
|
252 |
return leads.stream().filter(l -> aiChannel.equals(aiChannelOf(l))).collect(Collectors.toList());
|
| - |
|
253 |
}
|
| - |
|
254 |
|
| - |
|
255 |
/**
|
| - |
|
256 |
* Ira phone leads go to Meenakshi; every other AI lead to a random active BGC L1. If she is ever
|
| - |
|
257 |
* deactivated, phone leads fall back to the pool rather than stranding with an inactive owner.
|
| - |
|
258 |
*/
|
| - |
|
259 |
private int resolveAiLeadAssignee(Lead lead) {
|
| - |
|
260 |
if (IRA_PHONE.equals(lead.getAiChannel())) {
|
| - |
|
261 |
AuthUser phoneOwner = authRepository.selectById(IRA_PHONE_LEAD_ASSIGNEE);
|
| - |
|
262 |
if (phoneOwner != null && Boolean.TRUE.equals(phoneOwner.isActive())) {
|
| - |
|
263 |
return IRA_PHONE_LEAD_ASSIGNEE;
|
| - |
|
264 |
}
|
| - |
|
265 |
LOGGER.warn("AI lead intake: Ira phone owner {} is inactive, using the BGC L1 pool", IRA_PHONE_LEAD_ASSIGNEE);
|
| - |
|
266 |
}
|
| - |
|
267 |
return resolveAiLeadAssignee();
|
| - |
|
268 |
}
|
| - |
|
269 |
|
| 215 |
private ResponseEntity<Map<String, Object>> aiLeadResponse(HttpStatus status, String statusCode,
|
270 |
private ResponseEntity<Map<String, Object>> aiLeadResponse(HttpStatus status, String statusCode,
|
| 216 |
String statusMessage, String responseStatus, Object response) {
|
271 |
String statusMessage, String responseStatus, Object response) {
|
| 217 |
Map<String, Object> body = new HashMap<>();
|
272 |
Map<String, Object> body = new HashMap<>();
|
| 218 |
body.put("statusCode", statusCode);
|
273 |
body.put("statusCode", statusCode);
|
| 219 |
body.put("statusMessage", statusMessage);
|
274 |
body.put("statusMessage", statusMessage);
|
| Line 341... |
Line 396... |
| 341 |
.anyMatch(p -> p.getEscalationType() == null
|
396 |
.anyMatch(p -> p.getEscalationType() == null
|
| 342 |
|| !p.getEscalationType().isGreaterThanEqualTo(EscalationType.L4));
|
397 |
|| !p.getEscalationType().isGreaterThanEqualTo(EscalationType.L4));
|
| 343 |
}
|
398 |
}
|
| 344 |
|
399 |
|
| 345 |
@RequestMapping(value = "/getOpenLead", method = RequestMethod.GET)
|
400 |
@RequestMapping(value = "/getOpenLead", method = RequestMethod.GET)
|
| 346 |
public String getOpenLead(HttpServletRequest request, @RequestParam(name = "leadStatus", required = false, defaultValue = "All") List<LeadStatus> leadStatus, @RequestParam(name = "color", required = false, defaultValue = "All") List<String> color, @RequestParam(name = "startDate", required = false) LocalDate startDate, @RequestParam(name = "endDate", required = false) LocalDate endDate, Model model) throws Exception {
|
401 |
public String getOpenLead(HttpServletRequest request, @RequestParam(name = "leadStatus", required = false, defaultValue = "All") List<LeadStatus> leadStatus, @RequestParam(name = "color", required = false, defaultValue = "All") List<String> color, @RequestParam(name = "startDate", required = false) LocalDate startDate, @RequestParam(name = "endDate", required = false) LocalDate endDate, @RequestParam(name = "aiChannel", required = false, defaultValue = "") String aiChannel, Model model) throws Exception {
|
| 347 |
|
402 |
|
| 348 |
|
403 |
|
| 349 |
LocalDateTime startDateTime = startDate != null ? startDate.atStartOfDay() : null;
|
404 |
LocalDateTime startDateTime = startDate != null ? startDate.atStartOfDay() : null;
|
| 350 |
LocalDateTime endDateTime = endDate != null ? endDate.atTime(23, 59, 59) : null;
|
405 |
LocalDateTime endDateTime = endDate != null ? endDate.atTime(23, 59, 59) : null;
|
| 351 |
LOGGER.info("startDateTime: " + startDateTime + ", endDateTime: " + endDateTime);
|
406 |
LOGGER.info("startDateTime: " + startDateTime + ", endDateTime: " + endDateTime);
|
| Line 454... |
Line 509... |
| 454 |
leadCreators.add("daily-sync");
|
509 |
leadCreators.add("daily-sync");
|
| 455 |
leadCreators.addAll(csService.getAuthUserIds(ProfitMandiConstants.TICKET_CATEGORY_SALES, Arrays.asList(EscalationType.L1, EscalationType.L2)).stream().map(x -> x.getFullName()).collect(Collectors.toList()));
|
510 |
leadCreators.addAll(csService.getAuthUserIds(ProfitMandiConstants.TICKET_CATEGORY_SALES, Arrays.asList(EscalationType.L1, EscalationType.L2)).stream().map(x -> x.getFullName()).collect(Collectors.toList()));
|
| 456 |
leadCreators.addAll(csService.getAuthUserIds(ProfitMandiConstants.TICKET_CATEGORY_BGC, Arrays.asList(EscalationType.L1, EscalationType.L2, EscalationType.L3)).stream().map(x -> x.getFullName()).collect(Collectors.toList()));
|
511 |
leadCreators.addAll(csService.getAuthUserIds(ProfitMandiConstants.TICKET_CATEGORY_BGC, Arrays.asList(EscalationType.L1, EscalationType.L2, EscalationType.L3)).stream().map(x -> x.getFullName()).collect(Collectors.toList()));
|
| 457 |
model.addAttribute("leadCreators", leadCreators);
|
512 |
model.addAttribute("leadCreators", leadCreators);
|
| 458 |
}
|
513 |
}
|
| - |
|
514 |
leads = filterByAiChannel(leads, aiChannel);
|
| - |
|
515 |
model.addAttribute("selectedAiChannel", aiChannel);
|
| 459 |
List<Lead> weekLast = new ArrayList<>();
|
516 |
List<Lead> weekLast = new ArrayList<>();
|
| 460 |
List<Lead> weekThird = new ArrayList<>();
|
517 |
List<Lead> weekThird = new ArrayList<>();
|
| 461 |
List<Lead> weekSecond = new ArrayList<>();
|
518 |
List<Lead> weekSecond = new ArrayList<>();
|
| 462 |
List<Lead> weekFirst = new ArrayList<>();
|
519 |
List<Lead> weekFirst = new ArrayList<>();
|
| 463 |
LocalDateTime curDate = LocalDate.now().atStartOfDay();
|
520 |
LocalDateTime curDate = LocalDate.now().atStartOfDay();
|
| Line 707... |
Line 764... |
| 707 |
public ResponseEntity<?> getdownloadLeadDataByDate(HttpServletRequest request,
|
764 |
public ResponseEntity<?> getdownloadLeadDataByDate(HttpServletRequest request,
|
| 708 |
@RequestParam(name = "leadStatus", required = false, defaultValue = "All") List<LeadStatus> leadStatus,
|
765 |
@RequestParam(name = "leadStatus", required = false, defaultValue = "All") List<LeadStatus> leadStatus,
|
| 709 |
@RequestParam(name = "color", required = false, defaultValue = "All") List<String> color,
|
766 |
@RequestParam(name = "color", required = false, defaultValue = "All") List<String> color,
|
| 710 |
@RequestParam(name = "startDate", required = false) LocalDate startDate,
|
767 |
@RequestParam(name = "startDate", required = false) LocalDate startDate,
|
| 711 |
@RequestParam(name = "endDate", required = false) LocalDate endDate,
|
768 |
@RequestParam(name = "endDate", required = false) LocalDate endDate,
|
| - |
|
769 |
@RequestParam(name = "searchTerm", required = false, defaultValue = "") String searchTerm,
|
| 712 |
@RequestParam(name = "searchTerm", required = false, defaultValue = "") String searchTerm) throws Exception {
|
770 |
@RequestParam(name = "aiChannel", required = false, defaultValue = "") String aiChannel) throws Exception {
|
| 713 |
|
771 |
|
| 714 |
LoginDetails fofoDetails = cookiesProcessor.getCookiesObject(request);
|
772 |
LoginDetails fofoDetails = cookiesProcessor.getCookiesObject(request);
|
| 715 |
String emailId = fofoDetails.getEmailId();
|
773 |
String emailId = fofoDetails.getEmailId();
|
| 716 |
LocalDateTime startDateTime = startDate != null ? startDate.atStartOfDay() : null;
|
774 |
LocalDateTime startDateTime = startDate != null ? startDate.atStartOfDay() : null;
|
| 717 |
LocalDateTime endDateTime = endDate != null ? endDate.atTime(23, 59, 59) : null;
|
775 |
LocalDateTime endDateTime = endDate != null ? endDate.atTime(23, 59, 59) : null;
|
| Line 778... |
Line 836... |
| 778 |
leads.addAll(withinCreatedRange(leadFollowUp, startDateTime, endDateTime));
|
836 |
leads.addAll(withinCreatedRange(leadFollowUp, startDateTime, endDateTime));
|
| 779 |
}
|
837 |
}
|
| 780 |
}
|
838 |
}
|
| 781 |
}
|
839 |
}
|
| 782 |
|
840 |
|
| - |
|
841 |
leads = filterByAiChannel(leads, aiChannel);
|
| - |
|
842 |
|
| 783 |
// Build activity map and auth map (SAME AS VIEW ENDPOINT)
|
843 |
// Build activity map and auth map (SAME AS VIEW ENDPOINT)
|
| 784 |
// Guard both lookups on a non-empty result: an empty list would generate an invalid
|
844 |
// Guard both lookups on a non-empty result: an empty list would generate an invalid
|
| 785 |
// "IN ()" and 500 (e.g. a date range with no leads), so an empty range yields an empty CSV.
|
845 |
// "IN ()" and 500 (e.g. a date range with no leads), so an empty range yields an empty CSV.
|
| 786 |
Map<Integer, AuthUser> authIdAndAuthUserMap = new HashMap<>();
|
846 |
Map<Integer, AuthUser> authIdAndAuthUserMap = new HashMap<>();
|
| 787 |
if (!leads.isEmpty()) {
|
847 |
if (!leads.isEmpty()) {
|
| Line 797... |
Line 857... |
| 797 |
List<List<?>> rows = new ArrayList<>();
|
857 |
List<List<?>> rows = new ArrayList<>();
|
| 798 |
// Mirror the on-screen table's search box: keep only leads whose exported fields contain the term,
|
858 |
// Mirror the on-screen table's search box: keep only leads whose exported fields contain the term,
|
| 799 |
// so the CSV matches what the user is looking at.
|
859 |
// so the CSV matches what the user is looking at.
|
| 800 |
if (searchTerm != null && !searchTerm.trim().isEmpty()) {
|
860 |
if (searchTerm != null && !searchTerm.trim().isEmpty()) {
|
| 801 |
String term = searchTerm.trim().toLowerCase();
|
861 |
String term = searchTerm.trim().toLowerCase();
|
| 802 |
leads = leads.stream().filter(l -> Stream.of(l.getId(), l.getSource(), l.getFullName(), l.getOutLetName(),
|
862 |
leads = leads.stream().filter(l -> Stream.of(l.getId(), displaySource(l), l.getFullName(), l.getOutLetName(),
|
| 803 |
l.getLeadMobile(), l.getAddress(), l.getCity(), l.getState(), l.getCreatedBy(), l.getStatus(), l.getColor())
|
863 |
l.getLeadMobile(), l.getAddress(), l.getCity(), l.getState(), l.getCreatedBy(), l.getStatus(), l.getColor())
|
| 804 |
.anyMatch(v -> v != null && String.valueOf(v).toLowerCase().contains(term)))
|
864 |
.anyMatch(v -> v != null && String.valueOf(v).toLowerCase().contains(term)))
|
| 805 |
.collect(Collectors.toList());
|
865 |
.collect(Collectors.toList());
|
| 806 |
}
|
866 |
}
|
| 807 |
|
867 |
|
| Line 824... |
Line 884... |
| 824 |
assignTo = (user.getFirstName() + " " + user.getLastName()).trim();
|
884 |
assignTo = (user.getFirstName() + " " + user.getLastName()).trim();
|
| 825 |
}
|
885 |
}
|
| 826 |
|
886 |
|
| 827 |
rows.add(Arrays.asList(
|
887 |
rows.add(Arrays.asList(
|
| 828 |
lead.getId(),
|
888 |
lead.getId(),
|
| 829 |
lead.getSource(),
|
889 |
displaySource(lead),
|
| 830 |
lead.getFullName(),
|
890 |
lead.getFullName(),
|
| 831 |
outLetName,
|
891 |
outLetName,
|
| 832 |
potential,
|
892 |
potential,
|
| 833 |
lead.getLeadMobile(),
|
893 |
lead.getLeadMobile(),
|
| 834 |
lead.getAddress() != null ? lead.getAddress() : "",
|
894 |
lead.getAddress() != null ? lead.getAddress() : "",
|