Subversion Repositories SmartDukaan

Rev

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

Rev 37517 Rev 37522
Line 1888... Line 1888...
1888
      font-weight: 770;
1888
      font-weight: 770;
1889
      font-size: 13.5px;
1889
      font-size: 13.5px;
1890
      font-variant-numeric: tabular-nums
1890
      font-variant-numeric: tabular-nums
1891
    }
1891
    }
1892
 
1892
 
1893
    /* purchase-milestones (brand × RBM) container — restyles the injected Bootstrap fragment */
1893
    /* purchase-milestones (brand × person) — compact heatmap grid, custom-rendered client-side */
1894
    #rdrrPmWrap {
1894
    #rdrrPmWrap {
1895
      overflow-x: auto
1895
      overflow: hidden
1896
    }
1896
    }
1897
 
1897
 
1898
    #rdrrPmWrap table {
1898
    #rdrrPmWrap .pm-grid-wrap {
1899
      width: 100%;
1899
      overflow: auto;
-
 
1900
      max-height: 640px;
-
 
1901
      border: 1px solid var(--line);
-
 
1902
      border-radius: 10px;
-
 
1903
      background: #fff
-
 
1904
    }
-
 
1905
 
-
 
1906
    #rdrrPmWrap .pm-grid {
1900
      border-collapse: separate;
1907
      border-collapse: separate;
1901
      border-spacing: 0;
1908
      border-spacing: 0;
-
 
1909
      width: 100%;
1902
      font-size: 12.5px;
1910
      font-size: 12.5px;
1903
      margin-bottom: 12px;
-
 
1904
      background: #fff
1911
      background: #fff
1905
    }
1912
    }
1906
 
1913
 
-
 
1914
    /* --- header (brand strip) --- */
1907
    #rdrrPmWrap table th, #rdrrPmWrap table td {
1915
    #rdrrPmWrap .pm-grid thead th {
-
 
1916
      position: sticky;
-
 
1917
      top: 0;
-
 
1918
      z-index: 3;
1908
      padding: 8px 8px;
1919
      padding: 10px 12px;
-
 
1920
      color: #fff;
1909
      border-bottom: 1px solid var(--line2);
1921
      font-weight: 700;
1910
      border-right: 1px solid var(--line2);
1922
      letter-spacing: .06em;
1911
      text-align: right;
1923
      font-size: 11.5px;
1912
      font-variant-numeric: tabular-nums;
1924
      text-transform: uppercase;
1913
      color: var(--ink);
1925
      text-align: center;
1914
      vertical-align: middle
1926
      white-space: nowrap;
-
 
1927
      background: linear-gradient(180deg, var(--brand2), var(--brand));
-
 
1928
      border-bottom: 2px solid rgba(0, 0, 0, .08);
-
 
1929
      box-shadow: 0 2px 0 rgba(0, 0, 0, .04)
1915
    }
1930
    }
1916
 
1931
 
1917
    #rdrrPmWrap table th:first-child, #rdrrPmWrap table td:first-child {
1932
    #rdrrPmWrap .pm-grid thead th.pm-corner {
-
 
1933
      left: 0;
-
 
1934
      z-index: 5;
-
 
1935
      background: #fff;
-
 
1936
      color: var(--ink);
1918
      text-align: left;
1937
      text-align: left;
-
 
1938
      min-width: 180px;
1919
      border-left: 1px solid var(--line2);
1939
      border-right: 1px solid var(--line);
-
 
1940
      padding: 8px 14px
-
 
1941
    }
-
 
1942
 
-
 
1943
    #rdrrPmWrap .pm-corner-lbl {
-
 
1944
      font-size: 12px;
1920
      font-weight: 650;
1945
      font-weight: 700;
-
 
1946
      letter-spacing: .05em;
-
 
1947
      color: var(--ink)
-
 
1948
    }
-
 
1949
 
-
 
1950
    #rdrrPmWrap .pm-corner-hint {
-
 
1951
      font-size: 10.5px;
-
 
1952
      color: var(--soft);
-
 
1953
      font-weight: 500;
-
 
1954
      letter-spacing: .02em;
-
 
1955
      text-transform: none;
-
 
1956
      margin-top: 2px
-
 
1957
    }
-
 
1958
 
-
 
1959
    /* --- rows --- */
-
 
1960
    #rdrrPmWrap .pm-grid tbody tr.pm-row + tr.pm-row td {
-
 
1961
      border-top: 1px solid var(--line2)
-
 
1962
    }
-
 
1963
 
-
 
1964
    #rdrrPmWrap .pm-grid tbody tr.pm-row:hover td:not(.pm-name) {
-
 
1965
      outline: 1px solid rgba(59, 91, 219, .35);
-
 
1966
      outline-offset: -1px
-
 
1967
    }
-
 
1968
 
-
 
1969
    #rdrrPmWrap .pm-grid tbody tr.pm-row:hover .pm-name {
-
 
1970
      background: #eef2fb
-
 
1971
    }
-
 
1972
 
-
 
1973
    /* --- name column (sticky) --- */
-
 
1974
    #rdrrPmWrap .pm-name {
1921
      position: sticky;
1975
      position: sticky;
1922
      left: 0;
1976
      left: 0;
-
 
1977
      z-index: 2;
1923
      background: #fff;
1978
      background: #fff;
1924
      z-index: 1;
1979
      padding: 10px 14px;
1925
      min-width: 130px
1980
      min-width: 180px;
-
 
1981
      max-width: 220px;
-
 
1982
      border-right: 1px solid var(--line);
-
 
1983
      vertical-align: middle
1926
    }
1984
    }
1927
 
1985
 
1928
    #rdrrPmWrap table thead th {
1986
    #rdrrPmWrap .pm-name-txt {
-
 
1987
      font-weight: 650;
1929
      text-transform: uppercase;
1988
      font-size: 13px;
-
 
1989
      color: var(--ink);
-
 
1990
      line-height: 1.2;
1930
      letter-spacing: .04em;
1991
      white-space: nowrap;
-
 
1992
      overflow: hidden;
-
 
1993
      text-overflow: ellipsis
-
 
1994
    }
-
 
1995
 
-
 
1996
    #rdrrPmWrap .pm-name-sub {
1931
      font-size: 10.5px;
1997
      font-size: 10.5px;
1932
      color: var(--soft);
1998
      color: var(--soft);
-
 
1999
      margin-top: 2px;
-
 
2000
      letter-spacing: .04em;
-
 
2001
      text-transform: uppercase;
-
 
2002
      font-weight: 600
-
 
2003
    }
-
 
2004
 
-
 
2005
    /* --- data cells --- */
-
 
2006
    #rdrrPmWrap .pm-cell {
-
 
2007
      padding: 9px 12px;
-
 
2008
      text-align: right;
-
 
2009
      vertical-align: middle;
-
 
2010
      font-variant-numeric: tabular-nums;
-
 
2011
      color: var(--ink);
-
 
2012
      transition: outline-color .1s
-
 
2013
    }
-
 
2014
 
-
 
2015
    #rdrrPmWrap .pm-cell-zero {
-
 
2016
      color: #cbd0d9;
-
 
2017
      font-size: 15px;
-
 
2018
      font-weight: 500
-
 
2019
    }
-
 
2020
 
-
 
2021
    #rdrrPmWrap .pm-billed {
1933
      font-weight: 700;
2022
      font-weight: 700;
1934
      background: #fafbfe;
2023
      font-size: 13.5px;
1935
      border-top: 1px solid var(--line);
-
 
1936
      border-bottom: 1px solid var(--line);
2024
      line-height: 1.15;
1937
      text-align: center
2025
      color: var(--ink)
1938
    }
2026
    }
1939
 
2027
 
1940
    #rdrrPmWrap table thead th[colspan] {
2028
    #rdrrPmWrap .pm-meta {
-
 
2029
      margin-top: 3px;
1941
      font-size: 12px;
2030
      font-size: 10.5px;
-
 
2031
      color: var(--soft);
-
 
2032
      font-weight: 500;
-
 
2033
      letter-spacing: .02em;
1942
      padding: 8px 6px;
2034
      display: flex;
-
 
2035
      justify-content: flex-end;
1943
      color: #fff
2036
      gap: 8px;
-
 
2037
      line-height: 1.15
1944
    }
2038
    }
1945
 
2039
 
1946
    #rdrrPmWrap table thead th p {
2040
    #rdrrPmWrap .pm-partners {
1947
      margin: 0;
-
 
1948
      font-size: 13px;
2041
      color: #177a3a
1949
      font-weight: 770;
-
 
1950
      letter-spacing: .01em;
-
 
1951
      line-height: 1.1
-
 
1952
    }
2042
    }
1953
 
2043
 
1954
    #rdrrPmWrap table thead tr:first-child th:first-child {
-
 
1955
      background: #fafbfe;
2044
    #rdrrPmWrap .pm-po {
1956
      color: var(--soft)
2045
      color: #4a55a8
1957
    }
2046
    }
1958
 
2047
 
1959
    #rdrrPmWrap table tbody tr:hover {
2048
    #rdrrPmWrap .pm-unit {
-
 
2049
      font-size: 9.5px;
-
 
2050
      opacity: .75;
1960
      background: #f6f8fd
2051
      margin-left: 1px
1961
    }
2052
    }
1962
 
2053
 
-
 
2054
    /* --- totals row (pinned at top, distinct) --- */
1963
    #rdrrPmWrap table tbody tr:hover td:first-child {
2055
    #rdrrPmWrap .pm-total-row .pm-name {
1964
      background: #f6f8fd
2056
      background: linear-gradient(180deg, #f2f5fb, #eaeff9);
-
 
2057
      border-right: 1px solid var(--line);
-
 
2058
      border-bottom: 2px solid var(--brand)
1965
    }
2059
    }
1966
 
2060
 
1967
    #rdrrPmWrap table tr.green td {
2061
    #rdrrPmWrap .pm-total-row .pm-name-txt {
1968
      background: #eaf7ef !important;
2062
      color: var(--brand);
1969
      font-weight: 770;
2063
      font-weight: 800
1970
      color: #177a3a
-
 
1971
    }
2064
    }
1972
 
2065
 
1973
    #rdrrPmWrap table tr.green td:first-child {
2066
    #rdrrPmWrap .pm-total-row .pm-name-sub {
1974
      background: #eaf7ef !important;
2067
      color: var(--brand);
1975
      color: #177a3a
2068
      opacity: .8
-
 
2069
    }
-
 
2070
 
-
 
2071
    #rdrrPmWrap .pm-cell-total {
-
 
2072
      background: linear-gradient(180deg, #f2f5fb, #eaeff9) !important;
-
 
2073
      border-bottom: 2px solid var(--brand)
1976
    }
2074
    }
1977
 
2075
 
-
 
2076
    #rdrrPmWrap .pm-cell-total .pm-billed {
-
 
2077
      color: var(--brand);
-
 
2078
      font-weight: 800;
-
 
2079
      font-size: 14px
-
 
2080
    }
-
 
2081
 
1978
    /* known brand accents (extend as more brands appear) */
2082
    /* --- brand accent header gradients (slug matches JS output) --- */
-
 
2083
    #rdrrPmWrap .pm-grid thead th.total {
-
 
2084
      background: linear-gradient(180deg, #3b5bdb, #28449e)
-
 
2085
    }
-
 
2086
 
1979
    #rdrrPmWrap table thead th.vivo {
2087
    #rdrrPmWrap .pm-grid thead th.vivo {
1980
      background: linear-gradient(180deg, #00baf2, #0a8fb5)
2088
      background: linear-gradient(180deg, #00baf2, #0a8fb5)
1981
    }
2089
    }
1982
 
2090
 
1983
    #rdrrPmWrap table thead th.oppo {
2091
    #rdrrPmWrap .pm-grid thead th.oppo {
1984
      background: linear-gradient(180deg, #20a558, #177a3a)
2092
      background: linear-gradient(180deg, #20a558, #177a3a)
1985
    }
2093
    }
1986
 
2094
 
1987
    #rdrrPmWrap table thead th.samsung {
2095
    #rdrrPmWrap .pm-grid thead th.samsung {
1988
      background: linear-gradient(180deg, #0b3d91, #071e51)
2096
      background: linear-gradient(180deg, #0b3d91, #071e51)
1989
    }
2097
    }
1990
 
2098
 
1991
    #rdrrPmWrap table thead th.alcatel {
2099
    #rdrrPmWrap .pm-grid thead th.alcatel {
1992
      background: linear-gradient(180deg, #5c7cfa, #3b5bdb)
2100
      background: linear-gradient(180deg, #5c7cfa, #3b5bdb)
1993
    }
2101
    }
1994
 
2102
 
1995
    #rdrrPmWrap table thead th.motorola {
2103
    #rdrrPmWrap .pm-grid thead th.motorola {
1996
      background: linear-gradient(180deg, #495057, #212529)
2104
      background: linear-gradient(180deg, #495057, #212529)
1997
    }
2105
    }
1998
 
2106
 
1999
    #rdrrPmWrap table thead th.hmd {
2107
    #rdrrPmWrap .pm-grid thead th.hmd {
2000
      background: linear-gradient(180deg, #7048e8, #5b34c9)
2108
      background: linear-gradient(180deg, #7048e8, #5b34c9)
2001
    }
2109
    }
2002
 
2110
 
2003
    #rdrrPmWrap table thead th.xiaomi {
2111
    #rdrrPmWrap .pm-grid thead th.xiaomi {
2004
      background: linear-gradient(180deg, #ffa726, #e8590c)
2112
      background: linear-gradient(180deg, #ffa726, #e8590c)
2005
    }
2113
    }
2006
 
2114
 
2007
    #rdrrPmWrap table thead th.lava {
2115
    #rdrrPmWrap .pm-grid thead th.lava {
2008
      background: linear-gradient(180deg, #e03131, #a11515)
2116
      background: linear-gradient(180deg, #e03131, #a11515)
2009
    }
2117
    }
2010
 
2118
 
2011
    #rdrrPmWrap table thead th.poco {
2119
    #rdrrPmWrap .pm-grid thead th.poco {
2012
      background: linear-gradient(180deg, #ffd43b, #e8ac00);
2120
      background: linear-gradient(180deg, #ffd43b, #e8ac00);
2013
      color: #3a2500
2121
      color: #3a2500
2014
    }
2122
    }
2015
 
2123
 
2016
    #rdrrPmWrap table thead th.realme {
2124
    #rdrrPmWrap .pm-grid thead th.realme {
2017
      background: linear-gradient(180deg, #f59f00, #e8590c)
2125
      background: linear-gradient(180deg, #f59f00, #e8590c)
2018
    }
2126
    }
2019
 
2127
 
2020
    #rdrrPmWrap table thead th.apple {
2128
    #rdrrPmWrap .pm-grid thead th.apple {
2021
      background: linear-gradient(180deg, #495057, #141a2b)
2129
      background: linear-gradient(180deg, #495057, #141a2b)
2022
    }
2130
    }
2023
 
2131
 
2024
    #rdrrPmWrap table thead th.oneplus {
2132
    #rdrrPmWrap .pm-grid thead th.oneplus {
2025
      background: linear-gradient(180deg, #e03131, #a11515)
2133
      background: linear-gradient(180deg, #e03131, #a11515)
2026
    }
2134
    }
2027
 
2135
 
2028
    #rdrrPmWrap table thead th.tecno {
2136
    #rdrrPmWrap .pm-grid thead th.tecno {
2029
      background: linear-gradient(180deg, #0ca678, #087a56)
2137
      background: linear-gradient(180deg, #0ca678, #087a56)
2030
    }
2138
    }
2031
 
2139
 
2032
    #rdrrPmWrap table thead th.ai + {
-
 
2033
      background: linear-gradient(180deg, #7048e8, #5b34c9)
-
 
2034
    }
-
 
2035
 
-
 
2036
    #rdrrPmWrap table thead th.total {
-
 
2037
      background: linear-gradient(180deg, #3b5bdb, #28449e)
-
 
2038
    }
-
 
2039
 
-
 
2040
    /* default gradient for any unmapped brand */
-
 
2041
    #rdrrPmWrap table thead th[colspan]:not(.vivo):not(.oppo):not(.samsung):not(.alcatel):not(.motorola):not(.hmd):not(.xiaomi):not(.lava):not(.poco):not(.realme):not(.apple):not(.oneplus):not(.tecno):not(.total) {
-
 
2042
      background: linear-gradient(180deg, var(--brand2), var(--brand))
-
 
2043
    }
-
 
2044
 
-
 
2045
    #rdrrPmWrap table td.currency {
-
 
2046
      font-weight: 600
-
 
2047
    }
-
 
2048
 
-
 
2049
    @media (max-width: 960px) {
2140
    @media (max-width: 960px) {
2050
      .kpis {
2141
      .kpis {
2051
        grid-template-columns:repeat(2, 1fr)
2142
        grid-template-columns:repeat(2, 1fr)
2052
      }
2143
      }
2053
 
2144
 
Line 3050... Line 3141...
3050
      return html;
3141
      return html;
3051
    }
3142
    }
3052
 
3143
 
3053
    // ------------------------- Today PO & Billed matrix -------------------------
3144
    // ------------------------- Today PO & Billed matrix -------------------------
3054
    // Reuses the existing /indent/today_po_table_fragment endpoint (unchanged).
3145
    // Reuses the existing /indent/today_po_table_fragment endpoint (unchanged).
3055
    // Injects the returned HTML and restyles it via #rdrrPmWrap CSS.
3146
    // Parses the returned HTML into a data model and re-renders a compact
-
 
3147
    // heatmap grid — one row per person, one column per brand, cell showing
-
 
3148
    // Billed (primary) + Partners/PO (secondary), with per-brand heat shading.
3056
    function loadPurchaseMilestones() {
3149
    function loadPurchaseMilestones() {
3057
      var cat = $("#rdrrPmCategory").val() || "RBM";
3150
      var cat = $("#rdrrPmCategory").val() || "RBM";
3058
      $("#rdrrPmWrap").html('<div class="loading"><div class="spinner"></div>Loading&hellip;</div>');
3151
      $("#rdrrPmWrap").html('<div class="loading"><div class="spinner"></div>Loading&hellip;</div>');
3059
      $.ajax({
3152
      $.ajax({
3060
        url: "/indent/today_po_table_fragment",
3153
        url: "/indent/today_po_table_fragment",
3061
        type: "GET",
3154
        type: "GET",
3062
        data: {categoryType: cat},
3155
        data: {categoryType: cat},
3063
        success: function (html) {
3156
        success: function (html) {
-
 
3157
          try {
3064
          $("#rdrrPmWrap").html(html);
3158
            var model = parsePurchaseMilestonesFragment(html);
3065
          $("#rdrrPmWrap .currency").each(function () {
3159
            $("#rdrrPmWrap").html(renderPurchaseMilestonesGrid(model));
3066
            var elem = $(this);
3160
          } catch (e) {
3067
            var n = parseInt(elem.text().replace(/[^\d.-]/g, ""), 10);
3161
            $("#rdrrPmWrap").html('<div class="empty">Failed to render purchase-milestones data.</div>');
3068
            if (!isNaN(n)) elem.text(numberToIndianCurrency(n));
-
 
3069
          });
3162
          }
3070
        },
3163
        },
3071
        error: function () {
3164
        error: function () {
3072
          $("#rdrrPmWrap").html('<div class="empty">Failed to load purchase-milestones data.</div>');
3165
          $("#rdrrPmWrap").html('<div class="empty">Failed to load purchase-milestones data.</div>');
3073
        }
3166
        }
3074
      });
3167
      });
Line 3081... Line 3174...
3081
      var other = s.substring(0, s.length - 3);
3174
      var other = s.substring(0, s.length - 3);
3082
      if (other !== "") lastThree = "," + lastThree;
3175
      if (other !== "") lastThree = "," + lastThree;
3083
      return other.replace(/\B(?=(\d{2})+(?!\d))/g, ",") + lastThree;
3176
      return other.replace(/\B(?=(\d{2})+(?!\d))/g, ",") + lastThree;
3084
    }
3177
    }
3085
 
3178
 
-
 
3179
    // Compact currency: >= 1L => "12.4 L", >= 1K => "45.2 K", else raw.
-
 
3180
    function pmCompactCurrency(n) {
-
 
3181
      n = Number(n) || 0;
-
 
3182
      if (n >= 10000000) return "₹" + (n / 10000000).toFixed(2) + " Cr";
-
 
3183
      if (n >= 100000) return "₹" + (n / 100000).toFixed(2) + " L";
-
 
3184
      if (n >= 1000) return "₹" + (n / 1000).toFixed(1) + " K";
-
 
3185
      return "₹" + numberToIndianCurrency(Math.round(n));
-
 
3186
    }
-
 
3187
 
-
 
3188
    // Parse the injected multi-table fragment into:
-
 
3189
    //   { brands: [{name, slug}], rows: [{name, cells: {brand: {po, count, billed}}, totals: {po, count, billed}}] }
-
 
3190
    function parsePurchaseMilestonesFragment(html) {
-
 
3191
      var doc = new DOMParser().parseFromString(html, "text/html");
-
 
3192
      var tables = doc.querySelectorAll("table");
-
 
3193
      var brandOrder = [];
-
 
3194
      var brandSet = {};
-
 
3195
      var rowsMap = {};   // name -> {name, order, cells}
-
 
3196
      var rowOrder = [];
-
 
3197
 
-
 
3198
      tables.forEach(function (tbl) {
-
 
3199
        var theadRows = tbl.querySelectorAll("thead tr");
-
 
3200
        if (theadRows.length < 1) return;
-
 
3201
        // First thead row: [AuthUser th] + brand ths (colspan=3)
-
 
3202
        var brandThs = theadRows[0].querySelectorAll("th[colspan]");
-
 
3203
        var brands = [];
-
 
3204
        brandThs.forEach(function (th) {
-
 
3205
          var label = (th.textContent || "").trim().toUpperCase();
-
 
3206
          if (!label) return;
-
 
3207
          brands.push(label);
-
 
3208
          if (!brandSet[label]) {
-
 
3209
            brandSet[label] = true;
-
 
3210
            brandOrder.push({name: label, slug: label.toLowerCase().replace(/[^a-z0-9]+/g, "")});
-
 
3211
          }
-
 
3212
        });
-
 
3213
 
-
 
3214
        tbl.querySelectorAll("tbody tr").forEach(function (tr) {
-
 
3215
          var tds = tr.querySelectorAll("td");
-
 
3216
          if (tds.length < 1 + brands.length * 3) return;
-
 
3217
          var name = (tds[0].textContent || "").trim();
-
 
3218
          if (!name) return;
-
 
3219
          var row = rowsMap[name];
-
 
3220
          if (!row) {
-
 
3221
            row = {name: name, cells: {}, isTotal: /^all\b/i.test(name)};
-
 
3222
            rowsMap[name] = row;
-
 
3223
            rowOrder.push(name);
-
 
3224
          }
-
 
3225
          for (var i = 0; i < brands.length; i++) {
-
 
3226
            var b = brands[i];
-
 
3227
            var po = parseFloat((tds[1 + i * 3].textContent || "0").replace(/[^\d.-]/g, "")) || 0;
-
 
3228
            var count = parseInt((tds[1 + i * 3 + 1].textContent || "0").replace(/[^\d.-]/g, ""), 10) || 0;
-
 
3229
            var billed = parseFloat((tds[1 + i * 3 + 2].textContent || "0").replace(/[^\d.-]/g, "")) || 0;
-
 
3230
            row.cells[b] = {po: po, count: count, billed: billed};
-
 
3231
          }
-
 
3232
        });
-
 
3233
      });
-
 
3234
 
-
 
3235
      // Pull the TOTAL brand out of brandOrder and keep it first (fixed column).
-
 
3236
      var totalIdx = brandOrder.findIndex(function (b) {
-
 
3237
        return b.name === "TOTAL";
-
 
3238
      });
-
 
3239
      var totalBrand = totalIdx >= 0 ? brandOrder.splice(totalIdx, 1)[0] : null;
-
 
3240
 
-
 
3241
      // Sort remaining brands by grand total Billed (biggest first) using the "All …" row if present.
-
 
3242
      var totalRowName = rowOrder.find(function (n) {
-
 
3243
        return /^all\b/i.test(n);
-
 
3244
      });
-
 
3245
      var totalRow = totalRowName ? rowsMap[totalRowName] : null;
-
 
3246
      if (totalRow) {
-
 
3247
        brandOrder.sort(function (a, b) {
-
 
3248
          var av = (totalRow.cells[a.name] || {}).billed || 0;
-
 
3249
          var bv = (totalRow.cells[b.name] || {}).billed || 0;
-
 
3250
          return bv - av;
-
 
3251
        });
-
 
3252
      }
-
 
3253
      if (totalBrand) brandOrder.unshift(totalBrand);
-
 
3254
 
-
 
3255
      // Order rows: total first, others by grand total Billed (from TOTAL brand or sum).
-
 
3256
      var totalRowIdx = totalRowName ? rowOrder.indexOf(totalRowName) : -1;
-
 
3257
      if (totalRowIdx > 0) {
-
 
3258
        rowOrder.splice(totalRowIdx, 1);
-
 
3259
        rowOrder.unshift(totalRowName);
-
 
3260
      }
-
 
3261
      var head = totalRowIdx >= 0 ? [rowOrder.shift()] : [];
-
 
3262
      rowOrder.sort(function (a, b) {
-
 
3263
        var av = (rowsMap[a].cells["TOTAL"] || {}).billed || 0;
-
 
3264
        var bv = (rowsMap[b].cells["TOTAL"] || {}).billed || 0;
-
 
3265
        return bv - av;
-
 
3266
      });
-
 
3267
      rowOrder = head.concat(rowOrder);
-
 
3268
 
-
 
3269
      // Per-brand max Billed for heatmap intensity (excludes the totals row).
-
 
3270
      var brandMax = {};
-
 
3271
      brandOrder.forEach(function (b) {
-
 
3272
        var max = 0;
-
 
3273
        rowOrder.forEach(function (name) {
-
 
3274
          var row = rowsMap[name];
-
 
3275
          if (row.isTotal) return;
-
 
3276
          var v = (row.cells[b.name] || {}).billed || 0;
-
 
3277
          if (v > max) max = v;
-
 
3278
        });
-
 
3279
        brandMax[b.name] = max;
-
 
3280
      });
-
 
3281
 
-
 
3282
      var rows = rowOrder.map(function (n) {
-
 
3283
        return rowsMap[n];
-
 
3284
      });
-
 
3285
      return {brands: brandOrder, rows: rows, brandMax: brandMax};
-
 
3286
    }
-
 
3287
 
-
 
3288
    function renderPurchaseMilestonesGrid(model) {
-
 
3289
      if (!model.rows.length || !model.brands.length) {
-
 
3290
        return '<div class="empty">No data.</div>';
-
 
3291
      }
-
 
3292
      var html = ['<div class="pm-grid-wrap"><table class="pm-grid"><thead><tr>'];
-
 
3293
      html.push('<th class="pm-corner">' +
-
 
3294
              '<div class="pm-corner-lbl">Person</div>' +
-
 
3295
              '<div class="pm-corner-hint">' + model.rows.length + ' · ' + model.brands.length + ' brands</div>' +
-
 
3296
              '</th>');
-
 
3297
      model.brands.forEach(function (b) {
-
 
3298
        html.push('<th class="pm-brand ' + b.slug + '">' + b.name + '</th>');
-
 
3299
      });
-
 
3300
      html.push('</tr></thead><tbody>');
-
 
3301
 
-
 
3302
      model.rows.forEach(function (row) {
-
 
3303
        html.push('<tr class="pm-row' + (row.isTotal ? ' pm-total-row' : '') + '">');
-
 
3304
        html.push('<td class="pm-name">' +
-
 
3305
                '<div class="pm-name-txt">' + escapeHtml(row.name) + '</div>' +
-
 
3306
                (row.isTotal ? '<div class="pm-name-sub">Grand total</div>' : '') +
-
 
3307
                '</td>');
-
 
3308
        model.brands.forEach(function (b) {
-
 
3309
          var c = row.cells[b.name] || {po: 0, count: 0, billed: 0};
-
 
3310
          var max = model.brandMax[b.name] || 0;
-
 
3311
          var intensity = (!row.isTotal && max > 0) ? Math.min(1, c.billed / max) : 0;
-
 
3312
          var isZero = c.billed === 0 && c.po === 0 && c.count === 0;
-
 
3313
          var bgAlpha = intensity > 0 ? (0.10 + intensity * 0.55).toFixed(2) : 0;
-
 
3314
          var style = (!row.isTotal && intensity > 0)
-
 
3315
                  ? ' style="background: rgba(59,91,219,' + bgAlpha + ')"' : '';
-
 
3316
          if (isZero) {
-
 
3317
            html.push('<td class="pm-cell pm-cell-zero">&mdash;</td>');
-
 
3318
          } else {
-
 
3319
            var billedTxt = pmCompactCurrency(c.billed);
-
 
3320
            var poTxt = c.po > 0 ? pmCompactCurrency(c.po) : '';
-
 
3321
            var partnerTxt = c.count > 0 ? (c.count + ' <span class="pm-unit">ptr</span>') : '';
-
 
3322
            html.push('<td class="pm-cell' + (row.isTotal ? ' pm-cell-total' : '') + '"' + style + '>' +
-
 
3323
                    '<div class="pm-billed">' + billedTxt + '</div>' +
-
 
3324
                    '<div class="pm-meta">' +
-
 
3325
                    (partnerTxt ? '<span class="pm-partners">' + partnerTxt + '</span>' : '') +
-
 
3326
                    (poTxt ? '<span class="pm-po">' + poTxt + '<span class="pm-unit"> PO</span></span>' : '') +
-
 
3327
                    '</div>' +
-
 
3328
                    '</td>');
-
 
3329
          }
-
 
3330
        });
-
 
3331
        html.push('</tr>');
-
 
3332
      });
-
 
3333
      html.push('</tbody></table></div>');
-
 
3334
      return html.join("");
-
 
3335
    }
-
 
3336
 
-
 
3337
    function escapeHtml(s) {
-
 
3338
      return String(s == null ? "" : s)
-
 
3339
              .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
-
 
3340
              .replace(/"/g, "&quot;").replace(/'/g, "&#39;");
-
 
3341
    }
-
 
3342
 
3086
    $("#rdrrPmCategory").on("change", loadPurchaseMilestones);
3343
    $("#rdrrPmCategory").on("change", loadPurchaseMilestones);
3087
 
3344
 
3088
    // ------------------------- RBM Performance Dashboard (MTD) -------------------------
3345
    // ------------------------- RBM Performance Dashboard (MTD) -------------------------
3089
    // No dedicated backend endpoint exists — the perf-dashboard table is only rendered
3346
    // No dedicated backend endpoint exists — the perf-dashboard table is only rendered
3090
    // inline in /indent/today_po_rbm. We fetch that page and extract just the table.
3347
    // inline in /indent/today_po_rbm. We fetch that page and extract just the table.