PageSourceSearch

https://jamail-hardwoods-leads.vercel.app/patch.js?v=20260722-khatauth

js jamail-hardwoods-leads.vercel.app collected 2026-10-03 07:08:31 UTC 286,069 bytes, 5,320 lines download raw bytes

1// patch.js - Runtime fixes for Jamail Hardwoods Portal
2// This file is loaded after app.js to fix known issues
3
4(function() {
5    'use strict';
6
7   // === LIGHT MODE ENFORCEMENT ===
8   // Ensure light mode is always enabled
9   if (document && document.documentElement && !document.documentElement.classList.contains('light')) {
10      document.documentElement.classList.add('light');
11   }
12
13   // Override toggleTheme to be a no-op (prevents any dark mode switching)
14   window.toggleTheme = function() {
15      // Theme switching disabled - light mode only
16      return false;
17   };
18
19   // === FIX 1: Increase product catalog search limit from 50 to 500 ===
20   if (typeof searchProductCatalog === 'function') {
21         const _origSearch = searchProductCatalog;
22         window.searchProductCatalog = function(query) {
23                 if (!query && typeof _productCatalogCache !== 'undefined' && _productCatalogCache) {
24                           return _productCatalogCache.slice(0, 500);
25                 }
26                 const results = _origSearch(query);
27                 return results;
28         };
29   }
30
31   // === FIX 2: Override saveVendor to persist to Supabase ===
32   const _origSaveVendor = typeof saveVendor === 'function' ? saveVendor : null;
33    if (_origSaveVendor) {
34          window.saveVendor = function(editIdx) {
35                  const name = document.getElementById('vnd-f-name')?.value?.trim();
36                  const address = document.getElementById('vnd-f-address')?.value?.trim() || '';
37                  const acctNum = document.getElementById('vnd-f-acct')?.value?.trim() || '';
38                  const phone = document.getElementById('vnd-f-phone')?.value?.trim() || '';
39                  const freight = document.getElementById('vnd-f-freight')?.value?.trim() || '';
40                  const website = document.getElementById('vnd-f-website')?.value?.trim() || '';
41                  const notes = document.getElementById('vnd-f-notes')?.value?.trim() || '';
42                  const productsStr = document.getElementById('vnd-f-products')?.value?.trim() || '';
43                  const repName = document.getElementById('vnd-f-rep-name')?.value?.trim() || '';
44                  const repEmail = document.getElementById('vnd-f-rep-email')?.value?.trim() || '';
45                  const repPhone = document.getElementById('vnd-f-rep-phone')?.value?.trim() || '';
46                  const products = productsStr ? productsStr.split(',').map(p => p.trim()).filter(Boolean) : [];
47                  _origSaveVendor(editIdx);
48                  if (!name) return;
49                  try {
50                            const dbEntry = {
51                                        name, address, account_number: acctNum, phone, freight,
52                                        products, rep_name: repName, rep_email: repEmail,
53                                        rep_phone: repPhone, website, notes, status: 'active',
54                                        updated_at: new Date().toISOString()
55                            };
56                            sb.from('vendors').upsert(dbEntry, { onConflict: 'name' })
57                              .then(({error}) => {
58                                            if (error) {
59                                                            console.error('Vendor DB save error:', error);
60                                                            toast('Warning: vendor saved locally but failed to save to database');
61                                            } else {
62                                                            console.log('Vendor saved to DB:', name);
63                                            }
64                              });
65                  } catch(e) { console.error('Vendor save patch error:', e); }
66          };
67    }
68
69   // === FIX 3: Override saveInstaller to persist to Supabase ===
70   const _origSaveInstaller = typeof saveInstaller === 'function' ? saveInstaller : null;
71    if (_origSaveInstaller) {
72          window.saveInstaller = function(editIdx) {
73                  const name = document.getElementById('inst-f-name')?.value?.trim();
74                  const company = document.getElementById('inst-f-company')?.value?.trim() || '';
75                  const phone = document.getElementById('inst-f-phone')?.value?.trim() || '';
76                  const email = document.getElementById('inst-f-email')?.value?.trim() || '';
77                  const specialty = document.getElementById('inst-f-specialty')?.value?.trim() || '';
78                  const notes = document.getElementById('inst-f-notes')?.value?.trim() || '';
79                  _origSaveInstaller(editIdx);
80                  if (!name) return;
81                  try {
82                            const dbEntry = {
83                                        name, company, phone, email,
84                                        specialty: specialty ? specialty.split(',').map(s =>
84 s.trim()).filter(Boolean) : [],
85                                        notes, status: 'active',
86                                        updated_at: new Date().toISOString()
87                            };
88                            sb.from('installers').upsert(dbEntry, { onConflict: 'name' })
89                              .then(({error}) => {
90                                            if (error) {
91                                                            console.error('Installer DB save error:', error);
92                                                            toast('Warning: installer saved locally but failed to save to database');
93                                            } else {
94                                                            console.log('Installer saved to DB:', name);
95                                            }
96                              });
97                  } catch(e) { console.error('Installer save patch error:', e); }
98          };
99    }
100
101   console.log('[patch.js] Runtime fixes applied (FIX 1-3)');
102})();
103
104// === FIX 4: Carton calculation + auto-round-up for ALL flooring products ===
105(function initCartonCalc() {
106  var _updating = false;
107
108  // === FIX 26: strict SKU/name match first; ambiguous color falls through ===
109  function findCoverage(text) {
110    if (typeof _productCatalogCache === 'undefined' || !_productCatalogCache) return 0;
111
112    // STRICT 1: exact SKU appears in the line text
113    for (var i = 0; i < _productCatalogCache.length; i++) {
114      var p = _productCatalogCache[i];
115      if (!p.coverage) continue;
116      if (p.sku && p.sku.length >= 4 && text.indexOf(p.sku) !== -1) {
117        return parseFloat(p.coverage) || 0;
118      }
119    }
120
121    // STRICT 2: full product_name appears in the line text
122    for (var k = 0; k < _productCatalogCache.length; k++) {
123      var q = _productCatalogCache[k];
124      if (!q.coverage || !q.product_name) continue;
125      if (q.product_name.length >= 10 && text.indexOf(q.product_name) !== -1) {
126        return parseFloat(q.coverage) || 0;
127      }
128    }
129
130    // FALLBACK: color-name heuristic (legacy behavior),
131    // but return 0 when candidates disagree instead of
132    // blindly returning the first one.
133    var matched = [];
134    for (var m = 0; m < _productCatalogCache.length; m++) {
135      var r = _productCatalogCache[m];
136      if (!r.coverage || !r.product_name) continue;
137      var parts = r.product_name.split(' - ');
138      var colorName = parts.length > 1 ? parts[parts.length - 1].trim() : '';
139      if (colorName && colorName.length > 2 && text.indexOf(colorName) !== -1) {
140        matched.push(r);
141      }
142    }
143    if (matched.length === 0) return 0;
144    var coverage = parseFloat(matched[0].coverage) || 0;
145    for (var j = 1; j < matched.length; j++) {
146      if (parseFloat(matched[j].coverage) !== coverage) return 0; // ambiguous — don't guess
147    }
148    return coverage;
149  }
150
151  function processEstimateItems() {
152    var estLines = document.getElementById('est-lines');
153    if (!estLines) return;
154    var items = estLines.querySelectorAll(':scope > div');
155    for (var i = 0; i < items.length; i++) {
156      var item = items[i];
157      var inputs = item.querySelectorAll('input[type="number"]');
158      if (inputs.length < 2) continue;
159      var text = item.textContent || '';
160      var coverage = findCoverage(text);
161      if (coverage <= 0) {
162        var oldBadge = item.querySelector('.carton-badge');
163        if (oldBadge) oldBadge.remove();
164        continue;
165      }
166      var priceInput = inputs[0];
167      var sqftInput = inputs[1];
168      var sqft = parseFloat(sqftInput.value) || 0;
169      var cartons = sqft > 0 ? Math.ceil(sqft / coverage) : 0;
170      var actualSqft = cartons * coverage;
171      var badge = item.querySelector('.carton-badge');
172      if (!badge) {
173        badge = document.createElement('div');
174        badge.className = 'carton-badge';
175        badge.style.cssText = 'font-size:12px;color:#c9a85c;margin:4px 0 0 0;font-weight:600;padding:2px 8px;';
176        var descDiv = item.children[2];
177        if (descDiv && descDiv.nextSibling) {
178          item.insertBefore(badge, descDiv.nextSibling);
179        } else {
180          item.appendChild(badge);
181        }
182      }
183      var badgeText = cartons > 0
184        ? '\u{1F4E6} ' + cartons + ' carton' + (cartons !== 1 ? 's' : '') + ' = ' + actualSqft.toFixed(2) + ' sf (' + coverage + ' sf/ctn)'
185        : '\u{1F4E6} ' + coverage + ' sf/carton';
186      if (badge.textContent !== badgeText) badge.textContent = badgeText;
187      if (!sqftInput._cartonListener) {
188        sqftInput._cartonListener = true;
189        sqftInput.addEventListener('input', function() {
190          setTimeout(processEstimateItems, 100);
191        });
192        sqftInput.addEventListener('blur', function() {
193          var sf = parseFloat(this.value) || 0;
194          if (sf <= 0 || coverage <= 0) return;
195          var crt = Math.ceil(sf / coverage);
196          var rounded = crt * coverage;
197          if (Math.abs(rounded - sf) > 0.01 && rounded > sf) {
198            this.value = rounded;
199            this.dispatchEvent(new Event('input', {bubbles: true}));
200            this.dispatchEvent(new Event('change', {bubbles: true}));
201          }
202        });
203      }
204    }
205  }
206
207  function processQuoteItems() {
208    var quoteItems = document.querySelectorAll('.quote-item');
209    if (quoteItems.length === 0) return;
210    quoteItems.forEach(function(itemDiv) {
211      var sku = (itemDiv.querySelector('.item-sku') || {}).value || '';
212      var desc = (itemDiv.querySelector('.item-description') || {}).value || '';
213      var sqftInput = itemDiv.querySelector('.item-sqft');
214      var wastePctInput = itemDiv.querySelector('.item-waste-pct');
215      if (!sqftInput) return;
216      var text = sku + ' ' + desc;
217      var coverage = findCoverage(text);
218      if (coverage <= 0) {
219        if (typeof _productCatalogCache !== 'undefined' && _productCatalogCache) {
220          var match = sku ? _productCatalogCache.find(function(p) { return p.sku === sku; }) : null;
221          if (!match && desc) match = _productCatalogCache.find(function(p) { return p.product_name === desc; });
222          coverage = (match && match.coverage) ? parseFloat(match.coverage) || 0 : 0;
223        }
224      }
225      if (coverage <= 0) {
226        var old = itemDiv.querySelector('.carton-badge');
227        if (old) old.remove();
228        return;
229      }
230      var sqft = parseFloat(sqftInput.value) || 0;
231      var cartons = sqft > 0 ? Math.ceil(sqft / coverage) : 0;
232      var actualSqft = cartons * coverage;
233      var sqftParent = sqftInput.parentNode;
234      var badge = sqftParent.querySelector('.carton-badge');
235      if (!badge) {
236        badge = document.createElement('div');
237        badge.className = 'carton-badge';
238        badge.style.cssText = 'font-size:11px;color:#c9a85c;margin-top:4px;font-weight:600;';
239        sqftParent.appendChild(badge);
240      }
241      var badgeText = cartons > 0
242        ? '\u{1F4E6} ' + cartons + ' carton' + (cartons !== 1 ? 's' : '') + ' = ' + actualSqft.toFixed(2) + ' sf (' + coverage + ' sf/ctn)'
243        : '\u{1F4E6} ' + coverage + ' sf/carton';
244      if (badge.textContent !== badgeText) badge.textContent = badgeText;
245      if (!sqftInput._cartonListener) {
246        sqftInput._cartonListener = true;
247        sqftInput.addEventListener('input', function() { setTimeout(processQuoteItems, 50); });
248        sqftInput.addEventListener('change', function() {
249          setTimeout(function() {
250            processQuoteItems();
251            if (typeof updateQuoteTotals === 'function') updateQuoteTotals();
252          }, 50);
253        });
254        (function(inp, cov) {
255          inp.addEventListener('blur', function() {
256            var sf = parseFloat(inp.value) || 0;
257            if (sf <= 0 || cov <= 0) return;
258            var crt = Math.ceil(sf / cov);
259            var rounded = crt * cov;
260            if (Math.abs(rounded - sf) > 0.01 && rounded > sf) {
261              inp.value = rounded.toFixed(2);
262              inp.dispatchEvent(new Event('input', {bubbles: true}));
263              inp.dispatchEvent(new Event('change', {bubbles: true}));
264              setTimeout(function() {
265                processQuoteItems();
266                if (typeof updateQuoteTotals === 'function') updateQuoteTotals();
267              }, 100);
268            }
269          });
270        })(sqftInput, coverage);
271      }
272    });
273  }
274
275  // === FIX 5: Labor line — show cost/sell rates in visible fields ===
276  function fixLaborLines() {
277    var quoteItems = document.querySelectorAll('.quote-item');
278    quoteItems.forEach(function(itemDiv) {
279      var typeEl = itemDiv.querySelector('.item-product-type');
280      if (!typeEl || typeEl.value !== 'Labor') return;
281      var laborCostEl = itemDiv.querySelector('.item-labor-cost');
282      var laborSellEl = itemDiv.querySelector('.item-labor-sell');
283      var costSfEl = itemDiv.querySelector('.item-cost-sf');
284      var sellSfEl = itemDiv.querySelector('.item-sell-sf');
285      var totalEl = itemDiv.querySelector('.item-line-total');
286      var sqftInput = itemDiv.querySelector('.item-sqft');
287      var matCostEl = itemDiv.querySelector('.item-material-cost');
288      var matSellEl = itemDiv.querySelector('.item-material-sell');
289      if (!laborSellEl || !costSfEl || !sellSfEl) return;
290      var lc = parseFloat(laborCostEl ? laborCostEl.value : '0') || 0;
291      var ls = parseFloat(laborSellEl.value) || 0;
292      var sf = parseFloat(sqftInput ? sqftInput.value : '0') || 0;
293      var costRate = lc, sellRate = ls;
294      if (sf > 0 && ls > sf) {
295        var wpEl = itemDiv.querySelector('.item-waste-pct');
296        var wp = wpEl ? (parseFloat(wpEl.value) || 10) : 10;
297        var tsf = sf * (1 + wp / 100);
298        if (tsf > 0) { costRate = lc / tsf; sellRate = ls / tsf; }
299      }
300      if (costRate > 0 && (!costSfEl.value || parseFloat(costSfEl.value) === 0)) {
301        costSfEl.value = costRate.toFixed(2);
302      }
303      if (sellRate > 0 && (!sellSfEl.value || parseFloat(sellSfEl.value) === 0)) {
304        sellSfEl.value = sellRate.toFixed(2);
305      }
306      if (matCostEl && parseFloat(matCostEl.value) > 0) matCostEl.value = '0';
307      if (matSellEl && parseFloat(matSellEl.value) > 0) matSellEl.value = '0';
308      if (ls > 0 && totalEl) {
309        var ct = parseFloat(totalEl.value) || 0;
310        if (ct > ls + 0.01) {
311          totalEl.value = ls.toFixed(2);
312        }
313      }
314      if (sqftInput && !sqftInput._laborFixListener) {
315        sqftInput._laborFixListener = true;
316        sqftInput.addEventListener('input', function() { setTimeout(fixLaborLines, 100); });
317        sqftInput.addEventListener('change', function() { setTimeout(fixLaborLines, 100); });
318      }
319    });
320  }
321
322  function updateAll() {
323    if (_updating) return;
324    _updating = true;
325    try {
326      processEstimateItems();
327      processQuoteItems();
328      fixLaborLines();
329    } finally { _updating = false; }
330  }
331
332  setInterval(updateAll, 500);
333  console.log('[patch.js] FIX 4+5: Carton calc + Labor display polling started');
334})();
335
336// === FIX 6: Invoice overlay close button — remove inline display:block that overrides CSS ===
337(function fixInvoiceClose() {
338  var _origRender = typeof renderInvoiceDetail === 'function' ? renderInvoiceDetail : null;
339  if (!_origRender) return;
340
341  window.renderInvoiceDetail = async function(invoice) {
342    await _origRender(invoice);
343    // After original renders, remove the inline display style so CSS classes work properly
344    var ov = document.getElementById('ov-invoice-detail');
345    if (ov) {
346      ov.style.removeProperty('display');
347      // Ensure it's open via the class
348      if (!ov.classList.contains('open')) ov.classList.add('open');
349    }
350  };
351  console.log('[patch.js] FIX 6: Invoice close button fix applied');
352})();
353
354// === FIX 7: Fix loadJsPDF race condition — AutoTable must load AFTER jsPDF ===
355(function fixLoadJsPDF() {
356  window.loadJsPDF = function() {
357    return new Promise(function(resolve, reject) {
358      // Check if already loaded
359      if (window.jspdf && window.jspdf.jsPDF) {
360        // Check autoTable on prototype (that's where the plugin registers)
361        try {
362          var testDoc = new window.jspdf.jsPDF();
363          if (typeof testDoc.autoTable === 'function') {
364            resolve();
365            return;
366          }
367        } catch(e) {}
368      }
369
370      function loadScript(src) {
371        return new Promise(function(res, rej) {
372          // Don't re-add if already in DOM
373          var existing = document.querySelector('script[src="' + src + '"]');
374          if (existing) { res(); return; }
375          var s = document.createElement('script');
376          s.src = src;
377          s.onload = res;
378          s.onerror = function() { rej(new Error('Failed to load: ' + src)); };
379          document.head.appendChild(s);
380        });
381      }
382
383      // Load jsPDF first, THEN AutoTable (it depends on jsPDF being present)
384      loadScript('https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js')
385        .then(function() {
386          return loadScript('https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.8.2/jspdf.plugin.autotable.min.js');
387        })
388        .then(resolve)
389        .catch(reject);
390    });
391  };
392  console.log('[patch.js] FIX 7: loadJsPDF sequential loading fix applied');
393})();
394
395// === FIX 8: View Contract — fetch HTML and open as blob to guarantee browser renders it ===
396(function fixViewContract() {
397  document.addEventListener('click', function(e) {
398    var link = e.target.closest('a[href*="view=contract"]');
399    if (!link) return;
400    e.preventDefault();
401    e.stopPropagation();
402    var url = link.href;
403    // Fetch the HTML and open it as a blob URL so the browser always renders it
404    fetch(url)
405      .then(function(resp) { return resp.text(); })
406      .then(function(html) {
407        var blob = new Blob([html], { type: 'text/html; charset=utf-8' });
408        var blobUrl = URL.createObjectURL(blob);
409        window.open(blobUrl, '_blank');
410        // Clean up blob URL after a delay
411        setTimeout(function() { URL.revokeObjectURL(blobUrl); }, 60000);
412      })
413      .catch(function(err) {
414        console.error('View Contract fetch error:', err);
415        // Fallback: just open the URL directly
416        window.open(url, '_blank');
417      });
418  }, true);
419  console.log('[patch.js] FIX 8: View Contract blob rendering fix applied');
420})();
421
422// === FIX 9: Quote save — fix empty date strings and null id causing DB errors ===
423(function fixQuoteSave() {
424  var _origSaveQuote = typeof saveQuote === 'function' ? saveQuote : null;
425  if (!_origSaveQuote) return;
426
427  window.saveQuote = async function(quoteData, lineItems) {
428    // Fix 9a: Convert empty string dates to null (Postgres rejects "" for date columns)
429    if (quoteData.quote_date === '') quoteData.quote_date = null;
430    if (quoteData.valid_until === '') quoteData.valid_until = null;
431
432    // Fix 9b: Remove null id for new quotes (Postgres rejects null for serial NOT NULL columns)
433    if (quoteData.id === null || quoteData.id === undefined) {
434      delete quoteData.id;
435    }
436
437    // Fix 9c: Convert NaN numeric fields to 0
438    var numFields = ['material_subtotal', 'labor_subtotal', 'subtotal', 'discount',
439                     'freight', 'surcharge', 'processing_fee_pct', 'processing_fee_amount',
440                     'tax_rate', 'tax_amount', 'grand_total'];
441    for (var i = 0; i < numFields.length; i++) {
442      var f = numFields[i];
443      if (quoteData[f] !== undefined && (isNaN(quoteData[f]) || quoteData[f] === null)) {
444        quoteData[f] = 0;
445      }
446    }
447
448    // Fix 9d: Re-fetch quote_number right before save to avoid UNIQUE conflicts
449    if (!quoteData.id) {
450      try {
451        var freshNum = await getNextQuoteNumber();
452        quoteData.quote_number = freshNum;
453      } catch(e) { /* keep existing number */ }
454    }
455
456    return _origSaveQuote(quoteData, lineItems);
457  };
458  console.log('[patch.js] FIX 9: Quote save date/null/NaN fix applied');
459})();
460
461// === FIX 10: New invoices use JH-XXXXXX format instead of INV-timestamp ===
462(function fixInvoiceNumberFormat() {
463  var _origCreateInvoiceFromQuote = typeof createInvoiceFromQuote === 'function' ? createInvoiceFromQuote : null;
464  if (!_origCreateInvoiceFromQuote) return;
465
466  window.createInvoiceFromQuote = async function(quoteId) {
467    // Get the next invoice number in JH- format
468    var nextNum = 100000;
469    try {
470      var { data } = await sb.from('invoices')
471        .select('invoice_number')
472        .like('invoice_number', 'JH-%')
473        .order('invoice_number', { ascending: false })
474        .limit(1);
475      if (data && data.length > 0) {
476        var lastNum = parseInt(data[0].invoice_number.replace('JH-', ''), 10);
477        if (!isNaN(lastNum)) nextNum = lastNum + 1;
478      }
479    } catch(e) { console.error('Invoice number lookup error:', e); }
480
481    window._nextInvoiceNum = 'JH-' + nextNum;
482
483    var result = await _origCreateInvoiceFromQuote(quoteId);
484
485    // After creation, fix the invoice_number if it used INV- format
486    if (window._nextInvoiceNum) {
487      try {
488        await sb.from('invoices')
489          .update({ invoice_number: window._nextInvoiceNum })
490          .like('invoice_number', 'INV-%')
491          .eq('quote_id', quoteId);
492      } catch(e) { console.error('Invoice number fix error:', e); }
493      delete window._nextInvoiceNum;
494    }
495
496    return result;
497  };
498  console.log('[patch.js] FIX 10: Invoice number format fix (JH-XXXXXX) applied');
499})();
500
501// === FIX 11: Project Tracker layout & color fix to match Operations pages ===
502(function fixProjectTrackerLayout() {
503
504  // Override renderProjectsTab to match Quotes/Invoices/WO layout
505  var _origRenderProjectsTab = typeof renderProjectsTab === 'function' ? renderProjectsTab : null;
506  if (!_origRenderProjectsTab) { console.log('[patch.js] FIX 11: renderProjectsTab not found, skipping'); return; }
507
508  window.renderProjectsTab = async function() {
509    var tab = document.getElementById('tab-projects');
510    if (!tab) return;
511
512    tab.innerHTML = '\
513    <div style="padding:20px;background:var(--d1);min-height:100vh;">\
514      <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;flex-wrap:wrap;gap:10px;">\
515        <h2 style="color:var(--t);margin:0;">Project Tracker</h2>\
516        <div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">\
517          <div style="position:relative;">\
518            <input id="proj-search" type="text" placeholder="Search projects..." onkeyup="filterProjects()" style="padding:8px 12px 8px 34px;background:var(--d2);color:var(--t);border:1px solid var(--b);border-radius:6px;width:220px;font-size:14px;">\
519            <span style="position:absolute;left:10px;top:50%;transform:translateY(-50%);color:var(--t3);">&#128269;</span>\
520          </div>\
521          <select id="proj-status-filter" onchange="filterProjects()" style="padding:8px 16px;background:var(--d2);color:var(--t);border:1px solid var(--b);border-radius:6px;font-size:14px;cursor:pointer;">\
522            <option value="">All Statuses</option>\
523          </select>\
524          <button onclick="openInstallerTechsPanel()" style="padding:10px 20px;background:var(--d3);color:var(--gold);border:1px solid var(--gold);border-radius:6px;cursor:pointer;font-weight:bold;">\
525            &#128119; Manage Techs\
526          </button>\
527          <button onclick="openNewProjectForm()" style="padding:10px 20px;background:var(--gold);color:#000;border:none;border-radius:6px;cursor:pointer;font-weight:bold;">\
528            + New Project\
529          </button>\
530        </div>\
531      </div>\
532      <div id="proj-filters" style="display:flex;gap:10px;margin-bottom:20px;flex-wrap:wrap;"></div>\
533      <div id="proj-summary-cards" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;margin-bottom:20px;"></div>\
534      <div id="proj-list" style="display:flex;flex-direction:column;gap:10px;"></div>\
535    </div>';
536
537    // Populate status filter options
538    var sel = document.getElementById('proj-status-filter');
539    if (sel && typeof PROJECT_STATUSES !== 'undefined') {
540      PROJECT_STATUSES.forEach(function(s) {
541        var opt = document.createElement('option');
542        opt.value = s.value;
543        opt.textContent = s.icon + ' ' + s.label;
544        sel.appendChild(opt);
545      });
546    }
547
548    // Build filter pills
549    var pillContainer = document.getElementById('proj-filters');
550    if (pillContainer && typeof PROJECT_STATUSES !== 'undefined') {
551      var pillColors = {
552        planning: '#4a4a4a', material_ordered: '#003d7a', material_in_transit: '#003d7a',
553        material_received: '#1a5a2f', scheduled: '#b8860b', in_progress: '#003d7a',
554        complete: '#1a5a2f', punch_list: '#b8860b', project_closed: '#4a4a4a'
555      };
556      var pillsHtml = '<button class="filter-pill active" data-status="all" style="padding:8px 16px;background:var(--d3);color:var(--t);border:1px solid var(--b);border-radius:20px;cursor:pointer;">All</button>';
557      PROJECT_STATUSES.forEach(function(s) {
558        var bg = pillColors[s.value] || '#4a4a4a';
559        pillsHtml += '<button class="filter-pill" data-status="' + s.value + '" style="padding:8px 16px;background:' + bg + ';color:var(--t);border:1px solid var(--b);border-radius:20px;cursor:pointer;">' + s.icon + ' ' + s.label + '</button>';
560      });
561      pillContainer.innerHTML = pillsHtml;
562
563      // Pill click handlers
564      pillContainer.querySelectorAll('.filter-pill').forEach(function(pill) {
565        pill.addEventListener('click', function(e) {
566          pillContainer.querySelectorAll('.filter-pill').forEach(function(p) { p.classList.remove('active'); });
567          e.target.classList.add('active');
568          var status = e.target.dataset.status;
569          var sel2 = document.getElementById('proj-status-filter');
570          if (sel2) sel2.value = (status === 'all') ? '' : status;
571          filterProjects();
572        });
573      });
574    }
575
576    await loadProjects();
577    renderProjectSummary();
578    renderProjectList();
579  };
580
581  // Override renderProjectSummary with corrected theme vars
582  window.renderProjectSummary = function() {
583    var el = document.getElementById('proj-summary-cards');
584    if (!el || typeof PROJECT_STATUSES === 'undefined') return;
585
586    var counts = {};
587    PROJECT_STATUSES.forEach(function(s) { counts[s.value] = 0; });
588    (typeof projectsData !== 'undefined' ? projectsData : []).forEac
588h(function(p) {
589      if (counts[p.status] !== undefined) counts[p.status]++;
590    });
591
592    el.innerHTML = PROJECT_STATUSES.filter(function(s) {
593      return counts[s.value] > 0 || ['planning','in_progress','complete'].indexOf(s.value) >= 0;
594    }).map(function(s) {
595      return '<div onclick="document.getElementById(\'proj-status-filter\').value=\'' + s.value + '\';filterProjects()" style="background:var(--d3);border:1px solid var(--b);border-radius:8px;padding:14px;cursor:pointer;text-align:center;transition:all 0.2s;border-left:3px solid ' + s.color + ';">'
596        + '<div style="font-size:22px;">' + s.icon + '</div>'
597        + '<div style="font-size:22px;font-weight:700;color:' + s.color + ';">' + counts[s.value] + '</div>'
598        + '<div style="font-size:11px;color:var(--t3);text-transform:uppercase;letter-spacing:0.5px;">' + s.label + '</div>'
599        + '</div>';
600    }).join('');
601  };
602
603  // Override renderProjectList with corrected theme vars
604  window.renderProjectList = function() {
605    var el = document.getElementById('proj-list');
606    if (!el) return;
607
608    var statusFilter = (document.getElementById('proj-status-filter') || {}).value || '';
609    var search = ((document.getElementById('proj-search') || {}).value || '').toLowerCase();
610    var data = typeof projectsData !== 'undefined' ? projectsData : [];
611
612    var filtered = data;
613    if (statusFilter) filtered = filtered.filter(function(p) { return p.status === statusFilter; });
614    if (search) filtered = filtered.filter(function(p) {
615      return (p.name || '').toLowerCase().indexOf(search) >= 0 ||
616        (p.customer || '').toLowerCase().indexOf(search) >= 0 ||
617        (p.job_address || '').toLowerCase().indexOf(search) >= 0 ||
618        (p.project_number || '').toLowerCase().indexOf(search) >= 0 ||
619        (p.installer_name || '').toLowerCase().indexOf(search) >= 0;
620    });
621
622    if (filtered.length === 0) {
623      el.innerHTML = '<div style="text-align:center;padding:40px;color:var(--t3);"><div style="font-size:48px;margin-bottom:10px;">&#128203;</div><p>No projects found</p></div>';
624      return;
625    }
626
627    el.innerHTML = filtered.map(function(p) {
628      var si = getStatusInfo(p.status);
629      var daysInstall = p.install_start_date ? Math.ceil((new Date(p.install_start_date) - new Date()) / 86400000) : null;
630      var daysETA = p.material_eta ? Math.ceil((new Date(p.material_eta) - new Date()) / 86400000) : null;
631      var urgentBadge = '';
632      if (daysInstall !== null && daysInstall >= 0 && daysInstall <= 3 && p.status !== 'complete') {
633        urgentBadge = '<span style="background:#ef4444;color:#fff;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600;">Install in ' + daysInstall + 'd</span>';
634      } else if (daysETA !== null && daysETA >= 0 && daysETA <= 2 && !p.material_received_date) {
635        urgentBadge = '<span style="background:#3b82f6;color:#fff;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600;">Delivery in ' + daysETA + 'd</span>';
636      }
637
638      return '<div onclick="openProjectDetail(' + p.id + ')" style="background:var(--d3);border:1px solid var(--b);border-radius:8px;padding:16px;cursor:pointer;transition:all 0.2s;border-left:3px solid ' + si.color + ';">'
639        + '<div style="display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:8px;">'
640        + '<div style="flex:1;
640min-width:200px;">'
641        + '<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px;">'
642        + '<span style="font-weight:700;font-size:15px;color:var(--t);">' + esc(p.name || 'Untitled Project') + '</span>'
643        + urgentBadge + '</div>'
644        + '<div style="font-size:12px;color:var(--t2);">'
645        + esc(p.project_number || '') + (p.customer ? ' &bull; ' + esc(p.customer) : '')
646        + (p.job_address ? ' &bull; ' + esc(p.job_address) : '') + '</div></div>'
647        + '<div style="display:flex;gap:12px;align-items:center;flex-wrap:wrap;">'
648        + (p.installer_name ? '<span style="font-size:12px;color:var(--t2);">&#128119; ' + esc(p.installer_name) + '</span>' : '')
649        + (p.install_start_date ? '<span style="font-size:12px;color:var(--t2);">&#128197; ' + new Date(p.install_start_date).toLocaleDateString() + '</span>' : '')
650        + '<span style="display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:12px;font-size:11px;font-weight:600;background:' + si.color + '22;color:' + si.color + ';">' + si.icon + ' ' + si.label + '</span>'
651        + '</div></div>'
652        + renderProjectProgressBar(p) + '</div>';
653    }).join('');
654  };
655
656  // Override renderProjectProgressBar with gold gradient
657  window.renderProjectProgressBar = function(p) {
658    var stages = ['planning','material_ordered','material_in_transit','material_received','scheduled','in_progress','complete'];
659    var currentIdx = stages.indexOf(p.status);
660    if (currentIdx < 0) return '';
661    var pct = Math.round((currentIdx / (stages.length - 1)) * 100);
662    return '<div style="margin-top:10px;">'
663      + '<div style="display:flex;justify-content:space-between;font-size:10px;color:var(--t3);margin-bottom:3px;"><span>Planning</span><span>Complete</span></div>'
664      + '<div style="height:4px;background:var(--b);border-radius:2px;overflow:hidden;">'
665      + '<div style="height:100%;width:' + pct + '%;background:linear-gradient(90deg,var(--gold),#22c55e);border-radius:2px;transition:width 0.3s;"></div>'
666      + '</div></div>';
667  };
668
669  // Fix project detail overlay to use correct theme vars
670  var _origOpenProjectDetail = typeof openProjectDetail === 'function' ? openProjectDetail : null;
671  if (_origOpenProjectDetail) {
672    window.openProjectDetail = async function(projectId) {
673      await _origOpenProjectDetail(projectId);
674      // Post-render fix: replace incorrect CSS vars in the overlay
675      var ov = document.getElementById('ov-project-detail');
676      if (ov) {
677        var html = ov.innerHTML;
678        html = html.replace(/var\(--card\)/g, 'var(--d3)');
679        html = html.replace(/var\(--text\)/g, 'var(--t)');
680        html = html.replace(/var\(--border\)/g, 'var(--b)');
681        html = html.replace(/var\(--accent\)/g, 'var(--gold)');
682        html = html.replace(/var\(--bg\)/g, 'var(--d2)');
683        html = html.replace(/var\(--muted\)/g, 'var(--t2)');
684        ov.innerHTML = html;
685        // Re-open overlay since innerHTML reset may have closed it
686        setTimeout(function() { openOv('ov-project-detail'); }, 10);
687      }
688    };
689  }
690
691  console.log('[patch.js] FIX 11: Project Tracker layout & color fix applied');
692})();
693
694// === FIX 12: Tutorial / Help section injected into sidebar ===
695(function addTutorialSection() {
696
697  // Wait for DOM to be ready
698  function init() {
699    var sidebar = document.querySelector('.sidebar-nav, .sb-nav, nav');
700    if (!sidebar) { setTimeout(init, 500); return; }
701
702    // Repurpose existing "Help & Tutorials" nav link instead of creating a duplicate
703    var helpLink = document.querySelector('[data-tab="help"]');
704    if (helpLink) {
705      helpLink.setAttribute('data-tab', 'tutorial');
706      helpLink.setAttribute('onclick', "sideNav('tutorial',this)");
707    }
708
709    // Create the tab-tutorial container (reuse tab-help if it exists, or create new)
710    var helpTab = document.getElementById('tab-help');
711    if (helpTab) {
712      helpTab.id = 'tab-tutorial';
713    } else {
714      var mainContent = document.querySelector('.mc, .main-content, [id^="tab-"]')?.parentElement;
715      if (mainContent) {
716        var tabDiv = document.createElement('div');
717        tabDiv.id = 'tab-tutorial';
718        tabDiv.className = 'tab';
719        tabDiv.style.display = 'none';
720        mainContent.appendChild(tabDiv);
721      }
722    }
723
724    // Register the render function — unified Help & Tutorials page
725    window.renderTutorialTab = function() {
726      var tab = document.getElementById('tab-tutorial');
727      if (!tab) return;
728
729      // Build category quick-filter buttons from HELP_TUTORIALS
730      var catButtons = '';
731      var cats = typeof HELP_TUTORIALS !== 'undefined' ? HELP_TUTORIALS : [];
732      catButtons += '<button onclick="helpFilterCat(\'\')" class="hf-cat-btn hf-cat-active" data-cat="" style="padding:8px 14px;border-radius:20px;border:1px solid var(--gold);background:var(--gold);color:#111;font-size:12px;font-weight:700;cursor:pointer;transition:all .2s;white-space:nowrap">All Topics</button>';
733      cats.forEach(function(cat) {
734        catButtons += '<button onclick="helpFilterCat(\'' + cat.category.replace(/'/g,"\\'") + '\')" class="hf-cat-btn" data-cat="' + cat.category + '" style="padding:8px 14px;border-radius:20px;border:1px solid var(--b);background:var(--d3);color:var(--t2);font-size:12px;font-weight:600;cursor:pointer;transition:all .2s;white-space:nowrap">' + cat.icon + ' ' + cat.category + '</button>';
735      });
736
737      tab.innerHTML = '<div style="max-width:940px;margin:0 auto;padding:20px 16px">' +
738        // Header
739        '<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;margin-bottom:6px">' +
740          '<div><h1 style="margin:0;font-size:22px;font-weight:800;color:var(--t)">📖 Help & Tutorials</h1>' +
741          '<p style="margin:4px 0 0;font-size:13px;color:var(--t3)">Everything your reps need to know — search or browse by category</p></div>' +
742          '<div style="display:flex;gap:8px">' +
743            '<button onclick="helpToggleView(\'guide\')" id="hf-view-guide" style="padding:6px 14px;border-radius:8px;border:1px solid var(--gold);background:var(--gold);color:#111;font-size:12px;font-weight:700;cursor:pointer">📋 Quick Start Guide</button>' +
744            '<button onclick="helpToggleView(\'qa\')" id="hf-view-qa" style="padding:6px 14px;border-radius:8px;border:1px solid var(--b);background:var(--d3);color:var(--t2);font-size:12px;font-weight:600;cursor:pointer">❓ Q&A Search</button>' +
745          '</div>' +
746        '</div>' +
747        // Search bar — always visible
748        '<div style="position:sticky;top:0;z-index:10;background:var(--d);padding:12px 0 8px">' +
749          '<input id="help-search" type="text" placeholder="🔍 Type to search... (e.g. quote, margin, tax, kanban, carton, template)" ' +
750          'oninput="helpSmartSearch(this.value)" ' +
751          'style="width:100%;padding:14px 18px;background:var(--d2);color:var(--t);border:1px solid var(--gold);border-radius:12px;font-size:15px;box-sizing:border-box;outline:none">' +
752          '<div id="help-search-count" style="font-size:11px;color:var(--t3);margin-top:4px;padding-left:4px"></div>' +
753        '</div>' +
754        // Category filter pills
755        '<div id="hf-cat-pills" style="display:flex;flex-wrap:wrap;gap:8px;margin:10px 0 16px;padding-bottom:8px;border-bottom:1px solid var(--b)">' + catButtons + '</div>' +
756        // Quick Start Guide section
757        '<div id="hf-guide-section">' + buildTutorialHTML() + '</div>' +
758        // Q&A section
759        '<div id="hf-qa-section" style="display:none"><div id="help-content"></div></div>' +
760      '</div>';
761
762      // Render Q&A content
763      if (typeof filterHelpTopics === 'function') filterHelpTopics('');
764
765      // Smart search — searches BOTH guide and Q&A
766      window.helpSmartSearch = function(q) {
767        q = (q || '').trim();
768        if (!q) {
769          // Reset to current view
770          var guideBtn = document.getElementById('hf-view-guide');
771          if (guideBtn && guideBtn.style.background.indexOf('gold') > -1) {
772            document.getElementById('hf-guide-section').style.display = '';
773            document.getElementById('hf-qa-section').style.display = 'none';
774          }
775          if (typeof filterHelpTopics === 'function') filterHelpTopics('');
776          document.getElementById('help-search-count').textContent = '';
777          // Reset category pills
778          document.querySelectorAll('.hf-cat-btn').forEach(function(b){ b.style.background='var(--d3)';b.style.color='var(--t2)';b.style.borderColor='var(--b)'; });
779          var allBtn = document.querySelector('.hf-cat-btn[data-cat=""]');
780          if(allBtn){allBtn.style.background='var(--gold)';allBtn.style.color='#111';allBtn.style.borderColor='var(--gold)';}
781          return;
782        }
783        // When searching, show Q&A and hide guide
784        document.getElementById('hf-guide-section').style.display = 'none';
785        document.getElementById('hf-qa-section').style.display = '';
786        // Update view toggle buttons
787        document.getElementById('hf-view-guide').style.background = 'var(--d3)';
788        document.getElementById('hf-view-guide').style.color = 'var(--t2)';
789        document.getElementById('hf-view-guide').style.borderColor = 'var(--b)';
790        document.getElementById('hf-view-qa').style.background = 'var(--gold)';
791        document.getElementById('hf-view-qa').style.color = '#111';
792        document.getElementById('hf-view-qa').style.borderColor = 'var(--gold)';
793        if (typeof filterHelpTopics === 'function') filterHelpTopics(q);
794        // Count results
795        var results = document.querySelectorAll('#help-content [id^="help-body-"]');
796        var countEl = document.getElementById('help-search-count');
797        if (countEl) countEl.textContent = results.length + ' result' + (results.length !== 1 ? 's' : '') + ' for "' + q + '"';
798      };
799
800      // Toggle between Guide and Q&A views
801      window.helpToggleView = function(view) {
802        var guide = document.getElementById('hf-guide-section');
803        var qa = document.getElementById('hf-qa-section');
804        var gBtn = document.getElementById('hf-view-guide');
805        var qBtn = document.getElementById('hf-view-qa');
806        if (view === 'guide') {
807          guide.style.display = '';
808          qa.style.display = 'none';
809          gBtn.style.background = 'var(--gold)'; gBtn.style.color = '#111'; gBtn.style.borderColor = 'var(--gold)';
810          qBtn.style.background = 'var(--d3)'; qBtn.style.color = 'var(--t2)'; qBtn.style.borderColor = 'var(--b)';
811        } else {
812          guide.style.display = 'none';
813          qa.style.display = '';
814          qBtn.style.background = 'var(--gold)'; qBtn.style.color = '#111'; qBtn.style.borderColor = 'var(--gold)';
815          gBtn.style.background = 'var(--d3)'; gBtn.style.color = 'var(--t2)'; gBtn.style.borderColor = 'var(--b)';
816          if (typeof filterHelpTopics === 'function') filterHelpTopics('');
817        }
818        // Clear search
819        var si = document.getElementById('help-search');
820        if (si) si.value = '';
821        document.getElementById('help-search-count').textContent = '';
822      };
823
824      // Category filter
825      window.helpFilterCat = function(cat) {
826        // Switch to Q&A view
827        helpToggleView('qa');
828        // Highlight active pill
829        document.querySelectorAll('.hf-cat-btn').forEach(function(b) {
830          var isCurrent = b.getAttribute('data-cat') === cat;
831          b.style.background = isCurrent ? 'var(--gold)' : 'var(--d3)';
832          b.style.color = isCurrent ? '#111' : 'var(--t2)';
833          b.style.borderColor = isCurrent ? 'var(--gold)' : 'var(--b)';
834        });
835        if (typeof filterHelpTopics === 'function') filterHelpTopics(cat);
836      };
837    };
838
839    // Hook into sideNav to handle tutorial tab
840    var _origSideNav = typeof sideNav === 'function' ? sideNav : null;
841    if (_origSideNav) {
842      window.sideNav = function(name, el) {
843        // Always hide tutorial tab when navigating to ANY other tab
844        if (name !== 'tutorial') {
845          // Hide tutorial tab and clear any inline display styles we set
846          var tutTab = document.getElementById('tab-tutorial');
847          if (tutTab) tutTab.style.display = 'none';
848          // Clear inline display on all other tabs so CSS classes work properly
849          document.querySelectorAll('[id^="tab-"]:not(#tab-tutorial)').forEach(function(t) {
850            t.style.removeProperty('display');
851          });
852          return _origSideNav(name, el);
853        }
854        // Tutorial tab: let original sideNav update sidebar active state
855        _origSideNav(name, el);
856        // Hide all other tabs by removing their active class (don't use inl
856ine display)
857        document.querySelectorAll('[id^="tab-"]:not(#tab-tutorial)').forEach(function(t) {
858          t.classList.remove('active');
859          t.style.removeProperty('display');
860        });
861        // Show tutorial tab with inline display since it's not managed by goTab
862        var tutTab = document.getElementById('tab-tutorial');
863        if (tutTab) tutTab.style.display = 'block';
864        renderTutorialTab();
865        // Update page header
866        var header = document.querySelector('.mc-header h1, .page-title');
867        if (header) header.textContent = 'Tutorial';
868        window.location.hash = '#tutorial';
869      };
870    }
871  }
872
873  function buildTutorialHTML() {
874    var gold = 'var(--gold)';
875    var sections = [
876      {
877        id: 'tut-overview',
878        icon: '&#127968;',
879        title: 'How Jamail Hardwoods Portal Works',
880        content: '<p style="font-size:14px;line-height:1.8;color:var(--t2);margin-bottom:16px;">The portal follows a standard B2B flooring sales workflow. Here is the typical lifecycle of a project from first contact to completion:</p>'
881          + '<div style="display:flex;flex-wrap:wrap;gap:12px;margin-bottom:20px;">'
882          + stepBadge('1', 'Lead Created', '#4a4a4a')
883          + stepBadge('2', 'Quote Sent', '#003d7a')
884          + stepBadge('3', 'Quote Accepted', '#1a5a2f')
885          + stepBadge('4', 'Invoice Sent & Signed', '#b8860b')
886          + stepBadge('5', 'Work Order Created', '#003d7a')
887          + stepBadge('6', 'Materials Ordered (PO)', '#003d7a')
888          + stepBadge('7', 'Install Scheduled', '#b8860b')
889          + stepBadge('8', 'Project Completed', '#1a5a2f')
890          + '</div>'
891          + '<p style="font-size:13px;color:var(--t3);">Not every project follows all steps. A floor inspection, for example, may only need a Quote → Invoice → Paid → Done.</p>'
892      },
893      {
894        id: 'tut-quotes',
895        icon: '&#128196;',
896        title: 'Quotes',
897        content: '<h4 style="color:var(--gold);margin-bottom:8px;">Creating a Quote</h4>'
898          + '<ol style="color:var(--t2);line-height:2.2;padding-left:20px;">'
899          + '<li>Go to <strong>Quotes</strong> in the sidebar</li>'
900          + '<li>Click <strong>+ New Quote</strong></li>'
901          + '<li>Select the lead/company or create a new one</li>'
902          + '<li>Add line items — materials, labor, freight, surcharges</li>'
903          + '<li>Set the quote date and valid-until date</li>'
904          + '<li>Click <strong>Save</strong> (status: Draft)</li>'
905          + '</ol>'
906          + '<h4 style="color:var(--gold);margin:16px 0 8px;">Sending a Quote</h4>'
907          + '<ol style="color:var(--t2);line-height:2.2;padding-left:20px;">'
908          + '<li>Open the quote and click <strong>Send Quote</strong></li>'
909          + '<li>The client receives an email with a link to view the quote</li>'
910          + '<li>Status changes: Draft → <strong>Sent</strong> → <strong>Quote Viewed</strong> (when client opens it)</li>'
911          + '<li>Client can accept or request changes</li>'
912          + '<li>Once accepted, status becomes <strong>Accepted</strong></li>'
913          + '</ol>'
914          + '<h4 style="color:var(--gold);margin:16px 0 8px;">Quote Statuses</h4>'
915          + statusTable([
916            ['Draft', 'Quote created but not sent yet'],
917            ['Sent', 'Emailed to client'],
918            ['Quote Viewed', 'Client opened the quote link'],
919            ['Quote Re-Opened', 'Client came back to view again'],
920            ['Accepted', 'Client accepted the quote'],
921            ['Declined', 'Client declined']
922          ])
923      },
924      {
925        id: 'tut-invoices',
926        icon: '&#128179;',
927        title: 'Invoices',
928        content: '<h4 style="color:var(--gold);margin-bottom:8px;">Creating an Invoice</h4>'
929          + '<ol style="color:var(--t2);line-height:2.2;padding-left:20px;">'
930          + '<li>From an accepted quote, click <strong>Create Invoice</strong></li>'
931          + '<li>Or go to <strong>Invoices → + Create Invoice</strong> for standalone invoices</li>'
932          + '<li>The invoice number is auto-generated as <strong>JH-XXXXXX</strong></li>'
933          + '<li>Review line items, totals, tax, freight, and processing fees</li>'
934          + '<li>Click <strong>Send to Customer</strong></li>'
935          + '</ol>'
936          + '<h4 style="color:var(--gold);margin:16px 0 8px;">Client Signing & Payment</h4>'
937          + '<ol style="color:var(--t2);line-height:2.2;padding-left:20px;">'
938          + '<li>Client receives email with invoice link</li>'
939          + '<li>Client reviews the contract and signs electronically</li>'
940          + '<li>Client pays via Stripe (credit card) or offline</li>'
941          + '<li>Status updates: Sent → <strong>Invoice Signed</strong> → <strong>Paid</strong> / <strong>Partially Paid</strong></li>'
942          + '</ol>'
943          + '<h4 style="color:var(--gold);margin:16px 0 8px;">Invoice Statuses</h4>'
944          + statusTable([
945            ['Draft', 'Created but not sent'],
946            ['Sent To Customer', 'Emailed to client'],
947            ['Invoice Confirmed', 'Client acknowledged receipt'],
948            ['Invoice Signed', 'Client signed the contract'],
949            ['Partially Paid', 'Some payment received'],
950            ['Fully Paid / Paid', 'Payment complete'],
951            ['Completed', 'Imported from FloorzAP as completed'],
952            ['Void', 'Invoice cancelled/voided'],
953            ['Cancelled', 'Invoice cancelled']
954          ])
955      },
956      {
957        id: 'tut-workorders',
958        icon: '&#128295;',
959        title: 'Work Orders',
960        content: '<h4 style="color:var(--gold);margin-bottom:8px;">Creating a Work Order</h4>'
961          + '<ol style="color:var(--t2);line-height:2.2;padding-left:20px;">'
962          + '<li>Go to <strong>Work Orders</strong> in the sidebar</li>'
963          + '<li>Work orders are typically created from an invoice or quote</li>'
964          + '<li>They track the actual job: materials cost, labor cost, contractor, schedule date</li>'
965          + '<li>Each WO has a number like <strong>WO-100XXX</strong></li>'
966          + '</ol>'
967          + '<h4 style="color:var(--gold);margin:16px 0 8px;">Managing a Work Order</h4>'
968          + '<ol style="color:var(--t2);line-height:2.2;padding-left:20px;">'
969          + '<li>Assign a <strong>contractor</strong> (installer)</li>'
970          + '<li>Set the <strong>schedule date</strong></li>'
971          + '<li>Track <strong>material cost vs. labor cost</strong> for profit calculations</li>'
972          + '<li>Record <strong>payments</strong> as they come in</li>'
973          + '<li>The WO shows real-time profit %, net profit, and commission</li>'
974          + '</ol>'
975          + '<h4 style="color:var(--gold);margin:16px 0 8px;">Work Order Statuses</h4>'
976          + statusTable([
977            ['Pending', 'WO created, not yet started'],
978            ['Materials Ordered', 'Waiting for materials to arrive'],
979            ['Scheduled', 'Install date set with contractor'],
980            ['In Progress', 'Installation underway (shows % complete)'],
981            ['Completed', 'Job finished']
982          ])
983      },
984      {
985        id: 'tut-pos',
986        icon: '&#128230;',
987        title: 'Purchase Orders',
988        content: '<h4 style="color:var(--gold);margin-bottom:8px;">What are Purchase Orders?</h4>'
989          + '<p style="color:var(--t2);line-height:1.8;margin-bottom:12px;">Purchase Orders (POs) are used to order materials from your suppliers/vendors. They track what was ordered, from whom, when it ships, and when it arrives.</p>'
990          + '<h4 style="color:var(--gold);margin:16px 0 8px;">Creating a PO</h4>'
991          + '<ol style="color:var(--t2);line-height:2.2;padding-left:20px;">'
992          + '<li>Go to <strong>Purchase Orders → + New PO</strong></li>'
993          + '<li>Select the vendor/supplier</li>'
994          + '<li>Add the materials being ordered with quantities and costs</li>'
995          + '<li>Link it to the relevant work order or project</li>'
996          + '<li>Save and send to vendor</li>'
997          + '</ol>'
998          + '<h4 style="color:var(--gold);margin:16px 0 8px;">PO Statuses</h4>'
999          + statusTable([
1000            ['Draft', 'PO created, not yet sent to vendor'],
1001            ['Sent', 'Sent to vendor for fulfillment'],
1002            ['Confirmed', 'Vendor confirmed the order'],
1003            ['Shipped', 'Materials are in transit'],
1004            ['Received', 'Materials arrived at job site or warehouse']
1005          ])
1006      },
1007      {
1008        id: 'tut-schedule',
1009        icon: '&#128197;',
1010        title: 'Schedule (Install & Delivery)',
1011        content: '<h4 style="color:var(--gold);margin-bottom:8px;">Install & Delivery Schedule</h4>'
1012          + '<p style="color:var(--t2);line-height:1.8;margin-bottom:12px;">The Schedule section manages contractor installs and material deliveries with a calendar view.</p>'
1013          + '<h4 style="color:var(--gold);margin:16px 0 8px;">Scheduling an Install</h4>'
1014          + '<ol style="color:var(--t2);line-height:2.2;padding-left:20px;">'
1015          + '<li>Go to <strong>Schedule → + New Install</strong></li>'
1016          + '<li>Select the job/work order</li>'
1017          + '<li>Assign a <strong>contractor</strong></li>'
1018          + '<li>Set <strong>start date, end date, start time, end time</strong></li>'
1019          + '<li>Add <strong>special instructions</strong> and contractor notes</li>'
1020          + '<li>Choose who to notify: Client, PM, Sales Rep</li>'
1021          + '</ol>'
1022          + '<h4 style="color:var(--gold);margin:16px 0 8px;">Schedule Statuses</h4>'
1023          + statusTable([
1024            ['New', 'Install entry created'],
1025            ['Assigned', 'Contractor assigned but not scheduled'],
1026            ['Ready To Schedule', 'Materials received, ready to book date'],
1027            ['Scheduled', 'Date confirmed with contractor'],
1028            ['In Progress', 'Installation underway'],
1029            ['Labor Complete', 'Install finished, pending payment'],
1030            ['Ready for Payment', 'Awaiting contractor payment'],
1031            ['Paid', 'Contractor has been paid']
1032          ])
1033      },
1034      {
1035        id: 'tut-projects',
1036        icon: '&#127959;',
1037        title: 'Project Tracker',
1038        content: '<h4 style="color:var(--gold);margin-bottom:8px;">What is the Project Tracker?</h4>'
1039          + '<p style="color:var(--t2);line-height:1.8;margin-bottom:12px;">The Project Tracker gives you a bird\'s-eye view of a job from planning through completion. It ties together deliveries, installations, milestones, and updates in one place.</p>'
1040          + '<h4 style="color:var(--gold);margin:16px 0 8px;">Project Lifecycle</h4>'
1041          + '<ol style="color:var(--t2);line-height:2.2;padding-left:20px;">'
1042          + '<li><strong>Planning</strong> — Project created, gathering details</li>'
1043          + '<li><strong>Material Ordered</strong> — PO sent to vendor</li>'
1044          + '<li><strong>Material In Transit</strong> — Shipment on the way</li>'
1045          + '<li><strong>Material Received</strong> — Delivered to site/warehouse</li>'
1046          + '<li><strong>Scheduled</strong> — Install date booked with contractor</li>'
1047          + '<li><strong>In Progress</strong> — Installation underway</li>'
1048          + '<li><strong>Complete</strong> — Job finished, click the <strong>Complete</strong> button</li>'
1049          + '</ol>'
1050          + '<h4 style="color:var(--gold);margin:16px 0 8px;">Marking a Project Complete</h4>'
1051          + '<ol style="color:var(--t2);line-height:2.2;padding-left:20px;">'
1052          + '<li>Open the project from the Project Tracker list</li>'
1053          + '<li>Change the status dropdown to <strong>Complete</strong></li>'
1054          + '<li>Or click the <strong>Complete</strong> button in the top-right corner</li>'
1055          + '<li>Go to the lead record and update the stage to <strong>Won</strong> or <strong>Active Client</strong></li>'
1056          + '</ol>'
1057      },
1058      {
1059        id: 'tut-completion',
1060        icon: '&#9989;',
1061        title: 'Closing Out a Job (Full Checklist)',
1062        content: '<p style="color:var(--t2);line-height:1.8;margin-bottom:16px;">When a project is done, make sure these items are checked off:</p>'
1063          + '<div style="background:var(--d2);border:1px solid var(--b);border-radius:8px;padding:20px;">'
1064          + '<div style="display:flex;flex-direction:column;gap:12px;">'
1065          + checkItem('Invoice status is <strong>Paid</strong> or <strong>Fully Paid</strong>')
1066          + checkItem('Contract is signed (check Signed badge on invoice)')
1067          + checkItem('Work Order status is <strong>Completed</strong>')
1068          + checkItem('Contractor has been paid (Schedule → Paid status)')
1069          + checkItem('Materials all received (no pending deliveries)')
1070          + checkItem('Project Tracker set to <strong>Complete</strong>')
1071          + checkItem('Lead stage updated to <strong>Won</strong> or <strong>Active Client</strong>')
1072          + checkItem('Any punch list items resolved')
1073          + '</div></div>'
1074      }
1075    ];
1076
1077    return '<div style="padding:20px;background:var(--d1);min-height:100vh;">'
1078      + '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:24px;">'
1079      + '<h2 style="color:var(--t);margin:0;">&#128218; Portal Tutorial & Help</h2></div>'
1080      + '<p style="color:var(--t2);margin-bottom:24px;font-size:14px;">Step-by-step guides for every section of the Jamail Hardwoods portal.</p>'
1081      + sections.map(function(s) {
1082        return '<div id="' + s.id + '" style="background:var(--d3);border:1px solid var(--b);border-radius:8px;padding:20px;margin-bottom:16px;">'
1083          + '<h3 style="color:var(--t);margin:0 0 16px 0;font-size:16px;">' + s.icon + ' ' + s.title + '</h3>'
1084          + s.content + '</div>';
1085      }).join('')
1086      + '</div>';
1087  }
1088
1089  function stepBadge(num, label, color) {
1090    return '<div style="display:flex;align-items:center;gap:6px;padding:6px 12px;background:' + color + '22;border:1px solid ' + color + ';border-radius:20px;">'
1091      + '<span style="background:' + color + ';color:#fff;width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;">
1091' + num + '</span>'
1092      + '<span style="color:var(--t);font-size:12px;font-weight:600;">' + label + '</span></div>';
1093  }
1094
1095  function statusTable(rows) {
1096    return '<div style="display:grid;grid-template-columns:auto 1fr;gap:8px 16px;margin-top:8px;">'
1097      + rows.map(function(r) {
1098        return '<div style="font-weight:600;color:var(--t);font-size:13px;">' + r[0] + '</div>'
1099          + '<div style="color:var(--t2);font-size:13px;">' + r[1] + '</div>';
1100      }).join('')
1101      + '</div>';
1102  }
1103
1104  function checkItem(text) {
1105    return '<div style="display:flex;align-items:center;gap:10px;color:var(--t2);font-size:14px;">'
1106      + '<span style="color:var(--gold);font-size:18px;">&#9744;</span>' + text + '</div>';
1107  }
1108
1109  // Initialize when DOM is ready
1110  if (document.readyState === 'loading') {
1111    document.addEventListener('DOMContentLoaded', init);
1112  } else {
1113    setTimeout(init, 300);
1114  }
1115
1116  console.log('[patch.js] FIX 12: Tutorial/Help section added');
1117})();
1118
1119// === FIX 13: Finance Hub ===
1120(function() {
1121  'use strict';
1122
1123  // ===== Utility Functions =====
1124  window.fmtMoney = function(n) {
1125    if (!n && n !== 0) return '0.00';
1126    return parseFloat(n).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
1127  };
1128
1129  // (Cowork hardening 2026-05-08) Date parser must treat YYYY-MM-DD strings
1130  // as LOCAL dates, not UTC. JavaScript's `new Date('2026-05-08')` parses
1131  // as UTC midnight, which renders as May 7 in any UTC-negative timezone
1132  // (Houston is UTC-5/-6). This caused payment dates to display one day
1133  // earlier than the actual payment date — Joey caught Samin's $242.24
1134  // commission showing 5/7/26 when the Excel sheet recorded it as 5/8/26.
1135  window.fmtDate = function(d) {
1136    if (!d) return '';
1137    // Pure date string YYYY-MM-DD — parse as local, no timezone shift
1138    if (typeof d === 'string') {
1139      var ds = d.slice(0, 10);
1140      var m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ds);
1141      if (m) return m[2] + '/' + m[3] + '/' + m[1];
1142    }
1143    // Datetime with time component — let Date handle it (timestamps from
1144    // created_at etc. are already timezone-aware)
1145    var date = new Date(d);
1146    return (date.getMonth() + 1).toString().padStart(2, '0') + '/' +
1147           date.getDate().toString().padStart(2, '0') + '/' +
1148           date.getFullYear();
1149  };
1150
1151  window.fmtDateISO = function(d) {
1152    if (!d) return '';
1153    if (typeof d === 'string') {
1154      var ds = d.slice(0, 10);
1155      if (/^\d{4}-\d{2}-\d{2}$/.test(ds)) return ds;
1156    }
1157    var date = new Date(d);
1158    return date.getFullYear() + '-' +
1159           (date.getMonth() + 1).toString().padStart(2, '0') + '-' +
1160           date.getDate().toString().padStart(2, '0');
1161  };
1162
1163  // ===== Finance Hub Rendering =====
1164  window.renderFinanceHub = function() {
1165    if (prof.role !== 'admin') {
1166      document.getElementById('tab-finance').innerHTML = '<div style="padding:20px; color:var(--t);">Finance Hub is admin-only. Contact an administrator.</div>';
1167      return;
1168    }
1169
1170    var html = `
1171      <div style="display:flex; flex-direction:column; height:100%; background:var(--d1);">
1172        <!-- Finance Sub-Nav -->
1173        <div style="padding:15px; border-bottom:1px solid var(--b); background:var(--d3); display:flex; gap:10px; flex-wrap:wrap;">
1174          <button id="fin-btn-commission" onclick="showFinanceSection('commission')" style="padding:8px 16px; border:1px solid var(--b); background:var(--gold); color:#fff; cursor:pointer; border-radius:4px; font-size:14px; font-weight:600;">Commission Dashboard</button>
1175          <button id="fin-btn-payroll" onclick="showFinanceSection('payroll')" style="padding:8px 16px; border:1px solid var(--b); background:var(--d3); color:var(--t); cursor:pointer; border-radius:4px; font-size:14px;">Payroll</button>
1176          <button id="fin-btn-installer" onclick="showFinanceSection('installer')" style="padding:8px 16px; border:1px solid var(--b); background:var(--d3); color:var(--t); cursor:pointer; border-radius:4px; font-size:14px;">Installer Payments</button>
1177          <button id="fin-btn-vendor" onclick="showFinanceSection('vendor')" style="padding:8px 16px; border:1px solid var(--b); background:var(--d3); color:var(--t); cursor:pointer; border-radius:4px; font-size:14px;">
1177Vendor Payments</button>
1178          <button id="fin-btn-deductions" onclick="showFinanceSection('deductions')" style="padding:8px 16px; border:1px solid var(--b); background:var(--d3); color:var(--t); cursor:pointer; border-radius:4px; font-size:14px;">Deductions</button>
1179          <button id="fin-btn-credits" onclick="showFinanceSection('credits')" style="padding:8px 16px; border:1px solid var(--b); background:var(--d3); color:var(--t); cursor:pointer; border-radius:4px; font-size:14px;">Store Credits</button>
1180          <button id="fin-btn-refunds" onclick="showFinanceSection('refunds')" style="padding:8px 16px; border:1px solid var(--b); background:var(--d3); color:var(--t); cursor:pointer; border-radius:4px; font-size:14px;">Client Refunds</button>
1181          <button id="fin-btn-storage" onclick="showFinanceSection('storage')" style="padding:8px 16px; border:1px solid var(--b); background:var(--d3); color:var(--t); cursor:pointer; border-radius:4px; font-size:14px;">Storage Fees</button>
1182          <button id="fin-btn-expenses" onclick="showFinanceSection('expenses')" style="padding:8px 16px; border:1px solid var(--b); background:var(--d3); color:var(--t); cursor:pointer; border-radius:4px; font-size:14px;">Office Expenses</button>
1183        </div>
1184
1185        <!-- Content Area -->
1186        <div style="flex:1; overflow-y:auto; padding:20px;">
1187          <!-- Commission Dashboard -->
1188          <div id="fin-sec-commission" style="display:block;">
1189            <h2 style="color:var(--t); margin-bottom:20px;">Commission Dashboard</h2>
1190            <div id="fin-commission-content" style="color:var(--t);">Loading...</div>
1191          </div>
1192
1193          <!-- Payroll -->
1194          <div id="fin-sec-payroll" style="display:none;">
1195            <h2 style="color:var(--t); margin-bottom:20px;">Payroll</h2>
1196            <div id="fin-payroll-content" style="color:var(--t);">Loading...</div>
1197          </div>
1198
1199          <!-- Installer Payments -->
1200          <div id="fin-sec-installer" style="display:none;">
1201            <h2 style="color:var(--t); margin-bottom:20px;">Installer Payments</h2>
1202            <button onclick="toggleFinForm('installer-add')" style="padding:8px 16px; margin-bottom:15px; background:var(--gold); color:#fff; border:none; cursor:pointer; border-radius:4px;">+ Add Payment</button>
1203            <div id="fin-installer-form" style="display:none; margin-bottom:20px; padding:15px; background:var(--d3); border:1px solid var(--b); border-radius:4px;"></div>
1204            <div id="fin-installer-content" style="color:var(--t);">Loading...</div>
1205          </div>
1206
1207          <!-- Vendor Payments -->
1208          <div id="fin-sec-vendor" style="display:none;">
1209            <h2 style="color:var(--t); margin-bottom:20px;">Vendor Payments</h2>
1210            <button onclick="toggleFinForm('vendor-add')" style="padding:8px 16px; margin-bottom:15px; background:var(--gold); color:#fff; border:none; cursor:pointer; border-radius:4px;">+ Add Payment</button>
1211            <div id="fin-vendor-form" style="display:none; margin-bottom:20px; padding:15px; background:var(--d3); border:1px solid var(--b); border-radius:4px;"></div>
1212            <div id="fin-vendor-content" style="color:var(--t);">Loading...</div>
1213          </div>
1214
1215          <!-- Deductions -->
1216          <div id="fin-sec-deductions" style="display:none;">
1217            <h2 style="color:var(--t); margin-bottom:20px;">Deductions</h2>
1218            <button onclick="toggleFinForm('deductions-add')" style="padding:8px 16px; margin-bottom:15px; background:var(--gold); color:#fff; border:none; cursor:pointer; border-radius:4px;">+ Add Deduction</button>
1219            <div id="fin-deductions-form" style="display:none; margin-bottom:20px; padding:15px; background:var(--d3); border:1px solid var(--b); border-radius:4px;"></div>
1220            <div id="fin-deductions-content" style="color:var(--t);">Loading...</div>
1221          </div>
1222
1223          <!-- Store Credits -->
1224          <div id="fin-sec-credits" style="display:none;">
1225            <h2 style="color:var(--t); margin-bottom:20px;">Store Credits</h2>
1226            <button onclick="toggleFinForm('credits-add')" style="padding:8px 16px; margin-bottom:15px; background:var(--gold); color:#fff; border:none; cursor:pointer; border-radius:4px;">+ Add Credit</button>
1227            <div id="fin-credits-form" style="display:none; margin-bottom:20px; padding:15px; background:var(--d3); border:1px solid var(--b); border-radius:4px;"></div>
1228            <div id="fin-credits-content" style="color:var(--t);">Loading...</div>
1229          </div>
1230
1231          <!-- Client Refunds -->
1232          <div id="fin-sec-refunds" style="display:none;">
1233            <h2 style="color:var(--t); margin-bottom:20px;">Client Refunds</h2>
1234            <button onclick="toggleFinForm('refunds-add')" style="padding:8px 16px; margin-bottom:15px; background:var(--gold); color:#fff; border:none; cursor:pointer; border-radius:4px;">+ Add Refund</button>
1235            <div id="fin-refunds-form" style="display:none; margin-bottom:20px; padding:15px; background:var(--d3); border:1px solid var(--b); border-radius:4px;"></div>
1236            <div id="fin-refunds-content" style="color:var(--t);">Loading...</div>
1237          </div>
1238
1239          <!-- Storage Fees -->
1240          <div id="fin-sec-storage" style="display:none;">
1241            <h2 style="color:var(--t); margin-bottom:20px;">Storage Fees</h2>
1242            <button onclick="toggleFinForm('storage-add')" style="padding:8px 16px; margin-bottom:15px; background:var(--gold); color:#fff; border:none; cursor:pointer; border-radius:4px;">+ Add Storage Fee</button>
1243            <div id="fin-storage-form" style="display:none; margin-bottom:20px; padding:15px; background:var(--d3); border:1px solid var(--b); border-radius:4px;"></div>
1244            <div id="fin-storage-content" style="color:var(--t);">Loading...</div>
1245          </div>
1246
1247          <!-- Office Expenses -->
1248          <div id="fin-sec-expenses" style="display:none;">
1249            <h2 style="color:var(--t); margin-bottom:20px;">Office Expenses</h2>
1250            <button onclick="toggleFinForm('expenses-add')" style="padding:8px 16px; margin-bottom:15px; background:var(--gold); color:#fff; border:none; cursor:pointer; border-radius:4px;">+ Add Expense</button>
1251            <div id="fin-expenses-form" style="display:none; margin-bottom:20px; padding:15px; background:var(--d3); border:1px solid var(--b); border-radius:4px;"></div>
1252            <div id="fin-expenses-content" style="color:var(--t);">Loading...</div>
1253          </div>
1254        </div>
1255      </div>
1256    `;
1257    document.getElementById('tab-finance').innerHTML = html;
1258    showFinanceSection('commission');
1259  };
1260
1261  // ===== Section Navigation =====
1262  window.showFinanceSection = function(section) {
1263    // Hide all sections
1264    document.querySelectorAll('[id^="fin-sec-"]').forEach(function(d) {
1265      d.style.display = 'none';
1266    });
1267    // Show selected
1268    var el = document.getElementById('fin-sec-' + section);
1269    if (el) el.style.display = 'block';
1270
1271    // Update button styles
1272    document.querySelectorAll('[id^="fin-btn-"]').forEach(function(b) {
1273      b.style.background = 'var(--d3)';
1274      b.style.color = 'var(--t)';
1275      b.style.borderColor = 'var(--b)';
1276    });
1277    var btn = document.getElementById('fin-btn-' + section);
1278    if (btn) {
1279      btn.style.background = 'var(--gold)';
1280      btn.style.color = '#fff';
1281      btn.style.borderColor = 'var(--gold)';
1282    }
1283
1284    // Load data
1285    loadFinanceSection(section);
1286  };
1287
1288  window.setFinDateRange = function(range) {
1289    _finDateRange = range;
1290    loadCommissionDashboard();
1291  };
1292
1293  window.loadFinanceSection = function(section) {
1294    if (section === 'commission') loadCommissionDashboard();
1295    else if (section === 'payroll') loadPayrollSection();
1296    else if (section === 'installer') loadInstallerPayments();
1297    else if (section === 'vendor') loadVendorPayments();
1298    else if (section === 'deductions') loadDeductions();
1299    else if (section === 'credits') loadStoreCredits();
1300    else if (section === 'refunds') loadClientRefunds();
1301    else if (section === 'storage') loadStorageFees();
1302    else if (section === 'expenses') loadOfficeExpenses();
1303  };
1304
1305  // ===== Commission Dashboard =====
1306  // Track the selected date range for commission filtering
1307  var _finDateRange = 'ytd'; // default to YTD 2026
1308
1309  window.loadCommissionDashboard = function() {
1310    var contentEl = document.getElementById('fin-commission-content');
1311    contentEl.innerHTML = 'Loading...';
1312
1313    // Calculate date range based on selection
1314    var now = new Date();
1315    var year = 2026;
1316    var rangeStart, rangeEnd, rangeLabel;
1317
1318    if (_finDateRange === 'ytd') {
1319      rangeStart = year + '-01-01';
1320      rangeEnd = fmtDateISO(now);
1321      rangeLabel = '2026 Year-to-Date (Jan 1 – Today)';
1322    } else if (_finDateRange === 'full') {
1323      rangeStart = year + '-01-01';
1324      rangeEnd = year + '-12-31';
1325      rangeLabel = 'Full Year 2026';
1326    } else if (_finDateRange === 'q1') {
1327      rangeStart = year + '-01-01'; rangeEnd = year + '-03-31';
1328      rangeLabel = 'Q1 2026 (Jan–Mar)';
1329    } else if (_finDateRange === 'q2') {
1330      rangeStart = year + '-04-01'; rangeEnd = year + '-06-30';
1331      rangeLabel = 'Q2 2026 (Apr–Jun)';
1332    } else if (_finDateRange === 'q3') {
1333      rangeStart = year + '-07-01'; rangeEnd = year + '-09-30';
1334      rangeLabel = 'Q3 2026 (Jul–Sep)';
1335    } else if (_finDateRange === 'q4') {
1336      rangeStart = year + '-10-01'; rangeEnd = year + '-12-31';
1337      rangeLabel = 'Q4 2026 (Oct–Dec)';
1338    } else if (_finDateRange === 'month') {
1339      var monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
1340      var monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0);
1341      rangeStart = fmtDateISO(monthStart);
1342      rangeEnd = fmtDateISO(monthEnd);
1343      rangeLabel = 'This Month (' + monthStart.toLocaleString('default',{month:'long'}) + ' ' + now.getFullYear() + ')';
1344    } else {
1345      rangeStart = year + '-01-01';
1346      rangeEnd = fmtDateISO(now);
1347      rangeLabel = '2026 Year-to-Date';
1348    }
1349
1350    // (Cowork hardening 2026-05-08) Proper multi-rep commission split
1351    // ---------------------------------------------------------------
1352    // OLD behavior: grouped invoices by raw `sales_rep` text. When an
1353    // invoice had two reps (e.g., "Patrick McNeill, Samin Seifpour"),
1354    // the comma-joined string was treated as a single rep name and
1355    // never matched rep_pay_config — so the deal showed $0 commission
1356    // even though both reps deserved a cut. Joey's data was missing
1357    // ~$28k of commission that way.
1358    //
1359    // NEW behavior: per-invoice allocation using the underlying
1360    // quote's commission_pct + second_rep_commission_pct. Falls back
1361    // to rep_pay_config when there's no quote.
1362    //
1363    // Commission base = max(0, wo_profit - tax_amount). Per Joey:
1364    // sales tax is not profit and shouldn't earn commission. The
1365    // existing wo_profit field includes tax in the calc; we strip
1366    // it out here so historical data computes correctly without a
1367    // backfill of every invoice's wo_profit field.
1368    Promise.all([
1369      sb.from('invoices').select('id,invoice_number,quote_id,sales_rep,total_sale,wo_profit,wo_net_profit,tax_amount').gte('created_date', rangeStart).lte('created_date', rangeEnd),
1370      sb.from('rep_pay_config').select('*'),
1371      sb.from('quotes').select('id,commission_pct,second_rep_commission_pct'),
1372      sb.from('rep_deductions').select('rep_name,amount,paid_amount,status').gte('deduction_date', rangeStart).lte('deduction_date', rangeEnd),
1373      // (Cowork hardening 2026-05-08) Pull commission payments in the same date
1374      // range so the dashboard can show Earned vs Paid vs Outstanding per rep.
1375      sb.from('rep_payments').select('rep_name,amount,payment_type').eq('payment_type', 'commission').gte('payment_date', rangeStart).lte('payment_date', rangeEnd)
1376    ]).then(function(results) {
1377      var invoices = results[0].data || [];
1378      var repConfigs = results[1].data || [];
1379      var quotes = results[2].data || [];
1380      var deductions = results[3].data || [];
1381      var commissionPayments = results[4].data || [];
1382
1383      // Lookups
1384      var quoteById = {};
1385      quotes.forEach(function(q) { quoteById[q.id] = q; });
1386      var configByName = {};
1387      repConfigs.forEach(function(c) { configByName[c.rep_name] = c; });
1388
1389      var repData = {};   // keyed by individual rep name
1390
1391      function bumpRep(name, deltaSales, deltaProfitBase, deltaCommission, ratePct) {
1392        if (!name) return;
1393        if (!repData[name]) {
1394          repData[name] = { deals: 0, totalSale: 0, profitBase: 0, commission: 0, deductions: 0, _rateSum: 0, _rateN: 0, config: configByName[name] || null };
1395        }
1396        repData[name].deals += 1;
1397        repData[name].totalSale += deltaSales;
1398        repData[name].profitBase += deltaProfitBase;
1399        repData[name].commission += deltaCommission;
1400        repData[name]._rateSum += ratePct;
1401        repData[name]._rateN += 1;
1402      }
1403
1404      invoices.forEach(function(inv) {
1405        var sr = (inv.sales_rep || '').trim();
1406        var reps = sr ? sr.split(',').map(function(s) { return s.trim(); }).filter(Boolean) : [];
1407        if (reps.length === 0) reps = ['Unknown'];
1408        var saleTotal = parseFloat(inv.total_sale || 0);
1409        var profitInclTax = parseFloat(inv.wo_profit || inv.wo_net_profit || 0);
1410        var tax = parseFloat(inv.tax_amount || 0);
1411        // Strip tax out of the commission base — tax is not profit.
1412        var commissionBase = Math.max(0, profitInclTax - tax);
1413
1414        var quote = inv.quote_id ? quoteById[inv.quote_id] : null;
1415        var firstPct = 0, secondPct = 0;
1416        if (quote && (quote.commission_pct || quote.second_rep_commission_pct)) {
1417          firstPct = parseFloat(quote.commission_pct || 0);
1418          secondPct = parseFloat(quote.second_rep_commission_pct || 0);
1419        } else if (configByName[reps[0]]) {
1420          var rawPct = parseFloat(configByName[reps[0]].commission_pct || 25);
1421          firstPct = rawPct > 1 ? rawPct : rawPct * 100;
1422        } else {
1423          firstPct = 25; // default
1424        }
1425        // Normalize fraction-style percents (0.25 → 25)
1426        if (firstPct > 0 && firstPct < 1) firstPct *= 100;
1427        if (secondPct > 0 && secondPct < 1) secondPct *= 100;
1428
1429        // Allocate proportionally so sales/profit totals match invoice totals
1430        var totalPct = firstPct + (reps.length > 1 ? secondPct : 0);
1431        var firstSalesShare, secondSalesShare;
1432        if (totalPct > 0) {
1433          firstSalesShare = firstPct / totalPct;
1434          secondSalesShare = secondPct / totalPct;
1435        } else {
1436          firstSalesShare = 1; secondSalesShare = 0;
1437        }
1438
1439        var firstCommission = commissionBase * (firstPct / 100);
1440        var secondCommission = commissionBase * (secondPct / 100);
1441        bumpRep(reps[0], saleTotal * firstSalesShare, commissionBase * firstSalesShare, firstCommission, firstPct);
1442        if (reps.length > 1) {
1443          bumpRep(reps[1], saleTotal * secondSalesShare, commissionBase * secondSalesShare, secondCommission, secondPct);
1444        }
1445      });
1446
1447      // Apply rep_deductions in date range — but ONLY count what's been
1448      // actually deducted (status='paid' = full amount; status='partial' =
1449      // paid_amount only; status='new' = informational, not yet taken).
1450      deductions.forEach(function(d) {
1451        if (!d.rep_name || !repData[d.rep_name]) return;
1452        var st = d.status || 'new';
1453        var amt = parseFloat(d.amount || 0);
1454        var paidAmt = parseFloat(d.paid_amount || 0);
1455        var deducted = (st === 'paid') ? amt : (st === 'partial' ? paidAmt : 0);
1456        repData[d.rep_name].deductions += deducted;
1457      });
1458
1459      // Apply commission payments — sum what's been paid in the same period
1460      commissionPayments.forEach(function(p) {
1461        if (!p.rep_name || !repData[p.rep_name]) return;
1462        if (!repData[p.rep_name].commissionPaid) repData[p.rep_name].commissionPaid = 0;
1463        repData[p.rep_name].commissionPaid += parseFloat(p.amount || 0);
1464      });
1465
1466      // Date range selector + summary header
1467      var totalSales = 0, totalProfit = 0, totalCommission = 0, totalDeductions = 0, totalCommPaid = 0, totalDeals = 0;
1468      Object.keys(repData).forEach(function(r) {
1469        totalSales += repData[r].totalSale;
1470        totalProfit += repData[r].profitBase;
1471        totalCommission += repData[r].commission;
1472        totalDeductions += repData[r].deductions;
1473        totalCommPaid += (repData[r].commissionPaid || 0);
1474        totalDeals += repData[r].deals;
1475      });
1476
1477      var html = '<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:16px; flex-wrap:wrap; gap:10px;">';
1478      html += '<div style="font-size:14px; color:var(--t2); font-weight:600;">' + rangeLabel + ' &mdash; ' + invoices.length + ' invoices</div>';
1479      html += '<select id="fin-date-range" onchange="setFinDateRange(this.value)" style="padding:8px 12px; background:var(--d3); border:1px solid var(--b); color:var(--t); border-radius:6px; font-size:13px;">';
1480      html += '<option value="ytd"' + (_finDateRange==='ytd'?' selected':'') + '>2026 YTD</option>';
1481      html += '<option value="full"' + (_finDateRange==='full'?' selected':'') + '>Full Year 2026</option>';
1482      html += '<option value="q1"' + (_finDateRange==='q1'?' selected':'') + '>Q1 (Jan-Mar)</option>';
1483      html += '<option value="q2"' + (_finDateRange==='q2'?' selected':'') + '>Q2 (Apr-Jun)</option>';
1484      html += '<option value="q3"' + (_finDateRange==='q3'?' selected':'') + '>Q3 (Jul-Sep)</option>';
1485      html += '<option value="q4"' + (_finDateRange==='q4'?' selected':'') + '>Q4 (Oct-Dec)</option>';
1486      html += '<option value="month"' + (_finDateRange==='month'?' selected':'') + '>This Month</option>';
1487      html += '</select></div>';
1488
1489      var netCommission = totalCommission - totalDeductions;
1490
1491      // KPI summary cards (5 now, including Deductions)
1492      html += '<div style="display:grid; grid-template-columns:repeat(5,1fr); gap:12px; margin-bottom:20px;">';
1493      html += '<div style="background:var(--d3); border:1px solid var(--b); border-radius:8px; padding:16px; text-align:center;">'
1494        + '<div style="font-size:10px; color:var(--t3); text-transform:uppercase; font-weight:600;">Total Deals</div>'
1495        + '<div style="font-size:22px; font-weight:700; color:var(--t); margin-top:4px;">' + totalDeals + '</div></div>';
1496      html += '<div style="background:var(--d3); border:1px solid var(--b); border-radius:8px; padding:16px; text-align:center;">'
1497        + '<div style="font-size:10px; color:var(--t3); text-transform:uppercase; font-weight:600;">Total Sales</div>'
1498        + '<div style="font-size:22px; font-weight:700; color:var(--t); margin-top:4px;">' + '$' + fmtMoney(totalSales) + '</div></div>';
1499      html += '<div style="background:var(--d3); border:1px solid var(--b); border-radius:8px; padding:16px; text-align:center;">'
1500        + '<div style="font-size:10px; color:var(--t3); text-transform:uppercase; font-weight:600;">Profit (ex tax)</div>'
1501        + '<div style="font-size:22px; font-weight:700; color:' + (totalProfit > 0 ? '#16a34a' : '#ef4444') + '; margin-top:4px;">' + '$' + fmtMoney(totalProfit) + '</div></div>';
1502      html += '<div style="background:var(--d3); border:1px solid var(--b); border-radius:8px; padding:16px; text-align:center;">'
1503        + '<div style="font-size:10px; color:var(--t3); text-transform:uppercase; font-weight:600;">Commission Gross</div>'
1504        + '<div style="font-size:22px; font-weight:700; color:var(--gold); margin-top:4px;">' + '$' + fmtMoney(totalCommission) + '</div>'
1505        + '<div style="font-size:10px; color:#ef4444; margin-top:2px;">&minus;$' + fmtMoney(totalDeductions) + ' ded.</div></div>';
1506      html += '<div style="background:var(--d3); border:2px solid var(--gold); border-radius:8px; padding:16px; text-align:center;">'
1507        + '<div style="font-size:10px; color:var(--gold); text-transform:uppercase; font-weight:700;">Net Commission</div>'
1508        + '<div style="font-size:24px; font-weight:800; color:var(--gold); margin-top:4px;">' + '$' + fmtMoney(netCommission) + '</div></div>';
1509      html += '</div>';
1510
1511      html += '<table style="width:100%; border-collapse:collapse; margin-top:10px;">';
1512      html += '<tr style="background:var(--d3); border-bottom:1px solid var(--b);">';
1513      html += '<th style="padding:10px; text-align:left; color:var(--t);">Rep Name</th>';
1514      html += '<th style="padding:10px; text-align:right; color:var(--t);">Deals</th>';
1515      html += '<th style="padding:10px; text-align:right; color:var(--t);">Sales</th>';
1516      html += '<th style="padding:10px; text-align:right; color:var(--t);">Profit (ex tax)</th>';
1517      html += '<th style="padding:10px; text-align:right; color:var(--t);">Avg Rate</th>';
1518      html += '<th style="padding:10px; text-align:right; color:var(--t);">Commission Earned</th>';
1519      html += '<th style="padding:10px; text-align:right; color:var(--t);">Deductions</th>';
1520      html += '<th style="padding:10px; text-align:right; color:#16a34a;">Paid</th>';
1521      html += '<th style="padding:10px; text-align:right; color:var(--gold);">Outstanding</th>';
1522      html += '</tr>';
1523
1524      var repNamesSorted = Object.keys(repData).sort(function(a, b) {
1525        var aOut = (repData[a].commission - repData[a].deductions) - (repData[a].commissionPaid || 0);
1526        var bOut = (repData[b].commission - repData[b].deductions) - (repData[b].commissionPaid || 0);
1527        return bOut - aOut;
1528      });
1529      repNamesSorted.forEach(function(repName) {
1530        var data = repData[repName];
1531        var avgRate = data._rateN > 0 ? (data._rateSum / data._rateN).toFixed(1) + '%' : '—';
1532        var paid = data.commissionPaid || 0;
1533        var owed = data.commission - data.deductions;  // net earned
1534        var outstanding = owed - paid;
1535        html += '<tr style="border-bottom:1px solid var(--b);">';
1536        html += '<td style="padding:10px; color:var(--t);">
1536' + repName + '</td>';
1537        html += '<td style="padding:10px; text-align:right; color:var(--t);">' + data.deals + '</td>';
1538        html += '<td style="padding:10px; text-align:right; color:var(--t);">$' + fmtMoney(data.totalSale) + '</td>';
1539        html += '<td style="padding:10px; text-align:right; color:' + (data.profitBase > 0 ? '#16a34a' : 'var(--t)') + ';">$' + fmtMoney(data.profitBase) + '</td>';
1540        html += '<td style="padding:10px; text-align:right; color:var(--t3); font-size:12px;">' + avgRate + '</td>';
1541        html += '<td style="padding:10px; text-align:right; color:var(--gold); font-weight:600;">$' + fmtMoney(data.commission) + '</td>';
1542        html += '<td style="padding:10px; text-align:right; color:' + (data.deductions > 0 ? '#ef4444' : 'var(--t3)') + ';">' + (data.deductions > 0 ? '−$' + fmtMoney(data.deductions) : '$0') + '</td>';
1543        html += '<td style="padding:10px; text-align:right; color:#16a34a;">$' + fmtMoney(paid) + '</td>';
1544        html += '<td style="padding:10px; text-align:right; color:' + (outstanding > 0.01 ? 'var(--gold)' : (outstanding < -0.01 ? '#3b82f6' : 'var(--t3)')) + '; font-weight:700;">' + (outstanding < 0 ? '$(' + fmtMoney(-outstanding) + ')' : '$' + fmtMoney(outstanding)) + '</td>';
1545        html += '</tr>';
1546      });
1547
1548      html += '</table>';
1549      html += '<div style="margin-top:14px; padding:12px 16px; background:var(--d3); border-left:3px solid var(--gold); border-radius:6px; font-size:12px; color:var(--t3); line-height:1.6;">';
1550      html += '<strong style="color:var(--gold);">How this is computed:</strong> Per-deal commission split using each quote\u2019s <code>commission_pct</code> + <code>second_rep_commission_pct</code> (handles unequal splits). Commission base = <strong>profit minus sales tax</strong> (per Joey: tax is not profit). Deductions are pulled from Finance Hub \u2192 Deductions for the same date range. Falls back to <code>rep_pay_config</code> rate when an invoice has no quote attached.';
1551      html += '</div>';
1552      contentEl.innerHTML = html;
1553    }).catch(function(err) {
1554      console.error('Commission dashboard error:', err);
1555      contentEl.innerHTML = '<div style="color:var(--t2);">Error loading commission data</div>';
1556    });
1557  };
1558
1559  // ===== Payroll Section =====
1560  // (Cowork hardening 2026-05-08) Payroll tab now shows actual rep_payments
1561  // history (salary + commission + bonus + reimbursement + adjustment),
1562  // backed by the FloorZap import (229 commission + 17 salary = 246 rows
1563  // through 2024–2026). Replaces the unused rep_payroll placeholder.
1564  window._finPayrollFilter = window._finPayrollFilter || { rep: 'all', type: 'all', dateRange: 'ytd' };
1565
1566  window.loadPayrollSection = function() {
1567    var contentEl = document.getElementById('fin-payroll-content');
1568    contentEl.innerHTML = 'Loading...';
1569
1570    var f = window._finPayrollFilter;
1571    var now = new Date();
1572    var rangeStart, rangeEnd;
1573    if (f.dateRange === 'ytd') { rangeStart = '2026-01-01'; rangeEnd = fmtDateISO(now); }
1574    else if (f.dateRange === 'last30') { var d=new Date(); d.setDate(d.getDate()-30); rangeStart=fmtDateISO(d); rangeEnd=fmtDateISO(now); }
1575    else if (f.dateRange === 'q1') { rangeStart='2026-01-01'; rangeEnd='2026-03-31'; }
1576    else if (f.dateRange === 'q2') { rangeStart='2026-04-01'; rangeEnd='2026-06-30'; }
1577    else if (f.dateRange === 'q3') { rangeStart='2026-07-01'; rangeEnd='2026-09-30'; }
1578    else if (f.dateRange === 'q4') { rangeStart='2026-10-01'; rangeEnd='2026-12-31'; }
1579    else if (f.dateRange === '2025') { rangeStart='2025-01-01'; rangeEnd='2025-12-31'; }
1580    else if (f.dateRange === '2024') { rangeStart='2024-01-01'; rangeEnd='2024-12-31'; }
1581    else { rangeStart = '2024-01-01'; rangeEnd = fmtDateISO(now); }
1582
1583    var query = sb.from('rep_payments').select('*').gte('payment_date', rangeStart).lte('payment_date', rangeEnd).order('payment_date', { ascending: false });
1584    if (f.rep !== 'all') query = query.eq('rep_name', f.rep);
1585    if (f.type !== 'all') query = query.eq('payment_type', f.type);
1586
1587    Promise.all([
1588      query,
1589      sb.from('rep_payments').select('rep_name')  // for the rep dropdown
1590    ]).then(function(results) {
1591      var payments = results[0].data || [];
1592      var allReps = {};
1593      (results[1].data || []).forEach(function(r) { if (r.rep_name) allReps[r.rep_name] = true; });
1594      var reps = Object.keys(allReps).sort();
1595
1596      // Aggregate per rep (filtered set)
1597      var perRep = {};
1598      payments.forEach(function(p) {
1599        var n = p.rep_name || 'Unknown';
1600        if (!perRep[n]) perRep[n] = { total: 0, salary: 0, commission: 0, bonus: 0, reimb: 0, count: 0 };
1601        var amt = parseFloat(p.amount || 0);
1602        perRep[n].total += amt;
1603        perRep[n].count += 1;
1604        if (p.payment_type === 'salary') perRep[n].salary += amt;
1605        else if (p.payment_type === 'commission') perRep[n].commission += amt;
1606        else if (p.payment_type === 'bonus') perRep[n].bonus += amt;
1607        else if (p.payment_type === 'reimbursement') perRep[n].reimb += amt;
1608      });
1609
1610      // Grand totals
1611      var gTotal = 0, gSalary = 0, gComm = 0;
1612      Object.keys(perRep).forEach(function(n) {
1613        gTotal += perRep[n].total; gSalary += perRep[n].salary; gComm += perRep[n].commission;
1614      });
1615
1616      var html = '';
1617
1618      // Header + Add Payment button
1619      html += '<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:14px; flex-wrap:wrap; gap:10px;">';
1620      html += '<h3 style="margin:0; color:var(--t);">Sales Rep Payment History</h3>';
1621      html += '<button onclick="openPayrollAddForm()" style="padding:8px 16px; background:var(--gold); color:#fff; border:none; cursor:pointer; border-radius:6px; font-size:13px; font-weight:600;">+ Record Payment</button>';
1622      html += '</div>';
1623
1624      // Filters
1625      html += '<div style="display:flex; gap:10px; align-items:center; margin-bottom:14px; flex-wrap:wrap;">';
1626      html += '<label style="font-size:11px; color:var(--t3); text-transform:uppercase; font-weight:700;">Rep:</label>';
1627      html += '<select onchange="setFinPayrollFilter(\'rep\', this.value)" style="padding:6px 12px; background:var(--d3); border:1px solid var(--b); color:var(--t); border-radius:6px; font-size:12px;">';
1628      html += '<option value="all"' + (f.rep==='all'?' selected':'') + '>All Reps</option>';
1629      reps.forEach(function(rn) { html += '<option value="' + rn.replace(/"/g,'&quot;') + '"' + (f.rep===rn?' selected':'') + '>' + rn + '</option>'; });
1630      html += '</select>';
1631      html += '<label style="font-size:11px; color:var(--t3); text-transform:uppercase; font-weight:700; margin-left:6px;">Type:</label>';
1632      html += '<select onchange="setFinPayrollFilter(\'type\', this.value)" style="padding:6px 12px; background:var(--d3); border:1px solid var(--b); color:var(--t); border-radius:6px; font-size:12px;">';
1633      ['all','salary','commission','bonus','reimbursement','deduction','adjustment'].forEach(function(t) {
1634        html += '<option value="' + t + '"' + (f.type===t?' selected':'') + '>' + t.toUpperCase() + '</option>';
1635      });
1636      html += '</select>';
1637      html += '<label style="font-size:11px; color:var(--t3); text-transform:uppercase; font-weight:700; margin-left:6px;">Period:</label>';
1638      html += '<select onchange="setFinPayrollFilter(\'dateRange\', this.value)" style="padding:6px 12px; background:var(--d3); border:1px solid var(--b); color:var(--t); border-radius:6px; font-size:12px;">';
1639      [['ytd','2026 YTD'],['last30','Last 30 Days'],['q1','Q1 2026'],['q2','Q2 2026'],['q3','Q3 2026'],['q4','Q4 2026'],['2025','All 2025'],['2024','All 2024'],['all','All Time']].forEach(function(r) {
1640        html += '<option value="' + r[0] + '"' + (f.dateRange===r[0]?' selected':'') + '>' + r[1] + '</option>';
1641      });
1642      html += '</select>';
1643      html += '<div style="flex:1"></div>';
1644      html += '<div style="font-size:11px; color:var(--t3);">' + payments.length + ' payments · ' + rangeStart + ' to ' + rangeEnd + '</div>';
1645      html += '</div>';
1646
1647      // Per-rep summary cards
1648      html += '<div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:10px; margin-bottom:18px;">';
1649      html += '<div style="background:var(--d3); border:2px solid var(--gold); border-radius:8px; padding:14px; text-align:center;">';
1650      html += '<div style="font-size:11px; color:var(--gold); text-transform:uppercase; font-weight:700;">Total Paid (Filtered)</div>';
1651      html += '<div style="font-size:22px; font-weight:800; color:var(--gold); margin-top:4px;">$' + fmtMoney(gTotal) + '</div>';
1652      html += '<div style="font-size:11px; color:var(--t3); margin-top:3px;">Salary $' + fmtMoney(gSalary) + ' · Commission $' + fmtMoney(gComm) + '</div>';
1653      html += '</div>';
1654      Object.keys(perRep).sort(function(a,b){ return perRep[b].total - perRep[a].total; }
1654).forEach(function(rn) {
1655        var d = perRep[rn];
1656        html += '<div style="background:var(--d3); border:1px solid var(--b); border-radius:8px; padding:14px;">';
1657        html += '<div style="font-size:11px; color:var(--t3); text-transform:uppercase; font-weight:700; margin-bottom:4px;">' + rn + '</div>';
1658        html += '<div style="font-size:18px; font-weight:800; color:var(--t);">$' + fmtMoney(d.total) + '</div>';
1659        if (d.salary > 0) html += '<div style="font-size:11px; color:var(--t3);">Salary: $' + fmtMoney(d.salary) + '</div>';
1660        if (d.commission > 0) html += '<div style="font-size:11px; color:var(--gold);">Commission: $' + fmtMoney(d.commission) + '</div>';
1661        if (d.bonus > 0) html += '<div style="font-size:11px; color:#10b981;">Bonus: $' + fmtMoney(d.bonus) + '</div>';
1662        html += '<div style="font-size:10px; color:var(--t3); margin-top:3px;">' + d.count + ' payments</div>';
1663        html += '</div>';
1664      });
1665      html += '</div>';
1666
1667      // Table
1668      html += '<div style="overflow-x:auto;"><table style="width:100%; border-collapse:collapse; font-size:13px; min-width:900px;">';
1669      html += '<thead><tr style="background:var(--d3); border-bottom:1px solid var(--b);">';
1670      html += '<th style="padding:10px; text-align:left; color:var(--t3); font-size:11px; text-transform:uppercase;">Date</th>';
1671      html += '<th style="padding:10px; text-align:left; color:var(--t3); font-size:11px; text-transform:uppercase;">Rep</th>';
1672      html += '<th style="padding:10px; text-align:left; color:var(--t3); font-size:11px; text-transform:uppercase;">Type</th>';
1673      html += '<th style="padding:10px; text-align:right; color:var(--t3); font-size:11px; text-transform:uppercase;">Amount</th>';
1674      html += '<th style="padding:10px; text-align:left; color:var(--t3); font-size:11px; text-transform:uppercase;">Method</th>';
1675      html += '<th style="padding:10px; text-align:left; color:var(--t3); font-size:11px; text-transform:uppercase;">From</th>';
1676      html += '<th style="padding:10px; text-align:left; color:var(--t3); font-size:11px; text-transform:uppercase;">WO Refs / Notes</th>';
1677      html += '<th style="padding:10px; text-align:center; color:var(--t3); font-size:11px; text-transform:uppercase;">Action</th>';
1678      html += '</tr></thead><tbody>';
1679
1680      if (payments.length === 0) {
1681        html += '<tr><td colspan="8" style="padding:30px; text-align:center; color:var(--t3);">No payments match the current filter.</td></tr>';
1682      }
1683      payments.forEach(function(p) {
1684        var typeColor = { salary:'#3b82f6', commission:'#f59e0b', bonus:'#10b981', reimbursement:'#8b5cf6', deduction:'#ef4444', adjustment:'#6b7280' }[p.payment_type] || 'var(--t3)';
1685        var typeLabel = (p.payment_type || 'unknown').toUpperCase();
1686        html += '<tr style="border-bottom:1px solid var(--b);">';
1687        html += '<td style="padding:10px; color:var(--t2); font-size:12px;">' + fmtDate(p.payment_date) + '</td>';
1688        html += '<td style="padding:10px; color:var(--t); font-weight:600;">' + (p.rep_name||'') + '</td>';
1689        html += '<td style="padding:10px;"><span style="display:inline-block; padding:2px 8px; border-radius:8px; background:' + typeColor + '22; color:' + typeColor + '; font-size:10px; font-weight:700; letter-spacing:0.5px;">' + typeLabel + '</span></td>';
1690        html += '<td style="padding:10px; text-align:right; color:' + (p.payment_type==='deduction' ? '#ef4444' : 'var(--gold)') + '; font-weight:700;">$' + fmtMoney(p.amount) + '</td>';
1691        html += '<td style="padding:10px; color:var(--t2); font-size:12px;">' + (p.method || '') + '</td>';
1692        html += '<td style="padding:10px; color:var(--t3); font-size:11px;">' + (p.from_account || '—') + '</td>';
1693        var ref = p.wo_references ? p.wo_references.length > 50 ? p.wo_references.slice(0,47) + '…' : p.wo_references : (p.notes ? (p.notes.length > 80 ? p.notes.slice(0,77) + '…' : p.notes) : '—');
1694        html += '<td style="padding:10px; color:var(--t3); font-size:11px;" title="' + (p.notes||'').replace(/"/g,'&quot;') + '">' + ref + '</td>';
1695        html += '<td style="padding:10px; text-align:center;">';
1696        html += '<button onclick="deleteFinRecord(\'rep_payments\', ' + p.id + ')" style="padding:3px 8px; background:transparent; color:#c33; border:1px solid #c33; cursor:pointer; border-radius:3px; font-size:11px;">Delete</button>';
1697        html += '</td>';
1698        html += '</tr>';
1699      });
1700      html += '</tbody></table></div>';
1701
1702      contentEl.innerHTML = html;
1703    }).catch(function(err) {
1704      console.error('Payroll error:', err);
1705      contentEl.innerHTML = '<div style="color:var(--t2);">Error loading payroll data</div>';
1706    });
1707  };
1708
1709  window.openPayrollForm = function() {
1710    // Legacy "Run Payroll" placeholder — replaced by openPayrollAddForm below.
1711    window.openPayrollAddForm();
1712  };
1713
1714  window.setFinPayrollFilter = function(field, value) {
1715    window._finPayrollFilter[field] = value;
1716    window.loadPayrollSection();
1717  };
1718
1719  // Inline modal for recording a new payment to a rep
1720  window.openPayrollAddForm = function() {
1721    var existing = document.getElementById('fin-payroll-modal');
1722    if (existing) existing.remove();
1723
1724    var activeReps = (typeof profs !== 'undefined' && Array.isArray(profs))
1725      ? profs.filter(function(p){ return p && p.full_name; }).map(function(p){ return p.full_name; }).sort()
1726      : ['Ashley Brooke','Aly Champagne','Joey Jamail','Patrick McNeill','Samin Seifpour'];
1727    var repOpts = '<option value="">— Select rep —</option>' + activeReps.map(function(n){ return '<option value="' + n.replace(/"/g,'&quot;') + '">' + n + '</option>'; }).join('');
1728
1729    var modal = document.createElement('div');
1730    modal.id = 'fin-payroll-modal';
1731    modal.style.cssText = 'position:fixed; inset:0; background:rgba(0,0,0,0.7); z-index:9999; display:flex; align-items:center; justify-content:center; padding:20px;';
1732    modal.innerHTML =
1733      '<div style="background:var(--d2); border:1px solid var(--b); border-radius:12px; padding:22px; max-width:560px; width:100%;">' +
1734      '<h3 style="margin:0 0 16px 0; color:var(--gold); font-size:16px;">Record Payment to Rep</h3>' +
1735      '<div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">' +
1736      '<div><label style="font-size:11px; color:var(--t3); display:block; margin-bottom:4px; font-weight:600;">Rep *</label>' +
1737      '<select id="pay-rep" style="width:100%; padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:6px;">' + repOpts + '</select></div>' +
1738      '<div><label style="font-size:11px; color:var(--t3); display:block; margin-bottom:4px; font-weight:600;">Date *</label>' +
1739      '<input type="date" id="pay-date" value="' + new Date().toISOString().split('T')[0] + '" style="width:100%; padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:6px;"></div>' +
1740      '<div><label style="font-size:11px; color:var(--t3); display:block; margin-bottom:4px; font-weight:600;">Amount *</label>' +
1741      '<input type="number" id="pay-amount" step="0.01" placeholder="0.00" style="width:100%; padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:6px;"></div>' +
1742      '<div><label style="font-size:11px; color:var(--t3); display:block; margin-bottom:4px; font-weight:600;">Type *</label>' +
1743      '<select id="pay-type" style="width:100%; padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:6px;">' +
1744      '<option value="commission">Commission</option><option value="salary">Salary</option><option value="bonus">Bonus</option><option value="reimbursement">Reimbursement</option><option value="adjustment">Adjustment</option>' +
1745      '</select></div>' +
1746      '<div><label style="font-size:11px; color:var(--t3); display:block; margin-bottom:4px; font-weight:600;">Method *</label>' +
1747      '<select id="pay-method" style="width:100%; padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:6px;">' +
1748      '<option value="zelle">Zelle</option><option value="ach">ACH</option><option value="account_transfer">Account Transfer</option><option value="check">Check</option><option value="cash">Cash</option><option value="wire">Wire</option><option value="credit_card">Credit Card</option><option value="other">Other</option>' +
1749      '</select></div>' +
1750      '<div><label style="font-size:11px; color:var(--t3); display:block; margin-bottom:4px; font-weight:600;">From Account</label>' +
1751      '<input type="text" id="pay-from" placeholder="Frost 9292" value="Frost 9292" style="width:100%; padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:6px;"></div>' +
1752      '<div style="grid-column:1/-1;"><label style="font-size:11px; color:var(--t3); display:block; margin-bottom:4px; font-weight:600;">WO References (optional)</label>' +
1753      '<input type="text" id="pay-wo" placeholder="JH-100311, JH-100358" style="width:100%; padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:6px;"></div>' +
1754      '<div style="grid-column:1/-1;"><label style="font-size:11px; color:var(--t3); display:block; margin-bottom:4px; font-weight:600;">Notes</label>' +
1755      '<textarea id="pay-notes" rows="2" placeholder="Any context for the audit trail" style="width:100%; padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:6px; resize:vertical; font-family:inherit;"></textarea></div>' +
1756      '</div>' +
1757      '<div style="display:flex; gap:10px; justify-content:flex-end; margin-top:16px;">' +
1758      '<button onclick="document.getElementById(\'fin-payroll-modal\').remove()" style="padding:8px 16px; background:transparent; color:var(--t3); border:1px solid var(--b); border-radius:6px; cursor:pointer;">Cancel</button>' +
1759      '<button onclick="savePayrollPayment()" style="padding:8px 16px; background:var(--gold); color:#fff; border:none; border-radius:6px; cursor:pointer; font-weight:600;">Save Payment</button>' +
1760      '</div></div>';
1761    document.body.appendChild(modal);
1762  };
1763
1764  window.savePayrollPayment = function() {
1765    var rep = document.getElementById('pay-rep').value;
1766    var date = document.getElementById('pay-date').value;
1767    var amt = parseFloat(document.getElementById('pay-amount').value);
1768    var type = document.getElementById('pay-type').value;
1769    var method = document.getElementById('pay-method').value;
1770    var fromAcct = document.getElementById('pay-from').value;
1771    var wo = (document.getElementById('pay-wo').value || '').trim();
1772    var notes = (document.getElementById('pay-notes').value || '').trim();
1773    if (!rep) { alert('Please select a rep.'); return; }
1774    if (!date) { alert('Please pick a date.'); return; }
1775    if (isNaN(amt) || amt <= 0) { alert('Please enter a valid amount.'); return; }
1776
1777    sb.from('rep_payments').insert([{
1778      rep_name: rep, payment_date: date, amount: amt, payment_type: type, method: method,
1779      from_account: fromAcct || null, wo_references: wo || null, notes: notes || null,
1780    }]).then(function(result) {
1781      if (result.error) { alert('Error: ' + result.error.message); return; }
1782      var modal = document.getElementById('fin-payroll-modal');
1783      if (modal) modal.remove();
1784      window.loadPayrollSection();
1785    }).catch(function(err) { alert('Error: ' + (err.message || err)); });
1786  };
1787
1788  // ===== Installer Payments =====
1789  window.loadInstallerPayments = function() {
1790    var contentEl = document.getElementById('fin-installer-content');
1791    contentEl.innerHTML = 'Loading...';
1792
1793    sb.from('installer_payments').select('*').order('created_at', { ascending: false }).then(function(result) {
1794      var records = result.data || [];
1795
1796      var html = '<table style="width:100%; border-collapse:collapse;">';
1797      html += '<tr style="background:var(--d3); border-bottom:1px solid var(--b);">';
1798      html += '<th style="padding:10px; text-align:left; color:var(--t);">Installer</th>';
1799      html += '<th style="padding:10px; text-align:right; color:var(--t);">Sqft</th>';
1800      html += '<th style="padding:10px; text-align:right; color:var(--t);">Rate/Sqft</th>';
1801      html += '<th style="padding:10px; text-align:right; color:var(--t);">Total Pay</th>';
1802      html += '<th style="padding:10px; text-align:center; color:var(--t);">Status</th>';
1803      html += '<th style="padding:10px; text-align:center; color:var(--t);">Actions</th>';
1804      html += '</tr>';
1805
1806      records.forEach(function(row) {
1807        html += '<tr style="border-bottom:1px solid var(--b);">';
1808        html += '<td style="padding:10px; color:var(--t);">
1808' + row.installer_name + '</td>';
1809        html += '<td style="padding:10px; text-align:right; color:var(--t);">' + parseFloat(row.sqft).toFixed(0) + '</td>';
1810        html += '<td style="padding:10px; text-align:right; color:var(--t);">' + '$' + fmtMoney(row.rate_per_sqft) + '</td>';
1811        html += '<td style="padding:10px; text-align:right; color:var(--gold);">' + '$' + fmtMoney(row.total_pay) + '</td>';
1812        html += '<td style="padding:10px; text-align:center; color:var(--t);">' + row.status + '</td>';
1813        html += '<td style="padding:10px; text-align:center;">';
1814        html += '<button onclick="deleteFinRecord(\'installer_payments\', ' + row.id + ')" style="padding:4px 8px; background:#c33; color:#fff; border:none; cursor:pointer; border-radius:3px; font-size:12px;">Delete</button>';
1815        html += '</td>';
1816        html += '</tr>';
1817      });
1818
1819      html += '</table>';
1820      contentEl.innerHTML = html;
1821    }).catch(function(err) {
1822      console.error('Installer payments error:', err);
1823      contentEl.innerHTML = '<div style="color:var(--t2);">Error loading data</div>';
1824    });
1825  };
1826
1827  // ===== Vendor Payments =====
1828  window.loadVendorPayments = function() {
1829    var contentEl = document.getElementById('fin-vendor-content');
1830    contentEl.innerHTML = 'Loading...';
1831
1832    sb.from('vendor_payment_tracking').select('*').order('created_at', { ascending: false }).then(function(result) {
1833      var records = result.data || [];
1834
1835      var html = '<table style="width:100%; border-collapse:collapse;">';
1836      html += '<tr style="background:var(--d3); border-bottom:1px solid var(--b);">';
1837      html += '<th style="padding:10px; text-align:left; color:var(--t);">Vendor</th>';
1838      html += '<th style="padding:10px; text-align:right; color:var(--t);">Amount</th>';
1839      html += '<th style="padding:10px; text-align:left; color:var(--t);">Invoice #</th>';
1840      html += '<th style="padding:10px; text-align:center; color:var(--t);">Date</th>';
1841      html += '<th style="padding:10px; text-align:center; color:var(--t);">Status</th>';
1842      html += '<th style="padding:10px; text-align:left; color:var(--t);">Notes</th>';
1843      html += '<th style="padding:10px; text-align:center; color:var(--t);">Actions</th>';
1844      html += '</tr>';
1845
1846      records.forEach(function(row) {
1847        html += '<tr style="border-bottom:1px solid var(--b);">';
1848        html += '<td style="padding:10px; color:var(--t);">' + row.vendor_name + '</td>';
1849        html += '<td style="padding:10px; text-align:right; color:var(--gold);">$' + fmtMoney(row.amount) + '</td>';
1850        html += '<td style="padding:10px; color:var(--t2);">' + (row.invoice_number || '-') + '</td>';
1851        html += '<td style="padding:10px; text-align:center; color:var(--t2);">' + fmtDate(row.payment_date) + '</td>';
1852        html += '<td style="padding:10px; text-align:center; color:var(--t);">' + row.status + '</td>';
1853        html += '<td style="padding:10px; color:var(--t2); font-size:12px;">' + (row.notes || '-') + '</td>';
1854        html += '<td style="padding:10px; text-align:center;">';
1855        html += '<button onclick="deleteFinRecord(\'vendor_payment_tracking\', ' + row.id + ')" style="padding:4px 8px; background:#c33; color:#fff; border:none; cursor:pointer; border-radius:3px; font-size:12px;">Delete</button>';
1856        html += '</td>';
1857        html += '</tr>';
1858      });
1859
1860      html += '</table>';
1861      contentEl.innerHTML = html;
1862    }).catch(function(err) {
1863      console.error('Vendor payments error:', err);
1864      contentEl.innerHTML = '<div style="color:var(--t2);">Error loading data</div>';
1865    });
1866  };
1867
1868  // ===== Deductions =====
1869  // Filter state, persisted across re-renders
1870  window._finDeductionsFilter = window._finDeductionsFilter || { rep: 'all', status: 'all' };
1871
1872  window.loadDeductions = function() {
1873    var contentEl = document.getElementById('fin-deductions-content');
1874    contentEl.innerHTML = 'Loading...';
1875
1876    sb.from('rep_deductions').select('*').order('created_at', { ascending: false }).then(function(result) {
1877      var allRecords = result.data || [];
1878      var f = window._finDeductionsFilter;
1879
1880      // Distinct reps for the filter dropdown
1881      var repSet = {};
1882      allRecords.forEach(function(r) { if (r.rep_name) repSet[r.rep_name] = true; });
1883      var reps = Object.keys(repSet).sort();
1884
1885      // Apply filters
1886      var records = allRecords.filter(function(r) {
1887        if (f.rep !== 'all' && r.rep_name !== f.rep) return false;
1888        if (f.status !== 'all' && (r.status || 'new') !== f.status) return false;
1889        return true;
1890      });
1891
1892      // Per-rep totals strip (always uses ALL records, not filtered, so user sees the full picture)
1893      var perRep = {};
1894      allRecords.forEach(function(r) {
1895        var n = r.rep_name || 'Unknown';
1896        if (!perRep[n]) perRep[n] = { total: 0, paid: 0, pending: 0, partial: 0 };
1897        var amt = parseFloat(r.amount || 0);
1898        var paidAmt = parseFloat(r.paid_amount || 0);
1899        var st = r.status || 'new';
1900        perRep[n].total += amt;
1901        if (st === 'paid') perRep[n].paid += amt;
1902        else if (st === 'partial') {
1903          perRep[n].partial += amt;
1904          perRep[n].paid += paidAmt;
1905          perRep[n].pending += (amt - paidAmt);
1906        }
1907        else perRep[n].pending += amt;
1908      });
1909
1910      var html = '';
1911
1912      // Filter bar
1913      html += '<div style="display:flex; gap:10px; align-items:center; margin-bottom:14px; flex-wrap:wrap;">';
1914      html += '<label style="font-size:11px; color:var(--t3); text-transform:uppercase; font-weight:700;">Rep:</label>';
1915      html += '<select id="fin-ded-rep-filter" onchange="setFinDeductionsFilter(\'rep\', this.value)" style="padding:6px 12px; background:var(--d3); border:1px solid var(--b); color:var(--t); border-radius:6px; font-size:12px;">';
1916      html += '<option value="all"' + (f.rep==='all'?' selected':'') + '>All Reps</option>';
1917      reps.forEach(function(rn) {
1918        html += '<option value="' + rn.replace(/"/g,'&quot;') + '"' + (f.rep===rn?' selected':'') + '>' + rn + '</option>';
1919      });
1920      html += '</select>';
1921      html += '<label style="font-size:11px; color:var(--t3); text-transform:uppercase; font-weight:700; margin-left:6px;">Status:</label>';
1922      html += '<select id="fin-ded-status-filter" onchange="setFinDeductionsFilter(\'status\', this.value)" style="padding:6px 12px; background:var(--d3); border:1px solid var(--b); color:var(--t); border-radius:6px; font-size:12px;">';
1923      html += '<option value="all"' + (f.status==='all'?' selected':'') + '>All Statuses</option>';
1924      html += '<option value="new"' + (f.status==='new'?' selected':'') + '>NEW (not yet deducted)</option>';
1925      html += '<option value="partial"' + (f.status==='partial'?' selected':'') + '>PARTIAL (partially deducted)</option>';
1926      html += '<option value="paid"' + (f.status==='paid'?' selected':'') + '>PAID (fully deducted)</option>';
1927      html += '</select>';
1928      html += '<div style="flex:1"></div>';
1929      html += '<div style="font-size:11px; color:var(--t3);">' + records.length + ' of ' + allRecords.length + ' rows</div>';
1930      html += '</div>';
1931
1932      // Per-rep summary cards
1933      html += '<div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:10px; margin-bottom:18px;">';
1934      Object.keys(perRep).sort().forEach(function(rn) {
1935        var d = perRep[rn];
1936        html += '<div style="background:var(--d3); border:1px solid var(--b); border-radius:8px; padding:12px;">';
1937        html += '<div style="font-size:11px; color:var(--t3); text-transform:uppercase; font-weight:700; margin-bottom:4px;">' + rn + '</div>';
1938        html += '<div style="font-size:18px; font-weight:800; color:var(--t);">$' + fmtMoney(d.total) + ' total</div>';
1939        html += '<div style="font-size:11px; color:#16a34a; margin-top:3px;">✓ Deducted: $' + fmtMoney(d.paid) + '</div>';
1940        html += '<div style="font-size:11px; color:#f59e0b;">⏳ Pending: $' + fmtMoney(d.pending) + '</div>';
1941        html += '</div>';
1942      });
1943      html += '</div>';
1944
1945      // Table
1946      html += '<table style="width:100%; border-collapse:collapse;">';
1947      html += '<tr style="background:var(--d3); border-bottom:1px solid var(--b);">';
1948      html += '<th style="padding:10px; text-align:left; color:var(--t3); font-size:11px; text-transform:uppercase;">Status</th>';
1949      html += '<th style="padding:10px; text-align:left; color:var(--t3); font-size:11px; text-transform:uppercase;">Rep Name</th>';
1950      html += '<th style="padding:10px; text-align:right; color:var(--t3); font-size:11px; text-transform:uppercase;">Amount</th>';
1951      html += '<th style="padding:10px; text-align:right; color:var(--t3); font-size:11px; text-transform:uppercase;">Deducted</th>';
1952      html += '<th style="padding:10px; text-align:left; color:var(--t3); font-size:11px; text-transform:uppercase;">Reason</th>';
1953      html += '<th style="padding:10px; text-align:center; color:var(--t3); font-size:11px; text-transform:uppercase;">Date</th>';
1954      html += '<th style="padding:10px; text-align:left; color:var(--t3); font-size:11px; text-transform:uppercase;">Job Ref</th>';
1955      html += '<th style="padding:10px; text-align:center; color:var(--t3); font-size:11px; text-transform:uppercase;">Actions</th>';
1956      html += '</tr>';
1957
1958      if (records.length === 0) {
1959        html += '<tr><td colspan="8" style="padding:30px; text-align:center; color:var(--t3);">No rows match the current filter.</td></tr>';
1960      }
1961
1962      records.forEach(function(row) {
1963        var st = row.status || 'new';
1964        var paidAmt = parseFloat(row.paid_amount || 0);
1965        var amt = parseFloat(row.amount || 0);
1966
1967        // Status badge styling
1968        var badgeStyle, badgeLabel;
1969        if (st === 'paid') {
1970          badgeStyle = 'background:#16a34a22; color:#16a34a;'; badgeLabel = '✓ PAID';
1971        } else if (st === 'partial') {
1972          badgeStyle = 'background:#f59e0b22; color:#f59e0b;'; badgeLabel = '◐ PARTIAL';
1973        } else {
1974          badgeStyle = 'background:#ef444422;
1974 color:#ef4444;'; badgeLabel = '⏳ NEW';
1975        }
1976
1977        // Row left-border based on status (visual cue)
1978        var rowBorder = st === 'paid' ? '' : (st === 'partial' ? 'border-left:3px solid #f59e0b;' : 'border-left:3px solid #ef4444;');
1979
1980        html += '<tr style="border-bottom:1px solid var(--b);' + rowBorder + '">';
1981        html += '<td style="padding:10px;"><span style="display:inline-block; padding:3px 9px; border-radius:10px; font-size:10px; font-weight:700; letter-spacing:0.5px; ' + badgeStyle + '">' + badgeLabel + '</span></td>';
1982        html += '<td style="padding:10px; color:var(--t); font-weight:600;">' + (row.rep_name||'') + '</td>';
1983        html += '<td style="padding:10px; text-align:right; color:#c33; font-weight:600;">-$' + fmtMoney(amt) + '</td>';
1984        html += '<td style="padding:10px; text-align:right; color:#16a34a;">' + (paidAmt > 0 ? '$' + fmtMoney(paidAmt) : '—') + '</td>';
1985        html += '<td style="padding:10px; color:var(--t2); font-size:12px;">' + (row.reason||'') + '</td>';
1986        html += '<td style="padding:10px; text-align:center; color:var(--t2); font-size:12px;">' + fmtDate(row.deduction_date) + '</td>';
1987        html += '<td style="padding:10px; color:var(--t2); font-size:12px;">' + (row.job_reference || '-') + '</td>';
1988        html += '<td style="padding:10px; text-align:center; white-space:nowrap;">';
1989        if (st !== 'paid') {
1990          html += '<button onclick="markDeductionStatus(' + row.id + ', \'paid\', ' + amt + ')" style="padding:4px 8px; background:#16a34a; color:#fff; border:none; cursor:pointer; border-radius:3px; font-size:11px; font-weight:600; margin-right:3px;">✓ Mark Paid</button>';
1991        }
1992        if (st === 'new') {
1993          html += '<button onclick="markDeductionStatus(' + row.id + ', \'partial\')" style="padding:4px 8px; background:#f59e0b; color:#fff; border:none; cursor:pointer; border-radius:3px; font-size:11px; font-weight:600; margin-right:3px;">◐ Partial…</button>';
1994        }
1995        if (st !== 'new') {
1996          html += '<button onclick="markDeductionStatus(' + row.id + ', \'new\')" title="Reset to NEW (mark as not yet deducted)" style="padding:4px 8px; background:transparent; color:var(--t3); border:1px solid var(--b); cursor:pointer; border-radius:3px; font-size:11px; margin-right:3px;">↶ Undo</button>';
1997        }
1998        html += '<button onclick="deleteFinRecord(\'rep_deductions\', ' + row.id + ')" style="padding:4px 8px; background:#c33; color:#fff; border:none; cursor:pointer; border-radius:3px; font-size:11px;">Delete</button>';
1999        html += '</td>';
2000        html += '</tr>';
2001      });
2002
2003      html += '</table>';
2004      html += '<div style="margin-top:14px; padding:10px 14px; background:var(--d3); border-left:3px solid var(--gold); border-radius:6px; font-size:12px; color:var(--t3); line-height:1.6;">';
2005      html += '<strong style="color:var(--gold);">How statuses work:</strong> ';
2006      html += '<span style="color:#ef4444;">⏳ NEW</span> = not yet deducted from the rep\'s commission · ';
2007      html += '<span style="color:#f59e0b;">◐ PARTIAL</span> = partially deducted (enter the deducted amount when marking) · ';
2008      html += '<span style="color:#16a34a;">✓ PAID</span> = fully deducted. ';
2009      html += 'The Commission Dashboard\'s Net Pay only subtracts amounts that are actually marked deducted, so NEW deductions show as informational without double-counting.';
2010      html += '</div>';
2011
2012      contentEl.innerHTML = html;
2013    }).catch(function(err) {
2014      console.error('Deductions error:', err);
2015      contentEl.innerHTML = '<div style="color:var(--t2);">Error loading data: ' + (err.message || err) + '</div>';
2016    });
2017  };
2018
2019  // Filter setter
2020  window.setFinDeductionsFilter = function(field, value) {
2021    window._finDeductionsFilter[field] = value;
2022    window.loadDeductions();
2023  };
2024
2025  // Mark status (paid / partial / new)
2026  window.markDeductionStatus = function(id, newStatus, fullAmount) {
2027    var update = { status: newStatus };
2028
2029    if (newStatus === 'paid') {
2030      update.paid_amount = fullAmount;
2031      update.paid_date = new Date().toISOString().split('T')[0];
2032      if (!confirm('Mark this deduction as PAID (fully deducted from rep commission)? Net Pay on the Commission Dashboard will reflect this.')) return;
2033    } else if (newStatus === 'partial') {
2034      var raw = prompt('Enter the amount that has been deducted so far (partial):', '0');
2035      if (raw === null) return;
2036      var partialAmt = parseFloat(raw);
2037      if (isNaN(partialAmt) || partialAmt < 0) { alert('Invalid amount'); return; }
2038      update.paid_amount = partialAmt;
2039      update.paid_date = new Date().toISOString().split('T')[0];
2040    } else if (newStatus === 'new') {
2041      update.paid_amount = 0;
2042      update.paid_date = null;
2043      if (!confirm('Reset this deduction to NEW (mark as NOT YET deducted)? Net Pay on the Commission Dashboard will add it back to the rep\'s pending balance.')) return;
2044    }
2045
2046    sb.from('rep_deductions').update(update).eq('id', id).then(function(result) {
2047      if (result.error) { alert('Error updating status: ' + result.error.message); return; }
2048      window.loadDeductions();
2049    }).catch(function(err) {
2050      alert('Error: ' + (err.message || err));
2051    });
2052  };
2053
2054  // ===== Store Credits =====
2055  window.loadStoreCredits = function() {
2056    var contentEl = document.getElementById('fin-credits-content');
2057    contentEl.innerHTML = 'Loading...';
2058
2059    sb.from('store_credits').select('*').order('created_at', { ascending: false }).then(function(result) {
2060      var records = result.data || [];
2061
2062      var html = '<table style="width:100%; border-collapse:collapse;">';
2063      html += '<tr style="background:var(--d3); border-bottom:1px solid var(--b);">';
2064      html += '<th style="padding:10px; text-align:left; color:var(--t);">Entity</th>';
2065      html += '<th style="padding:10px; text-align:left; color:var(--t);">Type</th>';
2066      html += '<th style="padding:10px; text-align:right; color:var(--t);">Amount</th>';
2067      html += '<th style="padding:10px; text-align:left; color:var(--t);">Reason</th>';
2068      html += '<th style="padding:10px; text-align:center; color:var(--t);">Status</th>';
2069      html += '<th style="padding:10px; text-align:center; color:var(--t);">Actions</th>';
2070      html += '</tr>';
2071
2072      records.forEach(function(row) {
2073        html += '<tr style="border-bottom:1px solid var(--b);">';
2074        html += '<td style="padding:10px; color:var(--t);">' + row.entity_name + '</td>';
2075        html += '<td style="padding:10px; color:var(--t2);">' + row.entity_type + '</td>';
2076        html += '<td style="padding:10px; text-align:right; color:var(--gold);">$' + fmtMoney(row.amount) + '</td>';
2077        html += '<td style="padding:10px; color:var(--t2);">' + row.reason + '</td>';
2078        html += '<td style="padding:10px; text-align:center; color:var(--t);">' + row.status + '</td>';
2079        html += '<td style="padding:10px; text-align:center;">';
2080        html += '<button onclick="deleteFinRecord(\'store_credits\', ' + row.id + ')" style="padding:4px 8px; background:#c33; color:#fff; border:none; cursor:pointer; border-radius:3px; font-size:12px;">Delete</button>';
2081        html += '</td>';
2082        html += '</tr>';
2083      });
2084
2085      html += '</table>';
2086      contentEl.innerHTML = html;
2087    }).catch(function(err) {
2088      console.error('Store credits error:', err);
2089      contentEl.innerHTML = '<div style="color:var(--t2);">Error loading data</div>';
2090    });
2091  };
2092
2093  // ===== Client Refunds =====
2094  window.loadClientRefunds = function() {
2095    var contentEl = document.getElementById('fin-refunds-content');
2096    contentEl.innerHTML = 'Loading...';
2097
2098    sb.from('client_refunds').select('*').order('created_at', { ascending: false }).then(function(result) {
2099      var records = result.data || [];
2100
2101      var html = '<table style="width:100%; border-collapse:collapse;">';
2102      html += '<tr style="background:var(--d3); border-bottom:1px solid var(--b);">';
2103      html += '<th style="padding:10px; text-align:left; color:var(--t);">Client</th>';
2104      html += '<th style="padding:10px; text-align:right; color:var(--t);">Amount</th>';
2105      html += '<th style="padding:10px; text-align:left; color:var(--t);">Reason</th>';
2106      html += '<th style="padding:10px; text-align:left; color:var(--t);">Invoice #</th>';
2107      html += '<th style="padding:10px; text-align:center; color:var(--t);">Date</th>';
2108      html += '<th style="padding:10px; text-align:center; color:var(--t);">Status</th>';
2109      html += '<th style="padding:10px; text-align:center; color:var(--t);">Actions</th>';
2110      html += '</tr>';
2111
2112      records.forEach(function(row) {
2113        html += '<tr style="border-bottom:1px solid var(--b);">';
2114        html += '<td style="padding:10px; color:var(--t);">' + row.client_name + '</td>';
2115        html += '<td style="padding:10px; text-align:right; color:#c33;">-$' + fmtMoney(row.amount) + '</td>';
2116        html += '<td style="padding:10px; color:var(--t2);">' + row.reason + '</td>';
2117        html += '<td style="padding:10px; color:var(--t2);">' + (row.invoice_id || '-') + '</td>';
2118        html += '<td style="padding:10px; text-align:center; color:var(--t2);">' + fmtDate(row.refund_date) + '</td>';
2119        html += '<td style="padding:10px; text-align:center; color:var(--t);">' + row.status + '</td>';
2120        html += '<td style="padding:10px; text-align:center;">';
2121        html += '<button onclick="deleteFinRecord(\'client_refunds\', ' + row.id + ')" style="padding:4px 8px; background:#c33; color:#fff; border:none; cursor:pointer; border-radius:3px; font-size:12px;">Delete</button>';
2122        html += '</td>';
2123        html += '</tr>';
2124      });
2125
2126      html += '</table>';
2127      contentEl.innerHTML = html;
2128    }).catch(function(err) {
2129      console.error('Client refunds error:', err);
2130      contentEl.innerHTML = '<div style="color:var(--t2);">Error loading data</div>';
2131    });
2132  };
2133
2134  // ===== Storage Fees =====
2135  window.loadStorageFees = function() {
2136    var contentEl = document.getElementById('fin-storage-content');
2137    contentEl.innerHTML = 'Loading...';
2138
2139    sb.from('storage_fees').select('*').order('created_at', { ascending: false }).then(function(result) {
2140      var records = result.data || [];
2141
2142      var html = '<table style="width:100%; border-collapse:collapse;">';
2143      html += '<tr style="background:var(--d3); border-bottom:1px solid var(--b);">';
2144      html += '<th style="padding:10px; text-align:left; color:var(--t);">Client</th>';
2145      html += '<th style="padding:10px; text-align:right; color:var(--t);">Monthly Rate</th>';
2146      html += '<th style="padding:10px; text-align:center; color:var(--t);">Start Date</th>';
2147      html += '<th style="padding:10px; text-align:center; color:var(--t);">End Date</th>';
2148      html += '<th style="padding:10px; text-align:right; color:var(--t);">Total Charged</th>';
2149      html += '<th style="padding:10px; text-align:center; color:var(--t);">Status</th>';
2150      html += '<th style="padding:10px; text-align:center; color:var(--t);">Actions</th>';
2151      html += '</tr>';
2152
2153      records.forEach(function(row) {
2154        html += '<tr style="border-bottom:1px solid var(--b);">';
2155        html += '<td style="padding:10px; color:var(--t);">
2155' + row.client_name + '</td>';
2156        html += '<td style="padding:10px; text-align:right; color:var(--t);">' + '$' + fmtMoney(row.monthly_rate) + '</td>';
2157        html += '<td style="padding:10px; text-align:center; color:var(--t2);">' + fmtDate(row.start_date) + '</td>';
2158        html += '<td style="padding:10px; text-align:center; color:var(--t2);">' + (row.end_date ? fmtDate(row.end_date) : '-') + '</td>';
2159        html += '<td style="padding:10px; text-align:right; color:var(--gold);">' + '$' + fmtMoney(row.total_charged) + '</td>';
2160        html += '<td style="padding:10px; text-align:center; color:var(--t);">' + row.status + '</td>';
2161        html += '<td style="padding:10px; text-align:center;">';
2162        html += '<button onclick="deleteFinRecord(\'storage_fees\', ' + row.id + ')" style="padding:4px 8px; background:#c33; color:#fff; border:none; cursor:pointer; border-radius:3px; font-size:12px;">Delete</button>';
2163        html += '</td>';
2164        html += '</tr>';
2165      });
2166
2167      html += '</table>';
2168      contentEl.innerHTML = html;
2169    }).catch(function(err) {
2170      console.error('Storage fees error:', err);
2171      contentEl.innerHTML = '<div style="color:var(--t2);">Error loading data</div>';
2172    });
2173  };
2174
2175  // ===== Office Expenses =====
2176  window.loadOfficeExpenses = function() {
2177    var contentEl = document.getElementById('fin-expenses-content');
2178    contentEl.innerHTML = 'Loading...';
2179
2180    sb.from('office_expenses').select('*').order('created_at', { ascending: false }).then(function(result) {
2181      var records = result.data || [];
2182
2183      var html = '<table style="width:100%; border-collapse:collapse;">';
2184      html += '<tr style="background:var(--d3); border-bottom:1px solid var(--b);">';
2185      html += '<th style="padding:10px; text-align:left; color:var(--t);">Category</th>';
2186      html += '<th style="padding:10px; text-align:left; color:var(--t);">Description</th>';
2187      html += '<th style="padding:10px; text-align:right; color:var(--t);">Amount</th>';
2188      html += '<th style="padding:10px; text-align:center; color:var(--t);">Date</th>';
2189      html += '<th style="padding:10px; text-align:left; color:var(--t);">Vendor</th>';
2190      html += '<th style="padding:10px; text-align:center; color:var(--t);">Recurring</th>';
2191      html += '<th style="padding:10px; text-align:center; color:var(--t);">Actions</th>';
2192      html += '</tr>';
2193
2194      records.forEach(function(row) {
2195        html += '<tr style="border-bottom:1px solid var(--b);">';
2196        html += '<td style="padding:10px; color:var(--t);">' + row.category + '</td>';
2197        html += '<td style="padding:10px; color:var(--t2);">' + row.description + '</td>';
2198        html += '<td style="padding:10px; text-align:right; color:#c33;">-$' + fmtMoney(row.amount) + '</td>';
2199        html += '<td style="padding:10px; text-align:center; color:var(--t2);">' + fmtDate(row.expense_date) + '</td>';
2200        html += '<td style="padding:10px; color:var(--t2);">' + (row.vendor || '-') + '</td>';
2201        html += '<td style="padding:10px; text-align:center; color:var(--t2);">' + (row.recurring ? 'Yes' : 'No') + '</td>';
2202        html += '<td style="padding:10px; text-align:center;">';
2203        html += '<button onclick="deleteFinRecord(\'office_expenses\', ' + row.id + ')" style="padding:4px 8px; background:#c33; color:#fff; border:none; cursor:pointer; border-radius:3px; font-size:12px;">Delete</button>';
2204        html += '</td>';
2205        html += '</tr>';
2206      });
2207
2208      html += '</table>';
2209      contentEl.innerHTML = html;
2210    }).catch(function(err) {
2211      console.error('Office expenses error:', err);
2212      contentEl.innerHTML = '<div style="color:var(--t2);">Error loading data</div>';
2213    });
2214  };
2215
2216  // ===== Form Toggle =====
2217  window.toggleFinForm = function(formType) {
2218    var formEl = document.getElementById('fin-' + formType.split('-')[0] + '-form');
2219    if (!formEl) return;
2220
2221    if (formEl.style.display === 'none' || !formEl.style.display) {
2222      formEl.style.display = 'block';
2223      buildFinForm(formType, formEl);
2224    } else {
2225      formEl.style.display = 'none';
2226    }
2227  };
2228
2229  // ===== Generic Form Builder =====
2230  window.buildFinForm = function(formType, containerEl) {
2231    var html = '';
2232    var baseType = formType.split('-')[0];
2233
2234    if (baseType === 'installer') {
2235      html = `
2236        <div style="display:grid; grid-template-columns:1fr 1fr; gap:15px;">
2237          <input id="fin-installer-name" type="text" placeholder="Installer Name" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2238          <input id="fin-installer-wo" type="text" placeholder="Work Order ID" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2239          <input id="fin-installer-sqft" type="number" placeholder="Sqft" step="0.01" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2240          <input id="fin-installer-rate" type="number" placeholder="Rate per Sqft" step="0.01" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2241          <select id="fin-installer-status" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;">
2242            <option value="pending">Pending</option>
2243            <option value="paid">Paid</option>
2244          </select>
2245          <input id="fin-installer-date" type="date" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2246        </div>
2247        <button onclick="saveFinRecord('installer_payments')" style="margin-top:10px; padding:8px 16px; background:var(--gold); color:#fff; border:none; cursor:pointer; border-radius:4px;">Save</button>
2248      `;
2249    } else if (baseType === 'vendor') {
2250      html = `
2251        <div style="display:grid; grid-template-columns:1fr 1fr; gap:15px;">
2252          <input id="fin-vendor-name" type="text" placeholder="Vendor Name" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2253          <input id="fin-vendor-amount" type="number" placeholder="Amount" step="0.01" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2254          <input id="fin-vendor-invoice" type="text" placeholder="Invoice Number" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2255          <input id="fin-vendor-date" type="date" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2256          <select id="fin-vendor-status" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;">
2257            <option value="pending">Pending</option>
2258            <option value="paid">Paid</option>
2259          </select>
2260          <input id="fin-vendor-notes" type="text" placeholder="Notes" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2261        </div>
2262        <button onclick="saveFinRecord('vendor_payment_tracking')" style="margin-top:10px; padding:8px 16px; background:var(--gold); color:#fff; border:none; cursor:pointer; border-radius:4px;">Save</button>
2263      `;
2264    } else if (baseType === 'deductions') {
2265      // Build the rep <select> options from window.profs (active reps).
2266      // Falls back to a hardcoded list if profs isn't loaded yet.
2267      var _activeReps = (typeof profs !== 'undefined' && Array.isArray(profs))
2268        ? profs.filter(function(p){ return p && p.role !== 'inactive' && p.full_name; }).slice().sort(function(a,b){ return (a.full_name||'').localeCompare(b.full_name||''); })
2269        : [
2270            { full_name: 'Ashley Brooke' },
2271            { full_name: 'Patrick McNeill' },
2272            { full_name: 'Samin Seifpour' },
2273            { full_name: 'Aly Champagne' },
2274            { full_name: 'Joey Jamail' }
2275          ];
2276      var _repOptions = '<option value="">— Select sales rep —</option>' +
2277        _activeReps.map(function(p){
2278          var name = (typeof esc === 'function') ? esc(p.full_name) : p.full_name;
2279          return '<option value="' + name + '">' + name + '</option>';
2280        }).join('');
2281      html = `
2282        <div style="display:grid; grid-template-columns:1fr 1fr; gap:15px;">
2283          <select id="fin-deductions-name" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;">${_repOptions}</select>
2284          <input id="fin-deductions-amount" type="number" placeholder="Amount" step="0.01" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2285          <input id="fin-deductions-reason" type="text" placeholder="Reason" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2286          <input id="fin-deductions-date" type="date" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2287          <input id="fin-deductions-jobref" type="text" placeholder="Job Reference (optional — e.g., INV-2578)" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2288          <select id="fin-deductions-status" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;">
2289            <option value="new" selected>NEW — not yet deducted</option>
2290            <option value="partial">PARTIAL — partially deducted</option>
2291            <option value="paid">PAID — fully deducted</option>
2292          </select>
2293        </div>
2294        <button onclick="saveFinRecord('rep_deductions')" style="margin-top:10px; padding:8px 16px; background:var(--gold); color:#fff; border:none; cursor:pointer; border-radius:4px;">Save</button>
2295      `;
2296    } else if (baseType === 'credits') {
2297      html = `
2298        <div style="display:grid; grid-template-columns:1fr 1fr; gap:15px;">
2299          <select id="fin-credits-type" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;">
2300            <option value="client">Client</option>
2301            <option value="vendor">Vendor</option>
2302          </select>
2303          <input id="fin-credits-entity" type="text" placeholder="Client/Ven
2303dor Name" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2304          <input id="fin-credits-amount" type="number" placeholder="Amount" step="0.01" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2305          <input id="fin-credits-reason" type="text" placeholder="Reason" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2306          <input id="fin-credits-date" type="date" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2307          <select id="fin-credits-status" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;">
2308            <option value="active">Active</option>
2309            <option value="used">Used</option>
2310            <option value="expired">Expired</option>
2311          </select>
2312        </div>
2313        <button onclick="saveFinRecord('store_credits')" style="margin-top:10px; padding:8px 16px; background:var(--gold); color:#fff; border:none; cursor:pointer; border-radius:4px;">Save</button>
2314      `;
2315    } else if (baseType === 'refunds') {
2316      html = `
2317        <div style="display:grid; grid-template-columns:1fr 1fr; gap:15px;">
2318          <input id="fin-refunds-client" type="text" placeholder="Client Name" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2319          <input id="fin-refunds-amount" type="number" placeholder="Amount" step="0.01" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2320          <input id="fin-refunds-reason" type="text" placeholder="Reason" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2321          <input id="fin-refunds-invoice" type="text" placeholder="Invoice ID" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2322          <input id="fin-refunds-date" type="date" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2323          <select id="fin-refunds-status" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;">
2324            <option value="pending">Pending</option>
2325            <option value="approved">Approved</option>
2326            <option value="completed">Completed</option>
2327          </select>
2328        </div>
2329        <button onclick="saveFinRecord('client_refunds')" style="margin-top:10px; padding:8px 16px; background:var(--gold); color:#fff; border:none; cursor:pointer; border-radius:4px;">Save</button>
2330      `;
2331    } else if (baseType === 'storage') {
2332      html = `
2333        <div style="display:grid; grid-template-columns:1fr 1fr; gap:15px;">
2334          <input id="fin-storage-client" type="text" placeholder="Client Name" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2335          <input id="fin-storage-rate" type="number" placeholder="Monthly Rate" step="0.01" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2336          <input id="fin-storage-start" type="date" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2337          <input id="fin-storage-end" type="date" placeholder="(optional)" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2338          <input id="fin-storage-total" type="number" placeholder="Total Charged" step="0.01" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2339          <select id="fin-storage-status" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;">
2340            <option value="active">Active</option>
2341            <option value="inactive">Inactive</option>
2342          </select>
2343        </div>
2344        <button onclick="saveFinRecord('storage_fees')" style="margin-top:10px; padding:8px 16px; background:var(--gold); color:#fff; border:none; cursor:pointer; border-radius:4px;">Save</button>
2345      `;
2346    } else if (baseType === 'expenses') {
2347      html = `
2348        <div style="display:grid; grid-template-columns:1fr 1fr; gap:15px;">
2349          <input id="fin-expenses-category" type="text" placeholder="Category (Rent, Utilities, etc)" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2350          <input id="fin-expenses-description" type="text" placeholder="Description" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2351          <input id="fin-expenses-amount" type="number" placeholder="Amount" step="0.01" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2352          <input id="fin-expenses-vendor" type="text" placeholder="Vendor" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2353          <input id="fin-expenses-date" type="date" style="padding:8px; background:var(--d1); border:1px solid var(--b); color:var(--t); border-radius:4px;" />
2354          <label style="display:flex; align-items:center; gap:8px; color:var(--t);">
2355            <input id="fin-expenses-recurring" type="checkbox" />
2356            Recurring
2357          </label>
2358        </div>
2359        <button onclick="saveFinRecord('office_expenses')" style="margin-top:10px; padding:8px 16px; background:var(--gold); color:#fff; border:none; cursor:pointer; border-radius:4px;">Save</button>
2360      `;
2361    }
2362
2363    containerEl.innerHTML = html;
2364  };
2365
2366  // ===== Save Finance Record =====
2367  window.saveFinRecord = function(tableName) {
2368    var record = {};
2369
2370    if (tableName === 'installer_payments') {
2371      record = {
2372        installer_name: document.getElementById('fin-installer-name').value,
2373        work_order_id: document.getElementById('fin-installer-wo').value || null,
2374        sqft: parseFloat(document.getElementById('fin-installer-sqft').value || 0
2374),
2375        rate_per_sqft: parseFloat(document.getElementById('fin-installer-rate').value || 0),
2376        total_pay: parseFloat(document.getElementById('fin-installer-sqft').value || 0) * parseFloat(document.getElementById('fin-installer-rate').value || 0),
2377        status: document.getElementById('fin-installer-status').value,
2378        payment_date: document.getElementById('fin-installer-date').value
2379      };
2380    } else if (tableName === 'vendor_payment_tracking') {
2381      record = {
2382        vendor_name: document.getElementById('fin-vendor-name').value,
2383        amount: parseFloat(document.getElementById('fin-vendor-amount').value || 0),
2384        invoice_number: document.getElementById('fin-vendor-invoice').value,
2385        payment_date: document.getElementById('fin-vendor-date').value,
2386        status: document.getElementById('fin-vendor-status').value,
2387        notes: document.getElementById('fin-vendor-notes').value
2388      };
2389    } else if (tableName === 'rep_deductions') {
2390      var _repNameVal = (document.getElementById('fin-deductions-name').value || '').trim();
2391      if (!_repNameVal) { alert('Please select a sales rep.'); return; }
2392      var _amount = parseFloat(document.getElementById('fin-deductions-amount').value || 0);
2393      var _statusEl = document.getElementById('fin-deductions-status');
2394      var _status = _statusEl ? _statusEl.value : 'new';
2395      var _paidAmt = 0, _paidDate = null;
2396      if (_status === 'paid') {
2397        _paidAmt = _amount;
2398        _paidDate = document.getElementById('fin-deductions-date').value || new Date().toISOString().split('T')[0];
2399      } else if (_status === 'partial') {
2400        var raw = prompt('How much has been deducted so far (partial)?', '0');
2401        if (raw === null) return;
2402        _paidAmt = parseFloat(raw) || 0;
2403        _paidDate = new Date().toISOString().split('T')[0];
2404      }
2405      record = {
2406        rep_name: _repNameVal,
2407        amount: _amount,
2408        reason: document.getElementById('fin-deductions-reason').value,
2409        deduction_date: document.getElementById('fin-deductions-date').value || null,
2410        job_reference: (document.getElementById('fin-deductions-jobref').value || '').trim() || null,
2411        status: _status,
2412        paid_amount: _paidAmt,
2413        paid_date: _paidDate,
2414      };
2415    } else if (tableName === 'store_credits') {
2416      record = {
2417        entity_type: document.getElementById('fin-credits-type').value,
2418        entity_name: document.getElementById('fin-credits-entity').value,
2419        amount: parseFloat(document.getElementById('fin-credits-amount').value || 0),
2420        reason: document.getElementById('fin-credits-reason').value,
2421        issued_date: document.getElementById('fin-credits-date').value,
2422        status: document.getElementById('fin-credits-status').value
2423      };
2424    } else if (tableName === 'client_refunds') {
2425      record = {
2426        client_name: document.getElementById('fin-refunds-client').value,
2427        amount: parseFloat(document.getElementById('fin-refunds-amount').value || 0),
2428        reason: document.getElementById('fin-refunds-reason').value,
2429        invoice_id: parseInt(document.getElementById('fin-refunds-invoice').value || 0),
2430        refund_date: document.getElementById('fin-refunds-date').value,
2431        status: document.getElementById('fin-refunds-status').value
2432      };
2433    } else if (tableName === 'storage_fees') {
2434      record = {
2435        client_name: document.getElementById('fin-storage-client').value,
2436        monthly_rate: parseFloat(document.getElementById('fin-storage-rate').value || 0),
2437        start_date: document.getElementById('fin-storage-start').value,
2438        end_date: document.getElementById('fin-storage-end').value || null,
2439        total_charged: parseFloat(document.getElementById('fin-storage-total').value || 0),
2440        status: document.getElementById('fin-storage-status').value
2441      };
2442    } else if (tableName === 'office_expenses') {
2443      record = {
2444        category: document.getElementById('fin-expenses-category').value,
2445        description: document.getElementById('fin-expenses-description').value,
2446        amount: parseFloat(document.getElementById('fin-expenses-amount').value || 0
2446),
2447        vendor: document.getElementById('fin-expenses-vendor').value,
2448        expense_date: document.getElementById('fin-expenses-date').value,
2449        recurring: document.getElementById('fin-expenses-recurring').checked
2450      };
2451    }
2452
2453    // Map table names to section names for form hiding and section reload
2454    var tableToSection = {
2455      'installer_payments': 'installer',
2456      'vendor_payment_tracking': 'vendor',
2457      'rep_deductions': 'deductions',
2458      'store_credits': 'credits',
2459      'client_refunds': 'refunds',
2460      'storage_fees': 'storage',
2461      'office_expenses': 'expenses'
2462    };
2463    var section = tableToSection[tableName] || tableName.split('_')[0];
2464
2465    sb.from(tableName).insert([record]).then(function(result) {
2466      if (result.error) {
2467        alert('Error saving record: ' + result.error.message);
2468        return;
2469      }
2470      var formEl = document.getElementById('fin-' + section + '-form');
2471      if (formEl) formEl.style.display = 'none';
2472      loadFinanceSection(section);
2473    }).catch(function(err) {
2474      console.error('Save error:', err);
2475      alert('Error saving record');
2476    });
2477  };
2478
2479  // ===== Delete Finance Record =====
2480  window.deleteFinRecord = function(tableName, recordId) {
2481    if (!confirm('Delete this record?')) return;
2482
2483    sb.from(tableName).delete().eq('id', recordId).then(function(result) {
2484      if (result.error) {
2485        alert('Error deleting record: ' + result.error.message);
2486        return;
2487      }
2488      // Determine section name from table
2489      var section = tableName.split('_')[0];
2490      if (tableName === 'installer_payments') section = 'installer';
2491      else if (tableName === 'vendor_payment_tracking') section = 'vendor';
2492      else if (tableName === 'rep_deductions') section = 'deductions';
2493      else if (tableName === 'store_credits') section = 'credits';
2494      else if (tableName === 'client_refunds') section = 'refunds';
2495      else if (tableName === 'storage_fees') section = 'storage';
2496      else if (tableName === 'office_expenses') section = 'expenses';
2497
2498      loadFinanceSection(section);
2499    }).catch(function(err) {
2500      console.error('Delete error:', err);
2501      alert('Error deleting record');
2502    });
2503  };
2504
2505  // ===== Tab Visibility Hook =====
2506  // Use _onTabSwitch callback instead of wrapping sideNav (avoids conflict with FIX 12 override)
2507  var _prevOnTabSwitch = typeof window._onTabSwitch === 'function' ? window._onTabSwitch : null;
2508  window._onTabSwitch = function(tab) {
2509    if (_prevOnTabSwitch) _prevOnTabSwitch(tab);
2510    if (tab === 'finance') {
2511      renderFinanceHub();
2512    }
2513  };
2514  // Also handle direct navigation via hash
2515  if (window.location.hash === '#finance') {
2516    setTimeout(function() { renderFinanceHub(); }, 500);
2517  }
2518
2519  console.log('[patch.js] FIX 13: Finance Hub loaded');
2520})();
2521
2522// === FIX 14: New Lead Notifications in Admin ===
2523(function() {
2524  'use strict';
2525
2526  // Create a notifications table in Supabase if not already done
2527  // Table: lead_notifications (id serial, lead_id text, company text, rep_name text, created_at timestamptz, read boolean default false)
2528
2529  // Ensure the table exists (will silently fail if already exists)
2530  (async function ensureNotifTable() {
2531    try {
2532      // Just try to select — if it fails, we'll create inline
2533      var { error } = await sb.from('lead_notifications').select('id').limit(1);
2534      if (error && error.code === '42P01') {
2535        // Table doesn't exist — we'll use activities table as fallback
2536        console.log('[patch.js] lead_notifications table not found, using activities fallback');
2537      }
2538    } catch(e) { /* ignore */ }
2539  })();
2540
2541  // ===== Hook into submitLead to create notification =====
2542  // FIX 39: Replaced lead-count comparison with an ID-set diff filtered by
2543  // assigned_rep. The old `leads.length > prevCount` check fired any time the
2544  // global `leads` array grew during the 2-second setTimeout — including
2545  // updates pushed in by Supabase real-time when another rep added a lead in
2546  // a different session, or even when the rep's own submitLead bailed early
2547  // on validation. That produced phantom 🔔 New Lead Notifications attributed
2548  // to the wrong action. The ID-diff + own-rep filter only fires when a NEW
2549  // lead row that didn't exist before AND belongs to the current rep appears,
2550  // which is the actual condition we want.
2551  var _origSubmitLead = window.submitLead;
2552  if (_origSubmitLead) {
2553    window.submitLead = async function() {
2554      var prevIds = new Set(
2555        (typeof leads !== 'undefined' && leads)
2556          ? leads.map(function(l){ return l.id; })
2557          : []
2558      );
2559      var myRepId = (typeof prof !== 'undefined' && prof) ? prof.id : null;
2560
2561      await _origSubmitLead.apply(this, arguments);
2562
2563      // Small delay to allow loadData to complete
2564      setTimeout(async function() {
2565        try {
2566          if (typeof leads === 'undefined' || !leads || !myRepId) return;
2567
2568          // Find leads that didn't exist before this submitLead AND are
2569          // assigned to this rep — i.e. ones THIS rep just inserted.
2570          var freshOwn = leads.filter(function(l) {
2571            if (!l || !l.id) return false;
2572            if (prevIds.has(l.id)) return false;     // existed before submit
2573            if (l.assigned_rep !== myRepId) return false; // someone else's
2574            // Defense in depth: require the lead to have been touched in the
2575            // last 10 seconds, ruling out paginated loads that surface old rows.
2576            var ts = new Date(l.updated_at || l.date_added || 0).getTime();
2577            if (!ts || (Date.now() - ts) > 10000) return false;
2578            return true;
2579          });
2580
2581          if (freshOwn.length === 0) return;
2582
2583          var newLead = freshOwn[0];
2584          await sb.from('lead_notifications').insert({
2585            lead_id: newLead.id || '',
2586            company: newLead.company || 'Unknown',
2587            rep_name: prof.full_name || 'Unknown',
2588            created_at: new Date().toISOString(),
2589            read: false
2590          }).then(function() {
2591            updateAdminBadge();
2592          }).catch(function() {
2593            // Fallback: just update badge from activities
2594            updateAdminBadge();
2595          });
2596        } catch(e) { console.log('[FIX 39] Notification hook error:', e); }
2597      }, 2000);
2598    };
2599  }
2600
2601  // ===== Update Admin Badge Count =====
2602  window.updateAdminBadge = async function() {
2603    try {
2604      var { data, error } = await sb.from('lead_notifications')
2605        .select('id')
2606        .eq('read', false);
2607
2608      if (!error && data) {
2609        var badge = document.getElementById('admin-badge');
2610        if (badge) {
2611          if (data.length > 0) {
2612            badge.textContent = data.length;
2613            badge.style.display = 'inline-flex';
2614          } else {
2615            badge.style.display = 'none';
2616          }
2617        }
2618      }
2619    } catch(e) { /* ignore */ }
2620  };
2621
2622  // ===== Add Notifications Button to Admin Tab =====
2623  function addNotifButtonToAdmin() {
2624    var adminBtnRow = document.querySelector('#tab-admin > div:first-child');
2625    if (!adminBtnRow) return;
2626    // Check if already added
2627    if (document.getElementById('admin-sec-notifications')) return;
2628
2629    // Add notification button
2630    var btn = document.createElement('button');
2631    btn.id = 'admin-sec-notifications';
2632    btn.onclick = function() { showAdminSection('notifications'); };
2633    btn.style.cssText = 'font-size:12px;font-weight:600;padding:8px 14px;border-radius:8px;cursor:pointer;border:1px solid var(--b);background:var(--d3);color:var(--t)';
2634    btn.innerHTML = '🔔 New Leads';
2635    adminBtnRow.insertBefore(btn, adminBtnRow.firstChild);
2636
2637    // Add notification content div
2638    var contentDiv = document.createElement('div');
2639    contentDiv.id = 'admin-notifications';
2640    contentDiv.style.display = 'none';
2641    var adminTab = document.getElementById('tab-admin');
2642    if (adminTab) {
2643      // Insert after the button row
2644      adminBtnRow.parentNode.insertBefore(contentDiv, adminBtnRow.nextSibling);
2645    }
2646  }
2647
2648  // ===== Render Notifications Section =====
2649  window.renderAdminNotifications = async function() {
2650    var container = document.getElementById('admin-notifications');
2651    if (!container) return;
2652
2653    container.innerHTML = '<div style="color:var(--t2);">Loading notifications...</div>';
2654
2655    try {
2656      var { data: notifs, error } = await sb.from('lead_notifications')
2657        .select('*')
2658        .order('created_at', { ascending: false })
2659        .limit(50);
2660
2661      if (error) {
2662        // Fallback: show recent activities of type "New Lead"
2663        var { data: acts } = await sb.from('activities')
2664          .select('*')
2665          .eq('activity_type', 'New Lead')
2666          .order('created_at', { ascending: false })
2667          .limit(50);
2668
2669        notifs = (acts || []).map(function(a) {
2670          return {
2671            id: a.id,
2672            company: a.summary || '',
2673            rep_name: '',
2674            created_at: a.created_at,
2675            read: true
2676          };
2677        });
2678      }
2679
2680      if (!notifs || notifs.length === 0) {
2681        container.innerHTML = '<div style="padding:20px;color:var(--t2);text-align:center;">No new lead notifications.</div>';
2682        return;
2683      }
2684
2685      var unreadCount = notifs.filter(function(n) { return !n.read; }).length;
2686
2687      var html = '<div class="st">🔔 New Lead Notifications</div>';
2688
2689      if (unreadCount > 0) {
2690        html += '<div style="margin-bottom:12px;display:flex;gap:8px;align-items:center;">'
2691          + '<span style="background:#ef4444;color:#fff;padding:2px 8px;border-radius:12px;font-size:11px;font-weight:700;">
2691' + unreadCount + ' unread</span>'
2692          + '<button onclick="markAllNotifsRead()" style="padding:4px 12px;background:var(--gold);color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:11px;font-weight:600;">Mark All Read</button>'
2693          + '</div>';
2694      }
2695
2696      html += '<div style="display:flex;flex-direction:column;gap:8px;">';
2697      notifs.forEach(function(n) {
2698        var isUnread = !n.read;
2699        var bg = isUnread ? 'var(--gold)11' : 'var(--d3)';
2700        var border = isUnread ? '2px solid var(--gold)' : '1px solid var(--b)';
2701        var timeAgo = getTimeAgo(n.created_at);
2702
2703        html += '<div style="padding:12px 16px;background:' + bg + ';border:' + border + ';border-radius:8px;display:flex;justify-content:space-between;align-items:center;">'
2704          + '<div>'
2705          + '<span style="font-weight:' + (isUnread ? '700' : '400') + ';color:var(--t);font-size:14px;">' + escHtml(n.company) + '</span>'
2706          + (n.rep_name ? '<span style="color:var(--t3);font-size:12px;margin-left:8px;">by ' + escHtml(n.rep_name) + '</span>' : '')
2707          + '</div>'
2708          + '<div style="display:flex;align-items:center;gap:8px;">'
2709          + '<span style="color:var(--t3);font-size:11px;">' + timeAgo + '</span>';
2710
2711        if (isUnread) {
2712          html += '<button onclick="markNotifRead(' + n.id + ')" style="padding:2px 8px;background:var(--d3);border:1px solid var(--b);border-radius:4px;cursor:pointer;font-size:10px;color:var(--t2);">✓</button>';
2713        }
2714        if (n.lead_id) {
2715          html += '<button onclick="openLead(\'' + n.lead_id + '\')" style="padding:2px 8px;background:var(--gold);color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:10px;">View</button>';
2716        }
2717        html += '</div></div>';
2718      });
2719      html += '</div>';
2720
2721      container.innerHTML = html;
2722    } catch(e) {
2723      container.innerHTML = '<div style="padding:20px;color:var(--t2);">Error loading notifications.</div>';
2724      console.error('[FIX 14]', e);
2725    }
2726  };
2727
2728  // ===== Mark single notification read =====
2729  window.markNotifRead = async function(id) {
2730    try {
2731      await sb.from('lead_notifications').update({ read: true }).eq('id', id);
2732      renderAdminNotifications();
2733      updateAdminBadge();
2734    } catch(e) { console.error(e); }
2735  };
2736
2737  // ===== Mark all notifications read =====
2738  window.markAllNotifsRead = async function() {
2739    try {
2740      await sb.from('lead_notifications').update({ read: true }).eq('read', false);
2741      renderAdminNotifications();
2742      updateAdminBadge();
2743    } catch(e) { console.error(e); }
2744  };
2745
2746  // ===== Hook showAdminSection to handle notifications =====
2747  var _origShowAdminSection = window.showAdminSection;
2748  if (_origShowAdminSection) {
2749    window.showAdminSection = function(section) {
2750      // Hide notification section when switching to other sections
2751      var notifDiv = document.getElementById('admin-notifications');
2752      if (notifDiv) notifDiv.style.display = 'none';
2753
2754      // Reset notification button style
2755      var notifBtn = document.getElementById('admin-sec-notifications');
2756      if (notifBtn) {
2757        notifBtn.style.background = 'var(--d3)';
2758        notifBtn.style.color = 'var(--t)';
2759        notifBtn.style.borderColor = 'var(--b)';
2760      }
2761
2762      if (section === 'notifications') {
2763        // Hide all other admin sections
2764        _origShowAdminSection('__none__'); // triggers hiding all sections
2765        if (notifDiv) notifDiv.style.display = 'block';
2766        if (notifBtn) {
2767          notifBtn.style.background = 'var(--sf-blue)';
2768          notifBtn.style.color = '#fff';
2769          notifBtn.style.borderColor = 'var(--sf-blue)';
2770        }
2771        renderAdminNotifications();
2772      } else {
2773        _origShowAdminSection(section);
2774      }
2775    };
2776  }
2777
2778  // ===== Helpers =====
2779  function escHtml(str) {
2780    if (!str) return '';
2781    var d = document.createElement('div');
2782    d.textContent = str;
2783    return d.innerHTML;
2784  }
2785
2786  function getTimeAgo(dateStr) {
2787    if (!dateStr) return '';
2788    var now = new Date();
2789    var then = new Date(dateStr);
2790    var diff = Math.floor((now - then) / 1000);
2791    if (diff < 60) return 'just now';
2792    if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
2793    if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
2794    if (diff < 604800) return Math.floor(diff / 86400) + 'd ago';
2795    return fmtDate(dateStr);
2796  }
2797
2798  // ===== Init =====
2799  function init() {
2800    addNotifButtonToAdmin();
2801    // Create the lead_notifications table if it doesn't exist
2802    // (Using raw SQL via Supabase's rpc or just try insert)
2803    updateAdminBadge();
2804
2805    // Poll for new notifications every 60 seconds
2806    setInterval(function() {
2807      if (typeof prof !== 'undefined' && prof && prof.role === 'admin') {
2808        updateAdminBadge();
2809      }
2810    }, 60000);
2811  }
2812
2813  if (document.readyState === 'loading') {
2814    document.addEventListener('DOMContentLoaded', function() { setTimeout(init, 500); });
2815  } else {
2816    setTimeout(init, 500);
2817  }
2818
2819  console.log('[patch.js] FIX 14: New Lead Notifications loaded');
2820})();
2821
2822
2823// === FIX 15: Inline Commission & Installer Pay on Invoices/Work Orders ===
2824(function() {
2825  'use strict';
2826
2827  // ===== Inject commission info into invoice detail views =====
2828  // When an invoice is opened (the detail overlay), add a commission summary card
2829
2830  function createCommissionBadge(repName, totalSale, costAmount, commissionPct) {
2831    var profit = (totalSale || 0) - (costAmount || 0);
2832    var ratePct = parseFloat(commissionPct || 30);
2833    var rate = ratePct > 1 ? ratePct / 100 : ratePct;
2834    var commission = profit * rate;
2835    if (profit < 0) profit = 0;
2836    if (commission < 0) commission = 0;
2837
2838    var badge = document.createElement('div');
2839    badge.className = 'fin-inline-badge';
2840    badge.style.cssText = 'margin:10px 0;padding:12px 16px;background:linear-gradient(135deg,#f8f4ef,#faf7f2);border:1px solid var(--gold);border-radius:8px;display:flex;gap:20px;align-items:center;flex-wrap:wrap;';
2841    badge.innerHTML = '<div style="display:flex;align-items:center;gap:6px;">'
2842      + '<span style="font-size:16px;">💰</span>'
2843      + '<span style="font-weight:700;color:var(--t);font-size:13px;">Commission</span></div>'
2844      + '<div style="font-size:12px;color:var(--t2);">Rep: <strong style="color:var(--t);">' + escH(repName || 'Unassigned') + '</strong></div>'
2845      + '<div style="font-size:12px;color:var(--t2);">Profit: <strong style="color:' + (profit > 0 ? '#16a34a' : '#ef4444') + ';">' + fmtM(profit) + '</strong></div>'
2846      + '<div style="font-size:12px;color:var(--t2);">Commission (' + Math.round((commissionRate || 0.30) * 100) + '%): <strong style="color:var(--gold);">' + fmtM(commission) + '</strong></div>';
2847    return badge;
2848  }
2849
2850  function createInstallerBadge(installerName, sqft, ratePerSqft) {
2851    var totalPay = (sqft || 0) * (ratePerSqft || 0);
2852    var badge = document.createElement('div');
2853    badge.className = 'fin-inline-badge';
2854    badge.style.cssText = 'margin:10px 0;padding:12px 16px;background:linear-gradient(135deg,#eff6ff,#f0f7ff);border:1px solid #3b82f6;border-radius:8px;display:flex;gap:20px;align-items:center;flex-wrap:wrap;';
2855    badge.innerHTML = '<div style="display:flex;align-items:center;gap:6px;">'
2856      + '<span style="font-size:16px;">🔧</span>'
2857      + '<span style="font-weight:700;color:var(--t);font-size:13px;">Installer Pay</span></div>'
2858      + '<div style="font-size:12px;color:var(--t2);">Installer: <strong style="color:var(--t);">' + escH(installerName || 'TBD') + '</strong></div>'
2859      + '<div style="font-size:12px;color:var(--t2);">Sqft: <strong style="color:var(--t);">' + (sqft ? sqft.toLocaleString() : '0') + '</strong></div>'
2860      + '<div style="font-size:12px;color:var(--t2);">Rate: <strong style="color:var(--t);">' + fmtM(ratePerSqft) + '/sqft</strong></div>'
2861      + '<div style="font-size:12px;color:var(--t2);">Total: <strong style="color:#3b82f6;">' + fmtM(totalPay) + '</strong></div>';
2862    return badge;
2863  }
2864
2865  function fmtM(n) {
2866    if (!n && n !== 0) return '$0.00';
2867    return '$' + parseFloat(n).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
2868  }
2869
2870  function escH(s) {
2871    if (!s) return '';
2872    var d = document.createElement('div');
2873    d.textContent = s;
2874    return d.innerHTML;
2875  }
2876
2877  // ===== Observer to inject badges into invoice/WO detail views =====
2878  async function getRepConfig(repName) {
2879    if (!repName) return { commission_pct: 30 };
2880    try {
2881      var { data } = await sb.from('rep_pay_config').select('*').ilike('rep_name', '%' + repName.split(' ')[0] + '%').limit(1);
2882      if (data && data.length > 0) return data[0];
2883    } catch(e) { /* ignore */ }
2884    return { commission_pct: 30 };
2885  }
2886
2887  async function getInstallerPayment(woId) {
2888    if (!woId) return null;
2889    try {
2890      var { data } = await sb.from('installer_payments').select('*').eq('work_order_id', woId).limit(1);
2891      if (data && data.length > 0) return data[0];
2892    } catch(e) { /* ignore */ }
2893    return null;
2894  }
2895
2896  // Watch for invoice detail overlays
2897  var observer = new MutationObserver(function(mutations) {
2898    mutations.forEach(function(mut) {
2899      mut.addedNodes.forEach(function(node) {
2900        if (node.nodeType !== 1) return;
2901
2902        // Check for invoice detail view — look for elements with invoice data
2903        var invoiceHeader = node.querySelector ? node.querySelector('.inv-detail-header, [data-invoice-id]') : null;
2904        if (!invoiceHeader && node.classList && (node.classList.contains('ov-body') || node.id === 'inv-detail')) {
2905          invoiceHeader = node;
2906        }
2907
2908        // Also check for work order detail views
2909        var woHeader = node.querySelector ? node.querySelector('.wo-detail-header, [data-wo-id]') : null;
2910        if (!woHeader && node.classList && node.id === 'wo-detail') {
2911          woHeader = node;
2912        }
2913      });
2914    });
2915  });
2916
2917  // Use a simpler approach: periodically check for open detail views and inject badges
2918  var lastInvId = null;
2919  var lastWoId = null;
2920
2921  setInterval(function() {
2922    if (typeof prof === 'undefined' || !prof || prof.role !== 'admin') return;
2923
2924    // Check for open invoice detail
2925    var invDetail = document.getElementById('inv-detail') || document.querySelector('.invoice-detail-view');
2926    if (invDetail && invDetail.offsetParent !== null) {
2927      var invId = invDetail.getAttribute('data-invoice-id') || invDetail.dataset?.invoiceId;
2928      if (!invId) {
2929        // Try to find it from content
2930        var idEl = invDetail.querySelector('[data-inv-id]');
2931        if (idEl) invId = idEl.getAttribute('data-inv-id');
2932      }
2933      // Only inject once per view
2934      if (!invDetail.querySelector('.fin-inline-badge')) {
2935        injectInvoiceFinance(invDetail);
2936      }
2937    }
2938
2939    // Check for open work order detail
2940    var woDetail = document.getElementById('wo-detail') || document.querySelector('.wo-detail-view');
2941    if (woDetail && woDetail.offsetParent !== null) {
2942      if (!woDetail.querySelector('.fin-inline-badge')) {
2943        injectWOFinance(woDetail);
2944      }
2945    }
2946  }, 2000);
2947
2948  async function injectInvoiceFinance(container) {
2949    // Find invoice data from the visible content
2950    var repEl = container.querySelector('[data-field="sales_rep"], .inv-rep');
2951    var totalEl = container.querySelector('[data-field="total_sale"], .inv-total');
2952    var costEl = container.querySelector('[data-field="cost_amount"], .inv-cost');
2953
2954    // Fallback: scan text content for rep name and amounts
2955    var repName = '';
2956    var totalSale = 0;
2957    var costAmount = 0;
2958
2959    if (repEl) repName = repEl.textContent.trim();
2960    if (totalEl) totalSale = parseFloat(totalEl.textContent.replace(/[^0-9.-]/g, '')) || 0;
2961    if (costEl) costAmount = parseFloat(costEl.textContent.replace(/[^0-9.-]/g, '')) || 0;
2962
2963    // If we couldn't find data from elements, try the global invoices array
2964    if (typeof invoices !== 'undefined' && invoices) {
2965      // Find the currently viewed invoice
2966      var visibleInvNum = container.querySelector('h2, h3, .st');
2967      if (visibleInvNum) {
2968        var invText = visibleInvNum.textContent;
2969        var matchInv = invoices.find(function(inv) {
2970          return invText.indexOf(inv.id) > -1 || invText.indexOf(inv.invoice_number) > -1;
2971        });
2972        if (matchInv) {
2973          repName = repName || matchInv.sales_rep || '';
2974          totalSale = totalSale || matchInv.total_sale || 0;
2975          costAmount = costAmount || matchInv.cost_amount || 0;
2976        }
2977      }
2978    }
2979
2980    var config = await getRepConfig(repName);
2981    var badge = createCommissionBadge(repName, totalSale, costAmount, config.commission_pct || 30);
2982
2983    // Insert at top of detail view
2984    var firstChild = container.querySelector('.st, h2, h3');
2985    if (firstChild && firstChild.nextSibling) {
2986      firstChild.parentNode.insertBefore(badge, firstChild.nextSibling);
2987    } else {
2988      container.insertBefore(badge, container.firstChild);
2989    }
2990  }
2991
2992  async function injectWOFinance(container) {
2993    // Find work order data
2994    var woId = container.getAttribute('data-wo-id') || '';
2995    var installerEl = container.querySelector('[data-field="installer"], .wo-installer');
2996    var sqftEl = container.querySelector('[data-field="sqft"], .wo-sqft');
2997
2998    var installerName = installerEl ? installerEl.textContent.trim() : '';
2999    var sqft = sqftEl ? parseFloat(sqftEl.textContent.replace(/[^0-9.
2999-]/g, '')) || 0 : 0;
3000
3001    // Try global work_orders array
3002    if (typeof work_orders !== 'undefined' && work_orders) {
3003      var visibleWoNum = container.querySelector('h2, h3, .st');
3004      if (visibleWoNum) {
3005        var woText = visibleWoNum.textContent;
3006        var matchWo = work_orders.find(function(wo) {
3007          return woText.indexOf(wo.id) > -1 || woText.indexOf(wo.wo_number) > -1;
3008        });
3009        if (matchWo) {
3010          woId = woId || matchWo.id;
3011          installerName = installerName || matchWo.installer || '';
3012          sqft = sqft || matchWo.sqft || matchWo.total_sqft || 0;
3013        }
3014      }
3015    }
3016
3017    // Get installer payment record
3018    var payment = await getInstallerPayment(woId);
3019    var ratePerSqft = payment ? payment.rate_per_sqft : 0;
3020
3021    if (installerName || sqft) {
3022      var badge = createInstallerBadge(installerName, sqft, ratePerSqft);
3023      var firstChild = container.querySelector('.st, h2, h3');
3024      if (firstChild && firstChild.nextSibling) {
3025        firstChild.parentNode.insertBefore(badge, firstChild.nextSibling);
3026      } else {
3027        container.insertBefore(badge, container.firstChild);
3028      }
3029    }
3030  }
3031
3032  console.log('[patch.js] FIX 15: Inline Finance badges loaded');
3033})();
3034
3035// === FIX 16: Outlook calendar sync — match button didn't update the calendar ===
3036// Ashley's bug: clicking "Match" on unmatched Outlook events in the review panel
3037// inserted the activity into Supabase but never refreshed the local `acts` array
3038// or called renderCalendar(), so the newly matched Friday events didn't appear on
3039// the calendar until a full page refresh. Also fixes matchAllSelected() which was
3040// checking the wrong DOM id (um-match-N instead of um-search-N).
3041(function fixOutlookMatchRefresh() {
3042  if (typeof window.matchSingleEvent !== 'function') {
3043    console.log('[patch.js] FIX 16: matchSingleEvent not found, skipping');
3044    return;
3045  }
3046  var _origMatchSingle = window.matchSingleEvent;
3047
3048  // Wrap matchSingleEvent — after the original does its insert + row UI update,
3049  // refresh the local acts array and re-render the calendar so the matched
3050  // event immediately appears on the correct date.
3051  window.matchSingleEvent = async function(idx, outlookId, repId) {
3052    var input = document.getElementById('um-search-' + idx);
3053    var hadMatch = !!(input && input.dataset && input.dataset.matchId);
3054    // Delegate to the original (does the insert + row status update)
3055    var result = await _origMatchSingle.apply(this, arguments);
3056    // Only refresh if the user actually matched something (not a skip)
3057    if (hadMatch && typeof sb !== 'undefined' && typeof sb.from === 'function') {
3058      try {
3059        var resp = await sb.from('activities').select('*')
3060          .order('created_at', { ascending: false }).range(0, 4999);
3061        if (resp && resp.data) {
3062          acts = resp.data;
3063          if (typeof renderCalendar === 'function') renderCalendar();
3064          // If the lead detail pane is open for this lead, refresh it too
3065          if (typeof aLead !== 'undefined' && aLead && typeof renderLead === 'function') {
3066            try { renderLead(); } catch (e) {}
3067          }
3068        }
3069      } catch (e) {
3070        console.warn('[patch.js] FIX 16: acts refresh failed', e);
3071      }
3072    }
3073    return result;
3074  };
3075
3076  // matchAllSelected was checking document.getElementById('um-match-' + i) which
3077  // never existed — the input id is 'um-search-' + i. Replace it with a version
3078  // that reads input.dataset.matchId (set by umSelectMatch when the user picks
3079  // a dropdown option). Only matches rows where the user has actually selected
3080  // something.
3081  window.matchAllSelected = async function() {
3082    var events = window._unmatchedEvents || [];
3083    var repId = window._unmatchedRepId;
3084    var count = 0;
3085    for (var i = 0; i < events.length; i++) {
3086      var input = document.getElementById('um-search-' + i);
3087      if (!input || !input.dataset || !input.dataset.matchId) continue;
3088      // Delegate one at a time to the wrapped matchSingleEvent
3089      // Note: we call _origMatchSingle directly here to avoid N redundant
3090      // acts-refreshes — we'll do one refresh at the end.
3091      try {
3092        await _origMatchSingle(i, events[i].id, repId);
3093        count++;
3094      } catch (e) {
3095        console.warn('[patch.js] FIX 16: match failed for row ' + i, e);
3096      }
3097    }
3098    if (count > 0) {
3099      try {
3100        var resp = await sb.from('activities').select('*')
3101          .order('created_at', { ascending: false }).range(0, 4999);
3102        if (resp && resp.data) acts = resp.data;
3103      } catch (e) {
3104        console.warn('[patch.js] FIX 16: bulk acts refresh failed', e);
3105      }
3106      if (typeof renderCalendar === 'function') renderCalendar();
3107      if (typeof toast === 'function') toast('Matched ' + count + ' events ✓', 'ok');
3108    } else {
3109      if (typeof toast === 'function') toast('Pick a lead/vendor/installer for at least one event first', 'info');
3110    }
3111  };
3112
3113  console.log('[patch.js] FIX 16: Outlook match-event calendar refresh applied');
3114})();
3115
3116// === FIX 17: Route Planner — Apple Maps, Portal Map, and Save/Load routes ===
3117// Joey's report:
3118//   (a) Apple Maps + Portal Map buttons not working
3119//   (b) Routes built on desktop don't carry over to mobile
3120//
3121// Three fixes:
3122//   1. Apple Maps URL: was http:// (blocked as mixed content on HTTPS) and
3123//      only passed one destination — all waypoints were dropped. Now uses
3124//      https:// and chains stops with "+to:" separator.
3125//   2. Portal Map: was using placeholder Google API key ("AIzaSyD-placeholder")
3126//      so the directions embed silently failed. Replaced with an OSM tile
3127//      centered on the route bbox plus a clean numbered stop list and direct
3128//      Google/Apple Maps launch buttons.
3129//   3. Persistence: routeStops was in-memory only. Added 💾 Save / 📂 Load
3130//      buttons that hit a new `rep_routes` table (per-rep, JSONB stops).
3131//      Feature-detects — if the table doesn't exist yet, buttons show a
3132//      "Ask Joey to apply the SQL migration" message instead of erroring.
3133//
3134// Companion SQL migration is in /sessions/amazing-elegant-rubin/mnt/outputs/
3135// 13-rep-routes-migration.sql — apply once in Supabase SQL Editor and the
3136// Save/Load buttons will start working for all reps.
3137(function fixRoutePlanner() {
3138  // ---- (1) Replace launchRoute with corrected Apple + Portal behavior ----
3139  if (typeof window.launchRoute !== 'function') {
3140    console.log('[patch.js] FIX 17: launchRoute not found, skipping');
3141    return;
3142  }
3143
3144  window.launchRoute = async function(type) {
3145    if (!Array.isArray(routeStops) || !routeStops.length) {
3146      if (typeof toast === 'function') toast('Add at least one stop first', 'err');
3147      return;
3148    }
3149    // Preserve the existing optimize-before-launch behavior
3150    if (typeof optimizeRouteOrder === 'function') {
3151      try { await optimizeRouteOrder(); } catch (e) {}
3152    }
3153
3154    // Current location
3155    var origin = '';
3156    try {
3157      var pos = await new Promise(function(res, rej) {
3158        navigator.geolocation.getCurrentPosition(res, rej, { timeout: 6000 });
3159      });
3160      origin = pos.coords.latitude + ',' + pos.coords.longitude;
3161    } catch (e) {
3162      if (typeof toast === 'function') toast('Could not get your location — showing route without it', 'info');
3163    }
3164
3165    var stops = routeStops.map(function(s) { return s.address || s.company; }).filter(Boolean);
3166    if (!stops.length) {
3167      if (typeof toast === 'function') toast('Stops have no addresses', 'err');
3168      return;
3169    }
3170    var dest = stops[stops.length - 1];
3171    var waypoints = stops.slice(0, -1);
3172
3173    if (type === 'google') {
3174      var gUrl = 'https://www.google.com/maps/dir/?api=1'
3175        + (origin ? '&origin=' + encodeURIComponent(origin) : '')
3176        + '&destination=' + encodeURIComponent(dest)
3177        + (waypoints.length ? '&waypoints=' + encodeURIComponent(waypoints.join('|')) : '');
3178      window.open(gUrl, '_blank');
3179      return;
3180    }
3181
3182    if (type === 'apple') {
3183      // FIX: https (was http — mixed-content blocked) + chain all waypoints
3184      // with "+to:" so Apple Maps hits every stop, not just the last one.
3185      var aDaddr = [].concat(waypoints, [dest])
3186        .map(function(a) { return encodeURIComponent(a); })
3187        .join('+to:');
3188      var aUrl = 'https://maps.apple.com/?'
3189        + (origin ? 'saddr=' + encodeURIComponent(origin) + '&' : '')
3190        + 'daddr=' + aDaddr;
3191      window.open(aUrl, '_blank');
3192      return;
3193    }
3194
3195    if (type === 'portal') {
3196      // FIX: render a usable portal preview with OSM (no API key needed)
3197      // plus a clean numbered stop list and launch buttons. Previous code
3198      // showed only the destination because the Google embed key was a
3199      // placeholder string.
3200      var frameEl = document.getElementById('portal-route-map-frame');
3201      if (!frameEl) return;
3202
3203      // Compute a rough bbox from known ZIP/CITY coordinates of the stops
3204      var coords = [];
3205      routeStops.forEach(function(s) {
3206        var lead = (typeof leads !== 'undefined' ? leads : []).find(function(l) { return l.id === s.id; });
3207        if (!lead) return;
3208        var zip = (lead.zip_code || '').toString().substring(0, 5);
3209        var city = (lead.city || '').trim();
3210        if (typeof ZIP_COORDS !== 'undefined' && ZIP_COORDS[zip]) coords.push(ZIP_COORDS[zip]);
3211        else if (typeof CITY_COORDS !== 'undefined' && CITY_COORDS[city]) coords.push(CITY_COORDS[city]);
3212      });
3213      var centerLat = 29.76, centerLng = -95.37, bboxPad = 0.3; // Houston default
3214      if (coords.length) {
3215        var lats = coords.map(function(c) { return c[0]; });
3216        var lngs = coords.map(function(c) { return c[1]; });
3217        var minLat = Math.min.apply(null, lats), maxLat = Math.max.apply(null, lats);
3218        var minLng = Math.min.apply(null, lngs), maxLng = Math.max.apply(null, lngs);
3219        centerLat = (minLat + maxLat) / 2;
3220        centerLng = (minLng + maxLng) / 2;
3221        bboxPad = Math.max(0.08, Math.max(maxLat - minLat, maxLng - minLng) * 0.6 + 0.05);
3222      }
3223      var mapSrc = 'https://www.openstreetmap.org/export/embed.html'
3224        + '?bbox=' + (centerLng - bboxPad) + ',' + (centerLat - bboxPad) + ','
3225        + (centerLng + bboxPad) + ',' + (centerLat + bboxPad)
3226        + '&layer=mapnik&marker=' + centerLat + ',' + centerLng;
3227
3228      var gBtnUrl = 'https://www.google.com/maps/dir/?api=1'
3229        + (origin ? '&origin=' + encodeURIComponent(origin) : '')
3230        + '&destination=' + encodeURIComponent(dest)
3231        + (waypoints.length ? '&waypoints=' + encodeURIComponent(waypoints.join('|')) : '');
3232      var aBtnDaddr = [].concat(waypoints, [dest]).map(function(a) { return encodeURIComponent(a); }).join('+to:');
3233      var aBtnUrl = 'https://maps.apple.com/?' + (origin ? 'saddr=' + encodeURIComponent(origin) + '&' : '') + 'daddr=' + aBtnDaddr;
3234
3235      var stopsHtml = routeStops.map(function(s, i) {
3236        var esc = function(t) { return (t || '').toString().replace(/</g, '&lt;'); };
3237        return '<div style="display:flex;gap:8px;align-items:flex-start;padding:8px 0;border-bottom:1px solid var(--b)">'
3238          + '<div style="background:var(--gold);color:#1a1008;border-radius:50%;width:24px;height:24px;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:12px;flex-shrink:0">' + (i + 1) + '</div>'
3239          + '<div style="flex:1;min-width:0">'
3240          +   '<div style="font-weight:600;font-size:13px;color:var(--t)">' + esc(s.company) + '</div>'
3241          +   (s.address ? '<div style="font-size:11px;color:var(--t3)">' + esc(s.address) + '</div>' : '')
3242          +   (s.phone ? '<div style="font-size:11px;color:var(--t3)">📞 ' + esc(s.phone) + '</div>' : '')
3243          + '</div>'
3244          + '</div>';
3245      }).join('');
3246
3247      frameEl.innerHTML =
3248        '<iframe src="' + mapSrc + '" style="width:100%;height:280px;border:0;border-radius:10px" loading="lazy"></iframe>'
3249        + '<div style="margin-top:12px">'
3250        +   '<div style="font-size:11px;color:var(--t3);margin-bottom:4px">Your route (' + routeStops.length + ' stop' + (routeStops.length !== 1 ? 's' : '') + '):</div>'
3251        +   stopsHtml
3252        + '</div>'
3253        + '<div style="display:flex;gap:8px;margin-top:12px;flex-wrap:wrap">'
3254        +   '<a href="' + gBtnUrl + '" target="_blank" style="flex:1;min-width:140px;text-align:center;background:var(--gold);color:#1a1008;padding:12px;border-radius:8px;font-weight:700;text-decoration:none">🗺️ Open in Google Maps</a>'
3255        +   '<a href="' + aBtnUrl + '" target="_blank" style="flex:1;min-width:140px;text-align:center;background:var(--d4);color:var(--t);border:1px solid var(--b);padding:12px;border-radius:8px;font-weight:700;text-decoration:none">🍎 Open in Apple Maps</a>'
3256        + '</div>';
3257      var ov = document.getElementById('ov-route-map');
3258      if (ov) ov.style.display = 'flex';
3259      return;
3260    }
3261  };
3262
3263  // ---- (2) Add Save Route / Load Saved Route buttons + functions ----
3264  // New functions attached to window so inline onclick works.
3265
3266  window.saveCurrentRoute = async function() {
3267    if (!Array.isArray(routeStops) || !routeStops.length) {
3268      if (typeof toast === 'function') toast('Add at least one stop first', 'err');
3269      return;
3270    }
3271    if (typeof sb === 'undefined' || typeof prof === 'undefined' || !prof) {
3272      if (typeof toast === 'function') toast('Not signed in', 'err');
3273      return;
3274    }
3275    var name = window.prompt('Name this route (e.g. "Tuesday Memorial run"):', '');
3276    if (!name) return;
3277    name = name.trim();
3278    if (!name) return;
3279    try {
3280      var resp = await sb.from('rep_routes').insert({
3281        rep_id: prof.id,
3282        name: name,
3283        stops: routeStops
3284      }).select();
3285      if (resp.error) {
3286        // Table missing or RLS denied — guide user
3287        if (resp.error.code === '42P01' || /relation .* does not exist/i.test(resp.error.message || '')) {
3288          if (typeof toast === 'function') toast('Saved Routes feature needs DB migration — ask Joey to apply rep_routes-migration.sql', 'err');
3289        } else {
3290          if (typeof toast === 'function') toast('Save failed: ' + resp.error.message, 'err');
3291        }
3292        console.warn('[patch.js] FIX 17: saveCurrentRoute error', resp.error);
3293        return;
3294      }
3295      if (typeof toast === 'function') toast('Route "' + name + '" saved ✓', 'ok');
3296    } catch (e) {
3297      console.warn('[patch.js] FIX 17: saveCurrentRoute exception', e);
3298      if (typeof toast === 'function') toast('Save failed — check console', 'err');
3299    }
3300  };
3301
3302  window.openSavedRoutes = async function() {
3303    if (typeof sb === 'undefined' || typeof prof === 'undefined' || !prof) {
3304      if (typeof toast === 'function') toast('Not signed in', 'err');
3305      return;
3306    }
3307    var resp;
3308    try {
3309      resp = await sb.from('rep_routes')
3310        .select('id,name,stops,created_at')
3311        .eq('rep_id', prof.id)
3312        .order('created_at', { ascending: false });
3313    } catch (e) {
3314      console.warn('[patch.js] FIX 17: openSavedRoutes exception', e);
3315      if (typeof toast === 'function') toast('Load failed — check console', 'err');
3316      return;
3317    }
3318    if (resp.error) {
3319      if (resp.error.code === '42P01' || /relation .* does not exist/i.test(resp.error.message || '')) {
3320        if (typeof toast === 'function') toast('Saved Routes feature needs DB migration — ask Joey to apply rep_routes-migration.sql', 'err');
3321      } else {
3322        if (typeof toast === 'function') toast('Load failed: ' + resp.error.message, 'err');
3323      }
3324      return;
3325    }
3326    var rows = resp.data || [];
3327    if (!rows.length) {
3328      if (typeof toast === 'function') toast('No saved routes yet. Hit 💾 Save first.', 'info');
3329      return;
3330    }
3331    var existing = document.getElementById('ov-saved-routes');
3332    if (existing) existing.remove();
3333    var esc = function(t) { return (t || '').toString().replace(/</g, '&lt;').replace(/'/g, '&#39;'); };
3334    var rowsHtml = rows.map(function(r) {
3335      var count = Array.isArray(r.stops) ? r.stops.length : 0;
3336      var when = r.created_at ? new Date(r.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) : '';
3337      return '<div style="background:var(--d4);border-radius:10px;padding:12px;margin-bottom:8px;display:flex;gap:10px;align-items:center">'
3338        + '<div style="flex:1;min-width:0">'
3339        +   '<div style="font-weight:700;font-size:13px;color:var(--t)">' + esc(r.name) + '</div>'
3340        +   '<div style="font-size:11px;color:var(--t3)">' + count + ' stop' + (count !== 1 ? 's' : '') + ' · ' + when + '</div>'
3341        + '</div>'
3342        + '<button onclick="loadSavedRoute(\'' + r.id + '\')" style="background:var(--gold);
3342color:#1a1008;border:none;border-radius:6px;padding:6px 12px;font-size:11px;font-weight:700;cursor:pointer">Load</button>'
3343        + '<button onclick="deleteSavedRoute(\'' + r.id + '\')" style="background:transparent;border:1px solid var(--b);color:var(--red);border-radius:6px;padding:6px 10px;font-size:11px;cursor:pointer" title="Delete">🗑️</button>'
3344        + '</div>';
3345    }).join('');
3346    var html = '<div id="ov-saved-routes" style="position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:9999;display:flex;align-items:center;justify-content:center;padding:16px" onclick="if(event.target===this)this.remove()">'
3347      + '<div style="background:var(--d2);border-radius:16px;padding:20px;max-width:500px;width:100%;max-height:80vh;overflow-y:auto">'
3348      +   '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">'
3349      +     '<div style="font-size:16px;font-weight:800;color:var(--t)">📂 Saved Routes</div>'
3350      +     '<button onclick="document.getElementById(\'ov-saved-routes\').remove()" style="background:none;border:none;color:var(--t3);font-size:18px;cursor:pointer">✕</button>'
3351      +   '</div>'
3352      +   rowsHtml
3353      + '</div>'
3354      + '</div>';
3355    document.body.insertAdjacentHTML('beforeend', html);
3356    window._savedRoutesCache = rows;
3357  };
3358
3359  window.loadSavedRoute = function(id) {
3360    var rows = window._savedRoutesCache || [];
3361    var r = rows.find(function(x) { return x.id === id; });
3362    if (!r) { if (typeof toast === 'function') toast('Route not found', 'err'); return; }
3363    if (!Array.isArray(r.stops)) { if (typeof toast === 'function') toast('Route data invali
3363d', 'err'); return; }
3364    routeStops = r.stops.slice();
3365    var ov = document.getElementById('ov-saved-routes'); if (ov) ov.remove();
3366    if (typeof renderRouteStops === 'function') renderRouteStops();
3367    if (typeof renderSuggestedRoutes === 'function') renderSuggestedRoutes();
3368    if (typeof renderRouteMap === 'function') renderRouteMap();
3369    if (typeof toast === 'function') toast('Loaded "' + r.name + '" — ' + r.stops.length + ' stops', 'ok');
3370  };
3371
3372  window.deleteSavedRoute = async function(id) {
3373    if (!confirm('Delete this saved route?')) return;
3374    try {
3375      var resp = await sb.from('rep_routes').delete().eq('id', id);
3376      if (resp.error) { if (typeof toast === 'function') toast('Delete failed: ' + resp.error.message, 'err'); return; }
3377      if (typeof toast === 'function') toast('Route deleted', 'ok');
3378      window.openSavedRoutes(); // refresh the list
3379    } catch (e) {
3380      console.warn('[patch.js] FIX 17: deleteSavedRoute exception', e);
3381    }
3382  };
3383
3384  // ---- (3) Inject Save/Load buttons into the Route Planner UI ----
3385  // Watch for the Route Planner tab becoming active and add buttons next to
3386  // the existing "Copy List / Clear All" row (avoids modifying portal-index.html).
3387  function injectSaveLoadButtons() {
3388    var stopsBox = document.getElementById('route-stops');
3389    if (!stopsBox) return;
3390    var container = stopsBox.parentElement;
3391    if (!container || container.querySelector('[data-patch17-btns]')) return;
3392    // Find the button rows — the second one has Copy/Clear
3393    var btnRows = container.querySelectorAll('div[style*="margin-top"]');
3394    if (!btnRows.length) return;
3395    // Create a new row for Save/Load that appears between the two existing rows
3396    var row = document.createElement('div');
3397    row.setAttribute('data-patch17-btns', '1');
3398    row.style.cssText = 'margin-top:8px;display:flex;gap:8px;flex-wrap:wrap';
3399    row.innerHTML =
3400        '<button onclick="saveCurrentRoute()" style="flex:1;min-width:120px;background:var(--d4);color:var(--t);border:1px solid var(--b);border-radius:8px;padding:8px;font-size:12px;font-weight:600;cursor:pointer">💾 Save Route</button>'
3401      + '<button onclick="openSavedRoutes()" style="flex:1;min-width:120px;background:var(--d4);color:var(--t);border:1px solid var(--b);border-radius:8px;padding:8px;font-size:12px;font-weight:600;cursor:pointer">📂 Load Saved</button>';
3402    // Insert after the last existing button row
3403    var lastBtnRow = btnRows[btnRows.length - 1];
3404    if (lastBtnRow && lastBtnRow.parentElement === container) {
3405      lastBtnRow.parentElement.insertBefore(row, lastBtnRow.nextSibling);
3406    }
3407  }
3408  // Try immediately and after each Route tab open
3409  if (document.readyState !== 'loading') injectSaveLoadButtons();
3410  document.addEventListener('DOMContentLoaded', injectSaveLoadButtons);
3411  // Also hook into tab changes — Route Planner is rendered on tab activation
3412  var _origGoTab = window.goTab_route_init;
3413  if (typeof _origGoTab === 'function') {
3414    window.goTab_route_init = function() {
3415      var r = _origGoTab.apply(this, arguments);
3416      setTimeout(injectSaveLoadButtons, 50);
3417      return r;
3418    };
3419  }
3420
3421  console.log('[patch.js] FIX 17: Route Planner Apple Maps + Portal Map + Save/Load applied');
3422
3423  // === FIX 18: Bust _productCatalogCache every time the material picker opens ===
3424  // Prevents stale coverage/pricing from lingering in a rep's tab after
3425  // someone edits product_catalog in Supabase. Added 2026-04-24 — triggered
3426  // by RIVA-1184 showing "1 sf/ctn" when DB was already updated to 16.
3427  (function(){
3428    const _origPick = typeof openMaterialPicker === 'function' ? openMaterialPicker : null;
3429    if (_origPick) {
3430      window.openMaterialPicker = function(){
3431        try { window._productCatalogCache = null; } catch(_) {}
3432        return _origPick.apply(this, arguments);
3433      };
3434    }
3435    const _origEditQuote = typeof editQuote === 'function' ? editQuote : null;
3436    if (_origEditQuote) {
3437      window.editQuote = function(){
3438        try { window._productCatalogCache = null; } catch(_) {}
3439        return _origEditQuote.apply(this, arguments);
3440      };
3441    }
3442    console.log('[patch.js] FIX 18: product catalog cache busting applied');
3443  })();
3444
3445  // === FIX 19: Hide "1 sf/ctn" badge on pieces sold individually ===
3446  // Moldings, stair-noses, adhesives, tapes, slabs all have coverage=1
3447  // because they're sold per piece, not per carton. The carton calculator
3448  // is noise for those products. This runs after every renderLineItems()
3449  // and strips the carton badge for any line where coverage <= 1.
3450  (function(){
3451    const _origRender = typeof renderLineItems === 'function' ? renderLineItems : null;
3452    if (!_origRender) return;
3453    window.renderLineItems = function(){
3454      const result = _origRender.apply(this, arguments);
3455      try {
3456        document.querySelectorAll('.item-cartons').forEach(cartonInput => {
3457          const parent = cartonInput.parentElement;
3458          if (!parent) return;
3459          const txt = parent.textContent || '';
3460          // Match "1 sf/box" or "1 sf/bucket" (the useless coverage=1 render)
3461          if (/\b1\s*sf\/(box|bucket)\b/.test(txt)) {
3462            parent.style.display = 'none';
3463          }
3464        });
3465      } catch(_) {}
3466      return result;
3467    };
3468    console.log('[patch.js] FIX 19: hide useless 1 sf/ctn badge applied');
3469  })();
3470
3471  // === FIX 20: Vendors now load from DB on render ==========================
3472  // Root cause: src/17-vendors.js ships a hardcoded VENDORS seed array and
3473  // never had a loadVendorsFromDB function wired in. Saves were writing to
3474  // Supabase correctly, but renderVendors() read from the in-memory seed
3475  // array only — so new vendors disappeared on refresh, edits didn't stick,
3476  // and deletes resurrected the seed row.
3477  // This fix: on every renderVendors(), merge DB rows into VENDORS keyed by
3478  // lowercased name, DB row wins, and stamp the uuid id onto the local
3479  // entry so the docs feature (FIX 21) can attach uploads to a real vendor.
3480  (function(){
3481    if (typeof window === 'undefined' || typeof sb === 'undefined') return;
3482
3483    let _vendorDbLoaded = false;
3484    let _vendorDbLoading = null;
3485
3486    window.loadVendorsFromDB = async function(force){
3487      if (force) _vendorDbLoaded = false;
3488      if (_vendorDbLoaded) return;
3489      if (_vendorDbLoading) return _vendorDbLoading;
3490      _vendorDbLoading = (async () => {
3491        try {
3492          const { data, error } = await sb.from('vendors').select('*').order('name');
3493          if (error) { console.error('[patch.js] FIX 20 vendor load err:', error); return; }
3494          if (!Array.isArray(data)) return;
3495          if (typeof VENDORS === 'undefined' || !Array.isArray(VENDORS)) return;
3496
3497          const byKey = new Map();
3498          VENDORS.forEach((v, idx) => { if (v && v.name) byKey.set(String(v.name).trim().toLowerCase(), idx); });
3499
3500          data.forEach(row => {
3501            const key = String(row.name || '').trim().toLowerCase();
3502            if (!key) return;
3503            const reps = [];
3504            if (row.rep_name || row.rep_email || row.rep_phone) {
3505              reps.push({ name: row.rep_name || '', email: row.rep_email || '', phone: row.rep_phone || '' });
3506            }
3507            const merged = {
3508              id: row.id,
3509              name: row.name,
3510              address: row.address || '',
3511              acctNum: row.account_number || '',
3512              phone: row.phone || '',
3513              freight: row.freight || '',
3514              products: Array.isArray(row.products) ? row.products : [],
3515              collections: row.collections || '',
3516              pricing: row.pricing || null,
3517              rep_name: row.rep_name || '',
3518              rep_email: row.rep_email || '',
3519              rep_phone: row.rep_phone || '',
3520              reps,
3521              website: row.website || '',
3522              notes: row.notes || '',
3523              status: row.status || 'active',
3524              _fromDb: true
3525            };
3526            if (byKey.has(key)) {
3527              // Preserve hardcoded pricing/fees if DB row doesn't carry them yet
3528              const existing = VENDORS[byKey.get(key)] || {};
3529              if (!merged.pricing && existing.pricing) merged.pricing = existing.pricing;
3530              if (!merged.collections && existing.collections) merged.collections = existing.collections;
3531              if (existing.fees) merged.fees = existing.fees;
3532              VENDORS[byKey.get(key)] = merged;
3533            } else {
3534              VENDORS.push(merged);
3535              byKey.set(key, VENDORS.length - 1);
3536            }
3537          });
3538          _vendorDbLoaded = true;
3539          console.log('[patch.js] FIX 20: merged', data.length, 'vendors from DB');
3540        } catch(e) {
3541          console.error('[patch.js] FIX 20 loadVendorsFromDB exception:', e);
3542        } finally {
3543          _vendorDbLoading = null;
3544        }
3545      })();
3546      return _vendorDbLoading;
3547    };
3548
3549    const _origRender = typeof renderVendors === 'function' ? renderVendors : null;
3550    if (_origRender) {
3551      window.renderVendors = async function(){
3552        if (!_vendorDbLoaded) { try { await window.loadVendorsFromDB(); } catch(_){} }
3553        return _origRender.apply(this, arguments);
3554      };
3555    }
3556
3557    // After a save, refetch the DB row and stamp its uuid + fields into VENDORS
3558    const _origSave = typeof saveVendor === 'function' ? saveVendor : null;
3559    if (_origSave) {
3560      window.saveVendor = async function(editIdx){
3561        const nameInput = document.getElementById('vnd-f-name');
3562        const savedName = nameInput ? String(nameInput.value || '').trim() : '';
3563        const result = await _origSave.apply(this, arguments);
3564        // Re-pull the canonical row from DB (upsert with onConflict:'name' either inserted or updated)
3565        try {
3566          if (savedName) {
3567            const { data, error } = await sb.from('vendors').select('*').eq('name', savedName).maybeSingle();
3568            if (!error && data && typeof VENDORS !== 'undefined' && Array.isArray(VENDORS)) {
3569              const key = savedName.toLowerCase();
3570              const idx = VENDORS.findIndex(v => v && v.name && v.name.toLowerCase() === key);
3571              if (idx >= 0) {
3572                VENDORS[idx].id = data.id;
3573                VENDORS[idx]._fromDb = true;
3574              }
3575            }
3576          }
3577        } catch(_){}
3578        return result;
3579      };
3580    }
3581
3582    // Delete: make sure DB delete was actually awaited so refreshes reflect it
3583    const _origDelete = typeof deleteVendor === 'function' ? deleteVendor : null;
3584    if (_origDelete) {
3585      window.deleteVendor = async function(idx){
3586        if (typeof VENDORS !== 'undefined' && VENDORS[idx] && VENDORS[idx].id) {
3587          if (!confirm('Remove this vendor? This cannot be undone.')) return;
3588          try {
3589            const { error } = await sb.from('vendors').delete().eq('id', VENDORS[idx].id);
3590            if (error) { alert('Could not delete vendor: ' + error.message); return; }
3591          } catch(e) { alert('Could not delete vendor: ' + (e.message||e)); return; }
3592          VENDORS.splice(idx, 1);
3593          const modal = document.getElementById('vendor-modal-overlay'); if (modal) modal.remove();
3594          if (typeof renderVendors === 'function') renderVendors();
3595          if (typeof toast === 'function') toast('Vendor removed ✓', 'ok');
3596          return;
3597        }
3598        // No DB id yet (hardcoded seed row) — fall back to original in-memory splice
3599        return _origDelete.apply(this, arguments);
3600      };
3601    }
3602
3603    console.log('[patch.js] FIX 20: vendor DB load + save roundtrip + delete applied');
3604  })();
3605
3606  // === FIX 21: Vendor documents upload UI ==================================
3607  // Adds a "Documents" section to the vendor edit modal. Uses Supabase storage
3608  // bucket "vendor-docs" and table public.vendor_documents.
3609  (function(){
3610    if (typeof window === 'undefined' || typeof sb === 'undefined') return;
3611    const BUCKET = 'vendor-docs';
3612    const DOC_TYPES = ['freight','price-list','install-guide','tech-spec','warranty','cert','msds','cut-sheet','other'];
3613
3614    async function listVendorDocs(vendorId){
3615      const { data, error } = await sb.from('vendor_documents')
3616        .select('*').eq('vendor_id', vendorId).eq('is_archived', false).order('uploaded_at', { ascending: false });
3617      if (error) { console.error('[patch.js] FIX 21 list err:', error); return []; }
3618      return data || [];
3619    }
3620
3621    async function uploadVendorDoc(vendorId, file, docType, notes){
3622      const safe = file.name.replace(/[^a-zA-Z0-9._-]/g, '_');
3623      const uuid = (crypto && crypto.randomUUID) ? crypto.randomUUID() : (Date.now()+'-'+Math.random().toString(36).slice(2));
3624      const path = vendorId + '/' + uuid + '-' + safe;
3625      const up = await sb.storage.from(BUCKET).upload(path, file, { contentType: file.type || undefined, upsert: false });
3626      if (up.error) throw up.error;
3627      let userId = null;
3628      try { const { data: ud } = await sb.auth.getUser(); userId = ud && ud.user ? ud.user.id : null; } catch(_){}
3629      const row = {
3630        vendor_id: vendorId, file_name: file.name, file_path: path,
3631        doc_type: docType || 'other', mime_type: file.type || null,
3632        size_bytes: file.size || null, notes: notes || null, uploaded_by: userId
3633      };
3634      const ins = await sb.from('vendor_documents').insert(row).select('*').maybeSingle();
3635      if (ins.error) {
3636        try { await sb.storage.from(BUCKET).remove([path]); } catch(_){}
3637        throw ins.error;
3638      }
3639      return ins.data;
3640    }
3641
3642    async function signedUrlFor(path, seconds){
3643      const { data, error } = await sb.storage.from(BUCKET).createSignedUrl(path, seconds || 3600);
3644      if (error) throw error;
3645      return data.signedUrl;
3646    }
3647
3648    window.vendorDocs_open = async function(docId){
3649      // FIX 38: Pop the new tab SYNCHRONOUSLY on the click, then redirect it
3650      // once the signed URL is ready. If we wait for the awaits (DB lookup +
3651      // createSignedUrl) and call window.open after, browsers have already
3652      // dropped the user-activation flag and silently block the popup —
3653      // which presented as "I clicked Open and nothing happened" for vendor
3654      // PDFs (AcousticMod report). Drop 'noopener' so we can set the new
3655      // tab's location after the awaits resolve. The destination is our own
3656      // signed Supabase storage URL, no XSS surface from window.opener.
3657      const newTab = window.open('about:blank', '_blank');
3658      try {
3659        const { data: d, error } = await sb.from('vendor_documents').select('file_path').eq('id', docId).maybeSingle();
3660        if (error || !d) throw error || new Error('Not found');
3661        const url = await signedUrlFor(d.file_path, 3600);
3662        if (newTab && !newTab.closed) {
3663          newTab.location.href = url;
3664        } else {
3665          // Popup was blocked despite synchronous open — fall back to
3666          // copying the URL so the rep can paste it in a new tab manually.
3667          try { await navigator.clipboard.writeText(url); } catch(_){}
3668          alert('Your browser blocked the popup. The signed link has been copied to your clipboard — paste it into a new tab to view the document.');
3669        }
3670      } catch(e){
3671        if (newTab && !newTab.closed) { try { newTab.close(); } catch(_){} }
3672        alert('Could not open document: ' + (e.message || e));
3673      }
3674    };
3675
3676    window.vendorDocs_delete = async function(docId, vendorId){
3677      if (!confirm('Delete this document?')) return;
3678      try {
3679        const { data: d } = await sb.from('vendor_documents').select('file_path').eq('id', docId).maybeSingle();
3680        if (d && d.file_path) { try { await sb.storage.from(BUCKET).remove([d.file_path]); } catch(_){} }
3681        const { error } = await sb.from('vendor_documents').delete().eq('id', docId);
3682        if (error) throw error;
3683        await window.vendorDocs_render(vendorId);
3684        // FIX 41: keep the vendor list's 📎 badge count in sync after delete.
3685        if (typeof window.refreshVendorDocBadges === 'function') {
3686          window.refreshVendorDocBadges();
3687        }
3688      } catch(e){ alert('Could not delete: ' + (e.message||e)); }
3689    };
3690
3691    window.vendorDocs_upload = async function(vendorId){
3692      const fileInput = document.getElementById('vdoc-file');
3693      const typeInput = document.getElementById('vdoc-type');
3694      const notesInput = document.getElementById('vdoc-notes');
3695      if (!fileInput || !fileInput.files || !fileInput.files[0]) { alert('Pick a file first'); return; }
3696      const file = fileInput.files[0];
3697      if (file.size > 26214400) { alert('File too big (25MB max)'); return; }
3698      const btn = document.getElementById('vdoc-upload-btn');
3699      if (btn) { btn.disabled = true; btn.textContent = 'Uploading…'; }
3700      try {
3701        await uploadVendorDoc(vendorId, file, typeInput ? typeInput.value : 'other', notesInput ? notesInput.value.trim() : '');
3702        if (typeof toast === 'function') toast('Document uploaded ✓', 'ok');
3703        fileInput.value = ''; if (notesInput) notesInput.value = '';
3704        await window.vendorDocs_render(vendorId);
3705        // FIX 41: keep the vendor list's 📎 badge count in sync without
3706        // requiring a full list re-render.
3707        if (typeof window.refreshVendorDocBadges === 'function') {
3708          window.refreshVendorDocBadges();
3709        }
3710      } catch(e) {
3711        alert('Upload failed: ' + (e.message || e));
3712      } finally {
3713        if (btn) { btn.disabled = false; btn.textContent = 'Upload'; }
3714      }
3715    };
3716
3717    window.vendorDocs_render = async function(vendorId){
3718      const host = document.getElementById('vdoc-list');
3719      if (!host) return;
3720      host.innerHTML = '<div style="color:var(--t3);font-size:12px;padding:8px;">Loading…</div>';
3721      const docs = await listVendorDocs(vendorId);
3722      if (!docs.length) { host.innerHTML = '<div style="color:var(--t3);font-size:12px;padding:8px;">No documents uploaded yet.</div>'; return; }
3723      function fmtSize(b){ if(!b) return ''; if(b<1024) return b+' B'; if(b<1048576) return (b/1024).toFixed(1)+' KB'; return (b/1048576).toFixed(1)+' MB'; }
3724      function fmtDate(s){ try { return new Date(s).toLocaleDateString(); } catch(_){ return ''; } }
3725      host.innerHTML = docs.map(d => (
3726        '<div style="display:flex;align-items:center;gap:10px;padding:8px 10px;border:1px solid var(--b);border-radius:6px;margin-bottom:6px;background:var(--d2);">'
3727        + '<div style="flex:1;min-width:0;">'
3728        + '<div style="font-size:13px;font-weight:600;color:var(--t);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + (d.file_name||'(unnamed)') + '</div>'
3729        + '<div style="font-size:11px;color:var(--t3);margin-top:2px;">'
3730        + '<span style="background:var(--d3);padding:2px 6px;border-radius:4px;margin-right:6px;">' + (d.doc_type||'other') + '</span>'
3731        + fmtDate(d.uploaded_at) + ' · ' + fmtSize(d.size_bytes)
3732        + (d.notes ? ' · ' + d.notes.replace(/</g,'&lt;') : '')
3733        + '</div>'
3734        + '</div>'
3735        + '<button onclick="vendorDocs_open(\'' + d.id + '\')" style="background:var(--d3);border:1px solid var(--b);border-radius:6px;padding:6px 10px;font-size:12px;color:var(--t);cursor:pointer;">Open</button>'
3736        + '<button onclick="vendorDocs_delete(\'' + d.id + '\',\'' + vend
3736orId + '\')" style="background:rgba(198,40,40,.08);border:1px solid rgba(198,40,40,.2);border-radius:6px;padding:6px 10px;font-size:12px;color:#c62828;cursor:pointer;">Delete</button>'
3737        + '</div>'
3738      )).join('');
3739    };
3740
3741    async function injectDocsSection(){
3742      const modal = document.getElementById('vendor-modal-overlay');
3743      if (!modal) return;
3744      if (modal.querySelector('#vdoc-section')) return;
3745      const saveBtn = modal.querySelector('button[onclick^="saveVendor"]');
3746      const buttonRow = saveBtn ? saveBtn.parentElement : null;
3747      if (!buttonRow) return;
3748
3749      // FIX 40: insert a placeholder section IMMEDIATELY (synchronously) so the
3750      // MutationObserver's next tick sees #vdoc-section and skips re-running
3751      // while we go async to look up the vendor id from the DB.
3752      const section = document.createElement('div');
3753      section.id = 'vdoc-section';
3754      section.style.cssText = 'margin-top:20px;padding-top:16px;border-top:1px solid var(--b);';
3755      section.innerHTML = '<div style="font-weight:600;font-size:13px;color:var(--t);margin-bottom:8px;">Documents</div>'
3756        + '<div style="color:var(--t3);font-size:12px;padding:10px;">Loading documents…</div>';
3757      buttonRow.parentNode.insertBefore(section, buttonRow);
3758
3759      // Determine vendor context: parse editIdx from save button onclick
3760      const oc = saveBtn.getAttribute('onclick') || '';
3761      const m = oc.match(/saveVendor\((\d+|null)\)/);
3762      const editIdx = m ? (m[1] === 'null' ? null : parseInt(m[1],10)) : null;
3763      const vendor = (editIdx !== null && typeof VENDORS !== 'undefined') ? VENDORS[editIdx] : null;
3764      let vendorId = vendor && vendor.id ? vendor.id : null;
3765
3766      // FIX 40: If the in-memory VENDORS entry has no id (vendors that exist in
3767      // the hardcoded src/17-vendors.js list but don't have a corresponding
3768      // public.vendors row matched by name), fall back to a name-based DB
3769      // lookup. Without this, the placeholder said "Save this vendor first…"
3770      // and the rep could never upload — even though the vendor existed in
3771      // the DB and just wasn't merged into VENDORS by name. Stamps the
3772      // resolved id back onto the in-memory entry so subsequent opens are
3773      // immediate.
3774      if (!vendorId && vendor && vendor.name) {
3775        try {
3776          const { data, error } = await sb.from('vendors').select('id').eq('name', vendor.name).maybeSingle();
3777          if (!error && data && data.id) {
3778            vendor.id = data.id;
3779            vendor._fromDb = true;
3780            vendorId = data.id;
3781          }
3782        } catch(_) { /* swallow — falls through to upsert below */ }
3783      }
3784
3785      // FIX 42: If the vendor STILL has no id after the name lookup, that
3786      // means it lives only in the hardcoded src/17-vendors.js seed list
3787      // and has never been written to public.vendors. The previous flow
3788      // forced reps to click Save Changes first, close the modal, then
3789      // reopen — a 2-round-trip workflow just to attach a PDF. Instead,
3790      // upsert a row right now using the data already in memory (same
3791      // fields saveVendor() persists) so the upload UI appears on first
3792      // open. onConflict:'name' makes this idempotent across reps.
3793      if (!vendorId && vendor && vendor.name) {
3794        try {
3795          const dbEntry = {
3796            name: vendor.name,
3797            address: vendor.address || '',
3798            account_number: vendor.acctNum || '',
3799            phone: vendor.phone || '',
3800            freight: vendor.freight || '',
3801            products: Array.isArray(vendor.products) ? vendor.products : [],
3802            rep_name: (vendor.reps && vendor.reps[0] && vendor.reps[0].name) || '',
3803            rep_email: (vendor.reps && vendor.reps[0] && vendor.reps[0].email) || '',
3804            rep_phone: (vendor.reps && vendor.reps[0] && vendor.reps[0].phone) || '',
3805            website: vendor.website || '',
3806            notes: vendor.notes || '',
3807            status: 'active'
3808          };
3809          const { data, error } = await sb.from('vendors')
3810            .upsert(dbEntry, { onConflict: 'name' })
3811            .select('id')
3812            .maybeSingle();
3813          if (!error && data && data.id) {
3814            vendor.id = data.id;
3815            vendor._fromDb = true;
3816            vendorId = data.id;
3817          } else if (error) {
3818            console.warn('[patch.js] FIX 42 auto-upsert error:', error.message || error);
3819          }
3820        } catch(_) { /* keep placeholder below if even upsert fails */ }
3821      }
3822
3823      if (!vendorId) {
3824        section.innerHTML =
3825          '<div style="font-weight:600;font-size:13px;color:var(--t);margin-bottom:8px;">Documents</div>'
3826          + '<div style="color:var(--t3);font-size:12px;padding:10px;background:var(--d2);border:1px dashed var(--b);border-radius:6px;">'
3827          + 'Save this vendor first, then reopen to upload documents (freight sheets, price lists, install guides, etc.).'
3828          + '</div>';
3829        return;
3830      }
3831
3832      const typeOpts = DOC_TYPES.map(t => '<option value="'+t+'">'+t+'</option>').join('');
3833      section.innerHTML =
3834        '<div style="font-weight:600;font-size:13px;color:var(--t);margin-bottom:8px;">Documents</div>'
3835        + '<div style="display:flex;gap:6px;align-items:center;margin-bottom:10px;flex-wrap:wrap;">'
3836        + '<input type="file" id="vdoc-file" style="flex:1;min-width:180px;padding:6px;background:var(--d2);border:1px solid var(--b);border-radius:6px;color:var(--t);font-size:12px;">'
3837        + '<select id="vdoc-type" style="padding:6px 8px;background:var(--d2);border:1px solid var(--b);border-radius:6px;color:var(--t);font-size:12px;">' + typeOpts + '</select>'
3838        + '<input id="vdoc-notes" placeholder="notes (optional)" style="flex:1;min-width:140px;padding:6px 8px;background:var(--d2);border:1px solid var(--b);border-radius:6px;color:var(--t);font-size:12px;">'
3839        + '<button id="vdoc-upload-btn" onclick="vendorDocs_upload(\'' + vend
3839orId + '\')" style="background:var(--gold);color:#fff;border:none;border-radius:6px;padding:7px 14px;font-size:12px;font-weight:600;cursor:pointer;">Upload</button>'
3840        + '</div>'
3841        + '<div id="vdoc-list"></div>';
3842
3843      window.vendorDocs_render(vendorId);
3844    }
3845
3846    // Observe modal insertion so docs section appears whenever the vendor modal opens
3847    const mo = new MutationObserver(() => { try { injectDocsSection(); } catch(_){} });
3848    try { mo.observe(document.body, { childList: true, subtree: false }); } catch(_){}
3849
3850    console.log('[patch.js] FIX 21: vendor documents UI wired');
3851  })();
3852
3853  // === FIX 41: 📎 doc-count badges on vendor list cards =====================
3854  // Joey's request: "when u go to vendor u dont see pdf attached - u have to
3855  // click edit to view it". Surfaces the count of attached vendor_documents
3856  // on each vendor card so reps can see which vendors have freight sheets,
3857  // price lists, install guides, etc. without opening every edit modal.
3858  // Wraps renderVendors so badges paint on every list refresh; exposes
3859  // window.refreshVendorDocBadges so upload/delete handlers can trigger a
3860  // repaint without re-rendering the entire list.
3861  (function vendorDocBadges(){
3862    if (typeof window === 'undefined' || typeof sb === 'undefined') return;
3863
3864    async function fetchDocCountsByVendor(){
3865      try {
3866        const { data, error } = await sb.from('vendor_documents')
3867          .select('vendor_id')
3868          .eq('is_archived', false);
3869        if (error || !Array.isArray(data)) return {};
3870        const counts = {};
3871        data.forEach(function(row){
3872          if (!row || !row.vendor_id) return;
3873          counts[row.vendor_id] = (counts[row.vendor_id] || 0) + 1;
3874        });
3875        return counts;
3876      } catch(_) { return {}; }
3877    }
3878
3879    function paintBadges(counts){
3880      const list = document.getElementById('vendor-list');
3881      if (!list || typeof VENDORS === 'undefined' || !Array.isArray(VENDORS)) return;
3882      const cards = list.querySelectorAll(':scope > div');
3883      cards.forEach(function(card){
3884        // Each card has an Edit button with onclick="openAddVendorModal(<idx>)"
3885        const editBtn = card.querySelector('button[onclick^="openAddVendorModal"]');
3886        if (!editBtn) return;
3887        const m = (editBtn.getAttribute('onclick') || '').match(/openAddVendorModal\((\d+)\)/);
3888        if (!m) return;
3889        const idx = parseInt(m[1], 10);
3890        const vendor = VENDORS[idx];
3891        if (!vendor || !vendor.id) return;
3892        const count = counts[vendor.id] || 0;
3893        // Strip any prior badge so re-renders don't stack duplicates
3894        const prior = card.querySelector('.vdoc-count-badge');
3895        if (prior) prior.remove();
3896        if (!count) return;
3897        // Find the vendor name element (first child div with the 16px font-weight 600 style)
3898        const nameDiv = card.querySelector('div[style*="font-size:16px"][style*="font-weight:600"]');
3899        if (!nameDiv) return;
3900        const badge = document.createElement('span');
3901        badge.className = 'vdoc-count-badge';
3902        badge.title = 'Click to view ' + count + ' attached document' + (count === 1 ? '' : 's');
3903        // FIX 42: badge is a clickable shortcut into the vendor's edit modal,
3904        // where the Documents section lists all attached files with Open
3905        // buttons. Calls openAddVendorModal(idx) — the same handler the
3906        // adjacent "Edit" button uses — so behavior is consistent.
3907        badge.style.cssText = 'background:rgba(139,94,60,.15);color:var(--gold);border:1px solid rgba(139,94,60,.4);border-radius:10px;padding:2px 8px;font-size:10px;font-weight:600;margin-left:8px;display:inline-flex;align-items:center;gap:3px;vertical-align:middle;cursor:pointer;';
3908        badge.textContent = '📎 ' + count + ' doc' + (count === 1 ? '' : 's');
3909        badge.addEventListener('click', function(ev){
3910          ev.stopPropagation();
3911          ev.preventDefault();
3912          if (typeof window.openAddVendorModal === 'function') {
3913            window.openAddVendorModal(idx);
3914          }
3915        });
3916        nameDiv.appendChild(badge);
3917      });
3918    }
3919
3920    async function refresh(){
3921      const counts = await fetchDocCountsByVendor();
3922      paintBadges(counts);
3923    }
3924
3925    // Expose so vendorDocs_upload / vendorDocs_delete (or anything else) can
3926    // trigger a repaint without re-rendering the whole vendor list.
3927    window.refreshVendorDocBadges = refresh;
3928
3929    // Wrap renderVendors. FIX 20 already wrapped it once; we layer another
3930    // wrapper on top — wrapping is composable.
3931    if (typeof window.renderVendors === 'function') {
3932      const _origRV = window.renderVendors;
3933      window.renderVendors = function(){
3934        const r = _origRV.apply(this, arguments);
3935        // The original may be sync OR a promise; either way, schedule paint
3936        // after the render flushes, plus a second paint to catch slow DB
3937        // merges from FIX 20.
3938        setTimeout(refresh, 250);
3939        setTimeout(refresh, 1200);
3940        return r;
3941      };
3942    }
3943
3944    console.log('[patch.js] FIX 41: vendor doc-count badges wired');
3945  })();
3946
3947  // === FIX 55: Installer documents upload UI (mirrors vendor docs FIX 21/40/42)
3948  // Adds a Documents section to the installer edit modal. Uses Supabase
3949  // storage bucket "installer-docs" and table public.installer_documents.
3950  // Joey wanted to attach driver license scans, COI, photos, etc. to each
3951  // installer record. Same upload + open + delete + auto-upsert pattern as
3952  // the vendor docs flow, ported.
3953  (function installerDocs(){
3954    if (typeof window === 'undefined' || typeof sb === 'undefined') return;
3955    const BUCKET = 'installer-docs';
3956    const DOC_TYPES = ['drivers-license','insurance-coi','w9','contract','photo','license-cert','other'];
3957
3958    async function listInstallerDocs(installerId){
3959      const { data, error } = await sb.from('installer_documents')
3960        .select('*').eq('installer_id', installerId).eq('is_archived', false).order('uploaded_at', { ascending: false });
3961      if (error) { console.error('[patch.js] FIX 55 list err:', error); return []; }
3962      return data || [];
3963    }
3964
3965    async function uploadInstallerDoc(installerId, file, docType, notes){
3966      const safe = file.name.replace(/[^a-zA-Z0-9._-]/g, '_');
3967      const uuid = (crypto && crypto.randomUUID) ? crypto.randomUUID() : (Date.now()+'-'+Math.random().toString(36).slice(2));
3968      const path = installerId + '/' + uuid + '-' + safe;
3969      const up = await sb.storage.from(BUCKET).upload(path, file, { contentType: file.type || undefined, upsert: false });
3970      if (up.error) throw up.error;
3971      let userId = null;
3972      try { const { data: ud } = await sb.auth.getUser(); userId = ud && ud.user ? ud.user.id : null; } catch(_){}
3973      const row = {
3974        installer_id: installerId, file_name: file.name, file_path: path,
3975        doc_type: docType || 'other', mime_type: file.type || null,
3976        size_bytes: file.size || null, notes: notes || null, uploaded_by: userId
3977      };
3978      const ins = await sb.from('installer_documents').insert(row).select('*').maybeSingle();
3979      if (ins.error) {
3980        try { await sb.storage.from(BUCKET).remove([path]); } catch(_){}
3981        throw ins.error;
3982      }
3983      return ins.data;
3984    }
3985
3986    async function signedUrlFor(path, seconds){
3987      const { data, error } = await sb.storage.from(BUCKET).createSignedUrl(path, seconds || 3600);
3988      if (error) throw error;
3989      return data.signedUrl;
3990    }
3991
3992    window.installerDocs_open = async function(docId){
3993      // FIX 38 pattern: synchronous popup BEFORE the awaits so the user-gesture
3994      // flag is still valid when window.open fires; then redirect to signed URL.
3995      const newTab = window.open('about:blank', '_blank');
3996      try {
3997        const { data: d, error } = await sb.from('installer_documents').select('file_path').eq('id', docId).maybeSingle();
3998        if (error || !d) throw error || new Error('Not found');
3999        const url = await signedUrlFor(d.file_path, 3600);
4000        if (newTab && !newTab.closed) {
4001          newTab.location.href = url;
4002        } else {
4003          try { await navigator.clipboard.writeText(url); } catch(_){}
4004          alert('Your browser blocked the popup. The signed link has been copied to your clipboard — paste it into a new tab to view the document.');
4005        }
4006      } catch(e){
4007        if (newTab && !newTab.closed) { try { newTab.close(); } catch(_){} }
4008        alert('Could not open document: ' + (e.message||e));
4009      }
4010    };
4011
4012    window.installerDocs_delete = async function(docId, installerId){
4013      if (!confirm('Delete this document?')) return;
4014      try {
4015        const { data: d } = await sb.from('installer_documents').select('file_path').eq('id', docId).maybeSingle();
4016        if (d && d.file_path) { try { await sb.storage.from(BUCKET).remove([d.file_path]); } catch(_){} }
4017        const { error } = await sb.from('installer_documents').delete().eq('id', docId);
4018        if (error) throw error;
4019        await window.installerDocs_render(installerId);
4020        if (typeof window.refreshInstallerDocBadges === 'function') window.refreshInstallerDocBadges();
4021      } catch(e){ alert('Could not delete: ' + (e.message||e)); }
4022    };
4023
4024    window.installerDocs_upload = async function(installerId){
4025      const fileInput = document.getElementById('idoc-file');
4026      const typeInput = document.getElementById('idoc-type');
4027      const notesInput = document.getElementById('idoc-notes');
4028      if (!fileInput || !fileInput.files || !fileInput.files[0]) { alert('Pick a file first'); return; }
4029      const file = fileInput.files[0];
4030      if (file.size > 26214400) { alert('File too big (25MB max)'); return; }
4031      const btn = document.getElementById('idoc-upload-btn');
4032      if (btn) { btn.disabled = true; btn.textContent = 'Uploading…'; }
4033      try {
4034        await uploadInstallerDoc(installerId, file, typeInput ? typeInput.value : 'other', notesInput ? notesInput.value.trim() : '');
4035        if (typeof toast === 'function') toast('Document uploaded ✓', 'ok');
4036        fileInput.value = ''; if (notesInput) notesInput.value = '';
4037        await window.installerDocs_render(installerId);
4038        if (typeof window.refreshInstallerDocBadges === 'function') window.refreshInstallerDocBadges();
4039      } catch(e) {
4040        alert('Upload failed: ' + (e.message || e));
4041      } finally {
4042        if (btn) { btn.disabled = false; btn.textContent = 'Upload'; }
4043      }
4044    };
4045
4046    window.installerDocs_render = async function(installerId){
4047      const host = document.getElementById('idoc-list');
4048      if (!host) return;
4049      host.innerHTML = '<div style="color:var(--t3);font-size:12px;padding:8px;">Loading…</div>';
4050      const docs = await listInstallerDocs(installerId);
4051      if (!docs.length) { host.innerHTML = '<div style="color:var(--t3);font-size:12px;padding:8px;">No documents uploaded yet.</div>'; return; }
4052      function fmtSize(b){ if(!b) return ''; if(b<1024) return b+' B'; if(b<1048576) return (b/1024).toFixed(1)+' KB'; return (b/1048576).toFixed(1)+' MB'; }
4053      function fmtDate(s){ try { return new Date(s).toLocaleDateString(); } catch(_){ return ''; } }
4054      host.innerHTML = docs.map(d => (
4055        '<div style="display:flex;align-items:center;gap:10px;padding:8px 10px;border:1px solid var(--b);border-radius:6px;margin-bottom:6px;background:var(--d2);">'
4056        + '<div style="flex:1;min-width:0;">'
4057        + '<div style="font-size:13px;font-weight:600;color:var(--t);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + (d.file_name||'(unnamed)') + '</div>'
4058        + '<div style="font-size:11px;color:var(--t3);margin-top:2px;">'
4059        + '<span style="background:var(--d3);padding:2px 6px;border-radius:4px;margin-right:6px;">' + (d.doc_type||'other') + '</span>'
4060        + fmtDate(d.uploaded_at) + ' · ' + fmtSize(d.size_bytes)
4061        + (d.notes ? ' · ' + d.notes.replace(/</g,'&lt;') : '')
4062        + '</div>'
4063        + '</div>'
4064        + '<button onclick="installerDocs_open(\'' + d.id + '\')" style="background:var(--d3);border:1px solid var(--b);border-radius:6px;padding:6px 10px;font-size:12px;color:var(--t);cursor:pointer;">Open</button>'
4065        + '<button onclick="installerDocs_delete(\'' + d.id + '\',\'' + installerId + '\')" style="background:rgba(198,40,40,.08);border:1px solid rgba(198,40,40,.2);border-radius:6px;padding:6px 10px;font-size:12px;color:#c62828;cursor:pointer;">Delete</button>'
4066        + '</div>'
4067      )).join('');
4068    };
4069
4070    async function injectDocsSection(){
4071      const modal = document.getElementById('installer-modal-overlay');
4072      if (!modal) return;
4073      if (modal.querySelector('#idoc-section')) return;
4074      const saveBtn = modal.querySelector('button[onclick^="saveInstaller"]');
4075      const buttonRow = saveBtn ? saveBtn.parentElement : null;
4076      if (!buttonRow) return;
4077
4078      // Synchronous placeholder so the MutationObserver doesn't double-inject.
4079      const section = document.createElement('div');
4080      section.id = 'idoc-section';
4081      section.style.cssText = 'margin-top:20px;padding-top:16px;border-top:1px solid var(--b);';
4082      section.innerHTML = '<div style="font-weight:600;font-size:13px;color:var(--t);margin-bottom:8px;">Documents</div>'
4083        + '<div style="color:var(--t3);font-size:12px;padding:10px;">Loading documents…</div>';
4084      buttonRow.parentNode.insertBefore(section, buttonRow);
4085
4086      // Resolve installer id from saveInstaller(idx) → INSTALLERS[idx]._id.
4087      const oc = saveBtn.getAttribute('onclick') || '';
4088      const m = oc.match(/saveInstaller\((\d+|null)\)/);
4089      const editIdx = m ? (m[1] === 'null' ? null : parseInt(m[1],10)) : null;
4090      const inst = (editIdx !== null && typeof INSTALLERS !== 'undefined') ? INSTALLERS[editIdx] : null;
4091      const installerId = inst && inst._id ? inst._id : null;
4092
4093      if (!installerId) {
4094        section.innerHTML =
4095          '<div style="font-weight:600;font-size:13px;color:var(--t);margin-bottom:8px;">Documents</div>'
4096          + '<div style="color:var(--t3);font-size:12px;padding:10px;background:var(--d2);border:1px dashed var(--b);border-radius:6px;">'
4097          + 'Save this installer first, then reopen to upload documents (driver license, COI, photos, etc.).'
4098          + '</div>';
4099        return;
4100      }
4101
4102      const typeOpts = DOC_TYPES.map(t => '<option value="'+t+'">'+t+'</option>').join('');
4103      section.innerHTML =
4104        '<div style="font-weight:600;font-size:13px;color:var(--t);margin-bottom:8px;">Documents</div>'
4105        + '<div style="display:flex;gap:6px;align-items:center;margin-bottom:10px;flex-wrap:wrap;">'
4106        + '<input type="file" id="idoc-file" style="flex:1;min-width:180px;padding:6px;background:var(--d2);border:1px solid var(--b);border-radius:6px;color:var(--t);font-size:12px;">'
4107        + '<select id="idoc-type" style="padding:6px 8px;background:var(--d2);border:1px solid var(--b);border-radius:6px;color:var(--t);font-size:12px;">' + typeOpts + '</select>'
4108        + '<input id="idoc-notes" placeholder="notes (optional)" style="flex:1;min-width:140px;padding:6px 8px;background:var(--d2);border:1px solid var(--b);border-radius:6px;color:var(--t);font-size:12px;">'
4109        + '<button id="idoc-upload-btn" onclick="installerDocs_upload(\'' + installerId + '\')" style="background:var(--gold);color:#fff;border:none;border-radius:6px;padding:7px 14px;font-size:12px;font-weight:600;cursor:pointer;">Upload</button>'
4110        + '</div>'
4111        + '<div id="idoc-list"></div>';
4112      window.installerDocs_render(installerId);
4113    }
4114
4115    const mo = new MutationObserver(() => { try { injectDocsSection(); } catch(_){} });
4116    try { mo.observe(document.body, { childList: true, subtree: false }); } catch(_){}
4117
4118    console.log('[patch.js] FIX 55: installer documents UI wired');
4119  })();
4120
4121  // === FIX 56: 📎 doc-count badges on installer list cards (mirrors FIX 41).
4122  (function installerDocBadges(){
4123    if (typeof window === 'undefined' || typeof sb === 'undefined') return;
4124
4125    async function fetchDocCountsByInstaller(){
4126      try {
4127        const { data, error } = await sb.from('installer_documents')
4128          .select('installer_id')
4129          .eq('is_archived', false);
4130        if (error || !Array.isArray(data)) return {};
4131        const counts = {};
4132        data.forEach(function(row){
4133          if (!row || !row.installer_id) return;
4134          counts[row.installer_id] = (counts[row.installer_id] || 0) + 1;
4135        });
4136        return counts;
4137      } catch(_) { return {}; }
4138    }
4139
4140    function paintBadges(counts){
4141      const list = document.getElementById('installer-list');
4142      if (!list || typeof INSTALLERS === 'undefined' || !Array.isArray(INSTALLERS)) return;
4143      const cards = list.querySelectorAll(':scope > div');
4144      cards.forEach(function(card){
4145        const editBtn = card.querySelector('button[onclick^="openAddInstallerModal"]');
4146        if (!editBtn) return;
4147        const m = (editBtn.getAttribute('onclick') || '').match(/openAddInstallerModal\((\d+)\)/);
4148        if (!m) return;
4149        const idx = parseInt(m[1], 10);
4150        const inst = INSTALLERS[idx];
4151        if (!inst || !inst._id) return;
4152        const count = counts[inst._id] || 0;
4153        const prior = card.querySelector('.idoc-count-badge');
4154        if (prior) prior.remove();
4155        if (!count) return;
4156        const nameDiv = card.querySelector('div[style*="font-size:16px"][style*="font-weight:600"], div[style*="font-size: 16px"][style*="font-weight: 600"]');
4157        if (!nameDiv) return;
4158        const badge = document.createElement('span');
4159        badge.className = 'idoc-count-badge';
4160        badge.title = 'Click to view ' + count + ' attached document' + (count === 1 ? '' : 's');
4161        badge.style.cssText = 'background:rgba(139,94,60,.15);color:var(--gold);border:1px solid rgba(139,94,60,.4);border-radius:10px;padding:2px 8px;font-size:10px;font-weight:600;margin-left:8px;display:inline-flex;align-items:center;gap:3px;vertical-align:middle;cursor:pointer;';
4162        badge.textContent = '📎 ' + count + ' doc' + (count === 1 ? '' : 's');
4163        badge.addEventListener('click', function(ev){
4164          ev.stopPropagation();
4165          ev.preventDefault();
4166          if (typeof window.openAddInstallerModal === 'function') {
4167            window.openAddInstallerModal(idx);
4168          }
4169        });
4170        nameDiv.appendChild(badge);
4171      });
4172    }
4173
4174    async function refresh(){
4175      const counts = await fetchDocCountsByInstaller();
4176      paintBadges(counts);
4177    }
4178
4179    window.refreshInstallerDocBadges = refresh;
4180
4181    if (typeof window.renderInstallers === 'function') {
4182      const _origRI = window.renderInstallers;
4183      window.renderInstallers = function(){
4184        const r = _origRI.apply(this, arguments);
4185        setTimeout(refresh, 250);
4186        setTimeout(refresh, 1200);
4187        return r;
4188      };
4189    }
4190
4191    console.log('[patch.js] FIX 56: installer doc-count badges wired');
4192  })();
4193
4194  // === FIX 22: Hide retired vendors from the UI ============================
4195  // Joey's request: "delete vendor DuChateau".
4196  // DuChateau lives in the hardcoded VENDORS seed array (src/17-vendors.js)
4197  // and not in the vendors DB table, so DB-level delete isn't possible.
4198  // This fix splices the named vendor(s) out of the in-memory VENDORS array
4199  // on page load (and again after a short delay to cover late DB merge from
4200  // FIX 20). Add future retired vendor names to HIDDEN_VENDOR_NAMES.
4201  (function hideRetiredVendors(){
4202    const HIDDEN_VENDOR_NAMES = ['DuChateau'];
4203    function purge(){
4204      try {
4205        if (typeof VENDORS === 'undefined' || !Array.isArray(VENDORS)) return 0;
4206        const hidden = HIDDEN_VENDOR_NAMES.map(h => h.toLowerCase());
4207        let removed = 0;
4208        for (let i = VENDORS.length - 1; i >= 0; i--) {
4209          const n = (VENDORS[i] && VENDORS[i].name || '').toLowerCase();
4210          if (n && hidden.includes(n)) { VENDORS.splice(i, 1); removed++; }
4211        }
4212        return removed;
4213      } catch(_) { return 0; }
4214    }
4215    // Immediate + delayed retry (in case VENDORS loads after patch.js)
4216    purge();
4217    setTimeout(purge, 200);
4218    setTimeout(purge, 1500);
4219    // Also purge after any renderVendors call so DB merges can't reintroduce
4220    if (typeof window !== 'undefined' && typeof window.renderVendors === 'function') {
4221      const _orig = window.renderVendors;
4222      window.renderVendors = async function(){
4223        const ret = await _orig.apply(this, arguments);
4224        purge();
4225        return ret;
4226      };
4227    }
4228    console.log('[patch.js] FIX 22: hidden vendor filter applied (' + HIDDEN_VENDOR_NAMES.join(', ') + ')');
4229  })();
4230
4231  // === FIX 23 / FIX 71: Custom Places Autocomplete on address inputs =======
4232  // Custom REST-based autocomplete (FIX 71). Rebuilt from scratch — the old
4233  // Google Maps JS SDK approach (FIX 23 → FIX 69 → FIX 70) was unreliable on
4234  // this Cloud project: the new <gmp-place-autocomplete> web component's
4235  // gmp-select event fired inconsistently, the legacy ctor left inputs unable
4236  // to take keystrokes, and load failures bricked the field with no recovery.
4237  //
4238  // FIX 71 hits the Places REST API directly (no SDK) and renders our own
4239  // dropdown. Cleaner code, fewer moving parts, predictable behaviour:
4240  //   1. user types → debounced fetch to places:autocomplete
4241  //   2. predictions render in a portal-styled dropdown anchored to the input
4242  //   3. click → fetch place details → fill street/city/state/zip
4243  //   4. keyboard nav (↑↓ Enter Esc) + click-outside-to-close
4244  //
4245  // Targeted inputs:
4246  //   #lf-addr          (Lead form — also fills #lf-ci, #lf-st, #lf-zip)
4247  //   #quote-job-address #sf-address #vnd-f-address #dp-stop-addr
4248  //   #np-address #cc-address (single-field — get full formatted address)
4249  (function placesAutocomplete(){
4250    // FIX 71: REST-based Places autocomplete. Bypasses the Google Maps JS SDK
4251    // entirely — direct HTTPS calls to places.googleapis.com with our own
4252    // dropdown. No SDK = no flaky <gmp-place-autocomplete> events, no
4253    // deprecated legacy ctor side-effects, predictable across all browsers.
4254
4255    // HTTP-referrer-restricted to portal.jamailhardwoods.com — safe to ship.
4256    const API_KEY = 'AIzaSyC0PXuiOAedcvwuM7HRr3bmZ29pgNz-AJ4';
4257
4258    // Inputs to attach to. Lead form has separate city/state/zip siblings;
4259    // others are single-field (get the full formatted address).
4260    const TARGETS = [
4261      { sel:'#lf-addr',           city:'#lf-ci', state:'#lf-st', zip:'#lf-zip' },
4262      { sel:'#quote-job-address' },
4263      { sel:'#sf-address' },
4264      { sel:'#vnd-f-address' },
4265      { sel:'#dp-stop-addr' },
4266      { sel:'#np-address' },
4267      { sel:'#cc-address' },
4268    ];
4269
4270    // ── Inject dropdown styles once ────────────────────────────────────
4271    if (!document.getElementById('jh-pac-styles')) {
4272      const css = document.createElement('style');
4273      css.id = 'jh-pac-styles';
4274      css.textContent = `
4275        .jh-pac-dropdown {
4276          position: fixed;
4277          z-index: 999999;
4278          background: #ffffff;
4279          border: 1px solid #E8E6E3;
4280          border-radius: 8px;
4281          box-shadow: 0 8px 24px rgba(0,0,0,0.18);
4282          font-family: Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
4283          font-size: 13px;
4284          color: #2a2a2a;
4285          max-height: 320px;
4286          overflow-y: auto;
4287          padding: 4px 0;
4288        }
4289        .jh-pac-item {
4290          padding: 9px 14px;
4291          cursor: pointer;
4292          line-height: 1.35;
4293          border-left: 3px solid transparent;
4294        }
4295        .jh-pac-item.active,
4296        .jh-pac-item:hover {
4297          background: #faf6ee;
4298          border-left-color: #B8860B;
4299        }
4300        .jh-pac-item .jh-pac-main { color: #2a2a2a; font-weight: 500; }
4301        .jh-pac-item .jh-pac-sec  { color: #888; font-size: 11.5px;
4301 margin-top: 1px; }
4302        .jh-pac-empty {
4303          padding: 12px 14px;
4304          color: #888;
4305          font-size: 12.5px;
4306          text-align: center;
4307        }
4308        .jh-pac-attribution {
4309          padding: 6px 14px;
4310          color: #aaa;
4311          font-size: 10px;
4312          letter-spacing: 0.5px;
4313          text-transform: uppercase;
4314          border-top: 1px solid #f0eee9;
4315          text-align: right;
4316        }
4317      `;
4318      (document.head || document.documentElement).appendChild(css);
4319    }
4320
4321    // ── Single shared dropdown element ─────────────────────────────────
4322    let dropdown = null;
4323    let activeInput = null;
4324    let activeTarget = null;     // { sel, city?, state?, zip? }
4325    let predictions = [];        // array of { placeId, mainText, secondaryText }
4326    let activeIdx = -1;
4327    let sessionToken = null;     // refreshes per autocomplete session for billing
4328    let abortCtl = null;
4329
4330    function ensureDropdown(){
4331      if (dropdown) return dropdown;
4332      dropdown = document.createElement('div');
4333      dropdown.className = 'jh-pac-dropdown';
4334      dropdown.style.display = 'none';
4335      document.body.appendChild(dropdown);
4336      // Click on a suggestion
4337      dropdown.addEventListener('mousedown', (e) => {
4338        const item = e.target.closest('.jh-pac-item');
4339        if (!item) return;
4340        e.preventDefault();
4341        const idx = parseInt(item.dataset.idx, 10);
4342        if (!isNaN(idx)) selectPrediction(idx);
4343      });
4344      return dropdown;
4345    }
4346
4347    function newSessionToken(){
4348      // RFC4122-ish token. Used by Places API for session-based billing.
4349      sessionToken = 'jh-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
4350      return sessionToken;
4351    }
4352
4353    function positionDropdown(input){
4354      if (!dropdown) return;
4355      const r = input.getBoundingClientRect();
4356      dropdown.style.left  = r.left + 'px';
4357      dropdown.style.top   = (r.bottom + 2) + 'px';
4358      dropdown.style.width = r.width + 'px';
4359    }
4360
4361    function hideDropdown(){
4362      if (dropdown) dropdown.style.display = 'none';
4363      activeInput = null;
4364      activeTarget = null;
4365      predictions = [];
4366      activeIdx = -1;
4367      if (abortCtl) { try { abortCtl.abort(); } catch(_){} abortCtl = null; }
4368    }
4369
4370    function renderDropdown(){
4371      const dd = ensureDropdown();
4372      if (!predictions.length) {
4373        dd.innerHTML = '<div class="jh-pac-empty">No matching addresses</div>'
4374                     + '<div class="jh-pac-attribution">Powered by Google</div>';
4375      } else {
4376        dd.innerHTML = predictions.map((p, i) => {
4377          const cls = i === activeIdx ? 'jh-pac-item active' : 'jh-pac-item';
4378          const main = (p.mainText || '').replace(/[<>]/g, '');
4379          const sec  = (p.secondaryText || '').replace(/[<>]/g, '');
4380          return '<div class="' + cls + '" data-idx="' + i + '">'
4381               + '<div class="jh-pac-main">' + main + '</div>'
4382               + (sec ? '<div class="jh-pac-sec">' + sec + '</div>' : '')
4383               + '</div>';
4384        }).join('') + '<div class="jh-pac-attribution">Powered by Google</div>';
4385      }
4386      dd.style.display = 'block';
4387      if (activeInput) positionDropdown(activeInput);
4388    }
4389
4390    // ── Places REST calls ──────────────────────────────────────────────
4391    async function fetchPredictions(query){
4392      if (!query || query.length < 3) { predictions = []; renderDropdown(); return; }
4393      if (!sessionToken) newSessionToken();
4394      if (abortCtl) { try { abortCtl.abort(); } catch(_){} }
4395      abortCtl = new AbortController();
4396      try {
4397        const res = await fetch('https://places.googleapis.com/v1/places:autocomplete', {
4398          method: 'POST',
4399          signal: abortCtl.signal,
4400          headers: {
4401            'Content-Type': 'application/json',
4402            'X-Goog-Api-Key': API_KEY,
4403            'X-Goog-FieldMask': 'suggestions.placePrediction.placeId,suggestions.placePrediction.text,suggestions.placePrediction.structuredFormat',
4404          },
4405          body: JSON.stringify({
4406            input: query,
4407            includedRegionCodes: ['us'],
4408            sessionToken: sessionToken,
4409          }),
4410        });
4411        if (!res.ok) {
4412          const txt = await res.text();
4413          console.warn('[patch.js] FIX 71 autocomplete: API ' + res.status, txt);
4414          // FIX 72: surface the actual cause to the rep instead of silent
4415          // dropdown failure. 403 almost always = referrer allowlist miss.
4416          if (res.status === 403 && typeof toast === 'function' && !window._jhPacToast403) {
4417            window._jhPacToast403 = true;
4418            toast('Address suggestions blocked by Google API key restriction. Loading correct domain... (refresh if it persists)', 'err');
4419          }
4420          predictions = [];
4421          renderDropdown();
4422          return;
4423        }
4424        const json = await res.json();
4425        predictions = (json.suggestions || [])
4426          .map(s => s.placePrediction)
4427          .filter(Boolean)
4428          .map(p => ({
4429            placeId: p.placeId,
4430            mainText: p.structuredFormat?.mainText?.text || p.text?.text || '',
4431            secondaryText: p.structuredFormat?.secondaryText?.text || '',
4432          }));
4433        activeIdx = predictions.length > 0 ? 0 : -1;
4434        renderDropdown();
4435      } catch (e) {
4436        if (e.name !== 'AbortError') console.warn('[patch.js] FIX 71 autocomplete fetch failed:', e.message);
4437      }
4438    }
4439
4440    async function fetchPlaceDetails(placeId){
4441      try {
4442        const res = await fetch('https://places.googleapis.com/v1/places/' + encodeURIComponent(placeId)
4443            + '?sessionToken=' + encodeURIComponent(sessionToken || ''), {
4444          headers: {
4445            'X-Goog-Api-Key': API_KEY,
4446            'X-Goog-FieldMask': 'formattedAddress,addressComponents',
4447          },
4448        });
4449        if (!res.ok) {
4450          console.warn('[patch.js] FIX 71 details: API ' + res.status, await res.text());
4451          return null;
4452        }
4453        return await res.json();
4454      } catch (e) {
4455        console.warn('[patch.js] FIX 71 details fetch failed:', e.message);
4456        return null;
4457      } finally {
4458        // session ends after a details call per Google's billing model
4459        sessionToken = null;
4460      }
4461    }
4462
4463    function parseComponents(comps){
4464      const out = { street:'', city:'', state:'', zip:'' };
4465      let sn = '', rt = '';
4466      (comps || []).forEach(c => {
4467        const t = c.types || [];
4468        const longText  = c.longText  || '';
4469        const shortText = c.shortText || '';
4470        if (t.includes('street_number')) sn = longText;
4471        if (t.includes('route')) rt = longText;
4472        if (t.includes('locality')) out.city = longText;
4473        if (!out.city && t.includes('sublocality')) out.city = longText;
4474        if (!out.city && t.includes('postal_town')) out.city = longText;
4475        if (t.includes('administrative_area_level_1')) out.state = shortText;
4476        if (t.includes('postal_code')) out.zip = longText;
4477      });
4478      out.street = [sn, rt].filter(Boolean).join(' ');
4479      return out;
4480    }
4481
4482    function setField(selector, value){
4483      if (!selector) return;
4484      const el = document.querySelector(selector);
4485      if (!el) return;
4486      el.value = value || '';
4487      try { el.dispatchEvent(new Event('input',  { bubbles:true })); } catch(_){}
4488      try { el.dispatchEvent(new Event('change', { bubbles:true })); } catch(_){}
4489    }
4490
4491    async function selectPrediction(idx){
4492      const pred = predictions[idx];
4493      if (!pred || !activeInput) { hideDropdown(); return; }
4494      const target = activeTarget;
4495      const input  = activeInput;
4496      hideDropdown();
4497      const details = await fetchPlaceDetails(pred.placeId);
4498      if (!details) {
4499        // Fallback: paste the prediction text into the input
4500        input.value = [pred.mainText, pred.secondaryText].filter(Boolean).join(', ');
4501        try { input.dispatchEvent(new Event('input',  { bubbles:true })); } catch(_){}
4502        try { input.dispatchEvent(new Event('change', { bubbles:true })); } catch(_){}
4503        return;
4504      }
4505      const parsed = parseComponents(details.addressComponents);
4506      const formatted = details.formattedAddress || '';
4507      const hasSiblings = !!(target && (target.city || target.state || target.zip));
4508      input.value = hasSiblings
4509        ? (parsed.street || formatted || input.value)
4510        : (formatted || parsed.street || input.value);
4511      try { input.dispatchEvent(new Event('input',  { bubbles:true })); } catch(_){}
4512      try { input.dispatchEvent(new Event('change', { bubbles:true })); } catch(_){}
4513      if (hasSiblings) {
4514        setField(target.city,  parsed.city);
4515        setField(target.state, parsed.state);
4516        setField(target.zip,   parsed.zip);
4517      }
4518    }
4519
4520    // ── Debounce helper ─────────────────────────────────────────────────
4521    function debounce(fn, ms){
4522      let t;
4523      return function(){
4524        const args = arguments, ctx = this;
4525        clearTimeout(t);
4526        t = setTimeout(() => fn.apply(ctx, args), ms);
4527      };
4528    }
4529
4530    const onTypeDebounced = debounce((value) => fetchPredictions(value), 220);
4531
4532    // ── Wire up an input ───────────────────────────────────────────────
4533    function attach(input, target){
4534      if (!input || input._jhPacAttached) return;
4535      input._jhPacAttached = true;
4536
4537      input.setAttribute('autocomplete', 'off');
4538
4539      input.addEventListener('focus', () => {
4540        activeInput = input;
4541        activeTarget = target;
4542        if (!sessionToken) newSessionToken();
4543        if (input.value && input.value.length >= 3) {
4544          fetchPredictions(input.value);
4545        }
4546      });
4547
4548      input.addEventListener('input', (e) => {
4549        activeInput = input;
4550        activeTarget = target;
4551        onTypeDebounced(e.target.value);
4552      });
4553
4554      input.addEventListener('keydown', (e) => {
4555        if (!dropdown || dropdown.style.display === 'none' || !predictions.length) return;
4556        if (e.key === 'ArrowDown') {
4557          e.preventDefault();
4558          activeIdx = Math.min(predictions.length - 1, activeIdx + 1);
4559          renderDropdown();
4560        } else if (e.key === 'ArrowUp') {
4561          e.preventDefault();
4562          activeIdx = Math.max(0, activeIdx - 1);
4563          renderDropdown();
4564        } else if (e.key === 'Enter') {
4565          if (activeIdx >= 0) {
4566            e.preventDefault();
4567            selectPrediction(activeIdx);
4568          }
4569        } else if (e.key === 'Escape') {
4570          hideDropdown();
4571        }
4572      });
4573
4574      input.addEventListener('blur', () => {
4575        // Delay so a click on a suggestion lands first
4576        setTimeout(() => {
4577          if (document.activeElement !== input) hideDropdown();
4578        }, 200);
4579      });
4580    }
4581
4582    function attachAll(){
4583      TARGETS.forEach(t => {
4584        document.querySelectorAll(t.sel).forEach(el => attach(el, t));
4585      });
4586    }
4587
4588    // Reposition / hide dropdown on scroll/resize
4589    window.addEventListener('scroll', () => { if (activeInput) positionDropdown(activeInput); }, true);
4590    window.addEventListener('resize', () => { if (activeInput) positionDropdown(activeInput); });
4591
4592    // Click outside the dropdown closes it
4593    document.addEventListener('mousedown', (e) => {
4594      if (!dropdown || dropdown.style.display === 'none') return;
4595      if (e.target === activeInput || dropdown.contains(e.target)) return;
4596      hideDropdown();
4597    });
4598
4599    // Watch DOM for new inputs (modals open / close)
4600    try {
4601      new MutationObserver(() => attachAll()).observe(document.body, { childList:true, subtree:true });
4602    } catch(_) {}
4603
4604    // Initial pass
4605    attachAll();
4606
4607    console.log('[patch.js] FIX 71: REST-based Places autocomplete wired (' + TARGETS.length + ' input selectors)');
4608  })();
4609
4610  // ============================================================================
4611  // FIX 24: Quote Change Requests — badge on list + panel in Quote Builder
4612  //
4613  // Client "Request Changes" from quote-view.html writes to quote_change_requests.
4614  // Before this fix, nothing in the portal surfaced them: rows would sit in the
4615  // table with status='pending' and no rep would ever see them. This fix:
4616  //
4617  //   (a) Fetches pending-count per quote_id and paints a red "📝 N pending"
4618  //       badge on each affected quote row after renderQuotesTab finishes.
4619  //   (b) Monkey-patches renderQuoteBuilder so that after the overlay is drawn,
4620  //       a "Change Requests" section is appended at the bottom using the
4621  //       existing renderChangeRequests() + markChangeRequestReviewed() funcs
4622  //       already defined in src/59-workflow-automation.js.
4623  //   (c) Wraps markChangeRequestReviewed so that after a request is applied
4624  //       or declined, the builder panel + row badges refresh automatically.
4625  // ============================================================================
4626  (function changeRequestsUI(){
4627    if (typeof sb === 'undefined') {
4628      // sb not ready yet — retry once the app has booted.
4629      document.addEventListener('DOMContentLoaded', function(){ setTimeout(changeRequestsUI, 500); }, { once: true });
4630      return;
4631    }
4632
4633    // ---- helpers ----------------------------------------------------------
4634    async function fetchPendingCountsByQuote(){
4635      try {
4636        const { data, error } = await sb.from('quote_change_requests').select('quote_id,status').eq('status','pending');
4637        if (error || !data) return {};
4638        const counts = {};
4639        data.forEach(function(r){
4640          const id = r.quote_id;
4641          counts[id] = (counts[id] || 0) + 1;
4642        });
4643        return counts;
4644      } catch (_) { return {}; }
4645    }
4646
4647    function paintBadgesOnQuoteRows(counts){
4648      const list = document.getElementById('quotes-list');
4649      if (!list) return;
4650      const rows = list.querySelectorAll('.quote-row');
4651      rows.forEach(function(row){
4652        const qid = parseInt(row.getAttribute('data-id'), 10);
4653        if (!qid) return;
4654        // Remove any prior badge so we don't stack duplicates on re-render
4655        const prior = row.querySelector('.cr-pending-badge');
4656        if (prior) prior.remove();
4657        const n = counts[qid] || 0;
4658        if (!n) return;
4659        const badge = document.createElement('div');
4660        badge.className = 'cr-pending-badge';
4661        badge.title = n + ' pending change request' + (n === 1 ? '' : 's') + ' from client';
4662        badge.style.cssText = 'display:inline-flex;align-items:center;gap:4px;background:#ef4444;color:#fff;font-size:10px;font-weight:700;padding:3px 8px;border-radius:10px;margin-left:6px;flex-shrink:0;white-space:nowrap;';
4663        badge.innerHTML = '📝 ' + n + ' pending';
4664        // Insert inside the first inner flex container (the header row) so it floats near the quote number.
4665        const flex = row.querySelector('div[style*="display:flex"]');
4666        (flex || row).appendChild(badge);
4667      });
4668    }
4669
4670    async function refreshChangeRequestBadges(){
4671      const counts = await fetchPendingCountsByQuote();
4672      paintBadgesOnQuoteRows(counts);
4673    }
4674
4675    // Expose so other code (and manual debugging) can trigger a repaint
4676    window.refreshChangeRequestBadges = refreshChangeRequestBadges;
4677
4678    // ---- (a) Badges on quote list after renderQuotesTab -------------------
4679    if (typeof window.renderQuotesTab === 'function') {
4680      const _origRenderQuotesTab = window.renderQuotesTab;
4681      window.renderQuotesTab = async function(){
4682        const r = await _origRenderQuotesTab.apply(this, arguments);
4683        // loadAndRenderQuotes runs async inside — wait a tick then paint.
4684        setTimeout(refreshChangeRequestBadges, 400);
4685        setTimeout(refreshChangeRequestBadges, 1200); // second paint after slower loads
4686        return r;
4687      };
4688    }
4689    // Also repaint any time a filter pill is clicked inside the quotes tab
4690    document.addEventListener('click', function(ev){
4691      const pill = ev.target && ev.target.closest && ev.target.closest('#tab-quotes .filter-pill');
4692      if (pill) setTimeout(refreshChangeRequestBadges, 400);
4693    }, true);
4694
4695    // ---- (b) Change Requests panel inside Quote Builder -------------------
4696    async function injectChangeRequestsPanel(quoteId){
4697      if (!quoteId) return;
4698      const overlay = document.getElementById('ov-quote-builder');
4699      if (!overlay) return;
4700      // Don't double-inject
4701      let panel = overlay.querySelector('#cr-panel-wrap');
4702      if (!panel) {
4703        panel = document.createElement('div');
4704        panel.id = 'cr-panel-wrap';
4705        panel.style.cssText = 'margin:24px 20px;padding:16px;background:var(--d2);border:1px solid var(--b);border-radius:8px;';
4706        panel.innerHTML = '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">' +
4707          '<div style="color:var(--t);font-weight:700;font-size:14px">📝 Client Change Requests</div>' +
4708          '<button id="cr-panel-refresh" style="background:transparent;border:1px solid var(--b);color:var(--t3);font-size:11px;padding:4px 10px;border-radius:4px;cursor:pointer">↻ Refresh</button>' +
4709          '</div>' +
4710          '<div id="cr-panel-body" style="color:var(--t3);font-size:13px">Loading…</div>';
4711        // Append near the bottom of the overlay's inner content
4712        const inner = overlay.querySelector('div[style*="max-width"]') || overlay;
4713        inner.appendChild(panel);
4714        const refreshBtn = panel.querySelector('#cr-panel-refresh');
4715        if (refreshBtn) refreshBtn.addEventListener('click', function(){ renderPanelBody(quoteId); });
4716      }
4717      await renderPanelBody(quoteId);
4718    }
4719
4720    async function renderPanelBody(quoteId){
4721      const body = document.getElementById('cr-panel-body');
4722      if (!body) return;
4723      body.innerHTML = '<div style="color:var(--t3);font-size:13px">Loading…</div>';
4724      try {
4725        if (typeof window.renderChangeRequests !== 'function') {
4726          body.innerHTML = '<div style="color:#ef4444;font-size:13px">renderChangeRequests() not available in this build.</div>';
4727          return;
4728        }
4729        const html = await window.renderChangeRequests(quoteId);
4730        body.innerHTML = html || '<div style="color:var(--t3);font-size:13px;padding:8px">No change requests</div>';
4731      } catch (e) {
4732        body.innerHTML = '<div style="color:#ef4444;font-size:13px">Failed to load change requests: ' + (e && e.message ? e.message : e) + '</div>';
4733      }
4734    }
4735
4736    if (typeof window.renderQuoteBuilder === 'function') {
4737      const _origRenderQuoteBuilder = window.renderQuoteBuilder;
4738      window.renderQuoteBuilder = async function(existingQuote){
4739        const r = await _origRenderQuoteBuilder.apply(this, arguments);
4740        const qid = existingQuote && existingQuote.id ? existingQuote.id : null;
4741        if (qid) setTimeout(function(){ injectChangeRequestsPanel(qid); }, 250);
4742        return r;
4743      };
4744    }
4745
4746    // ---- (c) Wrap markChangeRequestReviewed to refresh UI after review ----
4747    if (typeof window.markChangeRequestReviewed === 'function') {
4748      const _origMark = window.markChangeRequestReviewed;
4749      window.markChangeRequestReviewed = async function(requestId, status){
4750        const r = await _origMark.apply(this, arguments);
4751        // Re-render the panel in the open Quote Builder (if any)
4752        try {
4753          const overlay = document.getElementById('ov-quote-builder');
4754          if (overlay) {
4755            // Infer quoteId from the quote-number input that renderQuoteBuilder draws
4756            const qNumInput = overlay.querySelector('#quote-number');
4757            const qNum = qNumInput && qNumInput.value;
4758            if (qNum && typeof sb !== 'undefined') {
4759              const { data } = await sb.from('quotes').select('id').eq('quote_number', qNum).limit(1);
4760              const qid = data && data[0] && data[0].id;
4761              if (qid) await renderPanelBody(qid);
4762            }
4763          }
4764        } catch(_){}
4765        // And repaint list badges since pending count dropped
4766        refreshChangeRequestBadges();
4767        return r;
4768      };
4769    }
4770
4771    console.log('[patch.js] FIX 24: Change Requests UI wired (badges + panel + auto-refresh)');
4772  })();
4773
4774})();
4775
4776
4777// === FIX 27: Bucket/gallon pricing for adhesives, finishes, polys ===
4778// Replaces the default per-sqft math for non-sqft-priced products.
4779// For these items: line_total = ceil(sqft / coverage) × unit_price
4780(function bucketPricing() {
4781  var BUCKET_KEYWORDS = ['adhesive','glue','finish','stain','sealer','seal','poly','gallon','bucket','bona','lacquer','primer'];
4782  // (Cowork fix 2026-05-06) Categories that are sold by sqft/carton and must
4783  // NEVER be treated as bucket-priced, even if their name/category text
4784  // happens to contain a substring like "finish" (e.g. "Pre-finished Hardwood").
4785  // Without this guard, hardwood line items had their per-sqft price divided
4786  // by the carton coverage (e.g. $7.25/sf → $0.41/sf displayed) and the badge
4787  // label flickered between "carton" and "gallon" every 600 ms while the
4788  // setInterval poller fought the original render.
4789  var FLOORING_GUARD = /(hardwood|laminate|vinyl|lvp|lvt|plank|tile|carpet|engineered|moulding|molding|stair|tread)/i;
4790  function isBucketProduct(p) {
4791    if (!p) return false;
4792    var cat = (p.category || '').toLowerCase();
4793    var name = (p.product_name || '').toLowerCase();
4794    // Hard exclude any flooring/material category — it's sold by sqft, period.
4795    if (FLOORING_GUARD.test(cat)) return false;
4796    for (var i = 0; i < BUCKET_KEYWORDS.length; i++) {
4797      var k = BUCKET_KEYWORDS[i];
4798      if (cat.indexOf(k) !== -1 || name.indexOf(k) !== -1) return true;
4799    }
4800    return false;
4801  }
4802  function findCatalogMatch(sku, desc) {
4803    if (typeof _productCatalogCache === 'undefined' || !_productCatalogCache) return null;
4804    if (sku) {
4805      var bySku = _productCatalogCache.find(function(p){ return p.sku && p.sku === sku; });
4806      if (bySku) return bySku;
4807    }
4808    if (desc) {
4809      var byName = _productCatalogCache.find(function(p){ return p.product_name === desc; });
4810      if (byName) return byName;
4811    }
4812    return null;
4813  }
4814  function unitLabel(p) {
4815    var cat = (p.category || '').toLowerCase();
4816    var name = (p.product_name || '').toLowerCase();
4817    if (cat.indexOf('adhesive') !== -1 || cat.indexOf('glue') !== -1 || name.indexOf('bona') !== -1 || name.indexOf('adhesive') !== -1 || name.indexOf('glue') !== -1) return 'bucket';
4818    if (cat.indexOf('finish') !== -1 || cat.indexOf('seal') !== -1 || cat.indexOf('stain') !== -1 || cat.indexOf('poly') !== -1 || name.indexOf('gallon') !== -1) return 'gallon';
4819    return 'unit';
4820  }
4821  function tick() {
4822    if (typeof _productCatalogCache === 'undefined' || !_productCatalogCache) return;
4823    var items = document.querySelectorAll('.quote-item');
4824    if (!items || items.length === 0) return;
4825    var anyChanged = false;
4826    items.forEach(function(itemDiv) {
4827      var skuEl   = itemDiv.querySelector('.item-sku');
4828      var descEl  = itemDiv.querySelector('.item-description');
4829      var sqftEl  = itemDiv.querySelector('.item-sqft');
4830      var totalEl = itemDiv.querySelector('.item-line-total');
4831      var sellSfEl = itemDiv.querySelector('.item-sell-sf');
4832      var costSfEl = itemDiv.querySelector('.item-cost-sf');
4833      var matSellEl = itemDiv.querySelector('.item-material-sell');
4834      var matCostEl = itemDiv.querySelector('.item-material-cost');
4835      if (!sqftEl || !totalEl) return;
4836      var sku = (skuEl && skuEl.value) || '';
4837      var desc = (descEl && descEl.value) || '';
4838      var match = findCatalogMatch(sku, desc);
4839      if (!match || !isBucketProduct(match)) return;
4840      var coverage = parseFloat(match.coverage) || 0;
4841      var unitCost = parseFloat(match.unit_cost) || 0;
4842      var unitPrice = parseFloat(match.selling_price) || 0;
4843      var sqft = parseFloat(sqftEl.value) || 0;
4844      if (coverage <= 0 || sqft <= 0 || unitPrice <= 0) return;
4845      var units = Math.ceil(sqft / coverage);
4846      var newSell = +(units * unitPrice).toFixed(2);
4847      var newCost = +(units * unitCost).toFixed(2);
4848      // Write back
4849      if (matSellEl && Math.abs((parseFloat(matSellEl.value) || 0) - newSell) > 0.005) { matSellEl.value = newSell.toFixed(2); anyChanged = true; }
4850      if (matCostEl && Math.abs((parseFloat(matCostEl.value) || 0) - newCost) > 0.005) { matCostEl.value = newCost.toFixed(2); anyChanged = true; }
4851      if (totalEl  && Math.abs((parseFloat(totalEl.value)  || 0) - newSell) > 0.005) { totalEl.value  = newSell.toFixed(2);  anyChanged = true; }
4852      // Per-sqft equivalents (so downstream reporting stays consistent)
4853      if (sellSfEl) sellSfEl.value = (newSell / sqft).toFixed(4);
4854      if (costSfEl) costSfEl.value = (newCost / sqft).toFixed(4);
4855      // Mark line + relabel badge from "carton" → "bucket"/"gallon"
4856      itemDiv.setAttribute('data-bucket-priced', 'true');
4857      itemDiv.setAttribute('data-bucket-units', String(units));
4858      var label = unitLabel(match);
4859      var sqftParent = sqftEl.parentNode;
4860      var badge = sqftParent && sqftParent.querySelector('.carton-badge');
4861      if (badge) {
4862        var plural = units !== 1 ? 's' : '';
4863        var newBadgeText = '\u{1F4E6} ' + units + ' ' + label + plural + ' = ' + (units * coverage).toFixed(2) + ' sf (' + coverage + ' sf/' + label + ')';
4864        if (badge.textContent !== newBadgeText) badge.textContent = newBadgeText;
4865      }
4866    });
4867    // If we changed anything, recalculate the quote header totals
4868    if (anyChanged && typeof updateQuoteTotals === 'function') {
4869      try { updateQuoteTotals(); } catch (e) {}
4870    }
4871  }
4872  setInterval(tick, 600);
4873  console.log('[patch.js] FIX 27: Bucket/gallon pricing polling started');
4874})();
4875
4876// === FIX 28: Add Void + Delete buttons to invoice detail modal, hide soft-deleted invoices ===
4877// Joey requested ability to delete or void invoices from the UI. Schema review confirmed:
4878//   - `deleted_at` (timestamptz) is the existing soft-delete column convention
4879//   - status='Void' is the existing convention
4880//   - No void_reason / voided_at columns — we append the reason to `notes`
4881// This fix:
4882//   (a) wraps `loadInvoicesOps` so the list query filters out deleted_at IS NOT NULL
4883//   (b) wraps `renderInvoiceDetail` to inject Void + Delete buttons into the modal footer
4884//       and wire up handlers that confirm with the user (extra-strict if balance > 0)
4885// (Re-add of original FIX 22 work that was overwritten by a later deploy.)
4886(function patchInvoiceVoidDelete(){
4887  if (typeof loadInvoicesOps !== 'function' || typeof renderInvoiceDetail !== 'function') {
4888    console.warn('[patch.js] FIX 28 skipped: invoice functions not loaded');
4889    return;
4890  }
4891
4892  // (a) Hide soft-deleted invoices from the list
4893  const _origLoadInvoicesOps = loadInvoicesOps;
4894  window.loadInvoicesOps = async function(){
4895    const all = await _origLoadInvoicesOps.apply(this, arguments);
4896    return (all || []).filter(inv => !inv.deleted_at);
4897  };
4898
4899  // Helper: void an invoice (status='Void', append reason to notes)
4900  async function voidInvoiceFromUI(invoice){
4901    if (!invoice || !invoice.id) return;
4902    if (invoice.status === 'Void') {
4903      if (typeof toast === 'function') toast('Invoice is already Void', 'err');
4904      return;
4905    }
4906    const reason = prompt(
4907      'Void invoice ' + (invoice.invoice_number || '#' + invoice.id) + '?\n\n' +
4908      'Voiding marks the invoice as cancelled but keeps it visible in reports.\n' +
4909      'Enter a reason (required):'
4910    );
4911    if (reason === null) return; // cancelled
4912    const trimmed = (reason || '').trim();
4913    if (!trimmed) {
4914      if (typeof toast === 'function') toast('Void cancelled — reason required', 'err');
4915      return;
4916    }
4917    // Extra confirm if money has been paid
4918    const paid = Number(invoice.amount_paid || 0);
4919    if (paid > 0) {
4920      const ok = confirm(
4921        'WARNING: $' + paid.toLocaleString('en-US',{minimumFractionDigits:2}) +
4922        ' has already been paid on this invoice.\n\n' +
4923        'Voiding will NOT refund the customer. Proceed anyway?'
4924      );
4925      if (!ok) return;
4926    }
4927    const stamp = new Date().toISOString().slice(0,10);
4928    const userTag = (typeof prof !== 'undefined' && prof && prof.email) ? prof.email : 'admin';
4929    const newNote =
4930      '[' + stamp + '] VOIDED by ' + userTag + ' — Reason: ' + trimmed +
4931      (invoice.notes ? '\n\n' + invoice.notes : '');
4932    const { error } = await sb.from('invoices')
4933      .update({ status: 'Void', notes: newNote, updated_at: new Date().toISOString() })
4934      .eq('id', invoice.id);
4935    if (error) {
4936      if (typeof toast === 'function') toast('Void failed: ' + error.message, 'err');
4937      console.error('[patch.js] FIX 28 void error:', error);
4938      return;
4939    }
4940    if (typeof logAudit === 'function') {
4941      try { await logAudit('invoice_void', 'invoice', invoice.id, { reason: trimmed, prev_status: invoice.status }); } catch(e){}
4942    }
4943    if (typeof toast === 'function') toast('Invoice voided', 'ok');
4944    if (typeof closeOv === 'function') closeOv('ov-invoice-detail');
4945    if (typeof loadAndRenderInvoices === 'function') {
4946      const cur = (typeof currentFilter !== 'undefined' && currentFilter) ? currentFilter : 'all';
4947      await loadAndRenderInvoices(cur);
4948    }
4949  }
4950
4951  // Helper: soft-delete an invoice (sets deleted_at)
4952  async function deleteInvoiceFromUI(invoice){
4953    if (!invoice || !invoice.id) return;
4954    const paid = Number(invoice.amount_paid || 0);
4955    const total = Number(invoice.total_sale || 0);
4956
4957    // Check for related records that should block delete
4958    const [{ count: woCount }, { count: payCount }] = await Promise.all([
4959      sb.from('work_orders').select('id', { count:'exact', head:true }).eq('invoice_id', invoice.id),
4960      sb.from('invoice_payments').select('id', { count:'exact', head:true }).eq('invoice_id', invoice.id)
4961    ]);
4962    if ((woCount || 0) > 0) {
4963      if (typeof toast === 'function') toast('Cannot delete: invoice has ' + woCount + ' work order(s). Void instead.', 'err');
4964      return;
4965    }
4966    if ((payCount || 0) > 0 || paid > 0) {
4967      if (typeof toast === 'function') toast('Cannot delete: invoice has payments recorded. Void instead.', 'err');
4968      return;
4969    }
4970
4971    const ok = confirm(
4972      'DELETE invoice ' + (invoice.invoice_number || '#' + invoice.id) + '?\n\n' +
4973      'Total: $' + total.toLocaleString('en-US',{minimumFractionDigits:2}) + '\n' +
4974      'Paid:  $' + paid.toLocaleString('en-US',{minimumFractionDigits:2}) + '\n\n' +
4975      'This is a SOFT delete — the row is hidden but recoverable from the database.\n' +
4976      'For permanent removal, ask Claude to hard-delete from Supabase.\n\n' +
4977      'Continue?'
4978    );
4979    if (!ok) return;
4980
4981    const userTag = (typeof prof !== 'undefined' && prof && prof.email) ? prof.email : 'admin';
4982    const stamp = new Date().toISOString();
4983    const { error } = await sb.from('invoices')
4984      .update({ deleted_at: stamp, updated_at: stamp,
4985                notes: '[' + stamp.slice(0,10) + '] SOFT-DELETED by ' + userTag + (invoice.notes ? '\n\n' + invoice.notes : '') })
4986      .eq('id', invoice.id);
4987    if (error) {
4988      if (typeof toast === 'function') toast('Delete failed: ' + error.message, 'err');
4989      console.error('[patch.js] FIX 28 delete error:', error);
4990      return;
4991    }
4992    if (typeof logAudit === 'function') {
4993      try { await logAudit('invoice_delete', 'invoice', invoice.id, { soft: true, total: total }); } catch(e){}
4994    }
4995    if (typeof toast === 'function') toast('Invoice deleted', 'ok');
4996    if (typeof closeOv === 'function') closeOv('ov-invoice-detail');
4997    if (typeof loadAndRenderInvoices === 'function') {
4998      const cur = (typeof currentFilter !== 'undefined' && currentFilter) ? currentFilter : 'all';
4999      await loadAndRenderInvoices(cur);
5000    }
5001  }
5002
5003  // Expose helpers globally so they can be called from inline onclick if needed
5004  window.voidInvoiceFromUI = voidInvoiceFromUI;
5005  window.deleteInvoiceFromUI = deleteInvoiceFromUI;
5006
5007  // (b) Wrap renderInvoiceDetail to inject the buttons after the modal renders
5008  const _origRenderInvoiceDetail = renderInvoiceDetail;
5009  window.renderInvoiceDetail = async function(invoice){
5010    await _origRenderInvoiceDetail.apply(this, arguments);
5011    try {
5012      const closeBtn = document.getElementById('close-invoice-detail-btn');
5013      if (!closeBtn || !closeBtn.parentNode) return;
5014      const footer = closeBtn.parentNode;
5015      // Avoid duplicate buttons if the modal is re-rendered
5016      if (document.getElementById('btn-void-invoice') || document.getElementById('btn-delete-invoice')) return;
5017
5018      const isVoid = invoice && invoice.status === 'Void';
5019
5020      const voidBtn = document.createElement('button');
5021      voidBtn.id = 'btn-void-invoice';
5022      voidBtn.textContent = isVoid ? '🚫 Voided' : '🚫 Void Invoice';
5023      voidBtn.disabled = !!isVoid;
5024      voidBtn.style.cssText = 'padding:10px 20px;background:' + (isVoid ? '#444' : '#7a4a00') +
5025        ';color:#fff;border:1px solid var(--b);border-radius:6px;cursor:' + (isVoid ? 'default' : 'pointer') +
5026        ';font-weight:bold;opacity:' + (isVoid ? '0.6' : '1') + ';';
5027      if (!isVoid) voidBtn.addEventListener('click', () => voidInvoiceFromUI(invoice));
5028
5029      const delBtn = document.createElement('button');
5030      delBtn.id = 'btn-delete-invoice';
5031      delBtn.textContent = '🗑 Delete';
5032      delBtn.style.cssText = 'padding:10px 20px;background:#7a1a1a;color:#fff;border:1px solid #a33;border-radius:6px;cursor:pointer;font-weight:bold;';
5033      delBtn.addEventListener('click', () => deleteInvoiceFromUI(invoice));
5034
5035      // Insert before the Close button so the destructive actions sit at the right end
5036      footer.insertBefore(voidBtn, closeBtn);
5037      footer.insertBefore(delBtn, closeBtn);
5038    } catch (e) {
5039      console.error('[patch.js] FIX 28 inject error:', e);
5040    }
5041  };
5042
5043  console.log('[patch.js] FIX 28: invoice Void + Delete buttons wired; deleted_at filter active');
5044})();
5045
5046// === FIX 29: Lead Board / Job Board / Schedule / Dash Pro / Floorzap export salvage ===
5047// Some inline onclick handlers (Lead Board, Job Board, etc.) target functions defined
5048// in src/51..56. If app.js is built or deployed before those exports were wired 
5048into
5049// build.js's exportNames list, the inline handlers throw "X is not defined".
5050// patch.js loads AFTER app.js, so we can re-attach anything visible at script scope.
5051// (Re-add of original FIX 23 work that was overwritten by a later deploy.)
5052(function fix29ExportSalvage(){
5053  try {
5054    // Names defined in src/51..56 that inline onclick handlers depend on.
5055    var names = [
5056      // 55-kanban.js
5057      'renderLeadKanban','renderJobKanban','openKanbanBoard','showKanbanPanel',
5058      // 54-dashboard-pro.js
5059      'renderConversionsFunnel','renderKpiSidebar','renderPaymentProgress',
5060      'initDashboardTabs','switchDashTab','renderTeamRankings','initDashPro',
5061      // 56-wo-invoice-pro.js
5062      'getWoProgress','renderWoProgressBar','invoiceStatusBadge',
5063      'initQuickActionBar','initWoInvoicePro',
5064      // 51-schedule.js
5065      'renderScheduleTab','loadScheduleData','openScheduleForm',
5066      'saveScheduleEntry','deleteScheduleEntry','schedSwitchView',
5067      'schedApplyFilters','schedPrevMonth','schedNextMonth','schedShowDay',
5068      'schedSendNotification','schedCheckConflicts','schedPrefillContractor',
5069      'schedSearchLeads','schedSelectLead','schedAddQuickContractor'
5070    ];
5071    var fixed = 0, missing = [];
5072    for (var i = 0; i < names.length; i++) {
5073      var n = names[i];
5074      try {
5075        // Use a Function constructor probe to read from the global lexical scope
5076        // (top-level `function foo(){}` declarations are visible there even when
5077        // an `if (typeof foo)` check inside a nested IIFE returned undefined).
5078        var probe = new Function('try{return typeof ' + n + ' !== "undefined" ? ' + n + ' : undefined}catch(e){return undefined}');
5079        var fn = probe();
5080        if (typeof fn === 'function') {
5081          window[n] = fn;
5082          fixed++;
5083        } else if (typeof window[n] !== 'function') {
5084          missing.push(n);
5085        }
5086      } catch (e) { missing.push(n); }
5087    }
5088    console.log('[patch.js] FIX 29: salvaged ' + fixed + '/' + names.length + ' exports' +
5089      (missing.length ? ' (still missing: ' + missing.join(',') + ')' : ''));
5090
5091    // Fallback for openKanbanBoard if it's still missing — minimum viable
5092    // implementation so the Lead Board / Job Board nav items don't throw.
5093    if (typeof window.openKanbanBoard !== 'function') {
5094      window.openKanbanBoard = function(type){
5095        try {
5096          if (typeof sideNav === 'function') sideNav('quotes');
5097          else if (typeof goTab === 'function') goTab('quotes');
5098          var opsTab = document.getElementById('tab-quotes');
5099          if (!opsTab) {
5100            if (typeof toast === 'function') toast('Open Quotes tab first', 'err');
5101            return;
5102          }
5103          var wrap = document.getElementById('kanban-wrap');
5104          if (!wrap) {
5105            wrap = document.createElement('div');
5106            wrap.id = 'kanban-wrap';
5107            wrap.style.cssText = 'margin-bottom:20px;';
5108            wrap.innerHTML =
5109              '<div style="display:flex;gap:2px;margin-bottom:12px;background:var(--d3);border-radius:10px;padding:3px;">' +
5110                '<button id="kanban-lead-btn" onclick="showKanbanPanel(\'lead\')" style="flex:1;padding:8px 12px;border:none;border-radius:8px;cursor:pointer;font-size:12px;font-weight:600;font-family:inherit;background:var(--gold);color:#1a1008;">Lead Board</button>' +
5111                '<button id="kanban-job-btn" onclick="showKanbanPanel(\'job\')" style="flex:1;padding:8px 12px;border:none;border-radius:8px;cursor:pointer;font-size:12px;font-weight:600;font-family:inherit;background:transparent;color:var(--t3);">Job Board</button>' +
5112              '</div>' +
5113              '<div id="kanban-lead-board"></div>' +
5114              '<div id="kanban-job-board" style="display:none;"></div>';
5115            opsTab.insertBefore(wrap, opsTab.firstChild);
5116          }
5117          wrap.style.display = '';
5118          if (typeof window.showKanbanPanel === 'function') window.showKanbanPanel(type || 'lead');
5119          else if (typeof toast === 'function') toast('Kanban renderer missing — redeploy app.js', 'err');
5120        } catch (e) {
5121          console.error('[patch.js] FIX 29 openKanbanBoard fallback error:', e);
5122          if (typeof toast === 'function') toast('Lead/Job Board failed: ' + e.message, 'err');
5123        }
5124      };
5125      console.log('[patch.js] FIX 29: installed minimal openKanbanBoard fallback (renderers still required)');
5126    }
5127
5128    // Verify the inline-onclick handler for Lead/Job Board nav items will resolve.
5129    var navOk = (typeof window.openKanbanBoard === 'function');
5130    if (!navOk) {
5131      console.warn('[patch.js] FIX 29: Lead/Job Board nav items will still fail — openKanbanBoard not on window');
5132    }
5133  } catch (e) {
5134    console.error('[patch.js] FIX 29 error:', e);
5135  }
5136})();
5137
5138// === FIX 30: Editable Client Info on Invoice Detail modal ===
5139// Joey: "when u have invoice, why cant u make edits and click save?
5140// Invoice JH-100355 i want to add contact phone and address but not able to do this"
5141// renderInvoiceDetail in src/22-operations.js renders Client Information as read-only
5142// <div> tags with no save mechanism. This patch wraps renderInvoiceDetail and after it
5143// draws, injects an "✏️ Edit" button into the Client Information panel. Edit swaps the
5144// grid for <input> fields (Company, Customer, Contact, Phone, Email, Job Name,
5145// Job Address, Service Type). Save persists via Supabase and re-renders the modal.
5146(function () {
5147  try {
5148    var EDIT_FIELDS = [
5149      { key: 'company',       label: 'Company',      type: 'text' },
5150      { key: 'customer',      label: 'Customer',     type: 'text' },
5151      { key: 'contact_name',  label: 'Contact',      type: 'text' },
5152      { key: 'contact_phone', label: 'Phone',        type: 'tel'  },
5153      { key: 'contact_email', label: 'Email',        type: 'email' },
5154      { key: 'job_name',      label: 'Job Name',     type: 'text' },
5155      { key: 'job_address',   label: 'Job Address',  type: 'text' },
5156      { key: 'service_type',  label: 'Service Type', type: 'text' }
5157    ];
5158
5159    function escAttr(v) {
5160      return String(v == null ? '' : v).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
5161    }
5162
5163    function findClientInfoBlock() {
5164      var ov = document.getElementById('inv-detail-overlay');
5165      if (!ov) return null;
5166      var h3s = ov.querySelectorAll('h3');
5167      for (var i = 0; i < h3s.length; i++) {
5168        if ((h3s[i].textContent || '').trim().toLowerCase().indexOf('client information') === 0) {
5169          return h3s[i].parentElement;
5170        }
5171      }
5172      return null;
5173    }
5174
5175    function injectEditButton(invoiceId) {
5176      var panel = findClientInfoBlock();
5177      if (!panel) return;
5178      if (panel.querySelector('[data-fix30-edit-btn]')) return;
5179      var h3 = panel.querySelector('h3');
5180      if (!h3) return;
5181      h3.style.display = 'flex';
5182      h3.style.justifyContent = 'space-between';
5183      h3.style.alignItems = 'center';
5184      var btn = document.createElement('button');
5185      btn.setAttribute('data-fix30-edit-btn', '1');
5186      btn.type = 'button';
5187      btn.innerHTML = '✏️ Edit';
5188      btn.style.cssText = 'padding:4px 10px;background:var(--gold);
5188color:#1a1008;border:none;border-radius:4px;cursor:pointer;font-weight:bold;font-size:11px;';
5189      btn.addEventListener('click', function () { switchToEditMode(panel, invoiceId); });
5190      h3.appendChild(btn);
5191    }
5192
5193    function switchToEditMode(panel, invoiceId) {
5194      (async function () {
5195        var invoice = null;
5196        try {
5197          var sb = window.sb;
5198          if (sb && invoiceId) {
5199            var resp = await sb.from('invoices').select('*').eq('id', invoiceId).single();
5200            invoice = resp && resp.data;
5201          }
5202        } catch (err) {
5203          console.warn('[FIX 30] could not load invoice from DB:', err);
5204        }
5205        if (!invoice) {
5206          invoice = {};
5207          var labels = panel.querySelectorAll('label');
5208          labels.forEach(function (lbl) {
5209            var nx = lbl.nextElementSibling;
5210            var txt = (lbl.textContent || '').replace(':', '').trim().toLowerCase();
5211            var val = nx ? (nx.textContent || '').trim() : '';
5212            if (txt === 'company') invoice.company = val;
5213            else if (txt === 'customer') invoice.customer = val;
5214            else if (txt === 'contact') invoice.contact_name = val;
5215            else if (txt === 'phone') invoice.contact_phone = val;
5216            else if (txt === 'job name') invoice.job_name = val;
5217            else if (txt === 'job address') invoice.job_address = val;
5218            else if (txt === 'service type') invoice.service_type = val;
5219          });
5220        }
5221        renderEditForm(panel, invoiceId, invoice);
5222      })();
5223    }
5224
5225    function renderEditForm(panel, invoiceId, invoice) {
5226      var h3 = panel.querySelector('h3');
5227      var oldBtn = panel.querySelector('[data-fix30-edit-btn]');
5228      if (oldBtn) oldBtn.remove();
5229      var children = Array.from(panel.children);
5230      for (var i = 0; i < children.length; i++) {
5231        if (children[i] !== h3 && children[i].tagName === 'DIV') {
5232          children[i].remove();
5233        }
5234      }
5235      var form = document.createElement('div');
5236      form.setAttribute('data-fix30-edit-form', '1');
5237      form.style.cssText = 'display:grid;grid-template-columns:auto 1fr;gap:10px;align-items:center;';
5238      var inputStyle = 'width:100%;padding:6px 10px;background:var(--d1);border:1px solid var(--b);border-radius:4px;color:var(--t);font-size:13px;';
5239      var html = '';
5240      EDIT_FIELDS.forEach(function (f) {
5241        var val = invoice[f.key];
5242        html += '<label style="color:var(--t3);font-size:12px;">' + f.label + ':</label>';
5243        html += '<input data-fix30-field="' + f.key + '" type="' + f.type + '" value="' + escAttr(val || '') + '" style="' + inputStyle + '">';
5244      });
5245      form.innerHTML = html;
5246      panel.appendChild(form);
5247      var actions = document.createElement('div');
5248      actions.style.cssText = 'display:flex;gap:8px;justify-content:flex-end;margin-top:12px;';
5249      actions.innerHTML =
5250        '<button data-fix30-cancel type="button" style="padding:8px 14px;background:var(--d2);color:var(--t);border:1px solid var(--b);border-radius:4px;cursor:pointer;font-size:12px;">Cancel</button>' +
5251        '<button data-fix30-save   type="button" style="padding:8px 14px;background:#1a5a2f;color:#fff;border:none;border-radius:4px;cursor:pointer;font-weight:bold;font-size:12px;">💾 Save</button>';
5252      panel.appendChild(actions);
5253      panel.querySelector('[data-fix30-cancel]').addEventListener('click', function () {
5254        if (typeof window.renderInvoiceDetail === 'function') window.renderInvoiceDetail(invoiceId);
5255      });
5256      panel.querySelector('[data-fix30-save]').addEventListener('click', function () {
5257        saveInvoiceClientInfo(panel, invoiceId);
5258      });
5259    }
5260
5261    async function saveInvoiceClientInfo(panel, invoiceId) {
5262      var saveBtn = panel.querySelector('[data-fix30-save]');
5263      if (saveBtn) { saveBtn.disabled = true; saveBtn.innerHTML = '⏳ Saving...'; }
5264      try {
5265        var update = {};
5266        EDIT_FIELDS.forEach(function (f) {
5267          var input = panel.querySelector('[data-fix30-field="' + f.key + '"]');
5268          if (input) {
5269            var v = input.value.trim();
5270            update[f.key] = v === '' ? null : v;
5271          }
5272        });
5273        var sb = window.sb;
5274        if (!sb) throw new Error('Supabase client (sb) missing');
5275        var resp = await sb.from('invoices').update(update).eq('id', invoiceId).select().single();
5276        if (resp.error) throw resp.error;
5277        try {
5278          if (typeof window.logAudit === 'function') {
5279            window.logAudit('invoice', invoiceId, 'updated_client_info', { fields: Object.keys(update) });
5280          }
5281        } catch (_) {}
5282        if (typeof window.toast === 'function') window.toast('✅ Client info saved', 'ok');
5283        if (typeof window.renderInvoiceDetail === 'function') window.renderInvoiceDetail(invoiceId);
5284      } catch (e) {
5285        console.error('[FIX 30] save failed:', e);
5286        if (typeof window.toast === 'function') window.toast('Save failed: ' + (e.message || e), 'err');
5287        if (saveBtn) { saveBtn.disabled = false; saveBtn.innerHTML = '💾 Save'; }
5288      }
5289    }
5290
5291    function wrapRender() {
5292      if (typeof window.renderInvoiceDetail !== 'function') return false;
5293      if (window.renderInvoiceDetail.__fix30Wrapped) return true;
5294      var orig = window.renderInvoiceDetail;
5295      var wrapped = async function (invoiceId) {
5296        var result = await orig.apply(this, arguments);
5297        setTimeout(function () { injectEditButton(invoiceId); }, 50);
5298        return result;
5299      };
5300      wrapped.__fix30Wrapped = true;
5301      window.renderInvoiceDetail = wrapped;
5302      return true;
5303    }
5304
5305    if (!wrapRender()) {
5306      var attempts = 0;
5307      var iv = setInterval(function () {
5308        attempts++;
5309        if (wrapRender() || attempts > 40) {
5310          clearInterval(iv);
5311          if (attempts > 40) console.warn('[FIX 30] gave up waiting for renderInvoiceDetail');
5312        }
5313      }, 250);
5314    }
5315
5316    console.log('[patch.js] FIX 30: Editable Client Info on invoice detail modal — installed');
5317  } catch (e) {
5318    console.error('[patch.js] FIX 30 error:', e);
5319  }
5320})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.