Pārlūkot izejas kodu

fix col widths and totals by year and selected

Bernn 2 mēneši atpakaļ
vecāks
revīzija
ce9e2c90fd
1 mainītis faili ar 72 papildinājumiem un 31 dzēšanām
  1. 72 31
      index.php

+ 72 - 31
index.php

@@ -68,25 +68,28 @@ html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Ro
 #empty-state a{color:#3b82f6}
 
 /* ─── P&L Table ─── */
-table.plt{border-collapse:collapse;white-space:nowrap;font-size:12px;min-width:100%}
+table.plt{border-collapse:collapse;white-space:nowrap;font-size:12px;table-layout:fixed}
 
 /* Sticky header */
 table.plt thead th{
   background:#1e293b;color:#e2e8f0;font-weight:600;
   text-align:right;position:sticky;top:0;z-index:3;
   padding:7px 10px;border-bottom:2px solid #0f172a;
-  font-size:11px;letter-spacing:.2px
+  font-size:11px;letter-spacing:.2px;
+  width:100px;overflow:hidden
 }
 table.plt thead th.col-name{
   text-align:left;position:sticky;left:0;z-index:4;
-  width:220px;min-width:220px;max-width:220px
+  width:220px
+}
+/* Year-total column header */
+table.plt thead th.col-ytotal{
+  background:#0f172a;border-left:3px solid #3b82f6;color:#93c5fd;font-style:italic
 }
-table.plt thead th.col-total{border-left:2px solid #475569}
 
 /* Sticky name column */
 table.plt td.col-name{
   position:sticky;left:0;z-index:1;
-  width:220px;min-width:220px;max-width:220px;
   overflow:hidden;text-overflow:ellipsis;
   padding:5px 8px
 }
@@ -95,7 +98,15 @@ table.plt td.col-name{
 table.plt td.val{text-align:right;padding:5px 10px;font-variant-numeric:tabular-nums}
 table.plt td.val.neg{color:#ef4444}
 table.plt td.val.zero{color:#cbd5e1}
-table.plt td.col-total{border-left:2px solid #cbd5e1}
+
+/* Year-total value cells */
+table.plt td.col-ytotal{
+  border-left:3px solid #3b82f6;font-weight:700;
+  background:#eff6ff!important
+}
+tr.r-subtotal td.col-ytotal{background:#bfdbfe!important}
+tr.r-subtotal.L0 td.col-ytotal{background:#93c5fd!important}
+tr.r-total td.col-ytotal{background:#1e3a5f!important;color:#bfdbfe!important}
 
 /* indent */
 .i0{padding-left:8px!important}
@@ -177,7 +188,7 @@ tr.r-total td.neg{color:#fca5a5!important}
       <span id="sel-info"></span>
       <div id="total-wrap">
         <input type="checkbox" id="show-total" onchange="render()">
-        <label for="show-total">Show TOTAL column</label>
+        <label for="show-total">Show Year Totals</label>
       </div>
     </div>
 
@@ -337,12 +348,8 @@ async function load() {
   const wrap = document.getElementById('tbl-wrap');
   wrap.classList.add('loading');
 
-  // Include TOTAL column id if show-total is checked
+  // Year totals are computed client-side — only request selected months
   let ids = [...selected];
-  if (document.getElementById('show-total').checked) {
-    const tot = allMonths.find(m => m.is_total);
-    if (tot && !ids.includes(tot.id)) ids.push(tot.id);
-  }
 
   try {
     const res = await fetch('api.php?action=data&ids='+encodeURIComponent(ids.join(',')));
@@ -360,46 +367,80 @@ function render() {
   if (!plData || !plData.rows) return;
 
   const showTotal = document.getElementById('show-total').checked;
-  const cols = (plData.columns || []).filter(c => !c.is_total || showTotal);
+
+  // Only real month columns (no DB TOTAL)
+  const monthCols = (plData.columns || []).filter(c => !c.is_total);
+
+  // Group month columns by year, preserving order
+  const yearGroups = [];
+  monthCols.forEach(c => {
+    const last = yearGroups[yearGroups.length - 1];
+    if (last && last.year === c.year) { last.cols.push(c); }
+    else { yearGroups.push({ year: c.year, cols: [c] }); }
+  });
+
+  // Build flat display-column list: months interleaved with optional year-total sentinels
+  // sentinel: { isYTotal: true, year, cols: [...month cols for this year] }
+  const dispCols = [];
+  yearGroups.forEach(yg => {
+    yg.cols.forEach(c => dispCols.push(c));
+    if (showTotal) dispCols.push({ isYTotal: true, year: yg.year, cols: yg.cols });
+  });
 
   const selCount = selected.size;
   document.getElementById('sel-info').textContent =
-    selCount + ' month' + (selCount!==1?'s':'') + ' selected';
+    selCount + ' month' + (selCount !== 1 ? 's' : '') + ' selected';
+
+  // Helper: sum row values across a set of columns
+  function rowSum(row, cols) {
+    return cols.reduce((s, c) => {
+      const v = row.values[String(c.id)];
+      return s + (v !== undefined && v !== null ? parseFloat(v) || 0 : 0);
+    }, 0);
+  }
 
-  // Build header
+  // Build header row
   let h = '<table class="plt"><thead><tr><th class="col-name">Account</th>';
-  cols.forEach(c => {
-    const cls = c.is_total ? ' col-total' : '';
-    h += `<th class="${cls}">${esc(c.label)}</th>`;
+  dispCols.forEach(dc => {
+    if (dc.isYTotal) {
+      h += `<th class="col-ytotal">${dc.year}</th>`;
+    } else {
+      h += `<th>${esc(dc.label)}</th>`;
+    }
   });
   h += '</tr></thead><tbody>';
 
-  // Render rows — skip item rows where every selected column is 0/missing
+  // Render data rows — skip item rows with no data in any displayed month
   plData.rows.forEach(row => {
     if (row.type === 'item') {
-      const hasData = cols.some(c => {
+      const hasData = monthCols.some(c => {
         const v = row.values[String(c.id)];
         return v !== undefined && v !== null && parseFloat(v) !== 0;
       });
       if (!hasData) return;
     }
-    const L    = row.indent;          // 0, 1, or 2
-    const rCls = 'r-'+row.type + (row.type==='header'||row.type==='subtotal' ? ' L'+L : '');
-    const iCls = 'i'+L;
+
+    const L    = row.indent;
+    const rCls = 'r-' + row.type + (row.type === 'header' || row.type === 'subtotal' ? ' L' + L : '');
+    const iCls = 'i' + L;
 
     if (row.type === 'header') {
-      h += `<tr class="${rCls}"><td colspan="${cols.length+1}" class="col-name ${iCls}">${esc(row.name)}</td></tr>`;
+      h += `<tr class="${rCls}"><td colspan="${dispCols.length + 1}" class="col-name ${iCls}">${esc(row.name)}</td></tr>`;
       return;
     }
 
     h += `<tr class="${rCls}"><td class="col-name ${iCls}">${esc(row.name)}</td>`;
-    cols.forEach(c => {
-      const v   = row.values[String(c.id)];
-      const num = (v === undefined || v === null) ? 0 : parseFloat(v);
-      const vcls = 'val'
-        + (c.is_total ? ' col-total' : '')
-        + (num < 0 ? ' neg' : (num === 0 ? ' zero' : ''));
-      h += `<td class="${vcls}">${fmt(num)}</td>`;
+    dispCols.forEach(dc => {
+      if (dc.isYTotal) {
+        const sum  = rowSum(row, dc.cols);
+        const vcls = 'val col-ytotal' + (sum < 0 ? ' neg' : sum === 0 ? ' zero' : '');
+        h += `<td class="${vcls}">${fmt(sum)}</td>`;
+      } else {
+        const v   = row.values[String(dc.id)];
+        const num = (v === undefined || v === null) ? 0 : parseFloat(v);
+        const vcls = 'val' + (num < 0 ? ' neg' : num === 0 ? ' zero' : '');
+        h += `<td class="${vcls}">${fmt(num)}</td>`;
+      }
     });
     h += '</tr>';
   });