Subversion Repositories SmartDukaan

Rev

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

Rev 36913 Rev 36929
Line 38... Line 38...
38
        out.put("range", range);
38
        out.put("range", range);
39
        out.put("scopeCount", authIds == null ? 0 : authIds.size());
39
        out.put("scopeCount", authIds == null ? 0 : authIds.size());
40
 
40
 
41
        List<Map<String, Object>> coverage = coverageByLevel(s, start, end, auth);
41
        List<Map<String, Object>> coverage = coverageByLevel(s, start, end, auth);
42
        Map<String, Object> funnel = funnel(s, start, end, auth, dtr);
42
        Map<String, Object> funnel = funnel(s, start, end, auth, dtr);
-
 
43
        Map<String, Object> summary = journeySummary(s, start, end, dtr, funnel);
43
        Map<String, Object> deferral = deferral(s, start, end, auth);
44
        Map<String, Object> deferral = deferral(s, start, end, auth);
44
        Map<String, Object> utilization = utilization(s, start, end, dtr);
45
        Map<String, Object> utilization = utilization(s, start, end, dtr);
45
        Map<String, Object> travel = travel(s, start, end, auth, dtr);
46
        Map<String, Object> travel = travel(s, start, end, auth, dtr);
46
        Map<String, Object> outcomes = outcomes(s, start, end, auth);
47
        Map<String, Object> outcomes = outcomes(s, start, end, auth);
47
        List<Map<String, Object>> scorecard = scorecard(s, start, end, auth, dtr);
48
        List<Map<String, Object>> scorecard = scorecard(s, start, end, auth, dtr);
48
        Map<String, Object> kpis = kpis(s, start, end, auth, dtr, coverage, funnel, deferral);
49
        Map<String, Object> kpis = kpis(s, start, end, auth, dtr, coverage, funnel, deferral);
49
 
50
 
50
        out.put("kpis", kpis);
51
        out.put("kpis", kpis);
51
        out.put("funnel", funnel);
52
        out.put("funnel", funnel);
-
 
53
        out.put("summary", summary);
52
        out.put("coverageByLevel", coverage);
54
        out.put("coverageByLevel", coverage);
53
        out.put("scorecard", scorecard);
55
        out.put("scorecard", scorecard);
54
        out.put("utilization", utilization);
56
        out.put("utilization", utilization);
55
        out.put("deferral", deferral);
57
        out.put("deferral", deferral);
56
        out.put("travel", travel);
58
        out.put("travel", travel);
Line 68... Line 70...
68
    private Map<String, Object> kpis(Session s, String ms, String me, List<Integer> auth, List<Integer> dtr,
70
    private Map<String, Object> kpis(Session s, String ms, String me, List<Integer> auth, List<Integer> dtr,
69
                                     List<Map<String, Object>> coverage, Map<String, Object> funnel, Map<String, Object> deferral) {
71
                                     List<Map<String, Object>> coverage, Map<String, Object> funnel, Map<String, Object> deferral) {
70
        Map<String, Object> m = new LinkedHashMap<>();
72
        Map<String, Object> m = new LinkedHashMap<>();
71
 
73
 
72
        long planned = asLong(funnel.get("plannedStops"));
74
        long planned = asLong(funnel.get("plannedStops"));
-
 
75
        // Adherence is planned-visit completion: the numerator counts only completed
-
 
76
        // PLANNED visits (partner franchisee + approved leads), not every check-out
-
 
77
        // (ad-hoc office/warehouse stops don't count toward planned-visit adherence).
73
        long done = asLong(funnel.get("checkedOut"));
78
        // Self-assigned stops are excluded: task_name is pipe-delimited and ends with
-
 
79
        // a "… | SELF" marker for self-assigned visits, which aren't part of the plan.
-
 
80
        long done = countVisits(s, ms, me, dtr, "task_type IN ('franchisee-visit','lead') AND COALESCE(task_name,'') NOT LIKE '%SELF%' AND mark_type LIKE '%CHECKOUT%'");
74
        m.put("plannedStops", planned);
81
        m.put("plannedStops", planned);
75
        m.put("doneStops", done);
82
        m.put("doneStops", done);
76
        m.put("adherencePct", planned == 0 ? 0 : Math.round(100.0 * done / planned));
83
        m.put("adherencePct", planned == 0 ? 0 : Math.round(100.0 * done / planned));
77
 
84
 
78
        // L1 coverage row
85
        // L1 coverage row
Line 97... Line 104...
97
            "   AND (mark_type='PUNCHIN' OR (task_id=0 AND check_in_time IS NOT NULL AND check_in_time<>'00:00:00'))");
104
            "   AND (mark_type='PUNCHIN' OR (task_id=0 AND check_in_time IS NOT NULL AND check_in_time<>'00:00:00'))");
98
        q.setParameter("ms", ms).setParameter("me", me).setParameterList("dtr", dtr);
105
        q.setParameter("ms", ms).setParameter("me", me).setParameterList("dtr", dtr);
99
        long journeys = asLong(q.uniqueResult());
106
        long journeys = asLong(q.uniqueResult());
100
        m.put("journeys", journeys);
107
        m.put("journeys", journeys);
101
        m.put("avgVisitsPerActiveDay", journeys == 0 ? 0 : round1((double) asLong(funnel.get("checkedIn")) / journeys));
108
        m.put("avgVisitsPerActiveDay", journeys == 0 ? 0 : round1((double) asLong(funnel.get("checkedIn")) / journeys));
-
 
109
        // partner (franchisee) visits per active day — the subset that are partner
-
 
110
        // store visits, shown alongside the all-types visit average.
-
 
111
        long partnerCheckins = countVisits(s, ms, me, dtr, "task_type='franchisee-visit' AND mark_type LIKE 'CHECKIN%'");
-
 
112
        m.put("avgPartnersPerActiveDay", journeys == 0 ? 0 : round1((double) partnerCheckins / journeys));
-
 
113
        // lead visits per active day, split into planned (on the PJP / lead_route,
-
 
114
        // no SELF marker) vs unplanned (self-assigned ad-hoc leads, task_name carries
-
 
115
        // the pipe-delimited "SELF" tag).
-
 
116
        long plannedLeadCheckins   = countVisits(s, ms, me, dtr, "task_type='lead' AND COALESCE(task_name,'') NOT LIKE '%SELF%' AND mark_type LIKE 'CHECKIN%'");
-
 
117
        long unplannedLeadCheckins = countVisits(s, ms, me, dtr, "task_type='lead' AND COALESCE(task_name,'') LIKE '%SELF%' AND mark_type LIKE 'CHECKIN%'");
-
 
118
        m.put("avgPlannedLeadsPerActiveDay",   journeys == 0 ? 0 : round1((double) plannedLeadCheckins / journeys));
-
 
119
        m.put("avgUnplannedLeadsPerActiveDay", journeys == 0 ? 0 : round1((double) unplannedLeadCheckins / journeys));
102
 
120
 
103
        // avg in-store discussion minutes
121
        // avg in-store discussion minutes — overall, plus partner (franchisee) and
104
        NativeQuery<?> dq = s.createNativeQuery(
122
        // lead split so the card can show each separately.
105
            "SELECT ROUND(AVG(NULLIF(TIME_TO_SEC(time_spent),0))/60,0) FROM auth.location_tracking" +
123
        m.put("avgDiscussionMin", avgDiscussionMin(s, ms, me, dtr, null));
106
            " WHERE task_date>=:ms AND task_date<=:me AND task_id<>0 AND mark_type LIKE '%CHECKOUT%' AND user_id IN (:dtr)");
-
 
107
        dq.setParameter("ms", ms).setParameter("me", me).setParameterList("dtr", dtr);
124
        m.put("avgDiscussionPartnerMin", avgDiscussionMin(s, ms, me, dtr, "franchisee-visit"));
108
        m.put("avgDiscussionMin", asLong(dq.uniqueResult()));
125
        m.put("avgDiscussionLeadMin", avgDiscussionMin(s, ms, me, dtr, "lead"));
109
 
126
 
-
 
127
        // Auto "missed" = situations where planned execution broke down, split into
-
 
128
        // three causes: execs with no PJP at all, scheduled beat-days with the agenda
-
 
129
        // left unfilled, and scheduled beat-days the owner never punched in on.
-
 
130
        Map<String, Object> mb = missedBreakdown(s, ms, me, auth, coverage);
-
 
131
        m.put("autoMissedUnscheduled", mb.get("unscheduled"));
-
 
132
        m.put("autoMissedUnfilledAgenda", mb.get("unfilledAgenda"));
-
 
133
        m.put("autoMissedMissedPunchin", mb.get("missedPunchin"));
110
        m.put("autoMissed", asLong(deferral.get("total")));
134
        m.put("autoMissed", mb.get("total"));
111
        m.put("autoMissedSystemPct", asLong(deferral.get("systemPct")));
135
        m.put("autoMissedSystemPct", asLong(deferral.get("systemPct")));
112
 
136
 
113
        // leads created on route
137
        // leads created on route
114
        NativeQuery<?> lq = s.createNativeQuery(
138
        NativeQuery<?> lq = s.createNativeQuery(
115
            "SELECT COUNT(*) FROM `user`.lead WHERE created_timestamp>=:ms AND created_timestamp<=CONCAT(:me,' 23:59:59') AND auth_id IN (:auth)");
139
            "SELECT COUNT(*) FROM `user`.lead WHERE created_timestamp>=:ms AND created_timestamp<=CONCAT(:me,' 23:59:59') AND auth_id IN (:auth)");
Line 182... Line 206...
182
            " AND user_id IN (:dtr)");
206
            " AND user_id IN (:dtr)");
183
        q.setParameter("ms", ms).setParameter("me", me).setParameterList("dtr", dtr);
207
        q.setParameter("ms", ms).setParameter("me", me).setParameterList("dtr", dtr);
184
        return asLong(q.uniqueResult());
208
        return asLong(q.uniqueResult());
185
    }
209
    }
186
 
210
 
-
 
211
    // Avg in-store discussion minutes over checked-out visits. taskType null =
-
 
212
    // all visit types; otherwise restricted to that task_type (e.g. franchisee / lead).
-
 
213
    private long avgDiscussionMin(Session s, String ms, String me, List<Integer> dtr, String taskType) {
-
 
214
        NativeQuery<?> q = s.createNativeQuery(
-
 
215
            "SELECT ROUND(AVG(TIME_TO_SEC(check_out_time)-TIME_TO_SEC(check_in_time))/60,0) FROM auth.location_tracking" +
-
 
216
            " WHERE task_date>=:ms AND task_date<=:me AND task_id<>0 AND mark_type LIKE '%CHECKOUT%'" +
-
 
217
            " AND check_in_time IS NOT NULL AND check_in_time<>'00:00:00'" +
-
 
218
            " AND check_out_time IS NOT NULL AND check_out_time<>'00:00:00'" +
-
 
219
            " AND TIME_TO_SEC(check_out_time) > TIME_TO_SEC(check_in_time)" +
-
 
220
            (taskType != null ? " AND task_type=:tt" : "") +
-
 
221
            " AND user_id IN (:dtr)");
-
 
222
        q.setParameter("ms", ms).setParameter("me", me).setParameterList("dtr", dtr);
-
 
223
        if (taskType != null) q.setParameter("tt", taskType);
-
 
224
        return asLong(q.uniqueResult());
-
 
225
    }
-
 
226
 
-
 
227
    // ---- Journey summary table ---------------------------------------------
-
 
228
    // A flat per-period scorecard split into Planned (on the PJP / lead_route, no
-
 
229
    // SELF marker) vs Self Assigned (the ad-hoc "… | SELF" visits the exec added):
-
 
230
    //   Total Visits   — planned = scheduled planned stops; self = self-assigned check-ins
-
 
231
    //   Avg Time Spent — avg (check-out − check-in) minutes, each side
-
 
232
    //   Idle/Break     — average idle minutes per active day (single value)
-
 
233
    //   Deferred       — total − completed, each side (what was never closed out)
-
 
234
    //   Completed      — checkouts, each side
-
 
235
    private Map<String, Object> journeySummary(Session s, String ms, String me, List<Integer> dtr,
-
 
236
                                               Map<String, Object> funnel) {
-
 
237
        long plannedVisits   = asLong(funnel.get("plannedStops"));
-
 
238
        long completedPlanned = countVisits(s, ms, me, dtr, "task_type IN ('franchisee-visit','lead') AND COALESCE(task_name,'') NOT LIKE '%SELF%' AND mark_type LIKE '%CHECKOUT%'");
-
 
239
        long selfVisits      = countVisits(s, ms, me, dtr, "COALESCE(task_name,'') LIKE '%SELF%' AND mark_type LIKE 'CHECKIN%'");
-
 
240
        long completedSelf   = countVisits(s, ms, me, dtr, "COALESCE(task_name,'') LIKE '%SELF%' AND mark_type LIKE '%CHECKOUT%'");
-
 
241
 
-
 
242
        // idle = working − in-store − travel, averaged over active (punch-in) days
-
 
243
        NativeQuery<?> uq = s.createNativeQuery(
-
 
244
            "SELECT" +
-
 
245
            " (SELECT COALESCE(SUM(TIME_TO_SEC(time_spent)),0) FROM auth.location_tracking WHERE task_date>=:ms AND task_date<=:me AND task_id<>0 AND user_id IN (:dtr)) instore," +
-
 
246
            " (SELECT COALESCE(SUM(TIME_TO_SEC(transit_time)),0) FROM auth.location_tracking WHERE task_date>=:ms AND task_date<=:me AND task_id<>0 AND user_id IN (:dtr)) travel," +
-
 
247
            " (SELECT COALESCE(SUM(GREATEST(TIME_TO_SEC(check_out_time)-TIME_TO_SEC(check_in_time),0)),0) FROM auth.location_tracking WHERE task_date>=:ms AND task_date<=:me AND task_id=0 AND check_out_time IS NOT NULL AND check_out_time<>'00:00:00' AND check_in_time IS NOT NULL AND check_in_time<>'00:00:00' AND user_id IN (:dtr)) working," +
-
 
248
            " (SELECT COUNT(DISTINCT CONCAT(user_id,'-',task_date)) FROM auth.location_tracking WHERE task_date>=:ms AND task_date<=:me AND user_id IN (:dtr) AND (mark_type='PUNCHIN' OR (task_id=0 AND check_in_time IS NOT NULL AND check_in_time<>'00:00:00'))) journeys");
-
 
249
        uq.setParameter("ms", ms).setParameter("me", me).setParameterList("dtr", dtr);
-
 
250
        Object[] ur = (Object[]) uq.uniqueResult();
-
 
251
        double instore = asDouble(ur[0]), travel = asDouble(ur[1]), working = asDouble(ur[2]);
-
 
252
        long journeys = asLong(ur[3]);
-
 
253
        if (working < instore + travel) working = instore + travel;
-
 
254
        double idle = Math.max(working - instore - travel, 0);
-
 
255
        long idleAvgMin = journeys == 0 ? 0 : Math.round(idle / journeys / 60.0);
-
 
256
 
-
 
257
        Map<String, Object> m = new LinkedHashMap<>();
-
 
258
        m.put("plannedVisits", plannedVisits);
-
 
259
        m.put("selfVisits", selfVisits);
-
 
260
        m.put("avgTimePlannedMin", avgTimeSpentMin(s, ms, me, dtr, false));
-
 
261
        m.put("avgTimeSelfMin", avgTimeSpentMin(s, ms, me, dtr, true));
-
 
262
        m.put("idleAvgMin", idleAvgMin);
-
 
263
        m.put("deferredPlanned", Math.max(plannedVisits - completedPlanned, 0));
-
 
264
        m.put("deferredSelf", Math.max(selfVisits - completedSelf, 0));
-
 
265
        m.put("completedPlanned", completedPlanned);
-
 
266
        m.put("completedSelf", completedSelf);
-
 
267
        return m;
-
 
268
    }
-
 
269
 
-
 
270
    // Avg (check-out − check-in) minutes over checked-out visits, split by whether
-
 
271
    // the visit carries the pipe-delimited "… | SELF" self-assigned marker.
-
 
272
    private long avgTimeSpentMin(Session s, String ms, String me, List<Integer> dtr, boolean self) {
-
 
273
        NativeQuery<?> q = s.createNativeQuery(
-
 
274
            "SELECT ROUND(AVG(TIME_TO_SEC(check_out_time)-TIME_TO_SEC(check_in_time))/60,0) FROM auth.location_tracking" +
-
 
275
            " WHERE task_date>=:ms AND task_date<=:me AND task_id<>0 AND mark_type LIKE '%CHECKOUT%'" +
-
 
276
            " AND check_in_time IS NOT NULL AND check_in_time<>'00:00:00'" +
-
 
277
            " AND check_out_time IS NOT NULL AND check_out_time<>'00:00:00'" +
-
 
278
            " AND TIME_TO_SEC(check_out_time) > TIME_TO_SEC(check_in_time)" +
-
 
279
            (self ? " AND COALESCE(task_name,'') LIKE '%SELF%'" : " AND COALESCE(task_name,'') NOT LIKE '%SELF%'") +
-
 
280
            " AND user_id IN (:dtr)");
-
 
281
        q.setParameter("ms", ms).setParameter("me", me).setParameterList("dtr", dtr);
-
 
282
        return asLong(q.uniqueResult());
-
 
283
    }
-
 
284
 
187
    // ---- Coverage by level --------------------------------------------------
285
    // ---- Coverage by level --------------------------------------------------
188
 
286
 
189
    private List<Map<String, Object>> coverageByLevel(Session s, String ms, String me, List<Integer> auth) {
287
    private List<Map<String, Object>> coverageByLevel(Session s, String ms, String me, List<Integer> auth) {
190
        NativeQuery<?> q = s.createNativeQuery(
288
        NativeQuery<?> q = s.createNativeQuery(
191
            "SELECT p.escalation_type lvl, COUNT(DISTINCT p.auth_user_id) total_users," +
289
            "SELECT p.escalation_type lvl, COUNT(DISTINCT p.auth_user_id) total_users," +
Line 214... Line 312...
214
    private List<Map<String, Object>> scorecard(Session s, String ms, String me, List<Integer> auth, List<Integer> dtr) {
312
    private List<Map<String, Object>> scorecard(Session s, String ms, String me, List<Integer> auth, List<Integer> dtr) {
215
        NativeQuery<?> q = s.createNativeQuery(
313
        NativeQuery<?> q = s.createNativeQuery(
216
            "SELECT au.id uid, TRIM(CONCAT(au.first_name,' ',COALESCE(au.last_name,''))) nm, lvl.lvl," +
314
            "SELECT au.id uid, TRIM(CONCAT(au.first_name,' ',COALESCE(au.last_name,''))) nm, lvl.lvl," +
217
            " COALESCE(pl.planned,0)+COALESCE(plL.planned,0) planned, COALESCE(ac.done,0) done," +
315
            " COALESCE(pl.planned,0)+COALESCE(plL.planned,0) planned, COALESCE(ac.done,0) done," +
218
            " COALESCE(ds.disc_min,0) disc_min, COALESCE(wk.work_sec,0) work_sec," +
316
            " COALESCE(ds.disc_min,0) disc_min, COALESCE(wk.work_sec,0) work_sec," +
219
            " COALESCE(ld.leads,0) leads, du.id duid" +
317
            " COALESCE(ld.leads,0) leads, du.id duid, COALESCE(acp.donep,0) donep" +
220
            " FROM (SELECT DISTINCT auth_user_id FROM `user`.beat WHERE active=1 AND auth_user_id IN (:auth)) o" +
318
            " FROM (SELECT DISTINCT auth_user_id FROM `user`.beat WHERE active=1 AND auth_user_id IN (:auth)) o" +
221
            " JOIN auth.auth_user au ON au.id=o.auth_user_id" +
319
            " JOIN auth.auth_user au ON au.id=o.auth_user_id" +
222
            " LEFT JOIN dtr.users du ON du.email=au.email_id" +
320
            " LEFT JOIN dtr.users du ON du.email=au.email_id" +
223
            " LEFT JOIN (SELECT auth_user_id, MIN(escalation_type) lvl FROM cs.position WHERE category_id=4 GROUP BY auth_user_id) lvl ON lvl.auth_user_id=au.id" +
321
            " LEFT JOIN (SELECT auth_user_id, MIN(escalation_type) lvl FROM cs.position WHERE category_id=4 GROUP BY auth_user_id) lvl ON lvl.auth_user_id=au.id" +
224
            " LEFT JOIN (SELECT b.auth_user_id uid, COUNT(*) planned FROM `user`.beat_schedule sc" +
322
            " LEFT JOIN (SELECT b.auth_user_id uid, COUNT(*) planned FROM `user`.beat_schedule sc" +
Line 227... Line 325...
227
            " LEFT JOIN (SELECT b.auth_user_id uid, COUNT(*) planned FROM `user`.beat_schedule sc" +
325
            " LEFT JOIN (SELECT b.auth_user_id uid, COUNT(*) planned FROM `user`.beat_schedule sc" +
228
            "   JOIN `user`.beat b ON b.id=sc.beat_id AND b.active=1 JOIN `user`.lead_route lr ON lr.beat_id=sc.beat_id AND lr.schedule_date=sc.start_date AND lr.status='APPROVED'" +
326
            "   JOIN `user`.beat b ON b.id=sc.beat_id AND b.active=1 JOIN `user`.lead_route lr ON lr.beat_id=sc.beat_id AND lr.schedule_date=sc.start_date AND lr.status='APPROVED'" +
229
            "   WHERE sc.start_date>=:ms AND sc.start_date<=:me GROUP BY b.auth_user_id) plL ON plL.uid=au.id" +
327
            "   WHERE sc.start_date>=:ms AND sc.start_date<=:me GROUP BY b.auth_user_id) plL ON plL.uid=au.id" +
230
            " LEFT JOIN (SELECT user_id uid, COUNT(*) done FROM auth.location_tracking" +
328
            " LEFT JOIN (SELECT user_id uid, COUNT(*) done FROM auth.location_tracking" +
231
            "   WHERE task_date>=:ms AND task_date<=:me AND task_id<>0 AND mark_type LIKE 'CHECKIN%' GROUP BY user_id) ac ON ac.uid=du.id" +
329
            "   WHERE task_date>=:ms AND task_date<=:me AND task_id<>0 AND mark_type LIKE 'CHECKIN%' GROUP BY user_id) ac ON ac.uid=du.id" +
-
 
330
            // Adherence numerator: completed PLANNED visits only (partner franchisee +
-
 
331
            // approved leads), excluding ad-hoc office/warehouse stops and self-assigned
-
 
332
            // stops (task_name carries a "… | SELF" marker, which aren't part of the plan).
-
 
333
            " LEFT JOIN (SELECT user_id uid, COUNT(*) donep FROM auth.location_tracking" +
-
 
334
            "   WHERE task_date>=:ms AND task_date<=:me AND task_id<>0 AND task_type IN ('franchisee-visit','lead') AND COALESCE(task_name,'') NOT LIKE '%SELF%' AND mark_type LIKE '%CHECKOUT%' GROUP BY user_id) acp ON acp.uid=du.id" +
232
            " LEFT JOIN (SELECT user_id uid, ROUND(AVG(NULLIF(TIME_TO_SEC(time_spent),0))/60,0) disc_min FROM auth.location_tracking" +
335
            " LEFT JOIN (SELECT user_id uid, ROUND(AVG(NULLIF(TIME_TO_SEC(time_spent),0))/60,0) disc_min FROM auth.location_tracking" +
233
            "   WHERE task_date>=:ms AND task_date<=:me AND task_id<>0 AND mark_type LIKE '%CHECKOUT%' GROUP BY user_id) ds ON ds.uid=du.id" +
336
            "   WHERE task_date>=:ms AND task_date<=:me AND task_id<>0 AND mark_type LIKE '%CHECKOUT%' GROUP BY user_id) ds ON ds.uid=du.id" +
234
            " LEFT JOIN (SELECT user_id uid, SUM(GREATEST(TIME_TO_SEC(check_out_time)-TIME_TO_SEC(check_in_time),0)) work_sec FROM auth.location_tracking" +
337
            " LEFT JOIN (SELECT user_id uid, SUM(GREATEST(TIME_TO_SEC(check_out_time)-TIME_TO_SEC(check_in_time),0)) work_sec FROM auth.location_tracking" +
235
            "   WHERE task_date>=:ms AND task_date<=:me AND task_id=0 AND check_out_time IS NOT NULL AND check_out_time<>'00:00:00' AND check_in_time IS NOT NULL AND check_in_time<>'00:00:00' GROUP BY user_id) wk ON wk.uid=du.id" +
338
            "   WHERE task_date>=:ms AND task_date<=:me AND task_id=0 AND check_out_time IS NOT NULL AND check_out_time<>'00:00:00' AND check_in_time IS NOT NULL AND check_in_time<>'00:00:00' GROUP BY user_id) wk ON wk.uid=du.id" +
236
            " LEFT JOIN (SELECT auth_id uid, COUNT(*) leads FROM `user`.lead WHERE created_timestamp>=:ms AND created_timestamp<=CONCAT(:me,' 23:59:59') GROUP BY auth_id) ld ON ld.uid=au.id" +
339
            " LEFT JOIN (SELECT auth_id uid, COUNT(*) leads FROM `user`.lead WHERE created_timestamp>=:ms AND created_timestamp<=CONCAT(:me,' 23:59:59') GROUP BY auth_id) ld ON ld.uid=au.id" +
Line 243... Line 346...
243
        Map<Integer, Integer> geoByUser = geoOffsiteByUser(s, ms, me, dtr);
346
        Map<Integer, Integer> geoByUser = geoOffsiteByUser(s, ms, me, dtr);
244
 
347
 
245
        List<Map<String, Object>> list = new ArrayList<>();
348
        List<Map<String, Object>> list = new ArrayList<>();
246
        for (Object o : q.getResultList()) {
349
        for (Object o : q.getResultList()) {
247
            Object[] r = (Object[]) o;
350
            Object[] r = (Object[]) o;
248
            long planned = asLong(r[3]), done = asLong(r[4]);
351
            long planned = asLong(r[3]), done = asLong(r[4]), donePlanned = asLong(r[9]);
249
            Integer duid = (r[8] == null) ? null : ((Number) r[8]).intValue();
352
            Integer duid = (r[8] == null) ? null : ((Number) r[8]).intValue();
250
            Map<String, Object> m = new LinkedHashMap<>();
353
            Map<String, Object> m = new LinkedHashMap<>();
251
            m.put("authUserId", asLong(r[0]));
354
            m.put("authUserId", asLong(r[0]));
252
            m.put("userId", duid == null ? 0 : duid);
355
            m.put("userId", duid == null ? 0 : duid);
253
            m.put("name", asStr(r[1]));
356
            m.put("name", asStr(r[1]));
254
            m.put("level", r[2] == null ? "-" : asStr(r[2]));
357
            m.put("level", r[2] == null ? "-" : asStr(r[2]));
255
            m.put("planned", planned);
358
            m.put("planned", planned);
256
            m.put("done", done);
359
            m.put("done", done);
-
 
360
            // Adherence = completed PLANNED visits ÷ planned visits (not all visits).
257
            m.put("adherencePct", planned == 0 ? 0 : Math.round(100.0 * done / planned));
361
            m.put("adherencePct", planned == 0 ? 0 : Math.round(100.0 * donePlanned / planned));
258
            m.put("discussionMin", asLong(r[5]));
362
            m.put("discussionMin", asLong(r[5]));
259
            m.put("workingHrs", round1(asDouble(r[6]) / 3600.0));
363
            m.put("workingHrs", round1(asDouble(r[6]) / 3600.0));
260
            m.put("leads", asLong(r[7]));
364
            m.put("leads", asLong(r[7]));
261
            m.put("geoFlags", duid == null ? 0 : geoByUser.getOrDefault(duid, 0));
365
            m.put("geoFlags", duid == null ? 0 : geoByUser.getOrDefault(duid, 0));
262
            list.add(m);
366
            list.add(m);
Line 360... Line 464...
360
        m.put("systemPct", total == 0 ? 0 : Math.round(100.0 * system / total));
464
        m.put("systemPct", total == 0 ? 0 : Math.round(100.0 * system / total));
361
        m.put("humanPct", total == 0 ? 0 : Math.round(100.0 * human / total));
465
        m.put("humanPct", total == 0 ? 0 : Math.round(100.0 * human / total));
362
        return m;
466
        return m;
363
    }
467
    }
364
 
468
 
-
 
469
    // ---- Auto "missed" breakdown -------------------------------------------
-
 
470
    // Three distinct execution gaps behind the "Auto missed" card:
-
 
471
    //   unscheduled    — executives (sales positions) with no PJP at all in range
-
 
472
    //   unfilledAgenda — scheduled beat-days whose agenda was never filled
-
 
473
    //   missedPunchin  — scheduled beat-days the owner never punched in on
-
 
474
    private Map<String, Object> missedBreakdown(Session s, String ms, String me,
-
 
475
                                                List<Integer> auth, List<Map<String, Object>> coverage) {
-
 
476
        // unscheduled execs: total at each level minus those with a plan (reuse coverage)
-
 
477
        long unscheduled = 0;
-
 
478
        for (Map<String, Object> c : coverage) {
-
 
479
            unscheduled += Math.max(asLong(c.get("totalUsers")) - asLong(c.get("withPjp")), 0);
-
 
480
        }
-
 
481
 
-
 
482
        NativeQuery<?> ua = s.createNativeQuery(
-
 
483
            "SELECT COUNT(*) FROM `user`.beat_schedule sc" +
-
 
484
            " JOIN `user`.beat b ON b.id=sc.beat_id AND b.active=1 AND b.auth_user_id IN (:auth)" +
-
 
485
            " WHERE sc.start_date>=:ms AND sc.start_date<=:me AND sc.agenda_filled_timestamp IS NULL");
-
 
486
        ua.setParameter("ms", ms).setParameter("me", me).setParameterList("auth", auth);
-
 
487
        long unfilledAgenda = asLong(ua.uniqueResult());
-
 
488
 
-
 
489
        NativeQuery<?> mp = s.createNativeQuery(
-
 
490
            "SELECT COUNT(*) FROM `user`.beat_schedule sc" +
-
 
491
            " JOIN `user`.beat b ON b.id=sc.beat_id AND b.active=1 AND b.auth_user_id IN (:auth)" +
-
 
492
            " JOIN auth.auth_user au ON au.id=b.auth_user_id" +
-
 
493
            " JOIN dtr.users du ON du.email=au.email_id" +
-
 
494
            " WHERE sc.start_date>=:ms AND sc.start_date<=:me AND NOT EXISTS (" +
-
 
495
            "   SELECT 1 FROM auth.location_tracking lt WHERE lt.user_id=du.id AND lt.task_date=sc.start_date" +
-
 
496
            "     AND (lt.mark_type='PUNCHIN' OR (lt.task_id=0 AND lt.check_in_time IS NOT NULL AND lt.check_in_time<>'00:00:00')))");
-
 
497
        mp.setParameter("ms", ms).setParameter("me", me).setParameterList("auth", auth);
-
 
498
        long missedPunchin = asLong(mp.uniqueResult());
-
 
499
 
-
 
500
        Map<String, Object> m = new LinkedHashMap<>();
-
 
501
        m.put("unscheduled", unscheduled);
-
 
502
        m.put("unfilledAgenda", unfilledAgenda);
-
 
503
        m.put("missedPunchin", missedPunchin);
-
 
504
        m.put("total", unscheduled + unfilledAgenda + missedPunchin);
-
 
505
        return m;
-
 
506
    }
-
 
507
 
365
    // ---- Drill-down lists ---------------------------------------------------
508
    // ---- Drill-down lists ---------------------------------------------------
366
 
509
 
367
    // Per-executive PJP coverage (with/without a plan in range) — powers the
510
    // Per-executive PJP coverage (with/without a plan in range) — powers the
368
    // PJP coverage KPI and the coverage-by-level bar drill-downs.
511
    // PJP coverage KPI and the coverage-by-level bar drill-downs.
369
    private List<Map<String, Object>> coverageDetail(Session s, String ms, String me, List<Integer> auth) {
512
    private List<Map<String, Object>> coverageDetail(Session s, String ms, String me, List<Integer> auth) {