| Line 13... |
Line 13... |
| 13 |
import com.spice.profitmandi.dao.repository.auth.AuthRepository;
|
13 |
import com.spice.profitmandi.dao.repository.auth.AuthRepository;
|
| 14 |
import com.spice.profitmandi.dao.repository.dtr.LeadActivityRepository;
|
14 |
import com.spice.profitmandi.dao.repository.dtr.LeadActivityRepository;
|
| 15 |
import com.spice.profitmandi.dao.repository.dtr.LeadCallRepository;
|
15 |
import com.spice.profitmandi.dao.repository.dtr.LeadCallRepository;
|
| 16 |
import com.spice.profitmandi.dao.repository.dtr.LeadDndRepository;
|
16 |
import com.spice.profitmandi.dao.repository.dtr.LeadDndRepository;
|
| 17 |
import com.spice.profitmandi.dao.repository.dtr.LeadLiveLocationRepository;
|
17 |
import com.spice.profitmandi.dao.repository.dtr.LeadLiveLocationRepository;
|
| - |
|
18 |
import com.spice.profitmandi.dao.entity.cs.Position;
|
| - |
|
19 |
import com.spice.profitmandi.dao.entity.cs.Region;
|
| - |
|
20 |
import com.spice.profitmandi.dao.enumuration.cs.EscalationType;
|
| - |
|
21 |
import com.spice.profitmandi.dao.repository.cs.PositionRepository;
|
| 18 |
import com.spice.profitmandi.dao.repository.cs.RegionRepository;
|
22 |
import com.spice.profitmandi.dao.repository.cs.RegionRepository;
|
| - |
|
23 |
import com.spice.profitmandi.dao.entity.user.LeadCallAnswer;
|
| - |
|
24 |
import com.spice.profitmandi.dao.entity.user.LeadCallQuestion;
|
| - |
|
25 |
import com.spice.profitmandi.dao.repository.dtr.LeadCallChecklistRepository;
|
| 19 |
import com.spice.profitmandi.dao.repository.dtr.LeadRepository;
|
26 |
import com.spice.profitmandi.dao.repository.dtr.LeadRepository;
|
| 20 |
import com.spice.profitmandi.service.LmsAssignmentService;
|
27 |
import com.spice.profitmandi.service.LmsAssignmentService;
|
| 21 |
import com.spice.profitmandi.web.model.LoginDetails;
|
28 |
import com.spice.profitmandi.web.model.LoginDetails;
|
| 22 |
import com.spice.profitmandi.web.util.CookiesProcessor;
|
29 |
import com.spice.profitmandi.web.util.CookiesProcessor;
|
| 23 |
import org.apache.logging.log4j.LogManager;
|
30 |
import org.apache.logging.log4j.LogManager;
|
| Line 36... |
Line 43... |
| 36 |
|
43 |
|
| 37 |
import javax.servlet.http.HttpServletRequest;
|
44 |
import javax.servlet.http.HttpServletRequest;
|
| 38 |
import java.time.LocalDateTime;
|
45 |
import java.time.LocalDateTime;
|
| 39 |
import java.time.format.DateTimeFormatter;
|
46 |
import java.time.format.DateTimeFormatter;
|
| 40 |
import java.util.ArrayList;
|
47 |
import java.util.ArrayList;
|
| - |
|
48 |
import java.util.Arrays;
|
| 41 |
import java.util.HashMap;
|
49 |
import java.util.HashMap;
|
| 42 |
import java.util.HashSet;
|
50 |
import java.util.HashSet;
|
| 43 |
import java.util.List;
|
51 |
import java.util.List;
|
| 44 |
import com.spice.profitmandi.dao.model.LeadBrandModel;
|
52 |
import com.spice.profitmandi.dao.model.LeadBrandModel;
|
| 45 |
import com.spice.profitmandi.dao.model.LeadDetailModel;
|
53 |
import com.spice.profitmandi.dao.model.LeadDetailModel;
|
| Line 94... |
Line 102... |
| 94 |
|
102 |
|
| 95 |
@Autowired
|
103 |
@Autowired
|
| 96 |
private LeadCallRepository leadCallRepository;
|
104 |
private LeadCallRepository leadCallRepository;
|
| 97 |
|
105 |
|
| 98 |
@Autowired
|
106 |
@Autowired
|
| - |
|
107 |
private LeadCallChecklistRepository leadCallChecklistRepository;
|
| - |
|
108 |
|
| - |
|
109 |
@Autowired
|
| 99 |
private LeadDndRepository leadDndRepository;
|
110 |
private LeadDndRepository leadDndRepository;
|
| 100 |
|
111 |
|
| 101 |
@Autowired
|
112 |
@Autowired
|
| 102 |
private AuthRepository authRepository;
|
113 |
private AuthRepository authRepository;
|
| 103 |
|
114 |
|
| 104 |
@Autowired
|
115 |
@Autowired
|
| 105 |
private RegionRepository regionRepository;
|
116 |
private RegionRepository regionRepository;
|
| 106 |
|
117 |
|
| 107 |
@Autowired
|
118 |
@Autowired
|
| - |
|
119 |
private PositionRepository positionRepository;
|
| - |
|
120 |
|
| - |
|
121 |
@Autowired
|
| 108 |
private LmsAssignmentService lmsAssignmentService;
|
122 |
private LmsAssignmentService lmsAssignmentService;
|
| 109 |
|
123 |
|
| 110 |
@Autowired
|
124 |
@Autowired
|
| 111 |
private CookiesProcessor cookiesProcessor;
|
125 |
private CookiesProcessor cookiesProcessor;
|
| 112 |
|
126 |
|
| Line 652... |
Line 666... |
| 652 |
String label = lakhLabel(value);
|
666 |
String label = lakhLabel(value);
|
| 653 |
return label == null ? "(not set)" : label + "/mo";
|
667 |
return label == null ? "(not set)" : label + "/mo";
|
| 654 |
}
|
668 |
}
|
| 655 |
|
669 |
|
| 656 |
/**
|
670 |
/**
|
| - |
|
671 |
* First-call checklist for a lead (SOP §9 Path A step 2).
|
| - |
|
672 |
*
|
| - |
|
673 |
* <p>Returns the active question set, any answers already recorded, and — the part the caller
|
| - |
|
674 |
* cannot work out for itself — whether this is actually the lead's FIRST connected call.
|
| - |
|
675 |
*
|
| - |
|
676 |
* <p>"First" means no earlier <em>answered</em> call. A string of unanswered dials does not make
|
| - |
|
677 |
* the next one a second call: as far as the retailer is concerned nobody has spoken to them yet,
|
| - |
|
678 |
* and that is exactly when the checklist is worth asking.
|
| - |
|
679 |
*/
|
| - |
|
680 |
@RequestMapping(value = "/lms/checklist", method = RequestMethod.GET)
|
| - |
|
681 |
@ResponseBody
|
| - |
|
682 |
public ResponseEntity<?> checklist(@RequestParam(name = "leadId") int leadId,
|
| - |
|
683 |
@RequestParam(name = "leadCallId", required = false) Integer leadCallId) {
|
| - |
|
684 |
Lead lead = leadRepository.selectById(leadId);
|
| - |
|
685 |
if (lead == null) {
|
| - |
|
686 |
return responseSender.notFound("Lead not found");
|
| - |
|
687 |
}
|
| - |
|
688 |
|
| - |
|
689 |
// Only the questions that belong at the lead's CURRENT stage. A question tagged
|
| - |
|
690 |
// NEW|ASSIGNED is a first-call question by construction; one tagged VISITED is not, and
|
| - |
|
691 |
// showing the whole bank on every call is how a checklist becomes something people skip.
|
| - |
|
692 |
String stage = lead.getEffectiveStage() == null ? null : lead.getEffectiveStage().name();
|
| - |
|
693 |
List<LeadCallAnswer> answers = leadCallChecklistRepository.selectAnswersByLeadId(leadId);
|
| - |
|
694 |
Map<Integer, LeadCallAnswer> byQuestion = new HashMap<>();
|
| - |
|
695 |
for (LeadCallAnswer a : answers) {
|
| - |
|
696 |
byQuestion.put(a.getQuestionId(), a);
|
| - |
|
697 |
}
|
| - |
|
698 |
|
| - |
|
699 |
List<Map<String, Object>> out = new ArrayList<>();
|
| - |
|
700 |
int availablePoints = 0;
|
| - |
|
701 |
for (LeadCallQuestion q : leadCallChecklistRepository.selectActiveQuestions()) {
|
| - |
|
702 |
if (!q.appliesTo(stage)) {
|
| - |
|
703 |
continue;
|
| - |
|
704 |
}
|
| - |
|
705 |
LeadCallAnswer a = byQuestion.get(q.getId());
|
| - |
|
706 |
Map<String, Object> m = new LinkedHashMap<>();
|
| - |
|
707 |
m.put("id", q.getId());
|
| - |
|
708 |
m.put("prompt", q.getPrompt());
|
| - |
|
709 |
m.put("answerType", q.getAnswerType());
|
| - |
|
710 |
m.put("choices", q.getChoices() == null || q.getChoices().trim().isEmpty()
|
| - |
|
711 |
? new ArrayList<String>() : Arrays.asList(q.getChoices().split("\\|")));
|
| - |
|
712 |
m.put("required", q.isRequired());
|
| - |
|
713 |
m.put("points", q.getPoints());
|
| - |
|
714 |
m.put("answer", a == null ? null : a.getAnswer());
|
| - |
|
715 |
out.add(m);
|
| - |
|
716 |
availablePoints += q.getPoints();
|
| - |
|
717 |
}
|
| - |
|
718 |
|
| - |
|
719 |
// Score is over EVERY answer the lead has, not just the ones showing now — a lead that
|
| - |
|
720 |
// answered the early questions keeps those points when it moves on to a later stage.
|
| - |
|
721 |
int score = 0;
|
| - |
|
722 |
for (LeadCallAnswer a : answers) {
|
| - |
|
723 |
score += a.getPointsAwarded();
|
| - |
|
724 |
}
|
| - |
|
725 |
|
| - |
|
726 |
Map<String, Object> res = new LinkedHashMap<>();
|
| - |
|
727 |
res.put("leadId", leadId);
|
| - |
|
728 |
res.put("stage", stage);
|
| - |
|
729 |
res.put("questions", out);
|
| - |
|
730 |
res.put("answered", !answers.isEmpty());
|
| - |
|
731 |
res.put("score", score);
|
| - |
|
732 |
res.put("availablePoints", availablePoints);
|
| - |
|
733 |
res.put("firstCall", !leadCallChecklistRepository.hasEarlierConnectedCall(leadId, leadCallId));
|
| - |
|
734 |
// Worth showing on the panel: the checklist is about this retailer, not a generic form.
|
| - |
|
735 |
res.put("retailer", joinName(lead.getFirstName(), lead.getLastName()));
|
| - |
|
736 |
return responseSender.ok(res);
|
| - |
|
737 |
}
|
| - |
|
738 |
|
| - |
|
739 |
/** Record (or correct) the checklist answers for a lead. */
|
| - |
|
740 |
@RequestMapping(value = "/lms/checklist", method = RequestMethod.POST,
|
| - |
|
741 |
consumes = MediaType.APPLICATION_JSON_VALUE)
|
| - |
|
742 |
@ResponseBody
|
| - |
|
743 |
public ResponseEntity<?> saveChecklist(HttpServletRequest request, @RequestBody ChecklistRequest body) {
|
| - |
|
744 |
if (body == null || body.leadId <= 0) {
|
| - |
|
745 |
return responseSender.badRequest("Nothing to save");
|
| - |
|
746 |
}
|
| - |
|
747 |
Lead lead = leadRepository.selectById(body.leadId);
|
| - |
|
748 |
if (lead == null) {
|
| - |
|
749 |
return responseSender.notFound("Lead not found");
|
| - |
|
750 |
}
|
| - |
|
751 |
AuthUser me = currentUser(request);
|
| - |
|
752 |
if (me == null) {
|
| - |
|
753 |
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
| - |
|
754 |
}
|
| - |
|
755 |
|
| - |
|
756 |
Map<Integer, LeadCallQuestion> active = new HashMap<>();
|
| - |
|
757 |
for (LeadCallQuestion q : leadCallChecklistRepository.selectActiveQuestions()) {
|
| - |
|
758 |
active.put(q.getId(), q);
|
| - |
|
759 |
}
|
| - |
|
760 |
|
| - |
|
761 |
int saved = 0;
|
| - |
|
762 |
int points = 0;
|
| - |
|
763 |
List<String> summary = new ArrayList<>();
|
| - |
|
764 |
if (body.answers != null) {
|
| - |
|
765 |
for (ChecklistAnswer a : body.answers) {
|
| - |
|
766 |
LeadCallQuestion q = active.get(a.questionId);
|
| - |
|
767 |
// Ignore anything that is not a live question rather than storing an answer to
|
| - |
|
768 |
// something the screen never asked.
|
| - |
|
769 |
if (q == null) {
|
| - |
|
770 |
continue;
|
| - |
|
771 |
}
|
| - |
|
772 |
String value = trimToNull(a.answer);
|
| - |
|
773 |
if (value == null) {
|
| - |
|
774 |
continue;
|
| - |
|
775 |
}
|
| - |
|
776 |
if (value.length() > 1000) {
|
| - |
|
777 |
value = value.substring(0, 1000);
|
| - |
|
778 |
}
|
| - |
|
779 |
LeadCallAnswer row = new LeadCallAnswer();
|
| - |
|
780 |
row.setLeadId(body.leadId);
|
| - |
|
781 |
row.setLeadCallId(body.leadCallId);
|
| - |
|
782 |
row.setQuestionId(q.getId());
|
| - |
|
783 |
row.setAuthId(me.getId());
|
| - |
|
784 |
row.setAnswer(value);
|
| - |
|
785 |
// Snapshot the points as priced today, so re-pricing the question later does not
|
| - |
|
786 |
// retroactively change what this lead scored.
|
| - |
|
787 |
row.setPointsAwarded(q.getPoints());
|
| - |
|
788 |
leadCallChecklistRepository.saveAnswer(row);
|
| - |
|
789 |
saved++;
|
| - |
|
790 |
points += q.getPoints();
|
| - |
|
791 |
summary.add(q.getPrompt() + ": " + value);
|
| - |
|
792 |
}
|
| - |
|
793 |
}
|
| - |
|
794 |
|
| - |
|
795 |
if (saved > 0) {
|
| - |
|
796 |
// The trail is where the lead's story lives; a checklist that only existed in its own
|
| - |
|
797 |
// table would be invisible to anyone reading the record.
|
| - |
|
798 |
trail(body.leadId, me.getId(), latin1Safe("Checklist recorded (" + saved + " answer"
|
| - |
|
799 |
+ (saved == 1 ? "" : "s") + ", " + points + " pts) - " + String.join(" \u00b7 ", summary)));
|
| - |
|
800 |
}
|
| - |
|
801 |
|
| - |
|
802 |
Map<String, Object> res = new LinkedHashMap<>();
|
| - |
|
803 |
res.put("ok", true);
|
| - |
|
804 |
res.put("saved", saved);
|
| - |
|
805 |
res.put("points", points);
|
| - |
|
806 |
LOGGER.info("Checklist saved for lead {} ({} answers) by auth {}", body.leadId, saved, me.getId());
|
| - |
|
807 |
return responseSender.ok(res);
|
| - |
|
808 |
}
|
| - |
|
809 |
|
| - |
|
810 |
// ---- Assignment -------------------------------------------------------------------------
|
| - |
|
811 |
|
| - |
|
812 |
/** Sales category in {@code cs.position} — the same one auto-assignment resolves owners from. */
|
| - |
|
813 |
private static final int SALES_CATEGORY = com.spice.profitmandi.common.model.ProfitMandiConstants.TICKET_CATEGORY_SALES;
|
| - |
|
814 |
|
| - |
|
815 |
/** Tiers a lead can be assigned to. L6+ exist but are not lead owners. */
|
| - |
|
816 |
private static final List<EscalationType> SALES_TIERS = Arrays.asList(
|
| - |
|
817 |
EscalationType.L1, EscalationType.L2, EscalationType.L3, EscalationType.L4, EscalationType.L5);
|
| - |
|
818 |
|
| - |
|
819 |
/**
|
| - |
|
820 |
* Sales people a lead can be assigned to, grouped by escalation tier (L1–L5).
|
| - |
|
821 |
*
|
| - |
|
822 |
* <p>Read from {@code cs.position}, the same source auto-assignment resolves owners from, so the
|
| - |
|
823 |
* manual list and the engine can never disagree about who is a sales owner.
|
| - |
|
824 |
*
|
| - |
|
825 |
* <p>{@code regionId} does not filter the list — it marks who covers that region, so an agent can
|
| - |
|
826 |
* see the right owner first without being prevented from picking someone else. A lead in a
|
| - |
|
827 |
* region with no active owner still has to go to somebody.
|
| - |
|
828 |
*/
|
| - |
|
829 |
@RequestMapping(value = "/lms/salespeople", method = RequestMethod.GET)
|
| - |
|
830 |
@ResponseBody
|
| - |
|
831 |
public ResponseEntity<?> salespeople(@RequestParam(name = "regionId", required = false) Integer regionId) {
|
| - |
|
832 |
List<Position> positions = positionRepository.selectPositionByCategoryId(SALES_CATEGORY);
|
| - |
|
833 |
if (positions == null) {
|
| - |
|
834 |
positions = new ArrayList<>();
|
| - |
|
835 |
}
|
| - |
|
836 |
|
| - |
|
837 |
Set<Integer> ids = new HashSet<>();
|
| - |
|
838 |
for (Position p : positions) {
|
| - |
|
839 |
if (SALES_TIERS.contains(p.getEscalationType())) {
|
| - |
|
840 |
ids.add(p.getAuthUserId());
|
| - |
|
841 |
}
|
| - |
|
842 |
}
|
| - |
|
843 |
Map<Integer, AuthUser> users = new HashMap<>();
|
| - |
|
844 |
if (!ids.isEmpty()) {
|
| - |
|
845 |
for (AuthUser u : authRepository.selectByIds(new ArrayList<>(ids))) {
|
| - |
|
846 |
users.put(u.getId(), u);
|
| - |
|
847 |
}
|
| - |
|
848 |
}
|
| - |
|
849 |
|
| - |
|
850 |
// One row per person per tier: somebody can hold an L4 in one region and an L5 in another,
|
| - |
|
851 |
// and collapsing that would hide which hat they are being picked under.
|
| - |
|
852 |
Map<String, Map<String, Object>> byKey = new LinkedHashMap<>();
|
| - |
|
853 |
for (Position p : positions) {
|
| - |
|
854 |
if (!SALES_TIERS.contains(p.getEscalationType())) {
|
| - |
|
855 |
continue;
|
| - |
|
856 |
}
|
| - |
|
857 |
AuthUser u = users.get(p.getAuthUserId());
|
| - |
|
858 |
if (u == null || !Boolean.TRUE.equals(u.isActive())) {
|
| - |
|
859 |
continue; // never offer a departed colleague as an owner
|
| - |
|
860 |
}
|
| - |
|
861 |
String key = p.getAuthUserId() + ":" + p.getEscalationType().name();
|
| - |
|
862 |
Map<String, Object> m = byKey.get(key);
|
| - |
|
863 |
if (m == null) {
|
| - |
|
864 |
m = new LinkedHashMap<>();
|
| - |
|
865 |
m.put("authId", u.getId());
|
| - |
|
866 |
m.put("name", u.getFullName());
|
| - |
|
867 |
m.put("email", u.getEmailId());
|
| - |
|
868 |
m.put("tier", p.getEscalationType().name());
|
| - |
|
869 |
m.put("regions", new ArrayList<String>());
|
| - |
|
870 |
m.put("coversRegion", false);
|
| - |
|
871 |
byKey.put(key, m);
|
| - |
|
872 |
}
|
| - |
|
873 |
Region r = p.getRegionId() > 0 ? regionRepository.selectById(p.getRegionId()) : null;
|
| - |
|
874 |
if (r != null) {
|
| - |
|
875 |
@SuppressWarnings("unchecked")
|
| - |
|
876 |
List<String> regions = (List<String>) m.get("regions");
|
| - |
|
877 |
String code = r.getRegionCode() != null ? r.getRegionCode() : r.getName();
|
| - |
|
878 |
if (code != null && !regions.contains(code)) {
|
| - |
|
879 |
regions.add(code);
|
| - |
|
880 |
}
|
| - |
|
881 |
if (regionId != null && regionId > 0 && r.getId() == regionId) {
|
| - |
|
882 |
m.put("coversRegion", true);
|
| - |
|
883 |
}
|
| - |
|
884 |
}
|
| - |
|
885 |
}
|
| - |
|
886 |
|
| - |
|
887 |
Map<String, Object> res = new LinkedHashMap<>();
|
| - |
|
888 |
res.put("people", new ArrayList<>(byKey.values()));
|
| - |
|
889 |
List<String> tiers = new ArrayList<>();
|
| - |
|
890 |
for (EscalationType t : SALES_TIERS) {
|
| - |
|
891 |
tiers.add(t.name());
|
| - |
|
892 |
}
|
| - |
|
893 |
res.put("tiers", tiers);
|
| - |
|
894 |
return responseSender.ok(res);
|
| - |
|
895 |
}
|
| - |
|
896 |
|
| - |
|
897 |
/**
|
| - |
|
898 |
* Assign a lead to a sales person.
|
| - |
|
899 |
*
|
| - |
|
900 |
* <p>Sets the working owner ({@code assign_to}) only. The auto-resolved {@code owner_bm_id} is
|
| - |
|
901 |
* left alone: it records which BM the region routed to, and overwriting it on a manual hand-off
|
| - |
|
902 |
* would erase the routing decision the SLA and the HOLD queue are measured against.
|
| - |
|
903 |
*/
|
| - |
|
904 |
@RequestMapping(value = "/lms/assignLead", method = RequestMethod.POST)
|
| - |
|
905 |
@ResponseBody
|
| - |
|
906 |
public ResponseEntity<?> assignLead(HttpServletRequest request,
|
| - |
|
907 |
@RequestParam(name = "leadId") int leadId,
|
| - |
|
908 |
@RequestParam(name = "authId") int authId) {
|
| - |
|
909 |
Lead lead = leadRepository.selectById(leadId);
|
| - |
|
910 |
if (lead == null) {
|
| - |
|
911 |
return responseSender.notFound("Lead not found");
|
| - |
|
912 |
}
|
| - |
|
913 |
AuthUser me = currentUser(request);
|
| - |
|
914 |
if (me == null) {
|
| - |
|
915 |
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
| - |
|
916 |
}
|
| - |
|
917 |
AuthUser owner = authRepository.selectById(authId);
|
| - |
|
918 |
if (owner == null || !Boolean.TRUE.equals(owner.isActive())) {
|
| - |
|
919 |
return responseSender.badRequest("That person is not an active user");
|
| - |
|
920 |
}
|
| - |
|
921 |
|
| - |
|
922 |
// Must hold a sales position — otherwise any auth id could be made a lead owner, and the
|
| - |
|
923 |
// pipeline would show owners the assignment engine has never heard of.
|
| - |
|
924 |
EscalationType tier = null;
|
| - |
|
925 |
List<Position> held = positionRepository.selectPositionByAuthId(authId);
|
| - |
|
926 |
if (held != null) {
|
| - |
|
927 |
for (Position p : held) {
|
| - |
|
928 |
if (p.getCategoryId() == SALES_CATEGORY && SALES_TIERS.contains(p.getEscalationType())) {
|
| - |
|
929 |
tier = p.getEscalationType();
|
| - |
|
930 |
break;
|
| - |
|
931 |
}
|
| - |
|
932 |
}
|
| - |
|
933 |
}
|
| - |
|
934 |
if (tier == null) {
|
| - |
|
935 |
return responseSender.badRequest(owner.getFullName() + " does not hold a sales position (L1-L5)");
|
| - |
|
936 |
}
|
| - |
|
937 |
|
| - |
|
938 |
AuthUser previous = lead.getAssignTo() > 0 ? authRepository.selectById(lead.getAssignTo()) : null;
|
| - |
|
939 |
if (previous != null && previous.getId() == authId) {
|
| - |
|
940 |
return responseSender.badRequest("Already assigned to " + owner.getFullName());
|
| - |
|
941 |
}
|
| - |
|
942 |
|
| - |
|
943 |
lead.setAssignTo(authId);
|
| - |
|
944 |
// A lead sitting in HOLD because its region had no owner now has one.
|
| - |
|
945 |
if ("HOLD".equalsIgnoreCase(lead.getAssignmentStatus())) {
|
| - |
|
946 |
lead.setAssignmentStatus("ASSIGNED");
|
| - |
|
947 |
}
|
| - |
|
948 |
lead.setUpdatedTimestamp(LocalDateTime.now());
|
| - |
|
949 |
leadRepository.persist(lead);
|
| - |
|
950 |
|
| - |
|
951 |
trail(leadId, me.getId(), latin1Safe("Assigned to " + owner.getFullName() + " (" + tier.name() + ")"
|
| - |
|
952 |
+ (previous != null ? " from " + previous.getFullName() : "")));
|
| - |
|
953 |
|
| - |
|
954 |
LOGGER.info("Lead {} assigned to auth {} ({}) by auth {}", leadId, authId, tier, me.getId());
|
| - |
|
955 |
Map<String, Object> res = new LinkedHashMap<>();
|
| - |
|
956 |
res.put("ok", true);
|
| - |
|
957 |
res.put("leadId", leadId);
|
| - |
|
958 |
res.put("owner", owner.getFullName());
|
| - |
|
959 |
res.put("tier", tier.name());
|
| - |
|
960 |
return responseSender.ok(res);
|
| - |
|
961 |
}
|
| - |
|
962 |
|
| - |
|
963 |
// ---- Checklist question management (CRUD) -----------------------------------------------
|
| - |
|
964 |
|
| - |
|
965 |
/** Every question, active and inactive, for the management screen. */
|
| - |
|
966 |
@RequestMapping(value = "/lms/checklist/questions", method = RequestMethod.GET)
|
| - |
|
967 |
@ResponseBody
|
| - |
|
968 |
public ResponseEntity<?> listQuestions() {
|
| - |
|
969 |
List<Map<String, Object>> out = new ArrayList<>();
|
| - |
|
970 |
for (LeadCallQuestion q : leadCallChecklistRepository.selectAllQuestions()) {
|
| - |
|
971 |
out.add(questionJson(q));
|
| - |
|
972 |
}
|
| - |
|
973 |
Map<String, Object> res = new LinkedHashMap<>();
|
| - |
|
974 |
res.put("questions", out);
|
| - |
|
975 |
// The screen builds its stage picker from this rather than hardcoding the ladder, so a new
|
| - |
|
976 |
// LeadStage shows up without a front-end change.
|
| - |
|
977 |
List<String> stages = new ArrayList<>();
|
| - |
|
978 |
for (LeadStage st : LeadStage.HAPPY_PATH) {
|
| - |
|
979 |
stages.add(st.name());
|
| - |
|
980 |
}
|
| - |
|
981 |
res.put("stages", stages);
|
| - |
|
982 |
res.put("answerTypes", Arrays.asList("TEXT", "NUMBER", "YESNO", "CHOICE"));
|
| - |
|
983 |
return responseSender.ok(res);
|
| - |
|
984 |
}
|
| - |
|
985 |
|
| - |
|
986 |
/** Create or update a question. An id of 0 creates. */
|
| - |
|
987 |
@RequestMapping(value = "/lms/checklist/questions/save", method = RequestMethod.POST,
|
| - |
|
988 |
consumes = MediaType.APPLICATION_JSON_VALUE)
|
| - |
|
989 |
@ResponseBody
|
| - |
|
990 |
public ResponseEntity<?> saveQuestion(HttpServletRequest request, @RequestBody QuestionRequest body) {
|
| - |
|
991 |
if (body == null) {
|
| - |
|
992 |
return responseSender.badRequest("Nothing to save");
|
| - |
|
993 |
}
|
| - |
|
994 |
AuthUser me = currentUser(request);
|
| - |
|
995 |
if (me == null) {
|
| - |
|
996 |
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
| - |
|
997 |
}
|
| - |
|
998 |
String prompt = trimToNull(body.prompt);
|
| - |
|
999 |
if (prompt == null) {
|
| - |
|
1000 |
return responseSender.badRequest("The question text is required");
|
| - |
|
1001 |
}
|
| - |
|
1002 |
if (prompt.length() > 500) {
|
| - |
|
1003 |
return responseSender.badRequest("Keep the question under 500 characters");
|
| - |
|
1004 |
}
|
| - |
|
1005 |
String type = trimToNull(body.answerType) == null ? "TEXT" : body.answerType.trim().toUpperCase();
|
| - |
|
1006 |
if (!Arrays.asList("TEXT", "NUMBER", "YESNO", "CHOICE").contains(type)) {
|
| - |
|
1007 |
return responseSender.badRequest("Unknown answer type: " + type);
|
| - |
|
1008 |
}
|
| - |
|
1009 |
String choices = trimToNull(body.choices);
|
| - |
|
1010 |
if ("CHOICE".equals(type) && choices == null) {
|
| - |
|
1011 |
return responseSender.badRequest("A choice question needs at least one option");
|
| - |
|
1012 |
}
|
| - |
|
1013 |
if (body.points != null && body.points < 0) {
|
| - |
|
1014 |
return responseSender.badRequest("Points cannot be negative");
|
| - |
|
1015 |
}
|
| - |
|
1016 |
// Reject unknown stage names rather than storing a filter that silently matches nothing.
|
| - |
|
1017 |
String stages = trimToNull(body.stages);
|
| - |
|
1018 |
if (stages != null) {
|
| - |
|
1019 |
for (String st : stages.split("\\|")) {
|
| - |
|
1020 |
if (st.trim().isEmpty()) {
|
| - |
|
1021 |
continue;
|
| - |
|
1022 |
}
|
| - |
|
1023 |
try {
|
| - |
|
1024 |
LeadStage.valueOf(st.trim().toUpperCase());
|
| - |
|
1025 |
} catch (IllegalArgumentException e) {
|
| - |
|
1026 |
return responseSender.badRequest("Unknown stage: " + st.trim());
|
| - |
|
1027 |
}
|
| - |
|
1028 |
}
|
| - |
|
1029 |
}
|
| - |
|
1030 |
|
| - |
|
1031 |
LeadCallQuestion q;
|
| - |
|
1032 |
boolean creating = body.id <= 0;
|
| - |
|
1033 |
if (creating) {
|
| - |
|
1034 |
q = new LeadCallQuestion();
|
| - |
|
1035 |
} else {
|
| - |
|
1036 |
q = leadCallChecklistRepository.selectQuestionById(body.id);
|
| - |
|
1037 |
if (q == null) {
|
| - |
|
1038 |
return responseSender.notFound("Question not found");
|
| - |
|
1039 |
}
|
| - |
|
1040 |
}
|
| - |
|
1041 |
q.setPrompt(latin1Safe(prompt));
|
| - |
|
1042 |
q.setAnswerType(type);
|
| - |
|
1043 |
q.setChoices("CHOICE".equals(type) ? latin1Safe(choices) : null);
|
| - |
|
1044 |
q.setRequired(Boolean.TRUE.equals(body.required));
|
| - |
|
1045 |
q.setActive(body.active == null || Boolean.TRUE.equals(body.active));
|
| - |
|
1046 |
q.setPoints(body.points == null ? 0 : body.points);
|
| - |
|
1047 |
q.setStages(stages == null ? null : stages.toUpperCase());
|
| - |
|
1048 |
q.setSortOrder(body.sortOrder == null ? 0 : body.sortOrder);
|
| - |
|
1049 |
leadCallChecklistRepository.saveQuestion(q);
|
| - |
|
1050 |
|
| - |
|
1051 |
LOGGER.info("Checklist question {} by auth {}: {}", creating ? "created" : "updated", me.getId(), q.getPrompt());
|
| - |
|
1052 |
Map<String, Object> res = new LinkedHashMap<>();
|
| - |
|
1053 |
res.put("ok", true);
|
| - |
|
1054 |
res.put("id", q.getId());
|
| - |
|
1055 |
res.put("created", creating);
|
| - |
|
1056 |
return responseSender.ok(res);
|
| - |
|
1057 |
}
|
| - |
|
1058 |
|
| - |
|
1059 |
/**
|
| - |
|
1060 |
* Delete a question — but only one nobody has answered.
|
| - |
|
1061 |
*
|
| - |
|
1062 |
* <p>A question with answers is retired by clearing {@code active}, never deleted: the answers
|
| - |
|
1063 |
* reference it, and removing it would leave a lead's recorded history pointing at nothing.
|
| - |
|
1064 |
*/
|
| - |
|
1065 |
@RequestMapping(value = "/lms/checklist/questions/delete", method = RequestMethod.POST)
|
| - |
|
1066 |
@ResponseBody
|
| - |
|
1067 |
public ResponseEntity<?> deleteQuestion(HttpServletRequest request,
|
| - |
|
1068 |
@RequestParam(name = "id") int id) {
|
| - |
|
1069 |
AuthUser me = currentUser(request);
|
| - |
|
1070 |
if (me == null) {
|
| - |
|
1071 |
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
| - |
|
1072 |
}
|
| - |
|
1073 |
LeadCallQuestion q = leadCallChecklistRepository.selectQuestionById(id);
|
| - |
|
1074 |
if (q == null) {
|
| - |
|
1075 |
return responseSender.notFound("Question not found");
|
| - |
|
1076 |
}
|
| - |
|
1077 |
long answers = leadCallChecklistRepository.countAnswersForQuestion(id);
|
| - |
|
1078 |
if (answers > 0) {
|
| - |
|
1079 |
return responseSender.badRequest("This question has " + answers + " recorded answer"
|
| - |
|
1080 |
+ (answers == 1 ? "" : "s") + " - switch it off instead of deleting it, so the "
|
| - |
|
1081 |
+ "answers keep their question");
|
| - |
|
1082 |
}
|
| - |
|
1083 |
leadCallChecklistRepository.deleteQuestion(q);
|
| - |
|
1084 |
LOGGER.info("Checklist question {} deleted by auth {}", id, me.getId());
|
| - |
|
1085 |
Map<String, Object> res = new LinkedHashMap<>();
|
| - |
|
1086 |
res.put("ok", true);
|
| - |
|
1087 |
return responseSender.ok(res);
|
| - |
|
1088 |
}
|
| - |
|
1089 |
|
| - |
|
1090 |
private Map<String, Object> questionJson(LeadCallQuestion q) {
|
| - |
|
1091 |
Map<String, Object> m = new LinkedHashMap<>();
|
| - |
|
1092 |
m.put("id", q.getId());
|
| - |
|
1093 |
m.put("sortOrder", q.getSortOrder());
|
| - |
|
1094 |
m.put("prompt", q.getPrompt());
|
| - |
|
1095 |
m.put("answerType", q.getAnswerType());
|
| - |
|
1096 |
m.put("choices", q.getChoices());
|
| - |
|
1097 |
m.put("required", q.isRequired());
|
| - |
|
1098 |
m.put("active", q.isActive());
|
| - |
|
1099 |
m.put("points", q.getPoints());
|
| - |
|
1100 |
m.put("stages", q.getStages());
|
| - |
|
1101 |
m.put("answerCount", leadCallChecklistRepository.countAnswersForQuestion(q.getId()));
|
| - |
|
1102 |
return m;
|
| - |
|
1103 |
}
|
| - |
|
1104 |
|
| - |
|
1105 |
/** Payload for {@link #saveQuestion}. */
|
| - |
|
1106 |
public static class QuestionRequest {
|
| - |
|
1107 |
public int id;
|
| - |
|
1108 |
public String prompt;
|
| - |
|
1109 |
public String answerType;
|
| - |
|
1110 |
public String choices;
|
| - |
|
1111 |
public Boolean required;
|
| - |
|
1112 |
public Boolean active;
|
| - |
|
1113 |
public Integer points;
|
| - |
|
1114 |
public String stages;
|
| - |
|
1115 |
public Integer sortOrder;
|
| - |
|
1116 |
}
|
| - |
|
1117 |
|
| - |
|
1118 |
/** Payload for {@link #saveChecklist}. */
|
| - |
|
1119 |
public static class ChecklistRequest {
|
| - |
|
1120 |
public int leadId;
|
| - |
|
1121 |
public Integer leadCallId;
|
| - |
|
1122 |
public List<ChecklistAnswer> answers;
|
| - |
|
1123 |
}
|
| - |
|
1124 |
|
| - |
|
1125 |
/** One answer in that payload. */
|
| - |
|
1126 |
public static class ChecklistAnswer {
|
| - |
|
1127 |
public int questionId;
|
| - |
|
1128 |
public String answer;
|
| - |
|
1129 |
}
|
| - |
|
1130 |
|
| - |
|
1131 |
/**
|
| 657 |
* Region → owning BM/RSM, resolved live from {@code cs.position}. Drives the Create-Lead
|
1132 |
* Region → owning BM/RSM, resolved live from {@code cs.position}. Drives the Create-Lead
|
| 658 |
* assignment preview and the dashboard's routing table; {@code HOLD} means the region has no
|
1133 |
* assignment preview and the dashboard's routing table; {@code HOLD} means the region has no
|
| 659 |
* active owner and a lead created against it would wait in the HOLD queue.
|
1134 |
* active owner and a lead created against it would wait in the HOLD queue.
|
| 660 |
*/
|
1135 |
*/
|
| 661 |
@RequestMapping(value = "/lms/resolve-region", method = RequestMethod.GET)
|
1136 |
@RequestMapping(value = "/lms/resolve-region", method = RequestMethod.GET)
|