Subversion Repositories SmartDukaan

Rev

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

Rev 37487 Rev 37497
Line 2068... Line 2068...
2068
    <div class="brand">
2068
    <div class="brand">
2069
      <div class="crumb"><a href="/indent/today_po_rbm">&larr; Today PO / RBM</a> &middot; Sales &middot; Indent</div>
2069
      <div class="crumb"><a href="/indent/today_po_rbm">&larr; Today PO / RBM</a> &middot; Sales &middot; Indent</div>
2070
      <h1>RBM DRR Dashboard <span class="live"><i></i>Live</span></h1>
2070
      <h1>RBM DRR Dashboard <span class="live"><i></i>Live</span></h1>
2071
    </div>
2071
    </div>
2072
    <div class="hd-right">
2072
    <div class="hd-right">
-
 
2073
      <select id="rdrrTypeSelect" class="ctl" style="min-width:120px;height:38px;padding:8px 12px" title="Filter type">
-
 
2074
        <option value="RBM" selected>RBM</option>
-
 
2075
        <option value="SALES">Sales</option>
-
 
2076
      </select>
2073
      <select id="rdrrRbmFilter" class="ctl" style="min-width:220px;height:38px;padding:8px 12px">
2077
      <select id="rdrrLevelSelect" class="ctl" style="min-width:140px;height:38px;padding:8px 12px;display:none"
-
 
2078
              title="Sales level">
-
 
2079
        <option value="">Select level</option>
-
 
2080
        <option value="ASM">ASM (L1)</option>
-
 
2081
        <option value="CM">CM (L2)</option>
-
 
2082
        <option value="DM">DM (L3)</option>
-
 
2083
        <option value="BM">BM (L4)</option>
-
 
2084
        <option value="RSM">RSM (L5)</option>
-
 
2085
        <option value="NSM">NSM (L6)</option>
-
 
2086
      </select>
-
 
2087
      <select id="rdrrPersonSelect" class="ctl" style="min-width:230px;height:38px;padding:8px 12px" title="Person">
2074
        <option value="">All RBMs</option>
2088
        <option value="">All RBMs</option>
2075
      </select>
2089
      </select>
2076
      <input class="ctl" id="rdrrPeriod" type="text" placeholder="Select date range"/>
2090
      <input class="ctl" id="rdrrPeriod" type="text" placeholder="Select date range"/>
2077
      <button class="btn-r" id="rdrrApply">Apply</button>
2091
      <button class="btn-r" id="rdrrApply">Apply</button>
2078
    </div>
2092
    </div>
Line 2408... Line 2422...
2408
      if (p) {
2422
      if (p) {
2409
        startDate = p.startDate.format("YYYY-MM-DD");
2423
        startDate = p.startDate.format("YYYY-MM-DD");
2410
        endDate = p.endDate.format("YYYY-MM-DD");
2424
        endDate = p.endDate.format("YYYY-MM-DD");
2411
      }
2425
      }
2412
      load();
2426
      load();
-
 
2427
      // If a Sales filter is currently active, re-fetch its data for the new range.
-
 
2428
      if (CURRENT_SALES_AUTH_ID) {
-
 
2429
        $.ajax({
-
 
2430
          url: "/rbm-drr/sales-data",
-
 
2431
          type: "GET",
-
 
2432
          data: {startDate: startDate, endDate: endDate},
-
 
2433
          dataType: "json",
-
 
2434
          success: function (payload) {
-
 
2435
            SALES_PAYLOAD = payload;
-
 
2436
            renderFiltered();
-
 
2437
          },
-
 
2438
          error: function () {
-
 
2439
            SALES_PAYLOAD = {rows: []};
-
 
2440
            renderFiltered();
-
 
2441
          }
-
 
2442
        });
-
 
2443
      }
2413
    });
2444
    });
2414
 
2445
 
2415
    function load() {
2446
    function load() {
2416
      $("#rdrrTableWrap").html('<div class="loading"><div class="spinner"></div>Loading&hellip;</div>');
2447
      $("#rdrrTableWrap").html('<div class="loading"><div class="spinner"></div>Loading&hellip;</div>');
2417
      $("#rdrrAgingWrap").html('<div class="loading"><div class="spinner"></div>Loading&hellip;</div>');
2448
      $("#rdrrAgingWrap").html('<div class="loading"><div class="spinner"></div>Loading&hellip;</div>');
Line 2575... Line 2606...
2575
    }
2606
    }
2576
 
2607
 
2577
    // Cached latest payload so the "Target Status by RBM" modal can render
2608
    // Cached latest payload so the "Target Status by RBM" modal can render
2578
    // without a re-fetch when the KPI card is clicked.
2609
    // without a re-fetch when the KPI card is clicked.
2579
    var LAST_PAYLOAD = null;
2610
    var LAST_PAYLOAD = null;
-
 
2611
    var SALES_PAYLOAD = null;              // fetched on-demand from /rbm-drr/sales-data
-
 
2612
    var CURRENT_SALES_AUTH_ID = null;      // when set, renderFiltered() uses SALES_PAYLOAD
-
 
2613
    // Set of RBM authIds allowed by the current filter. null = no filter (show all).
2580
    var CURRENT_FILTER_AUTH_ID = "";  // "" = all RBMs
2614
    var CURRENT_FILTER_AUTH_IDS = null;
2581
 
2615
 
2582
    function render(data) {
2616
    function render(data) {
2583
      LAST_PAYLOAD = data;
2617
      LAST_PAYLOAD = data;
2584
      populateRbmFilter(data.rows || []);
-
 
2585
      renderFiltered();
2618
      renderFiltered();
2586
    }
2619
    }
2587
 
2620
 
2588
    // Build the dropdown options from the fetched rows (unique RBM names).
2621
    // Renders KPIs + grid from the appropriate payload:
2589
    // Preserves current selection if the RBM still exists in the new dataset.
2622
    //  - Sales filter active → SALES_PAYLOAD filtered to selected sales person (rows are sales × warehouse)
2590
    function populateRbmFilter(rows) {
-
 
2591
      var seen = {};
-
 
2592
      var options = [];
-
 
2593
      rows.forEach(function (r) {
-
 
2594
        if (!seen[r.authId]) {
-
 
2595
          seen[r.authId] = true;
-
 
2596
          options.push({id: r.authId, name: r.rbmName});
-
 
2597
        }
-
 
2598
      });
-
 
2599
      options.sort(function (a, b) {
-
 
2600
        return (a.name || "").localeCompare(b.name || "");
-
 
2601
      });
-
 
2602
      var sel = $("#rdrrRbmFilter");
-
 
2603
      var prev = sel.val() || "";
-
 
2604
      var html = '<option value="">All RBMs (' + options.length + ')</option>';
-
 
2605
      options.forEach(function (o) {
-
 
2606
        html += '<option value="' + o.id + '">' + escapeHtml(o.name || "Unknown") + '</option>';
-
 
2607
      });
-
 
2608
      sel.html(html);
-
 
2609
      // Restore prior selection if still valid; otherwise reset.
-
 
2610
      if (prev && seen[prev]) sel.val(prev); else CURRENT_FILTER_AUTH_ID = "";
-
 
2611
    }
-
 
2612
 
-
 
2613
    // Renders KPIs + grid from LAST_PAYLOAD filtered by CURRENT_FILTER_AUTH_ID.
2623
    //  - RBM filter or no filter → LAST_PAYLOAD filtered by CURRENT_FILTER_AUTH_IDS (rows are RBM × warehouse)
2614
    function renderFiltered() {
2624
    function renderFiltered() {
-
 
2625
      if (CURRENT_SALES_AUTH_ID && SALES_PAYLOAD) {
-
 
2626
        var salesRows = (SALES_PAYLOAD.rows || []).filter(function (r) {
-
 
2627
          return String(r.salesAuthId) === String(CURRENT_SALES_AUTH_ID);
-
 
2628
        }).map(function (r) {
-
 
2629
          // Normalise sales row into the same shape the RBM grid renderer expects.
-
 
2630
          return {
-
 
2631
            authId: r.salesAuthId, warehouseId: r.warehouseId,
-
 
2632
            rbmName: r.salesName + (r.role ? " (" + r.role + ")" : ""),
-
 
2633
            warehouseName: r.warehouseName,
-
 
2634
            totalAch: r.totalAch, totalTgt: r.totalTgt,
-
 
2635
            hidAch: r.hidAch, hidTgt: r.hidTgt,
-
 
2636
            fastAch: r.fastAch, fastTgt: r.fastTgt,
-
 
2637
            runAch: r.runAch, runTgt: r.runTgt,
-
 
2638
            slowAch: r.slowAch, slowTgt: r.slowTgt,
-
 
2639
            otherAch: r.otherAch, otherTgt: r.otherTgt
-
 
2640
          };
-
 
2641
        });
-
 
2642
        renderRowsAsGrid(salesRows, true);
-
 
2643
        return;
-
 
2644
      }
2615
      if (!LAST_PAYLOAD) return;
2645
      if (!LAST_PAYLOAD) return;
2616
      var allRows = LAST_PAYLOAD.rows || [];
2646
      var allRows = LAST_PAYLOAD.rows || [];
2617
      var rows = CURRENT_FILTER_AUTH_ID
2647
      var rows = CURRENT_FILTER_AUTH_IDS
2618
              ? allRows.filter(function (r) {
2648
              ? allRows.filter(function (r) {
2619
                return String(r.authId) === String(CURRENT_FILTER_AUTH_ID);
2649
                return CURRENT_FILTER_AUTH_IDS[String(r.authId)];
2620
              })
2650
              })
2621
              : allRows;
2651
              : allRows;
-
 
2652
      renderRowsAsGrid(rows, false);
-
 
2653
    }
-
 
2654
 
-
 
2655
    // Extracted from renderFiltered() so both RBM-source and Sales-source paths share it.
-
 
2656
    function renderRowsAsGrid(rows, isSalesView) {
2622
 
2657
 
2623
      // Recompute totals from the filtered subset. Cheaper + correct vs using server totals.
2658
      // Recompute totals from the filtered subset. Cheaper + correct vs using server totals.
2624
      var t = {
2659
      var t = {
2625
        totalAch: 0, totalTgt: 0, hidAch: 0, hidTgt: 0, fastAch: 0, fastTgt: 0,
2660
        totalAch: 0, totalTgt: 0, hidAch: 0, hidTgt: 0, fastAch: 0, fastTgt: 0,
2626
        runAch: 0, runTgt: 0, slowAch: 0, slowTgt: 0, otherAch: 0, otherTgt: 0,
2661
        runAch: 0, runTgt: 0, slowAch: 0, slowTgt: 0, otherAch: 0, otherTgt: 0,
Line 2648... Line 2683...
2648
      $("#rdrrKpiPct").html(pk.cls === "na" ? '<span class="u">n/a</span>' :
2683
      $("#rdrrKpiPct").html(pk.cls === "na" ? '<span class="u">n/a</span>' :
2649
              '<span class="pct ' + pk.cls + '">' + pk.text + '</span>');
2684
              '<span class="pct ' + pk.cls + '">' + pk.text + '</span>');
2650
      $("#rdrrKpiTop").html((t.rbmsAtOrAbove100 || 0) + ' <span class="u">/ ' + (t.rbmRowCount || 0) + '</span>');
2685
      $("#rdrrKpiTop").html((t.rbmsAtOrAbove100 || 0) + ' <span class="u">/ ' + (t.rbmRowCount || 0) + '</span>');
2651
 
2686
 
2652
      if (rows.length === 0) {
2687
      if (rows.length === 0) {
-
 
2688
        var emptyMsg;
-
 
2689
        if (isSalesView) {
2653
        $("#rdrrTableWrap").html('<div class="empty"><div class="ei">📭</div>' +
2690
          emptyMsg = 'No Sales target/achievement rows for this person in the range. Has the pushSalesTargets cron run today?';
-
 
2691
        } else if (CURRENT_FILTER_AUTH_IDS) {
2654
                (CURRENT_FILTER_AUTH_ID ? 'This RBM has no rows in the current range.' : 'No RBM rows in this range.') +
2692
          emptyMsg = 'No RBM rows in this range for the selected filter.';
2655
                '</div>');
2693
        } else {
-
 
2694
          emptyMsg = 'No RBM rows in this range.';
-
 
2695
        }
-
 
2696
        $("#rdrrTableWrap").html('<div class="empty"><div class="ei">📭</div>' + emptyMsg + '</div>');
2656
        return;
2697
        return;
2657
      }
2698
      }
2658
 
2699
 
2659
      var buckets = [
2700
      var buckets = [
2660
        {key: "hid", label: "HID"},
2701
        {key: "hid", label: "HID"},
Line 2939... Line 2980...
2939
 
2980
 
2940
    function openStatusModal() {
2981
    function openStatusModal() {
2941
      if (!LAST_PAYLOAD) return;
2982
      if (!LAST_PAYLOAD) return;
2942
      // Respect the RBM filter so the modal matches what's on screen.
2983
      // Respect the RBM filter so the modal matches what's on screen.
2943
      var rows = (LAST_PAYLOAD.rows || []).filter(function (r) {
2984
      var rows = (LAST_PAYLOAD.rows || []).filter(function (r) {
2944
        return !CURRENT_FILTER_AUTH_ID || String(r.authId) === String(CURRENT_FILTER_AUTH_ID);
2985
        return !CURRENT_FILTER_AUTH_IDS || CURRENT_FILTER_AUTH_IDS[String(r.authId)];
2945
      });
2986
      });
2946
      var achieved = [], pending = [];
2987
      var achieved = [], pending = [];
2947
      rows.forEach(function (r) {
2988
      rows.forEach(function (r) {
2948
        if (!r.totalTgt || r.totalTgt <= 0) return;
2989
        if (!r.totalTgt || r.totalTgt <= 0) return;
2949
        var pct = (r.totalAch / r.totalTgt) * 100;
2990
        var pct = (r.totalAch / r.totalTgt) * 100;
Line 3210... Line 3251...
3210
    });
3251
    });
3211
    $("#rdrrCallDrawerBack").on("click", function (e) {
3252
    $("#rdrrCallDrawerBack").on("click", function (e) {
3212
      if (e.target === this) $(this).removeClass("show");
3253
      if (e.target === this) $(this).removeClass("show");
3213
    });
3254
    });
3214
 
3255
 
-
 
3256
    // ------------------------- cascading filter (Type → [Level] → Person) -------------------------
-
 
3257
    // Flow:
-
 
3258
    //   Type = RBM   → Person dropdown = all RBMs. Pick one to filter to that RBM.
-
 
3259
    //   Type = SALES → Level dropdown appears (ASM/CM/DM/BM/RSM/NSM). Pick a level
-
 
3260
    //                  → Person dropdown = active people at that level. Pick a person
-
 
3261
    //                  → fetches /rbm-drr/sales-data and shows that person's aggregated rows.
-
 
3262
    // One backend call caches both the RBM list and the Sales list.
-
 
3263
    var ALL_RBMS = [];         // [{authId, name}, ...]
-
 
3264
    var ALL_SALES_BY_ROLE = {}; // {ASM: [...], CM: [...], ...}
-
 
3265
 
-
 
3266
    function loadFilterOptions() {
-
 
3267
      $.ajax({
-
 
3268
        url: "/rbm-drr/all-sales", type: "GET", dataType: "json",
-
 
3269
        success: function (payload) {
-
 
3270
          ALL_RBMS = (payload && payload.rbms) || [];
-
 
3271
          ALL_SALES_BY_ROLE = {};
-
 
3272
          ((payload && payload.sales) || []).forEach(function (s) {
-
 
3273
            (ALL_SALES_BY_ROLE[s.role] = ALL_SALES_BY_ROLE[s.role] || []).push(s);
-
 
3274
          });
-
 
3275
          repopulatePersonForCurrentType();
-
 
3276
        }
-
 
3277
      });
-
 
3278
    }
-
 
3279
 
3215
    // RBM filter — re-render KPIs + grid from the cached payload (no re-fetch).
3280
    // Refill the Person dropdown based on current Type (+ Level if Sales).
-
 
3281
    function repopulatePersonForCurrentType() {
-
 
3282
      var type = $("#rdrrTypeSelect").val();
-
 
3283
      var personSel = $("#rdrrPersonSelect");
-
 
3284
      if (type === "RBM") {
-
 
3285
        var html = '<option value="">All RBMs (' + ALL_RBMS.length + ')</option>';
-
 
3286
        ALL_RBMS.forEach(function (r) {
-
 
3287
          html += '<option value="' + r.authId + '">' + escapeHtml(r.name || "Unknown") + '</option>';
-
 
3288
        });
-
 
3289
        personSel.html(html).prop("disabled", false);
-
 
3290
        return;
-
 
3291
      }
-
 
3292
      // SALES
-
 
3293
      var level = $("#rdrrLevelSelect").val();
-
 
3294
      if (!level) {
-
 
3295
        personSel.html('<option value="">Pick a level first</option>').prop("disabled", true);
-
 
3296
        return;
-
 
3297
      }
-
 
3298
      var list = ALL_SALES_BY_ROLE[level] || [];
-
 
3299
      var salesHtml = '<option value="">All ' + escapeHtml(level) + 's (' + list.length + ')</option>';
-
 
3300
      list.forEach(function (s) {
-
 
3301
        salesHtml += '<option value="' + s.authId + '">' + escapeHtml(s.name || "Unknown") + '</option>';
-
 
3302
      });
-
 
3303
      personSel.html(salesHtml).prop("disabled", false);
-
 
3304
    }
-
 
3305
 
-
 
3306
    // Type toggles: show/hide Level; reset filter state; refresh Person options.
3216
    $("#rdrrRbmFilter").on("change", function () {
3307
    $("#rdrrTypeSelect").on("change", function () {
-
 
3308
      var type = $(this).val();
-
 
3309
      if (type === "SALES") {
-
 
3310
        $("#rdrrLevelSelect").css("display", "").val("");
-
 
3311
      } else {
-
 
3312
        $("#rdrrLevelSelect").css("display", "none").val("");
-
 
3313
      }
-
 
3314
      // Clear all filter state whenever the type changes.
-
 
3315
      CURRENT_FILTER_AUTH_IDS = null;
-
 
3316
      CURRENT_SALES_AUTH_ID = null;
-
 
3317
      SALES_PAYLOAD = null;
-
 
3318
      repopulatePersonForCurrentType();
-
 
3319
      renderFiltered();
-
 
3320
    });
-
 
3321
 
-
 
3322
    $("#rdrrLevelSelect").on("change", function () {
-
 
3323
      // New level → clear Sales filter state and repopulate Person.
3217
      CURRENT_FILTER_AUTH_ID = $(this).val() || "";
3324
      CURRENT_FILTER_AUTH_IDS = null;
-
 
3325
      CURRENT_SALES_AUTH_ID = null;
-
 
3326
      SALES_PAYLOAD = null;
-
 
3327
      repopulatePersonForCurrentType();
3218
      renderFiltered();
3328
      renderFiltered();
3219
    });
3329
    });
3220
 
3330
 
-
 
3331
    $("#rdrrPersonSelect").on("change", function () {
-
 
3332
      var authId = $(this).val();
-
 
3333
      var type = $("#rdrrTypeSelect").val();
-
 
3334
 
-
 
3335
      if (type === "RBM") {
-
 
3336
        // RBM filter (single authId) — no server round trip needed.
-
 
3337
        CURRENT_SALES_AUTH_ID = null;
-
 
3338
        SALES_PAYLOAD = null;
-
 
3339
        if (!authId) {
-
 
3340
          CURRENT_FILTER_AUTH_IDS = null;
-
 
3341
        } else {
-
 
3342
          var single = {};
-
 
3343
          single[String(authId)] = true;
-
 
3344
          CURRENT_FILTER_AUTH_IDS = single;
-
 
3345
        }
-
 
3346
        renderFiltered();
-
 
3347
        return;
-
 
3348
      }
-
 
3349
 
-
 
3350
      // SALES type
-
 
3351
      CURRENT_FILTER_AUTH_IDS = null;
-
 
3352
      if (!authId) {
-
 
3353
        // "All <role>s" — no filter (show everyone in the DRR grid, RBM view).
-
 
3354
        CURRENT_SALES_AUTH_ID = null;
-
 
3355
        SALES_PAYLOAD = null;
-
 
3356
        renderFiltered();
-
 
3357
        return;
-
 
3358
      }
-
 
3359
      // Pick a specific Sales person → fetch that person's aggregated data.
-
 
3360
      CURRENT_SALES_AUTH_ID = authId;
-
 
3361
      $("#rdrrTableWrap").html('<div class="loading"><div class="spinner"></div>Loading&hellip;</div>');
-
 
3362
      $.ajax({
-
 
3363
        url: "/rbm-drr/sales-data", type: "GET",
-
 
3364
        data: {startDate: startDate, endDate: endDate}, dataType: "json",
-
 
3365
        success: function (payload) {
-
 
3366
          SALES_PAYLOAD = payload;
-
 
3367
          renderFiltered();
-
 
3368
        },
-
 
3369
        error: function () {
-
 
3370
          SALES_PAYLOAD = {rows: []};
-
 
3371
          renderFiltered();
-
 
3372
        }
-
 
3373
      });
-
 
3374
    });
-
 
3375
 
3221
    // initial load
3376
    // initial load
-
 
3377
    loadFilterOptions();
3222
    load();
3378
    load();
3223
    loadPurchaseMilestones();
3379
    loadPurchaseMilestones();
3224
    loadPerformanceDashboard();
3380
    loadPerformanceDashboard();
3225
    loadCallTargetIframe();
3381
    loadCallTargetIframe();
3226
  })();
3382
  })();