Subversion Repositories SmartDukaan

Rev

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

Rev 37743 Rev 37749
Line 57... Line 57...
57
 
57
 
58
    /** Most-recent weeks shown in the SLA owner heat-map. */
58
    /** Most-recent weeks shown in the SLA owner heat-map. */
59
    private static final int HEATMAP_WEEKS = 6;
59
    private static final int HEATMAP_WEEKS = 6;
60
 
60
 
61
    /** Cap on rows returned to a drill-down drawer. Public so the drawer can say it is showing a slice. */
61
    /** Cap on rows returned to a drill-down drawer. Public so the drawer can say it is showing a slice. */
-
 
62
    /** Default drawer page size (drill) and the hard cap on search results. */
62
    public static final int DRILL_LIMIT = 100;
63
    public static final int DRILL_LIMIT = 100;
63
 
64
 
64
    @Autowired
65
    @Autowired
65
    private LeadRepository leadRepository;
66
    private LeadRepository leadRepository;
66
 
67
 
Line 91... Line 92...
91
    @Autowired
92
    @Autowired
92
    private LeadLiveLocationRepository leadLiveLocationRepository;
93
    private LeadLiveLocationRepository leadLiveLocationRepository;
93
 
94
 
94
    // ---- Command Center + shared summary ----------------------------------------------------
95
    // ---- Command Center + shared summary ----------------------------------------------------
95
 
96
 
-
 
97
    /**
96
    /** KPIs + lifecycle funnel + attention queue for the given region / created-date window. */
98
     * KPIs + lifecycle funnel + attention queue for the given region / created-date window. Stages come
-
 
99
     * from LeadRepository's evidence-aware expression: BEAT_PLANNED / VISITED / ONBOARDED are read off
-
 
100
     * lead_route, location_tracking and LOI + stock payment, never off the hand-set stage column.
-
 
101
     */
97
    public Map<String, Object> summary(Integer regionId, LocalDateTime from, LocalDateTime to) {
102
    public Map<String, Object> summary(Integer regionId, LocalDateTime from, LocalDateTime to) {
98
        Map<String, Long> stage = toCountMap(leadRepository.countByDerivedStage(regionId, from, to));
103
        Map<String, Long> stage = toCountMap(leadRepository.countByDerivedStage(regionId, from, to));
99
        long total = 0;
104
        long total = 0;
100
        for (Long v : stage.values()) {
105
        for (Long v : stage.values()) {
101
            total += v;
106
            total += v;
Line 110... Line 115...
110
            }
115
            }
111
            reached[i] = sum;
116
            reached[i] = sum;
112
        }
117
        }
113
 
118
 
114
        LocalDateTime now = LocalDateTime.now();
119
        LocalDateTime now = LocalDateTime.now();
115
        Object[] att = leadRepository.attentionCounts(regionId, now, now.plusHours(1));
120
        Object[] att = leadRepository.attentionCounts(regionId, now, now.plusHours(1), from, to);
116
        long breachedNoContact = num(att, 0);
121
        long breachedNoContact = num(att, 0);
117
        long hold = num(att, 1);
122
        long hold = num(att, 1);
118
        long slaUnder1h = num(att, 2);
123
        long slaUnder1h = num(att, 2);
119
        long missingDisp = num(att, 3);
124
        long missingDisp = num(att, 3);
120
        long qualifiedNoBeat = num(att, 4);
125
        long qualifiedNoBeat = num(att, 4);
Line 136... Line 141...
136
        kpis.add(kpi("awaiting", "Awaiting 1st contact", assigned, "SLA 3–5 hrs running", "amber"));
141
        kpis.add(kpi("awaiting", "Awaiting 1st contact", assigned, "SLA 3–5 hrs running", "amber"));
137
        kpis.add(kpi("sla", "SLA at risk / breached", breachedNoContact + slaUnder1h,
142
        kpis.add(kpi("sla", "SLA at risk / breached", breachedNoContact + slaUnder1h,
138
                breachedNoContact + " breached · " + slaUnder1h + " < 1 hr left", "red"));
143
                breachedNoContact + " breached · " + slaUnder1h + " < 1 hr left", "red"));
139
        kpis.add(kpi("contacted", "Contacted", contacted, "dispositioned", "navy"));
144
        kpis.add(kpi("contacted", "Contacted", contacted, "dispositioned", "navy"));
140
        kpis.add(kpi("qualified", "Qualified", qualified, "value validated", "navy"));
145
        kpis.add(kpi("qualified", "Qualified", qualified, "value validated", "navy"));
141
        kpis.add(kpi("onboarded", "Onboarded", onboarded, "of " + total + " in scope", "good"));
146
        kpis.add(kpi("onboarded", "Onboarded", onboarded, "LOI + stock payment · of " + total + " in scope", "good"));
142
 
147
 
143
        // Lifecycle funnel.
148
        // Lifecycle funnel.
144
        List<Map<String, Object>> funnel = new ArrayList<>();
149
        List<Map<String, Object>> funnel = new ArrayList<>();
145
        long topN = total > 0 ? total : 1;
150
        long topN = total > 0 ? total : 1;
146
        // Top of funnel = every lead in scope.
151
        // Top of funnel = every lead in scope.
Line 205... Line 210...
205
            for (Long v : stage.values()) {
210
            for (Long v : stage.values()) {
206
                total += v;
211
                total += v;
207
            }
212
            }
208
            long contacted = reachedAtLeast(stage, LeadStage.CONTACTED);
213
            long contacted = reachedAtLeast(stage, LeadStage.CONTACTED);
209
            long qualified = reachedAtLeast(stage, LeadStage.QUALIFIED);
214
            long qualified = reachedAtLeast(stage, LeadStage.QUALIFIED);
-
 
215
            long beatPlanned = reachedAtLeast(stage, LeadStage.BEAT_PLANNED);
210
            long visited = reachedAtLeast(stage, LeadStage.VISITED);
216
            long visited = reachedAtLeast(stage, LeadStage.VISITED);
211
            long onboarded = reachedAtLeast(stage, LeadStage.ONBOARDED);
217
            long onboarded = reachedAtLeast(stage, LeadStage.ONBOARDED);
212
 
218
 
213
            Map<String, Object> r = new LinkedHashMap<>();
219
            Map<String, Object> r = new LinkedHashMap<>();
214
            r.put("regionCode", code);
220
            r.put("regionCode", code);
215
            r.put("regionName", nameByCode.getOrDefault(code, "UNMAPPED".equals(code) ? "Unmapped" : code));
221
            r.put("regionName", nameByCode.getOrDefault(code, "UNMAPPED".equals(code) ? "Unmapped" : code));
216
            r.put("newCount", total);
222
            r.put("newCount", total);
217
            r.put("contacted", contacted);
223
            r.put("contacted", contacted);
218
            r.put("qualified", qualified);
224
            r.put("qualified", qualified);
-
 
225
            r.put("beatPlanned", beatPlanned);
219
            r.put("visited", visited);
226
            r.put("visited", visited);
220
            r.put("onboarded", onboarded);
227
            r.put("onboarded", onboarded);
221
            r.put("convPct", pct(onboarded, total));
228
            r.put("convPct", pct(onboarded, total));
222
            r.put("slaPct", slaByRegion.getOrDefault(code, 0));
229
            r.put("slaPct", slaByRegion.getOrDefault(code, 0));
223
            rows.add(r);
230
            rows.add(r);
Line 364... Line 371...
364
     */
371
     */
365
    public Map<String, Object> auditHygiene(LocalDateTime from, LocalDateTime to) {
372
    public Map<String, Object> auditHygiene(LocalDateTime from, LocalDateTime to) {
366
        LocalDateTime now = LocalDateTime.now();
373
        LocalDateTime now = LocalDateTime.now();
367
        Map<String, Object> m = new LinkedHashMap<>();
374
        Map<String, Object> m = new LinkedHashMap<>();
368
 
375
 
369
        // Same source the Command Center's attention tiles use, so the two cannot disagree.
376
        // Same source and same created-date window as the Command Center's attention tiles, so the
-
 
377
        // two cannot disagree.
370
        Object[] att = leadRepository.attentionCounts(null, now, now.plusHours(1));
378
        Object[] att = leadRepository.attentionCounts(null, now, now.plusHours(1), from, to);
371
        m.put("slaBreached", num(att, 0));
379
        m.put("slaBreached", num(att, 0));
372
        m.put("hold", num(att, 1));
380
        m.put("hold", num(att, 1));
373
        m.put("missingDisposition", num(att, 3));
381
        m.put("missingDisposition", num(att, 3));
374
 
382
 
375
        m.put("dndRegister", leadDndRepository.count());
383
        m.put("dndRegister", leadDndRepository.count());
Line 613... Line 621...
613
        return v instanceof LocalDateTime ? (LocalDateTime) v : null;
621
        return v instanceof LocalDateTime ? (LocalDateTime) v : null;
614
    }
622
    }
615
 
623
 
616
    // ---- Drill-down -------------------------------------------------------------------------
624
    // ---- Drill-down -------------------------------------------------------------------------
617
 
625
 
-
 
626
    /**
618
    /** Lead rows for a drill bucket, shaped for the drawer table. */
627
     * One page of lead rows for a drill bucket, shaped for the drawer table, plus the true total so
-
 
628
     * the drawer can page through every lead behind a bar instead of stopping at the first 100.
-
 
629
     */
619
    public List<Map<String, Object>> drill(String bucketType, String bucketKey, Integer regionId,
630
    public Map<String, Object> drill(String bucketType, String bucketKey, Integer regionId,
620
                                            LocalDateTime from, LocalDateTime to) {
631
                                     LocalDateTime from, LocalDateTime to, int page, int pageSize) {
-
 
632
        int size = Math.min(Math.max(pageSize, 1), MAX_PAGE_SIZE);
-
 
633
        int p = Math.max(page, 1);
621
        String type = bucketType == null ? "" : bucketType.trim().toUpperCase();
634
        String type = bucketType == null ? "" : bucketType.trim().toUpperCase();
622
        String key = bucketKey;
635
        String key = bucketKey;
623
        // The funnel asks for a stage cumulatively -- "reached CONTACTED" means CONTACTED or anything
636
        // The funnel asks for a stage cumulatively -- "reached CONTACTED" means CONTACTED or anything
624
        // beyond it, which is exactly how the bar was counted. Expand it here, where the happy path
637
        // beyond it, which is exactly how the bar was counted. Expand it here, where the happy path
625
        // already lives, rather than teaching the repository the order of the lifecycle.
638
        // already lives, rather than teaching the repository the order of the lifecycle.
626
        if ("STAGE_REACHED".equals(type)) {
639
        if ("STAGE_REACHED".equals(type)) {
627
            type = "STAGE_IN";
640
            type = "STAGE_IN";
628
            key = stagesFrom(bucketKey);
641
            key = stagesFrom(bucketKey);
629
        }
642
        }
630
        LocalDateTime now = LocalDateTime.now();
643
        LocalDateTime now = LocalDateTime.now();
-
 
644
        long total = leadRepository.countLeadsForBucket(type, key, regionId, now, now.plusHours(1), from, to);
631
        List<Integer> ids = leadRepository.selectLeadIdsForBucket(type, key, regionId,
645
        List<Integer> ids = leadRepository.selectLeadIdsForBucket(type, key, regionId,
632
                now, now.plusHours(1), from, to, DRILL_LIMIT);
646
                now, now.plusHours(1), from, to, (p - 1) * size, size);
633
        List<Map<String, Object>> out = new ArrayList<>();
647
        List<Map<String, Object>> rows = new ArrayList<>();
-
 
648
        Map<String, Object> out = new LinkedHashMap<>();
-
 
649
        out.put("leads", rows);
-
 
650
        out.put("total", total);
-
 
651
        out.put("page", p);
-
 
652
        out.put("pageSize", size);
-
 
653
        out.put("pages", (int) Math.ceil(total / (double) size));
634
        if (ids == null || ids.isEmpty()) {
654
        if (ids == null || ids.isEmpty()) {
635
            return out;
655
            return out;
636
        }
656
        }
637
        // selectAllByIds returns id-desc; preserve the bucket's newest-first order.
657
        // selectAllByIds returns id-desc; preserve the bucket's newest-first order.
638
        Map<Integer, Lead> leadById = new HashMap<>();
658
        Map<Integer, Lead> leadById = new HashMap<>();
Line 644... Line 664...
644
            Lead l = leadById.get(id);
664
            Lead l = leadById.get(id);
645
            if (l != null) {
665
            if (l != null) {
646
                ordered.add(l);
666
                ordered.add(l);
647
            }
667
            }
648
        }
668
        }
649
        return toRows(ordered);
669
        rows.addAll(toRows(ordered));
-
 
670
        return out;
650
    }
671
    }
651
 
672
 
652
    /**
673
    /**
653
     * The given stage and every stage after it on the happy path, comma-separated -- the set the
674
     * The given stage and every stage after it on the happy path, comma-separated -- the set the
654
     * cumulative funnel bar counted. An unrecognised stage (a terminal one such as NOT_INTERESTED,
675
     * cumulative funnel bar counted. An unrecognised stage (a terminal one such as NOT_INTERESTED,
Line 734... Line 755...
734
        if (!ownerIds.isEmpty()) {
755
        if (!ownerIds.isEmpty()) {
735
            for (AuthUser u : authRepository.selectByIds(new ArrayList<>(ownerIds))) {
756
            for (AuthUser u : authRepository.selectByIds(new ArrayList<>(ownerIds))) {
736
                owners.put(u.getId(), u);
757
                owners.put(u.getId(), u);
737
            }
758
            }
738
        }
759
        }
-
 
760
        // Evidence-aware stage in one batch, so a row says the same thing as the bar it came from.
-
 
761
        List<Integer> leadIds = new ArrayList<>(leads.size());
-
 
762
        for (Lead l : leads) {
-
 
763
            leadIds.add(l.getId());
-
 
764
        }
-
 
765
        Map<Integer, String> stages = leadRepository.selectEffectiveStages(leadIds);
739
 
766
 
740
        for (Lead l : leads) {
767
        for (Lead l : leads) {
741
            LeadStage eff = l.getEffectiveStage();
768
            LeadStage eff = effectiveStage(l, stages);
742
            AuthUser owner = owners.get(l.getAssignTo());
769
            AuthUser owner = owners.get(l.getAssignTo());
743
            Map<String, Object> r = new LinkedHashMap<>();
770
            Map<String, Object> r = new LinkedHashMap<>();
744
            r.put("id", l.getId());
771
            r.put("id", l.getId());
745
            r.put("lmsCode", l.getLmsCode() != null ? l.getLmsCode() : ("Lead #" + l.getId()));
772
            r.put("lmsCode", l.getLmsCode() != null ? l.getLmsCode() : ("Lead #" + l.getId()));
746
            r.put("name", trimName(l.getFirstName(), l.getLastName()));
773
            r.put("name", trimName(l.getFirstName(), l.getLastName()));
Line 767... Line 794...
767
    public Map<String, Object> record(int leadId) {
794
    public Map<String, Object> record(int leadId) {
768
        Lead l = leadRepository.selectById(leadId);
795
        Lead l = leadRepository.selectById(leadId);
769
        if (l == null) {
796
        if (l == null) {
770
            return null;
797
            return null;
771
        }
798
        }
772
        LeadStage eff = l.getEffectiveStage();
799
        LeadStage eff = effectiveStage(l, leadRepository.selectEffectiveStages(java.util.Collections.singletonList(leadId)));
773
        int stageIndex = -1;
800
        int stageIndex = -1;
774
        for (int i = 0; i < FUNNEL.length; i++) {
801
        for (int i = 0; i < FUNNEL.length; i++) {
775
            if (FUNNEL[i] == eff) {
802
            if (FUNNEL[i] == eff) {
776
                stageIndex = i;
803
                stageIndex = i;
777
            }
804
            }
Line 995... Line 1022...
995
        String f = first == null ? "" : first.trim();
1022
        String f = first == null ? "" : first.trim();
996
        String l = last == null ? "" : last.trim();
1023
        String l = last == null ? "" : last.trim();
997
        return (f + " " + l).trim();
1024
        return (f + " " + l).trim();
998
    }
1025
    }
999
 
1026
 
-
 
1027
    /**
-
 
1028
     * The dashboard's stage for a lead: the evidence-aware SQL value when the batch has it, else the
-
 
1029
     * column-only fallback (a lead deleted between the two queries, or an unparseable value).
-
 
1030
     */
-
 
1031
    private LeadStage effectiveStage(Lead l, Map<Integer, String> stages) {
-
 
1032
        LeadStage fromSql = stages == null ? null : safeStage(stages.get(l.getId()));
-
 
1033
        return fromSql != null ? fromSql : l.getEffectiveStage();
-
 
1034
    }
-
 
1035
 
1000
    private LeadStage safeStage(String s) {
1036
    private LeadStage safeStage(String s) {
-
 
1037
        if (s == null) {
-
 
1038
            return null;
-
 
1039
        }
1001
        try {
1040
        try {
1002
            return LeadStage.valueOf(s);
1041
            return LeadStage.valueOf(s);
1003
        } catch (Exception e) {
1042
        } catch (Exception e) {
1004
            return null;
1043
            return null;
1005
        }
1044
        }