/**
 * WP Roster — Frontend Application
 * Vanilla JS (no build step). Runs in WP Admin and on public pages via shortcode.
 * @version 1.0.0
 * @license GPL-2.0+
 */
(function (W) {
  'use strict';

  if (!W || !W.ajaxUrl) return;

  const AJAX  = W.ajaxUrl;
  const NONCE = W.nonce;
  const USER  = W.user  || {};
  const SET   = W.settings || {};

  /* ═══════════════ UTILITY ═══════════════ */

  /**
   * AJAX helper — uses wp_ajax_ actions instead of REST API.
   * This works on ALL WordPress sites regardless of REST API restrictions.
   *
   * GET  requests: params go in query string
   * POST requests: body goes as JSON in request body, action/nonce in query string
   */
  async function http(path, method = 'GET', body = null) {
    // Convert REST-style path to AJAX action name
    // e.g. '/shifts'            → 'empr_get_shifts'   (GET)
    // e.g. '/shifts'            → 'empr_create_shift' (POST)
    // e.g. '/shifts/5'          → 'empr_update_shift' (PUT) or 'empr_delete_shift' (DELETE)
    // e.g. '/shifts/5/sync'     → 'empr_sync_assignments'
    const action = pathToAction(path, method, body);

    let url = AJAX + '?action=' + action + '&nonce=' + encodeURIComponent(NONCE);

    // Append GET params from path query string
    const qIdx = path.indexOf('?');
    if (qIdx !== -1) {
      url += '&' + path.slice(qIdx + 1);
    }

    const opt = { method: method === 'GET' ? 'GET' : 'POST' };

    if (method !== 'GET' && body) {
      opt.headers = { 'Content-Type': 'application/json' };
      opt.body    = JSON.stringify(body);
    }

    // For DELETE we pass id in POST body since we don't send a body normally
    if (method === 'DELETE') {
      const idMatch = path.match(/\/([0-9]+)(?:\/|$)/);
      if (idMatch) {
        opt.headers = { 'Content-Type': 'application/json' };
        opt.body    = JSON.stringify({ id: parseInt(idMatch[1]) });
      }
    }

    const res  = await fetch(url, opt);
    const data = await res.json();

    if (!data.success) {
      const msg = (data.data && data.data.message) ? data.data.message : 'Request failed';
      throw new Error(msg);
    }
    return data.data;
  }

  /**
   * Map a REST-style path + method to a wp_ajax action name.
   */
  function pathToAction(path, method, body) {
    // Strip query string
    const p = path.split('?')[0].replace(/\/+$/, '');

    // Specific sub-routes first
    if (p === '/dashboard')               return 'empr_get_dashboard';
    if (p === '/settings')                return 'empr_get_settings';
    if (p === '/availability/all')        return 'empr_all_availability';
    if (p === '/availability')            return method === 'GET' ? 'empr_get_availability' : 'empr_save_availability';
    if (p === '/staff')                   return 'empr_get_staff';

    // Shift sub-actions
    if (p.match(/^\/shifts\/\d+\/sync$/))     return 'empr_sync_assignments';
    if (p.match(/^\/shifts\/\d+\/assign$/))   return 'empr_sync_assignments';
    if (p.match(/^\/shifts\/\d+\/unassign$/)) return 'empr_sync_assignments';

    // Shift CRUD
    if (p.match(/^\/shifts\/\d+$/)) {
      if (method === 'GET')    return 'empr_get_shifts';
      if (method === 'PUT')    return 'empr_update_shift';
      if (method === 'DELETE') return 'empr_delete_shift';
    }
    if (p === '/shifts') {
      return method === 'GET' ? 'empr_get_shifts' : 'empr_create_shift';
    }

    // Fallback
    return 'empr_get_dashboard';
  }

  const DAY_NAMES  = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
  const DAY_SHORT  = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];

  function fmtDT(dt)   { return dt ? new Date(dt).toLocaleString('en-AU',{dateStyle:'short',timeStyle:'short'}) : '—'; }
  function fmtDate(d)  { return d  ? new Date(d + 'T00:00').toLocaleDateString('en-AU')  : '—'; }
  function fmtTime(dt) { return dt ? new Date(dt).toLocaleTimeString('en-AU',{hour:'2-digit',minute:'2-digit',hour12:false}) : '—'; }
  function fmtDay(dt)  { return dt ? new Date(dt).toLocaleDateString('en-AU',{weekday:'short',day:'numeric',month:'short'}) : '—'; }
  function toLocal(dt) {
    if (!dt) return '';
    const d = new Date(dt);
    return [d.getFullYear(), pad(d.getMonth()+1), pad(d.getDate())].join('-') + 'T' + [pad(d.getHours()), pad(d.getMinutes())].join(':');
  }
  function pad(n)    { return String(n).padStart(2,'0'); }
  function weekRange() {
    const now = new Date(), dow = now.getDay();
    const mon = new Date(now); mon.setDate(now.getDate() - ((dow+6)%7)); mon.setHours(0,0,0,0);
    const sun = new Date(mon); sun.setDate(mon.getDate()+6); sun.setHours(23,59,59);
    // Use local date parts — NOT toISOString() which returns UTC and can give wrong date
    const fmt = d => [d.getFullYear(), pad(d.getMonth()+1), pad(d.getDate())].join('-');
    return { start: fmt(mon), end: fmt(sun) };
  }
  function elapsedStr(from) {
    const mins = Math.round((Date.now() - new Date(from).getTime()) / 60000);
    if (mins < 60) return `${mins}m`;
    return `${Math.floor(mins/60)}h ${mins%60}m`;
  }

  /* ═══════════════ UI PRIMITIVES ═══════════════ */

  function toast(msg, type = 'ok') {
    document.querySelectorAll('.wpr-toast').forEach(e => e.remove());
    const el = document.createElement('div');
    el.className = `wpr-toast wpr-toast--${type}`;
    const icon = { ok:'✓', err:'✕', info:'ℹ' }[type] || '●';
    el.innerHTML = `<span class="wpr-toast__icon">${icon}</span><span>${msg}</span>`;
    document.body.appendChild(el);
    requestAnimationFrame(() => el.classList.add('wpr-toast--in'));
    setTimeout(() => { el.classList.remove('wpr-toast--in'); setTimeout(() => el.remove(), 400); }, 3500);
  }

  function spin(el) { el.innerHTML = '<div class="wpr-spin"><div></div><div></div><div></div><div></div></div>'; }

  /**
   * Copy each column's header text onto the cells beneath it as data-label, so
   * the phone card-stack layout in responsive.css can render "Label   value"
   * rows via td::before { content: attr(data-label) }.
   *
   * Cells under an empty header (the row-action columns) are left unlabelled on
   * purpose, and cells that span columns — empty-state rows like "No records" —
   * are skipped so they stay full width.
   *
   * Kept in sync with the same helper in admin/js/app-core.js; this frontend
   * bundle is a separate SPA that shares only the wpr-* class names.
   *
   * @param {ParentNode} root Subtree to scan.
   */
  function stampTableLabels(root) {
    root.querySelectorAll('table.wpr-table:not(.wpr-table--nostack)').forEach(table => {
      const heads = Array.from(table.querySelectorAll('thead th')).map(th => th.textContent.trim());
      if (!heads.length) return;
      table.querySelectorAll('tbody tr').forEach(row => {
        Array.from(row.children).forEach((cell, i) => {
          if (cell.hasAttribute('colspan')) return;
          if (heads[i]) cell.setAttribute('data-label', heads[i]);
        });
      });
    });
  }

  /**
   * Keep data-label in sync with whatever is on screen. Views rewrite innerHTML
   * from many places and there is no single render choke point, so watch the
   * document rather than hooking every call site. Setting an attribute does not
   * itself trigger a childList record, so this cannot feed back on itself.
   */
  function initTableLabels() {
    stampTableLabels(document);
    let queued = false;
    new MutationObserver(() => {
      if (queued) return;
      queued = true;
      requestAnimationFrame(() => { queued = false; stampTableLabels(document); });
    }).observe(document.body, { childList: true, subtree: true });
  }

  function modal({ title, body, actions = [], size = 'md' }) {
    document.querySelector('.wpr-overlay')?.remove();
    const ov = document.createElement('div');
    ov.className = 'wpr-overlay';
    ov.innerHTML = `
      <div class="wpr-modal wpr-modal--${size}" role="dialog" aria-modal="true">
        <header class="wpr-modal__head">
          <span class="wpr-modal__title">${title}</span>
          <button class="wpr-modal__x" aria-label="Close">✕</button>
        </header>
        <div class="wpr-modal__body">${body}</div>
        <footer class="wpr-modal__foot">
          ${actions.map(a => `<button class="wpr-btn ${a.cls||''}" data-action="${a.id}">${a.label}</button>`).join('')}
        </footer>
      </div>`;
    document.body.appendChild(ov);
    ov.querySelector('.wpr-modal__x').onclick = () => ov.remove();
    ov.addEventListener('mousedown', e => { if (e.target === ov) ov.remove(); });
    actions.forEach(a => {
      if (a.fn) ov.querySelector(`[data-action="${a.id}"]`).addEventListener('click', () => a.fn(ov));
    });
    setTimeout(() => ov.classList.add('wpr-overlay--in'), 10);
    return ov;
  }

  function badge(text, variant = 'neutral') {
    return `<span class="wpr-badge wpr-badge--${variant}">${text}</span>`;
  }

  /* ═══════════════ SKELETON LOADING ═══════════════ */
  function skeletonRows(n = 4) {
    return Array.from({length: n}, () =>
      `<tr>${Array.from({length:5},()=>'<td><div class="wpr-skel"></div></td>').join('')}</tr>`
    ).join('');
  }

  /* ═══════════════ VIEWS ═══════════════ */

  /* ── DASHBOARD ── */
  async function viewDashboard(el) {
    spin(el);
    try {
      const d = await http('/dashboard');
      const upNext = (d.my_upcoming_shifts || []).slice(0, 6);
      el.innerHTML = `
        <div class="wpr-view wpr-view--dashboard">
          <div class="wpr-page-header">
            <div>
              <h2 class="wpr-page-title">Dashboard</h2>
              <p class="wpr-page-sub">Welcome back, <strong>${USER.name}</strong></p>
            </div>
          </div>

          <div class="wpr-kpi-grid">
            <div class="wpr-kpi">
              <div class="wpr-kpi__label">Hours This Week</div>
              <div class="wpr-kpi__val">${d.hours_this_week || 0}<span class="wpr-kpi__unit">h</span></div>
              <div class="wpr-kpi__sub">${weekRange().start} – ${weekRange().end}</div>
            </div>
            <div class="wpr-kpi">
              <div class="wpr-kpi__label">Upcoming Shifts</div>
              <div class="wpr-kpi__val">${upNext.length}</div>
              <div class="wpr-kpi__sub">This week</div>
            </div>
            ${USER.isManager ? `
            <div class="wpr-kpi">
              <div class="wpr-kpi__label">Shifts Today</div>
              <div class="wpr-kpi__val">${d.shifts_today || 0}</div>
              <div class="wpr-kpi__sub">Published</div>
            </div>
            <div class="wpr-kpi">
              <div class="wpr-kpi__label">Total Staff</div>
              <div class="wpr-kpi__val">${d.total_staff || 0}</div>
              <div class="wpr-kpi__sub">Active roster members</div>
            </div>` : ''}
          </div>

          <div class="wpr-section-title">Upcoming Shifts This Week</div>
          ${upNext.length
            ? `<div class="wpr-shift-cards">${upNext.map(s => shiftCard(s, [], true)).join('')}</div>`
            : `<div class="wpr-empty"><span>📭</span><p>No shifts this week.</p></div>`
          }
        </div>`;
    } catch (e) { el.innerHTML = errBox(e.message); }
  }

  /* ── SHIFTS ── */
  async function viewShifts(el) {
    spin(el);
    const wr = weekRange();
    let rangeStart = wr.start, rangeEnd = wr.end;

    async function load() {
      spin(el);
      try {
        const [shifts, staff] = await Promise.all([
          http(`/shifts?start=${rangeStart}%2000%3A00%3A00&end=${rangeEnd}%2023%3A59%3A59&status=published`),
          USER.isManager ? http('/staff') : Promise.resolve([]),
        ]);
        render(shifts, staff);
      } catch(e) { el.innerHTML = errBox(e.message); }
    }

    function render(shifts, staff) {
      el.innerHTML = `
        <div class="wpr-view">
          <div class="wpr-page-header">
            <h2 class="wpr-page-title">Shifts</h2>
            <div class="wpr-page-header__actions">
              <div class="wpr-daterange">
                <input type="date" id="wpr-rs" class="wpr-input wpr-input--sm" value="${rangeStart}">
                <span>–</span>
                <input type="date" id="wpr-re" class="wpr-input wpr-input--sm" value="${rangeEnd}">
                <button class="wpr-btn wpr-btn--ghost wpr-btn--sm" id="wpr-apply">Apply</button>
              </div>
              ${USER.isManager ? `<button class="wpr-btn wpr-btn--primary" id="wpr-new-shift">+ New Shift</button>` : ''}
            </div>
          </div>
          ${shifts.length
            ? `<div class="wpr-shift-cards">${shifts.map(s => shiftCard(s, staff)).join('')}</div>`
            : `<div class="wpr-empty"><span>📅</span><p>No shifts found for this period.</p></div>`
          }
        </div>`;

      el.querySelector('#wpr-apply')?.addEventListener('click', () => {
        rangeStart = el.querySelector('#wpr-rs').value;
        rangeEnd   = el.querySelector('#wpr-re').value;
        load();
      });

      if (USER.isManager) {
        el.querySelector('#wpr-new-shift')?.addEventListener('click', () => shiftModal(null, staff, load));
        el.querySelectorAll('[data-shift-edit]').forEach(b => {
          const s = shifts.find(x => x.id == b.dataset.shiftEdit);
          b.addEventListener('click', () => shiftModal(s, staff, load));
        });
        el.querySelectorAll('[data-shift-del]').forEach(b => {
          b.addEventListener('click', async () => {
            if (!confirm('Delete this shift and all assignments?')) return;
            try { await http(`/shifts/${b.dataset.shiftDel}`, 'DELETE', {id:parseInt(b.dataset.shiftDel)}); toast('Shift deleted.'); load(); }
            catch (e) { toast(e.message, 'err'); }
          });
        });
        el.querySelectorAll('[data-shift-assign]').forEach(b => {
          const s = shifts.find(x => x.id == b.dataset.shiftAssign);
          b.addEventListener('click', () => assignModal(s, staff, load));
        });
      }
    }

    await load();
  }

  function shiftCard(shift, staff = [], compact = false) {
    const ids   = shift.assigned_ids ? shift.assigned_ids.split(',').map(Number) : [];
    const names = staff.filter(u => ids.includes(Number(u.id))).map(u => u.display_name);
    const pct   = shift.max_staff > 0 ? Math.round(ids.length / shift.max_staff * 100) : 0;
    const color = shift.color || '#2563EB';

    return `
      <article class="wpr-scard" style="--scard-color:${color}">
        <div class="wpr-scard__stripe"></div>
        <div class="wpr-scard__body">
          <div class="wpr-scard__top">
            <strong class="wpr-scard__title">${shift.department || shift.title || 'Shift'}</strong>
          </div>
          <div class="wpr-scard__meta">
            <span>📅 ${fmtDay(shift.start_datetime)}</span>
            <span>⏰ ${fmtTime(shift.start_datetime)}–${fmtTime(shift.end_datetime)}</span>
            ${shift.location ? `<span>📍 ${shift.location}</span>` : ''}
          </div>
          <div class="wpr-scard__staff">
            <div class="wpr-scard__staffbar">
              <div class="wpr-scard__staffbar-fill" style="width:${pct}%"></div>
            </div>
            <span class="wpr-scard__staffcount">${ids.length}/${shift.max_staff} staff</span>
            ${names.length ? `<span class="wpr-scard__names">${names.join(', ')}</span>` : ''}
          </div>
          ${shift.notes && !compact ? `<p class="wpr-scard__notes">${shift.notes}</p>` : ''}
          ${USER.isManager && !compact ? `
          <div class="wpr-scard__actions">
            <button class="wpr-btn wpr-btn--xs" data-shift-assign="${shift.id}">Assign</button>
            <button class="wpr-btn wpr-btn--xs wpr-btn--ghost" data-shift-edit="${shift.id}">Edit</button>
            <button class="wpr-btn wpr-btn--xs wpr-btn--danger" data-shift-del="${shift.id}">Delete</button>
          </div>` : ''}
        </div>
      </article>`;
  }

  function shiftModal(shift, staff, onSave) {
    const depts = (SET.departments || []).map(r => `<option value="${r}" ${shift?.department===r?'selected':''}>${r}</option>`).join('');
    const locs  = (SET.locations  || []).map(l => `<option value="${l}" ${shift?.location===l?'selected':''}>${l}</option>`).join('');
    const colors = ['#2563EB','#16A34A','#DC2626','#D97706','#7C3AED','#0891B2','#BE185D','#374151'];

    modal({
      title: shift ? 'Edit Shift' : 'New Shift',
      size: 'lg',
      body: `
        <div class="wpr-form-grid">
          <div class="wpr-field wpr-field--full"><label>Department</label><select id="sf-r" class="wpr-input"><option value="">— Select department —</option>${depts}</select></div>
          <div class="wpr-field"><label>Location</label><select id="sf-l" class="wpr-input"><option value="">Select…</option>${locs}</select></div>
          <div class="wpr-field"><label>Start *</label><input id="sf-s" class="wpr-input" type="datetime-local" value="${toLocal(shift?.start_datetime)}"></div>
          <div class="wpr-field"><label>End *</label><input id="sf-e" class="wpr-input" type="datetime-local" value="${toLocal(shift?.end_datetime)}"></div>
          <div class="wpr-field"><label>Max Staff</label><input id="sf-m" class="wpr-input" type="number" min="1" max="50" value="${shift?.max_staff||1}"></div>
          <div class="wpr-field wpr-field--full">
            <label>Colour</label>
            <div class="wpr-color-row">
              ${colors.map(c => `<button type="button" class="wpr-color-swatch ${(shift?.color||'#2563EB')===c?'wpr-color-swatch--sel':''}" data-color="${c}" style="background:${c}"></button>`).join('')}
            </div>
            <input type="hidden" id="sf-c" value="${shift?.color||'#2563EB'}">
          </div>
          <div class="wpr-field wpr-field--full"><label>Notes</label><textarea id="sf-n" class="wpr-input" rows="3" placeholder="Optional notes visible to assigned staff">${shift?.notes||''}</textarea></div>
        </div>`,
      actions: [
        { id:'cancel', label:'Cancel', cls:'wpr-btn--ghost', fn: ov => ov.remove() },
        { id:'save',   label: shift ? 'Update Shift' : 'Create Shift', cls:'wpr-btn--primary', fn: async ov => {
          const v = ov.querySelector.bind(ov);
          const data = {
            department:     v('#sf-r').value,
            title:          v('#sf-r').value || 'Shift',
            start_datetime: v('#sf-s').value.replace('T',' ') + ':00',
            end_datetime:   v('#sf-e').value.replace('T',' ') + ':00',
            location:       v('#sf-l').value,
            max_staff:      parseInt(v('#sf-m').value)||1,
            color:          v('#sf-c').value,
            notes:          v('#sf-n').value.trim(),
          };
          try {
            if (shift) await http(`/shifts/${shift.id}`, 'PUT', Object.assign({}, data, { id: shift.id }));
            else       await http('/shifts', 'POST', data);
            toast(shift ? 'Shift updated.' : 'Shift created.'); ov.remove(); onSave();
          } catch(e) { toast(e.message, 'err'); }
        }},
      ],
    });
    // Colour swatches
    document.querySelectorAll('.wpr-color-swatch').forEach(sw => {
      sw.addEventListener('click', () => {
        document.querySelectorAll('.wpr-color-swatch').forEach(x => x.classList.remove('wpr-color-swatch--sel'));
        sw.classList.add('wpr-color-swatch--sel');
        document.querySelector('#sf-c').value = sw.dataset.color;
      });
    });
  }

  function assignModal(shift, staff, onSave) {
    const ids = shift.assigned_ids ? shift.assigned_ids.split(',').map(Number) : [];
    modal({
      title: `Assign Staff — ${shift.title}`,
      size: 'md',
      body: `
        <p class="wpr-modal-meta">${fmtDay(shift.start_datetime)} · ${fmtTime(shift.start_datetime)}–${fmtTime(shift.end_datetime)}</p>
        <div class="wpr-assign-list">
          ${staff.map(u => `
            <label class="wpr-assign-row">
              <input type="checkbox" value="${u.id}" ${ids.includes(Number(u.id))?'checked':''}>
              <span class="wpr-assign-name">${u.display_name}</span>
              <span class="wpr-assign-role">${(u.roles||[]).join(', ')}</span>
            </label>`).join('')}
        </div>`,
      actions: [
        { id:'cancel', label:'Cancel', cls:'wpr-btn--ghost', fn: ov => ov.remove() },
        { id:'save', label:'Save Assignments', cls:'wpr-btn--primary', fn: async ov => {
          const selected = [...ov.querySelectorAll('.wpr-assign-list input:checked')].map(c => Number(c.value));
          try {
            await http(`/shifts/${shift.id}/sync`, 'POST', { shift_id: shift.id, user_ids: selected });
            toast('Assignments saved.'); ov.remove(); onSave();
          } catch(e) { toast(e.message, 'err'); }
        }},
      ],
    });
  }

  /* ── AVAILABILITY ── */
  async function viewAvailability(el) {
    spin(el);
    async function load() {
      spin(el);
      try {
        const avail = await http('/availability');
        render(avail);
      } catch(e) { el.innerHTML = errBox(e.message); }
    }

    function render(avail) {
      const byDay = {};
      avail.forEach(a => { byDay[a.day_of_week] = a; });

      el.innerHTML = `
        <div class="wpr-view">
          <div class="wpr-page-header">
            <h2 class="wpr-page-title">My Availability</h2>
          </div>
          <p class="wpr-help">Set the days and hours you are available to work. Managers use this when scheduling shifts.</p>
          <div class="wpr-avail-grid">
            ${DAY_NAMES.map((day, i) => {
              const a = byDay[i];
              const chk = a?.is_available == 1;
              return `
                <div class="wpr-avail-row ${chk?'wpr-avail-row--on':'wpr-avail-row--off'}" data-day="${i}">
                  <label class="wpr-avail-check">
                    <input type="checkbox" class="wpr-avail-cb" ${chk?'checked':''}> <span>${day}</span>
                  </label>
                  <div class="wpr-avail-times">
                    <input type="time" class="wpr-input wpr-input--sm wpr-avail-from" value="${a?.start_time?.slice(0,5)||'09:00'}" ${!chk?'disabled':''}>
                    <span>to</span>
                    <input type="time" class="wpr-input wpr-input--sm wpr-avail-to" value="${a?.end_time?.slice(0,5)||'17:00'}" ${!chk?'disabled':''}>
                  </div>
                </div>`;
            }).join('')}
          </div>
          <div style="margin-top:1.5rem">
            <button class="wpr-btn wpr-btn--primary" id="wpr-save-avail">Save Availability</button>
          </div>
        </div>`;

      el.querySelectorAll('.wpr-avail-cb').forEach(cb => {
        cb.addEventListener('change', () => {
          const row = cb.closest('.wpr-avail-row');
          row.classList.toggle('wpr-avail-row--on',  cb.checked);
          row.classList.toggle('wpr-avail-row--off', !cb.checked);
          row.querySelectorAll('input[type=time]').forEach(i => i.disabled = !cb.checked);
        });
      });

      el.querySelector('#wpr-save-avail').addEventListener('click', async () => {
        const slots = [...el.querySelectorAll('.wpr-avail-row')].map(row => ({
          day_of_week:  parseInt(row.dataset.day),
          start_time:   row.querySelector('.wpr-avail-from').value + ':00',
          end_time:     row.querySelector('.wpr-avail-to').value   + ':00',
          is_available: row.querySelector('.wpr-avail-cb').checked ? 1 : 0,
        }));
        try { await http('/availability','POST',{slots}); toast('Availability saved!'); }
        catch(e) { toast(e.message,'err'); }
      });
    }

    await load();
  }

  /* ── STAFF ── */
  async function viewStaff(el) {
    spin(el);
    try {
      const staff = await http('/staff');
      el.innerHTML = `
        <div class="wpr-view">
          <div class="wpr-page-header">
            <h2 class="wpr-page-title">Staff <span class="wpr-count">${staff.length}</span></h2>
          </div>
          <div class="wpr-table-wrap">
            <table class="wpr-table">
              <thead><tr><th>Name</th><th>Email</th><th>Roles</th><th></th></tr></thead>
              <tbody>
                ${staff.map(u => `
                  <tr>
                    <td><strong>${u.display_name}</strong></td>
                    <td>${u.email}</td>
                    <td>${(u.roles||[]).map(r => badge(r,'neutral')).join(' ')}</td>
                    <td><button class="wpr-btn wpr-btn--xs wpr-btn--ghost" data-avail="${u.id}" data-name="${u.display_name}">View Availability</button></td>
                  </tr>`).join('')}
              </tbody>
            </table>
          </div>
        </div>`;

      el.querySelectorAll('[data-avail]').forEach(b => {
        b.addEventListener('click', async () => {
          try {
            const avail = await http(`/availability?user_id=${b.dataset.avail}`);
            const byDay = {};
            avail.forEach(a => { byDay[a.day_of_week] = a; });
            modal({
              title: `Availability — ${b.dataset.name}`,
              body: `<div class="wpr-table-wrap">
                <table class="wpr-table">
                  <thead><tr><th>Day</th><th>Available</th><th>Hours</th></tr></thead>
                  <tbody>${DAY_SHORT.map((d,i) => {
                    const a = byDay[i];
                    return `<tr>
                      <td><strong>${d}</strong></td>
                      <td>${a?.is_available == 1 ? '✅ Yes' : '❌ No'}</td>
                      <td>${a?.is_available == 1 ? `${a.start_time.slice(0,5)} – ${a.end_time.slice(0,5)}` : '—'}</td>
                    </tr>`;
                  }).join('')}</tbody>
                </table>
              </div>`,
              actions: [{ id:'close', label:'Close', cls:'wpr-btn--ghost', fn: ov => ov.remove() }],
            });
          } catch(e) { toast(e.message,'err'); }
        });
      });
    } catch(e) { el.innerHTML = errBox(e.message); }
  }

  /* ═══════════════ NAVIGATION ═══════════════ */

  function nav(wrap, currentPage) {
    const items = [
      { id:'wp-roster',             page:'',            label:'Dashboard',   icon:'⊞', always:true  },
      { id:'wp-roster-shifts',      page:'shifts',      label:'Shifts',      icon:'◫', always:true  },
      { id:'wp-roster-availability',page:'availability',label:'Availability',icon:'◷', always:true  },
      { id:'wp-roster-staff',       page:'staff',       label:'Staff',       icon:'⊞', manager:true },
    ].filter(i => i.always || (i.manager && USER.isManager));

    const isActive = (i) => i.page === currentPage || (currentPage === '' && i.page === '');

    const sidebar = document.createElement('aside');
    sidebar.className = 'wpr-sidebar';
    sidebar.innerHTML = `
      <div class="wpr-sidebar__brand">
        <span class="wpr-sidebar__logo">⬡</span>
        <span class="wpr-sidebar__name">${SET.companyName || 'WP Roster'}</span>
      </div>
      <nav class="wpr-sidebar__nav">
        ${items.map(i => `
          <a class="wpr-nav-item ${isActive(i)?'wpr-nav-item--active':''}" data-nav="${i.page}" href="#">
            <span class="wpr-nav-item__icon">${i.icon}</span>
            <span class="wpr-nav-item__label">${i.label}</span>
          </a>`).join('')}
      </nav>
      <div class="wpr-sidebar__foot">
        <span class="wpr-sidebar__user">${USER.name}</span>
      </div>`;

    wrap.prepend(sidebar);

    sidebar.querySelectorAll('[data-nav]').forEach(a => {
      a.addEventListener('click', e => {
        e.preventDefault();
        sidebar.querySelectorAll('.wpr-nav-item').forEach(x => x.classList.remove('wpr-nav-item--active'));
        a.classList.add('wpr-nav-item--active');
        const content = wrap.querySelector('.wpr-content');
        if (content) route(a.dataset.nav, content);
      });
    });
  }

  function route(page, content) {
    content.innerHTML = '';
    const views = {
      '':           viewDashboard,
      'dashboard':  viewDashboard,
      'shifts':     viewShifts,
      'availability': viewAvailability,
      'staff':      viewStaff,
    };
    (views[page] || viewDashboard)(content);
  }

  function errBox(msg) {
    return `<div class="wpr-err-box"><strong>Error:</strong> ${msg}</div>`;
  }

  /* ═══════════════ BOOTSTRAP ═══════════════ */

  function applyBrandColor(hex) {
    if (!hex || !/^#[0-9A-Fa-f]{6}$/i.test(hex)) return;
    const r = parseInt(hex.slice(1, 3), 16);
    const g = parseInt(hex.slice(3, 5), 16);
    const b = parseInt(hex.slice(5, 7), 16);
    const ch = function(v) { return Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, '0'); };
    const brandH    = '#' + ch(r * .85)             + ch(g * .85)             + ch(b * .85);
    const brandPale = '#' + ch(r * .08 + 255 * .92) + ch(g * .08 + 255 * .92) + ch(b * .08 + 255 * .92);
    var el = document.getElementById('emr-brand-vars');
    if (!el) { el = document.createElement('style'); el.id = 'emr-brand-vars'; document.head.appendChild(el); }
    el.textContent = ':root{--wpr-brand:' + hex + ';--wpr-brand-h:' + brandH + ';--wpr-brand-pale:' + brandPale + '}';
  }

  function boot() {
    if (SET.primaryColor) applyBrandColor(SET.primaryColor);

    // Label table cells so they can card-stack on phones.
    initTableLabels();

    document.querySelectorAll('#emr-app, .emr-app').forEach(app => {
      if (app.dataset.booted) return;
      app.dataset.booted = '1';

      const page = app.dataset.page || '';

      app.classList.add('wpr-shell');

      const content = document.createElement('main');
      content.className = 'wpr-content';
      app.appendChild(content);

      nav(app, page);
      route(page, content);
    });
  }

  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
  else boot();

})(window.EMPR);
