Subversion Repositories SmartDukaan

Rev

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

Rev 37692 Rev 37737
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 &rarr; owning BM/RSM, resolved live from {@code cs.position}. Drives the Create-Lead
1132
     * Region &rarr; 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)