Subversion Repositories SmartDukaan

Rev

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

Rev 37692 Rev 37737
Line 11... Line 11...
11
import com.spice.profitmandi.dao.entity.user.LeadLiveLocation;
11
import com.spice.profitmandi.dao.entity.user.LeadLiveLocation;
12
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;
13
import com.spice.profitmandi.dao.entity.user.LeadCall;
14
import com.spice.profitmandi.dao.repository.dtr.LeadCallRepository;
14
import com.spice.profitmandi.dao.repository.dtr.LeadCallRepository;
15
import com.spice.profitmandi.dao.repository.dtr.LeadDndRepository;
15
import com.spice.profitmandi.dao.repository.dtr.LeadDndRepository;
-
 
16
import com.spice.profitmandi.dao.entity.auth.VonageAgent;
-
 
17
import com.spice.profitmandi.dao.repository.auth.VonageAgentRepository;
-
 
18
import com.spice.profitmandi.dao.repository.dtr.LeadRecordingAccessRepository;
16
import com.spice.profitmandi.dao.repository.dtr.LeadLiveLocationRepository;
19
import com.spice.profitmandi.dao.repository.dtr.LeadLiveLocationRepository;
17
import com.spice.profitmandi.dao.repository.dtr.LeadRepository;
20
import com.spice.profitmandi.dao.repository.dtr.LeadRepository;
18
import org.apache.logging.log4j.LogManager;
21
import org.apache.logging.log4j.LogManager;
19
import org.apache.logging.log4j.Logger;
22
import org.apache.logging.log4j.Logger;
20
import org.springframework.beans.factory.annotation.Autowired;
23
import org.springframework.beans.factory.annotation.Autowired;
Line 78... Line 81...
78
 
81
 
79
    @Autowired
82
    @Autowired
80
    private LeadDndRepository leadDndRepository;
83
    private LeadDndRepository leadDndRepository;
81
 
84
 
82
    @Autowired
85
    @Autowired
-
 
86
    private LeadRecordingAccessRepository leadRecordingAccessRepository;
-
 
87
 
-
 
88
    @Autowired
-
 
89
    private VonageAgentRepository vonageAgentRepository;
-
 
90
 
-
 
91
    @Autowired
83
    private LeadLiveLocationRepository leadLiveLocationRepository;
92
    private LeadLiveLocationRepository leadLiveLocationRepository;
84
 
93
 
85
    // ---- Command Center + shared summary ----------------------------------------------------
94
    // ---- Command Center + shared summary ----------------------------------------------------
86
 
95
 
87
    /** KPIs + lifecycle funnel + attention queue for the given region / created-date window. */
96
    /** KPIs + lifecycle funnel + attention queue for the given region / created-date window. */
Line 292... Line 301...
292
        out.put("total", total);
301
        out.put("total", total);
293
        out.put("rows", rows);
302
        out.put("rows", rows);
294
        return out;
303
        return out;
295
    }
304
    }
296
 
305
 
-
 
306
    // ---- Standard report suite (SOP §18.2) --------------------------------------------------
-
 
307
 
-
 
308
    /**
-
 
309
     * BGC Daily: calls made, connect rate, average talk time and dispositions — per agent, per day.
-
 
310
     *
-
 
311
     * <p>Real telephony, not the {@code contacted / days} proxy the tile used to show. That proxy
-
 
312
     * was invented before {@code user.lead_call} existed and reported a plausible-looking number
-
 
313
     * that no call had ever contributed to.
-
 
314
     */
-
 
315
    public Map<String, Object> bgcDaily(LocalDateTime from, LocalDateTime to) {
-
 
316
        List<Object[]> rows = leadCallRepository.countDailyByAgent(from, to);
-
 
317
        java.util.Set<Integer> authIds = new java.util.HashSet<>();
-
 
318
        for (Object[] r : rows) {
-
 
319
            if (r[0] != null) authIds.add(((Number) r[0]).intValue());
-
 
320
        }
-
 
321
        Map<Integer, AuthUser> agents = new HashMap<>();
-
 
322
        if (!authIds.isEmpty()) {
-
 
323
            for (AuthUser u : authRepository.selectByIds(new ArrayList<>(authIds))) agents.put(u.getId(), u);
-
 
324
        }
-
 
325
 
-
 
326
        List<Map<String, Object>> out = new ArrayList<>();
-
 
327
        long totCalls = 0, totConn = 0, totTalk = 0;
-
 
328
        for (Object[] r : rows) {
-
 
329
            int authId = r[0] == null ? 0 : ((Number) r[0]).intValue();
-
 
330
            long calls = ((Number) r[2]).longValue();
-
 
331
            long conn = ((Number) r[3]).longValue();
-
 
332
            long talk = ((Number) r[4]).longValue();
-
 
333
            totCalls += calls; totConn += conn; totTalk += talk;
-
 
334
            AuthUser a = agents.get(authId);
-
 
335
            Map<String, Object> m = new LinkedHashMap<>();
-
 
336
            m.put("authId", authId);
-
 
337
            m.put("agent", a != null ? a.getFullName() : ("#" + authId));
-
 
338
            m.put("day", r[1] == null ? null : r[1].toString());
-
 
339
            m.put("calls", calls);
-
 
340
            m.put("connected", conn);
-
 
341
            m.put("connectPct", calls > 0 ? Math.round(conn * 1000.0 / calls) / 10.0 : 0);
-
 
342
            // Average over CONNECTED calls: dividing by every dial would make a day of unanswered
-
 
343
            // ringing look like short conversations rather than no conversations.
-
 
344
            m.put("avgTalkSeconds", conn > 0 ? Math.round(talk / (double) conn) : 0);
-
 
345
            m.put("longestSeconds", ((Number) r[5]).longValue());
-
 
346
            out.add(m);
-
 
347
        }
-
 
348
 
-
 
349
        Map<String, Object> res = new LinkedHashMap<>();
-
 
350
        res.put("rows", out);
-
 
351
        res.put("calls", totCalls);
-
 
352
        res.put("connected", totConn);
-
 
353
        res.put("connectPct", totCalls > 0 ? Math.round(totConn * 1000.0 / totCalls) / 10.0 : 0);
-
 
354
        res.put("avgTalkSeconds", totConn > 0 ? Math.round(totTalk / (double) totConn) : 0);
-
 
355
        res.put("talkMinutes", Math.round(totTalk / 60.0));
-
 
356
        return res;
-
 
357
    }
-
 
358
 
-
 
359
    /**
-
 
360
     * Audit Hygiene (SOP §17): the breaches an audit review actually looks for.
-
 
361
     *
-
 
362
     * <p>Each figure is either measured or reported as unavailable — never estimated. A hygiene
-
 
363
     * report that guesses is worse than one that admits a gap, because the gap is the finding.
-
 
364
     */
-
 
365
    public Map<String, Object> auditHygiene(LocalDateTime from, LocalDateTime to) {
-
 
366
        LocalDateTime now = LocalDateTime.now();
-
 
367
        Map<String, Object> m = new LinkedHashMap<>();
-
 
368
 
-
 
369
        // Same source the Command Center's attention tiles use, so the two cannot disagree.
-
 
370
        Object[] att = leadRepository.attentionCounts(null, now, now.plusHours(1));
-
 
371
        m.put("slaBreached", num(att, 0));
-
 
372
        m.put("hold", num(att, 1));
-
 
373
        m.put("missingDisposition", num(att, 3));
-
 
374
 
-
 
375
        m.put("dndRegister", leadDndRepository.count());
-
 
376
        List<Object[]> breaches = leadCallRepository.selectDndBreaches(from, to, 50);
-
 
377
        List<Map<String, Object>> bl = new ArrayList<>();
-
 
378
        for (Object[] b : breaches) {
-
 
379
            Map<String, Object> row = new LinkedHashMap<>();
-
 
380
            row.put("callId", ((Number) b[0]).intValue());
-
 
381
            row.put("leadId", ((Number) b[1]).intValue());
-
 
382
            row.put("authId", b[2] == null ? 0 : ((Number) b[2]).intValue());
-
 
383
            row.put("mobile", b[3] == null ? null : b[3].toString());
-
 
384
            row.put("when", b[4] == null ? null : b[4].toString());
-
 
385
            bl.add(row);
-
 
386
        }
-
 
387
        m.put("dndBreaches", bl);
-
 
388
        m.put("dndBreachCount", bl.size());
-
 
389
 
-
 
390
        m.put("recordingOpens", leadRecordingAccessRepository.countInWindow(from, to, true));
-
 
391
        m.put("recordingDenied", leadRecordingAccessRepository.countInWindow(from, to, false));
-
 
392
        return m;
-
 
393
    }
-
 
394
 
-
 
395
    // ---- Live call board ---------------------------------------------------------------------
-
 
396
 
-
 
397
    /**
-
 
398
     * A non-terminal call older than this is not believed. Only the browser's 3-second poll
-
 
399
     * terminalises a VBC call, so an agent who closed the tab mid-call leaves a row that is
-
 
400
     * forever RINGING/ANSWERED with a null ended_at. Without this the board would show them On
-
 
401
     * Call indefinitely — wrong in its single most-read column.
-
 
402
     */
-
 
403
    private static final int LIVE_CALL_STALE_MINUTES = 30;
-
 
404
    /** Just off a call and no outcome logged yet. */
-
 
405
    private static final int WRAP_UP_MINUTES = 2;
-
 
406
    /** No call for this long and the agent reads as idle rather than merely available. */
-
 
407
    private static final int IDLE_MINUTES = 15;
-
 
408
 
-
 
409
    /**
-
 
410
     * Per-agent live board (the LMS answer to the RBM Call Target Summary).
-
 
411
     *
-
 
412
     * <p>The roster is {@code auth.vonage_agent}, not every user with leads: an agent without a
-
 
413
     * device mapping cannot dial at all, so they have no place on a call board.
-
 
414
     *
-
 
415
     * <p>Status is derived from call activity — Vonage exposes no presence of any kind, so there is
-
 
416
     * deliberately no Login or Break here rather than a guess dressed up as one.
-
 
417
     */
-
 
418
    public Map<String, Object> liveCallBoard() {
-
 
419
        LocalDateTime now = LocalDateTime.now();
-
 
420
        LocalDateTime dayStart = now.toLocalDate().atStartOfDay();
-
 
421
 
-
 
422
        Map<Integer, Object[]> calls = byAuthId(leadCallRepository.selectLiveAgentBoard(dayStart, now));
-
 
423
        Map<Integer, Object[]> leads = byAuthId(leadRepository.selectAgentLeadCounters(now));
-
 
424
        Map<Integer, Object[]> acts = byAuthId(leadActivityRepository.countAgentActivity(dayStart, now));
-
 
425
 
-
 
426
        List<VonageAgent> roster = vonageAgentRepository.selectAll();
-
 
427
        java.util.Set<Integer> ids = new java.util.HashSet<>();
-
 
428
        for (VonageAgent a : roster) {
-
 
429
            ids.add(a.getAuthId());
-
 
430
        }
-
 
431
        Map<Integer, AuthUser> users = new HashMap<>();
-
 
432
        if (!ids.isEmpty()) {
-
 
433
            for (AuthUser u : authRepository.selectByIds(new ArrayList<>(ids))) {
-
 
434
                users.put(u.getId(), u);
-
 
435
            }
-
 
436
        }
-
 
437
 
-
 
438
        List<Map<String, Object>> rows = new ArrayList<>();
-
 
439
        long totCalls = 0, totConn = 0, totTalk = 0, onCallNow = 0;
-
 
440
 
-
 
441
        for (VonageAgent agent : roster) {
-
 
442
            int authId = agent.getAuthId();
-
 
443
            Object[] c = calls.get(authId);
-
 
444
            Object[] l = leads.get(authId);
-
 
445
            Object[] a = acts.get(authId);
-
 
446
            AuthUser u = users.get(authId);
-
 
447
 
-
 
448
            long cCalls = c == null ? 0 : num(c, 1);
-
 
449
            long cConn = c == null ? 0 : num(c, 2);
-
 
450
            long cTalk = c == null ? 0 : num(c, 3);
-
 
451
            LocalDateTime lastStarted = c == null ? null : ts(c, 5);
-
 
452
            LocalDateTime lastEnded = c == null ? null : ts(c, 6);
-
 
453
            Integer liveCallId = c == null || c[7] == null ? null : ((Number) c[7]).intValue();
-
 
454
            LocalDateTime liveStarted = c == null ? null : ts(c, 9);
-
 
455
 
-
 
456
            // A live row past the staleness window is an abandoned tab, not a conversation.
-
 
457
            boolean stale = liveStarted != null && liveStarted.isBefore(now.minusMinutes(LIVE_CALL_STALE_MINUTES));
-
 
458
            boolean live = liveCallId != null && !stale;
-
 
459
 
-
 
460
            String status;
-
 
461
            LocalDateTime since;
-
 
462
            if (!agent.isActive()) {
-
 
463
                status = "—";
-
 
464
                since = null;
-
 
465
            } else if (live) {
-
 
466
                status = "On Call";
-
 
467
                since = liveStarted;
-
 
468
                onCallNow++;
-
 
469
            } else if (lastEnded != null && lastEnded.isAfter(now.minusMinutes(WRAP_UP_MINUTES))) {
-
 
470
                status = "Wrap Up";
-
 
471
                since = lastEnded;
-
 
472
            } else if (lastEnded != null && lastEnded.isBefore(now.minusMinutes(IDLE_MINUTES))) {
-
 
473
                status = "Idle";
-
 
474
                since = lastEnded;
-
 
475
            } else if (lastEnded != null) {
-
 
476
                status = "Available";
-
 
477
                since = lastEnded;
-
 
478
            } else {
-
 
479
                status = "Available";
-
 
480
                since = null;
-
 
481
            }
-
 
482
 
-
 
483
            Map<String, Object> r = new LinkedHashMap<>();
-
 
484
            r.put("authId", authId);
-
 
485
            r.put("agent", u != null ? u.getFullName() : agent.getVonageUserName());
-
 
486
            r.put("extension", agent.getVbcExtension());
-
 
487
            r.put("status", status);
-
 
488
            // Computed, never stored — the RBM board does the same rather than persisting a clock.
-
 
489
            r.put("sinceSeconds", since == null ? null : java.time.Duration.between(since, now).getSeconds());
-
 
490
            r.put("staleCall", stale);
-
 
491
            r.put("liveCallId", live ? liveCallId : null);
-
 
492
            r.put("liveLeadId", live && c[8] != null ? ((Number) c[8]).intValue() : null);
-
 
493
            r.put("calls", cCalls);
-
 
494
            r.put("connected", cConn);
-
 
495
            r.put("connectPct", cCalls > 0 ? Math.round(cConn * 1000.0 / cCalls) / 10.0 : 0);
-
 
496
            r.put("talkSeconds", cTalk);
-
 
497
            r.put("longestSeconds", c == null ? 0 : num(c, 4));
-
 
498
            r.put("lastCallAt", lastStarted == null ? null : lastStarted.toString());
-
 
499
            r.put("assigned", l == null ? 0 : num(l, 1));
-
 
500
            r.put("slaBreached", l == null ? 0 : num(l, 2));
-
 
501
            r.put("touched", a == null ? 0 : num(a, 1));
-
 
502
            r.put("dispositioned", a == null ? 0 : num(a, 2));
-
 
503
            rows.add(r);
-
 
504
 
-
 
505
            totCalls += cCalls; totConn += cConn; totTalk += cTalk;
-
 
506
        }
-
 
507
 
-
 
508
        Map<String, Object> out = new LinkedHashMap<>();
-
 
509
        out.put("rows", rows);
-
 
510
        out.put("agents", rows.size());
-
 
511
        out.put("onCallNow", onCallNow);
-
 
512
        out.put("calls", totCalls);
-
 
513
        out.put("connected", totConn);
-
 
514
        out.put("connectPct", totCalls > 0 ? Math.round(totConn * 1000.0 / totCalls) / 10.0 : 0);
-
 
515
        out.put("talkMinutes", Math.round(totTalk / 60.0));
-
 
516
        out.put("asOf", now.toString());
-
 
517
        return out;
-
 
518
    }
-
 
519
 
-
 
520
    /**
-
 
521
     * The full call log for a window — every call, with ring time, talk time, outcome and recording.
-
 
522
     *
-
 
523
     * <p>The agent's extension comes from {@code auth.vonage_agent}, not from the call's
-
 
524
     * {@code from_number}: click2dial addresses the agent by DEVICE, so that column holds an opaque
-
 
525
     * id like {@code VH7NdQbBHcmsR8wFLsiU}, which means nothing to anyone reading a report.
-
 
526
     */
-
 
527
    public Map<String, Object> callLog(LocalDateTime from, LocalDateTime to, int page, int pageSize) {
-
 
528
        int size = Math.min(Math.max(pageSize, 1), MAX_PAGE_SIZE);
-
 
529
        int p = Math.max(page, 1);
-
 
530
        long total = leadCallRepository.countCallLog(from, to);
-
 
531
        List<Object[]> rows = leadCallRepository.selectCallLog(from, to, (p - 1) * size, size);
-
 
532
 
-
 
533
        java.util.Set<Integer> authIds = new java.util.HashSet<>();
-
 
534
        for (Object[] r : rows) {
-
 
535
            if (r[7] != null) {
-
 
536
                authIds.add(((Number) r[7]).intValue());
-
 
537
            }
-
 
538
        }
-
 
539
        Map<Integer, AuthUser> users = new HashMap<>();
-
 
540
        Map<Integer, String> extByAuth = new HashMap<>();
-
 
541
        if (!authIds.isEmpty()) {
-
 
542
            for (AuthUser u : authRepository.selectByIds(new ArrayList<>(authIds))) {
-
 
543
                users.put(u.getId(), u);
-
 
544
            }
-
 
545
            for (VonageAgent a : vonageAgentRepository.selectAll()) {
-
 
546
                extByAuth.put(a.getAuthId(), a.getVbcExtension());
-
 
547
            }
-
 
548
        }
-
 
549
 
-
 
550
        List<Map<String, Object>> out = new ArrayList<>();
-
 
551
        for (Object[] r : rows) {
-
 
552
            int authId = r[7] == null ? 0 : ((Number) r[7]).intValue();
-
 
553
            AuthUser u = users.get(authId);
-
 
554
            Map<String, Object> m = new LinkedHashMap<>();
-
 
555
            m.put("callId", ((Number) r[0]).intValue());
-
 
556
            m.put("startedAt", str(ts(r, 1)));
-
 
557
            m.put("answeredAt", str(ts(r, 2)));
-
 
558
            m.put("endedAt", str(ts(r, 3)));
-
 
559
            m.put("status", r[4] == null ? null : r[4].toString());
-
 
560
            // Negative or null when never answered — normalise to null so the UI shows a dash.
-
 
561
            Long ring = r[5] == null ? null : ((Number) r[5]).longValue();
-
 
562
            m.put("ringSeconds", ring != null && ring >= 0 ? ring : null);
-
 
563
            m.put("talkSeconds", num(r, 6));
-
 
564
            m.put("authId", authId);
-
 
565
            m.put("agent", u != null ? u.getFullName() : (authId > 0 ? "#" + authId : "—"));
-
 
566
            m.put("extension", extByAuth.get(authId));
-
 
567
            m.put("leadId", r[8] == null ? null : ((Number) r[8]).intValue());
-
 
568
            m.put("toNumber", r[9] == null ? null : r[9].toString());
-
 
569
            m.put("direction", r[10] == null ? null : r[10].toString());
-
 
570
            m.put("lmsCode", r[11] == null ? null : r[11].toString());
-
 
571
            m.put("retailer", r[12] == null ? null : r[12].toString().trim());
-
 
572
            m.put("outlet", r[13] == null ? null : r[13].toString());
-
 
573
            m.put("recording", r[14] == null ? "none" : r[14].toString());
-
 
574
            m.put("disposition", r[15] == null ? null : r[15].toString());
-
 
575
            out.add(m);
-
 
576
        }
-
 
577
 
-
 
578
        Map<String, Object> res = new LinkedHashMap<>();
-
 
579
        res.put("rows", out);
-
 
580
        res.put("total", total);
-
 
581
        res.put("page", p);
-
 
582
        res.put("pageSize", size);
-
 
583
        res.put("pages", (int) Math.ceil(total / (double) size));
-
 
584
        return res;
-
 
585
    }
-
 
586
 
-
 
587
    private String str(LocalDateTime t) {
-
 
588
        return t == null ? null : t.toString();
-
 
589
    }
-
 
590
 
-
 
591
    /** Index an aggregate result by its first column (the auth id). */
-
 
592
    private Map<Integer, Object[]> byAuthId(List<Object[]> rows) {
-
 
593
        Map<Integer, Object[]> m = new HashMap<>();
-
 
594
        if (rows != null) {
-
 
595
            for (Object[] r : rows) {
-
 
596
                if (r[0] != null) {
-
 
597
                    m.put(((Number) r[0]).intValue(), r);
-
 
598
                }
-
 
599
            }
-
 
600
        }
-
 
601
        return m;
-
 
602
    }
-
 
603
 
-
 
604
    /** Native queries hand back java.sql.Timestamp; the entity side works in LocalDateTime. */
-
 
605
    private LocalDateTime ts(Object[] row, int i) {
-
 
606
        Object v = row[i];
-
 
607
        if (v == null) {
-
 
608
            return null;
-
 
609
        }
-
 
610
        if (v instanceof java.sql.Timestamp) {
-
 
611
            return ((java.sql.Timestamp) v).toLocalDateTime();
-
 
612
        }
-
 
613
        return v instanceof LocalDateTime ? (LocalDateTime) v : null;
-
 
614
    }
-
 
615
 
297
    // ---- Drill-down -------------------------------------------------------------------------
616
    // ---- Drill-down -------------------------------------------------------------------------
298
 
617
 
299
    /** Lead rows for a drill bucket, shaped for the drawer table. */
618
    /** Lead rows for a drill bucket, shaped for the drawer table. */
300
    public List<Map<String, Object>> drill(String bucketType, String bucketKey, Integer regionId,
619
    public List<Map<String, Object>> drill(String bucketType, String bucketKey, Integer regionId,
301
                                            LocalDateTime from, LocalDateTime to) {
620
                                            LocalDateTime from, LocalDateTime to) {
Line 397... Line 716...
397
            r.put("slaState", lmsAssignmentService.slaState(l));
716
            r.put("slaState", lmsAssignmentService.slaState(l));
398
            r.put("value", l.getPotential());
717
            r.put("value", l.getPotential());
399
            // Both are sortable columns on the pipeline table, so the row has to carry them.
718
            // Both are sortable columns on the pipeline table, so the row has to carry them.
400
            r.put("city", l.getCity());
719
            r.put("city", l.getCity());
401
            r.put("createdBy", l.getCreatedBy());
720
            r.put("createdBy", l.getCreatedBy());
-
 
721
            // Lets the assign picker surface whoever covers this lead's region first.
-
 
722
            r.put("regionId", l.getRegionId());
402
            out.add(r);
723
            out.add(r);
403
        }
724
        }
404
        return out;
725
        return out;
405
    }
726
    }
406
 
727