Subversion Repositories SmartDukaan

Rev

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

Rev 37651 Rev 37692
Line 5... Line 5...
5
import com.spice.profitmandi.dao.entity.user.Lead;
5
import com.spice.profitmandi.dao.entity.user.Lead;
6
import com.spice.profitmandi.dao.enumuration.dtr.LeadStage;
6
import com.spice.profitmandi.dao.enumuration.dtr.LeadStage;
7
import com.spice.profitmandi.dao.entity.user.LeadActivity;
7
import com.spice.profitmandi.dao.entity.user.LeadActivity;
8
import com.spice.profitmandi.dao.repository.auth.AuthRepository;
8
import com.spice.profitmandi.dao.repository.auth.AuthRepository;
9
import com.spice.profitmandi.dao.repository.cs.RegionRepository;
9
import com.spice.profitmandi.dao.repository.cs.RegionRepository;
-
 
10
import com.spice.profitmandi.dao.entity.user.LeadDnd;
-
 
11
import com.spice.profitmandi.dao.entity.user.LeadLiveLocation;
10
import com.spice.profitmandi.dao.repository.dtr.LeadActivityRepository;
12
import com.spice.profitmandi.dao.repository.dtr.LeadActivityRepository;
-
 
13
import com.spice.profitmandi.dao.entity.user.LeadCall;
-
 
14
import com.spice.profitmandi.dao.repository.dtr.LeadCallRepository;
-
 
15
import com.spice.profitmandi.dao.repository.dtr.LeadDndRepository;
-
 
16
import com.spice.profitmandi.dao.repository.dtr.LeadLiveLocationRepository;
11
import com.spice.profitmandi.dao.repository.dtr.LeadRepository;
17
import com.spice.profitmandi.dao.repository.dtr.LeadRepository;
12
import org.apache.logging.log4j.LogManager;
18
import org.apache.logging.log4j.LogManager;
13
import org.apache.logging.log4j.Logger;
19
import org.apache.logging.log4j.Logger;
14
import org.springframework.beans.factory.annotation.Autowired;
20
import org.springframework.beans.factory.annotation.Autowired;
15
import org.springframework.stereotype.Service;
21
import org.springframework.stereotype.Service;
Line 38... Line 44...
38
    private static final Logger LOGGER = LogManager.getLogger(LmsDashboardService.class);
44
    private static final Logger LOGGER = LogManager.getLogger(LmsDashboardService.class);
39
 
45
 
40
    /** Ordered happy-path for cumulative "reached" funnel math. */
46
    /** Ordered happy-path for cumulative "reached" funnel math. */
41
    private static final LeadStage[] FUNNEL = LeadStage.HAPPY_PATH;
47
    private static final LeadStage[] FUNNEL = LeadStage.HAPPY_PATH;
42
 
48
 
-
 
49
    /** Hard ceiling on one page of the pipeline table — a caller asking for 10,000 rows gets 500. */
-
 
50
    private static final int MAX_PAGE_SIZE = 500;
-
 
51
 
43
    /** Leads not touched for this many days count as "ageing" (SOP §18.2). */
52
    /** Leads not touched for this many days count as "ageing" (SOP §18.2). */
44
    private static final int AGEING_DAYS = 7;
53
    private static final int AGEING_DAYS = 7;
45
 
54
 
46
    /** Most-recent weeks shown in the SLA owner heat-map. */
55
    /** Most-recent weeks shown in the SLA owner heat-map. */
47
    private static final int HEATMAP_WEEKS = 6;
56
    private static final int HEATMAP_WEEKS = 6;
Line 62... Line 71...
62
    private LmsAssignmentService lmsAssignmentService;
71
    private LmsAssignmentService lmsAssignmentService;
63
 
72
 
64
    @Autowired
73
    @Autowired
65
    private LeadActivityRepository leadActivityRepository;
74
    private LeadActivityRepository leadActivityRepository;
66
 
75
 
-
 
76
    @Autowired
-
 
77
    private LeadCallRepository leadCallRepository;
-
 
78
 
-
 
79
    @Autowired
-
 
80
    private LeadDndRepository leadDndRepository;
-
 
81
 
-
 
82
    @Autowired
-
 
83
    private LeadLiveLocationRepository leadLiveLocationRepository;
-
 
84
 
67
    // ---- Command Center + shared summary ----------------------------------------------------
85
    // ---- Command Center + shared summary ----------------------------------------------------
68
 
86
 
69
    /** KPIs + lifecycle funnel + attention queue for the given region / created-date window. */
87
    /** KPIs + lifecycle funnel + attention queue for the given region / created-date window. */
70
    public Map<String, Object> summary(Integer regionId, LocalDateTime from, LocalDateTime to) {
88
    public Map<String, Object> summary(Integer regionId, LocalDateTime from, LocalDateTime to) {
71
        Map<String, Long> stage = toCountMap(leadRepository.countByDerivedStage(regionId, from, to));
89
        Map<String, Long> stage = toCountMap(leadRepository.countByDerivedStage(regionId, from, to));
Line 302... Line 320...
302
        }
320
        }
303
        return toRows(ordered);
321
        return toRows(ordered);
304
    }
322
    }
305
 
323
 
306
    /**
324
    /**
-
 
325
     * The Lead-pipeline table: every matching lead, not a client-side slice of the first page.
-
 
326
     *
-
 
327
     * <p>Returns the page plus the true total, so the table can report "50 of 229" rather than
-
 
328
     * implying the twelve rows it drew are all there are. A non-empty {@code q} searches across all
-
 
329
     * leads and ignores the date window — see {@code LeadRepository.selectPipeline}.
-
 
330
     */
-
 
331
    public Map<String, Object> pipeline(String q, Integer regionId, LocalDateTime from, LocalDateTime to,
-
 
332
                                        String sort, String dir, int page, int pageSize) {
-
 
333
        int size = Math.min(Math.max(pageSize, 1), MAX_PAGE_SIZE);
-
 
334
        int p = Math.max(page, 1);
-
 
335
        boolean searching = q != null && !q.trim().isEmpty();
-
 
336
 
-
 
337
        long total = leadRepository.countPipeline(q, regionId, from, to);
-
 
338
        List<Lead> leads = leadRepository.selectPipeline(q, regionId, from, to, sort, dir, (p - 1) * size, size);
-
 
339
 
-
 
340
        Map<String, Object> out = new LinkedHashMap<>();
-
 
341
        out.put("rows", toRows(leads));
-
 
342
        out.put("total", total);
-
 
343
        out.put("page", p);
-
 
344
        out.put("pageSize", size);
-
 
345
        out.put("pages", (int) Math.ceil(total / (double) size));
-
 
346
        out.put("sort", sort);
-
 
347
        out.put("dir", dir);
-
 
348
        // The caller renders a different caption when the date filter is not in play, so it has to
-
 
349
        // be told which mode the server actually used rather than inferring it.
-
 
350
        out.put("searching", searching);
-
 
351
        return out;
-
 
352
    }
-
 
353
 
-
 
354
    /**
307
     * Free-text lookup for the dashboard search box: LMS id / retailer / business / mobile / city.
355
     * Free-text lookup for the dashboard search box: LMS id / retailer / business / mobile / city.
308
     * Same row shape as {@link #drill}, so results render in the same drawer table.
356
     * Same row shape as {@link #drill}, so results render in the same drawer table.
309
     */
357
     */
310
    public List<Map<String, Object>> search(String term) {
358
    public List<Map<String, Object>> search(String term) {
311
        if (term == null || term.trim().isEmpty()) {
359
        if (term == null || term.trim().isEmpty()) {
Line 346... Line 394...
346
            r.put("stage", eff != null ? eff.name() : null);
394
            r.put("stage", eff != null ? eff.name() : null);
347
            r.put("stageLabel", pretty(eff));
395
            r.put("stageLabel", pretty(eff));
348
            r.put("owner", owner != null ? owner.getFullName() : (l.getAssignTo() > 0 ? "#" + l.getAssignTo() : "—"));
396
            r.put("owner", owner != null ? owner.getFullName() : (l.getAssignTo() > 0 ? "#" + l.getAssignTo() : "—"));
349
            r.put("slaState", lmsAssignmentService.slaState(l));
397
            r.put("slaState", lmsAssignmentService.slaState(l));
350
            r.put("value", l.getPotential());
398
            r.put("value", l.getPotential());
-
 
399
            // Both are sortable columns on the pipeline table, so the row has to carry them.
-
 
400
            r.put("city", l.getCity());
-
 
401
            r.put("createdBy", l.getCreatedBy());
351
            out.add(r);
402
            out.add(r);
352
        }
403
        }
353
        return out;
404
        return out;
354
    }
405
    }
355
 
406
 
Line 391... Line 442...
391
        if (!ids.isEmpty()) {
442
        if (!ids.isEmpty()) {
392
            for (AuthUser u : authRepository.selectByIds(new ArrayList<>(ids))) {
443
            for (AuthUser u : authRepository.selectByIds(new ArrayList<>(ids))) {
393
                actors.put(u.getId(), u);
444
                actors.put(u.getId(), u);
394
            }
445
            }
395
        }
446
        }
-
 
447
        // Calls keyed by id, so a disposition in the trail can carry the recording of the call it came
-
 
448
        // from. A disposition is the agent's account of the conversation; having to hunt for the audio
-
 
449
        // in a separate list to check it against the recording is the friction worth removing.
-
 
450
        Map<Integer, LeadCall> callById = new HashMap<>();
-
 
451
        List<LeadCall> calls = leadCallRepository.selectByLeadId(leadId);
-
 
452
        if (calls != null) {
-
 
453
            for (LeadCall c : calls) {
-
 
454
                callById.put(c.getId(), c);
-
 
455
            }
-
 
456
        }
-
 
457
 
396
        List<Map<String, Object>> trail = new ArrayList<>();
458
        List<Map<String, Object>> trail = new ArrayList<>();
397
        for (LeadActivity a : acts) {
459
        for (LeadActivity a : acts) {
398
            AuthUser act = actors.get(a.getAuthId());
460
            AuthUser act = actors.get(a.getAuthId());
399
            Map<String, Object> t = new LinkedHashMap<>();
461
            Map<String, Object> t = new LinkedHashMap<>();
400
            t.put("when", a.getCreatedTimestamp() == null ? null : a.getCreatedTimestamp().toString());
462
            t.put("when", a.getCreatedTimestamp() == null ? null : a.getCreatedTimestamp().toString());
401
            t.put("actor", act != null ? act.getFullName() : (a.getAuthId() > 0 ? "#" + a.getAuthId() : "System"));
463
            t.put("actor", act != null ? act.getFullName() : (a.getAuthId() > 0 ? "#" + a.getAuthId() : "System"));
402
            t.put("type", a.getCommunicationType() == null ? null : a.getCommunicationType().name());
464
            t.put("type", a.getCommunicationType() == null ? null : a.getCommunicationType().name());
403
            t.put("body", a.getRemark());
465
            t.put("body", a.getRemark());
404
            t.put("scheduled", a.getSchelduleTimestamp() == null ? null : a.getSchelduleTimestamp().toString());
466
            t.put("scheduled", a.getSchelduleTimestamp() == null ? null : a.getSchelduleTimestamp().toString());
-
 
467
 
-
 
468
            // Either handle is enough to play: the gated endpoint serves our archived copy when there
-
 
469
            // is one and proxies the vendor's otherwise, so an agent is not made to wait on the
-
 
470
            // archival sweep to hear a call. The URL is never the raw vendor one — playback goes
-
 
471
            // through /lms/recording/{id}, which enforces the §15 matrix and logs the open.
-
 
472
            LeadCall call = a.getLeadCallId() == null ? null : callById.get(a.getLeadCallId());
-
 
473
            if (call != null && (call.getRecordingObjectKey() != null || call.getRecordingUuid() != null)) {
-
 
474
                t.put("callId", call.getId());
-
 
475
                t.put("durationSeconds", call.getDurationSeconds());
-
 
476
                t.put("archived", call.getRecordingObjectKey() != null);
-
 
477
            }
405
            trail.add(t);
478
            trail.add(t);
406
        }
479
        }
407
 
480
 
408
        Map<String, Object> m = new LinkedHashMap<>();
481
        Map<String, Object> m = new LinkedHashMap<>();
409
        m.put("id", l.getId());
482
        m.put("id", l.getId());
Line 428... Line 501...
428
        m.put("assignmentStatus", l.getAssignmentStatus());
501
        m.put("assignmentStatus", l.getAssignmentStatus());
429
        m.put("slaState", lmsAssignmentService.slaState(l));
502
        m.put("slaState", lmsAssignmentService.slaState(l));
430
        m.put("firstContactDue", l.getFirstContactDue() == null ? null : l.getFirstContactDue().toString());
503
        m.put("firstContactDue", l.getFirstContactDue() == null ? null : l.getFirstContactDue().toString());
431
        m.put("firstContactedAt", l.getFirstContactedAt() == null ? null : l.getFirstContactedAt().toString());
504
        m.put("firstContactedAt", l.getFirstContactedAt() == null ? null : l.getFirstContactedAt().toString());
432
        m.put("unreachableCount", l.getUnreachableCount());
505
        m.put("unreachableCount", l.getUnreachableCount());
-
 
506
 
-
 
507
        // The dashboard's record tab acts on the lead rather than only displaying it, so it needs
-
 
508
        // the same three things the in-app screen reads before it offers a control:
-
 
509
        //   - the parts of the address, not just the joined string, or an edit would have to
-
 
510
        //     re-split "addr, city, state" and would corrupt any value containing a comma;
-
 
511
        //   - the region id, so the edit form can preselect it (regionCode alone cannot);
-
 
512
        //   - whether the number is blocked and whether the geo-pin is approved, so the call and
-
 
513
        //     beat controls can say why they are unavailable instead of failing on submit.
-
 
514
        m.put("regionId", l.getRegionId());
-
 
515
        m.put("address", l.getAddress());
-
 
516
        m.put("city", l.getCity());
-
 
517
        m.put("state", l.getState());
-
 
518
 
-
 
519
        LeadDnd dnd = leadDndRepository.selectByMobile(l.getLeadMobile());
-
 
520
        m.put("dnd", dnd != null);
-
 
521
        m.put("dndReason", dnd == null ? null : dnd.getReason());
-
 
522
 
-
 
523
        // The pin itself, not just a yes/no: the record card offers a map link when it is approved
-
 
524
        // and pre-fills the manual-verify form with the existing coords when it is not, so nudging
-
 
525
        // a slightly-off pin does not mean typing it from scratch.
-
 
526
        LeadLiveLocation geo = leadLiveLocationRepository.selectByLeadId(leadId);
-
 
527
        m.put("geoApproved", geo != null && geo.getStatus() != null
-
 
528
                && "APPROVED".equalsIgnoreCase(geo.getStatus().toString()));
-
 
529
        m.put("geoStatus", geo == null || geo.getStatus() == null ? null : geo.getStatus().toString());
-
 
530
        if (geo != null && (geo.getLatitude() != 0 || geo.getLongitude() != 0)) {
-
 
531
            m.put("geoLat", geo.getLatitude());
-
 
532
            m.put("geoLng", geo.getLongitude());
-
 
533
        }
-
 
534
        m.put("geoPhotoId", geo == null ? 0 : geo.getImageDocumentId());
-
 
535
 
433
        m.put("trail", trail);
536
        m.put("trail", trail);
434
        return m;
537
        return m;
435
    }
538
    }
436
 
539
 
437
    private String joinLoc(String a, String c, String s) {
540
    private String joinLoc(String a, String c, String s) {