<?php
// ============================================================
//  MediCore — Shared Layout Footer
//  File: includes/footer.php
//  Always include at the bottom of every page.
// ============================================================
?>
  </div><!-- /content -->
</div><!-- /content-area -->

</div><!-- /main-row -->
</div><!-- /app -->

<!-- ── Global modal container (same as original HTML) ── -->
<div id="mroot"></div>

<!-- ── Shared JS ─────────────────────────────────────── -->
<script>
// ── Modal helpers ─────────────────────────────────────────────
// Deliberately NOT closeable by clicking the dark overlay — a modal
// mid-sale/mid-edit (e.g. Sale Entry's "view all alternatives", any
// add/edit form) should only close via an explicit action: Esc, the
// × button, or a Cancel/Save button inside the modal's own content.
// An accidental outside click used to silently discard whatever the
// user was doing.
function showModal(html, title, size) {
  document.getElementById('mroot').innerHTML =
    `<div class="moverlay">
       <div class="mbox${size==='lg'?' mbox-lg':size==='xl'?' mbox-xl':''}">
         <div class="mtitle">${title}<button class="mclose" onclick="closeModal()">×</button></div>
         ${html}
       </div>
     </div>`;
}
function closeModal() { document.getElementById('mroot').innerHTML = ''; }

// Esc closes the topmost modal — the one explicit keyboard escape
// hatch this is still meant to support. Only acts when a modal is
// actually open, so it never interferes with a page's own Esc
// handling (e.g. Sale Entry's "Esc clears the search box").
document.addEventListener('keydown', (e) => {
  if (e.key !== 'Escape') return;
  const root = document.getElementById('mroot');
  if (root && root.innerHTML.trim() !== '') closeModal();
});

// ── Sidebar "Reports" submenu toggle ────────────────────────
function toggleReportsMenu(btn) {
  const wrap = btn.closest('.ni-submenu');
  const list = wrap.querySelector('.ni-submenu-list');
  const isOpen = wrap.classList.contains('open');
  wrap.classList.toggle('open', !isOpen);
  list.style.display = isOpen ? 'none' : 'block';
}

// ── Number formatter (matches original fmt()) ───────────────
function fmt(n, cur = '') {
  return cur + parseFloat(n||0).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
function fmtN(n) { return parseFloat(n||0).toFixed(2); }

// ── Flash auto-hide ──────────────────────────────────────────
document.querySelectorAll('.flash-success,.flash-error').forEach(el => {
  setTimeout(() => el.style.display = 'none', 4000);
});

// ── AJAX helper — POST JSON to api/ endpoints ────────────────
// Robust: never throws. Always resolves to {success, ...} or
// {success:false, error:"..."} so callers never hang on "Loading…"
// A 25s client-side timeout (AbortController) guarantees this even if
// the server-side request itself hangs (long-running query, deadlock,
// etc.) — without it, a stuck server request left the caller's
// "Loading…" placeholder on screen forever with no way to recover.
async function apiPost(url, data, timeoutMs = 25000) {
  let r;
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);
  try {
    r = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json',
                  'X-Requested-With': 'XMLHttpRequest' },
      body: JSON.stringify(data),
      signal: controller.signal,
    });
  } catch (netErr) {
    if (netErr.name === 'AbortError') {
      return { success: false, error: 'Request timed out — the server took too long to respond.' };
    }
    return { success: false, error: 'Network error — could not reach server.' };
  } finally {
    clearTimeout(timer);
  }

  // Session expired → PHP redirected to login.php (returns HTML, not JSON)
  if (r.status === 401 || r.status === 403) {
    const txt = await r.text().catch(() => '');
    const parsed = tryParseJson(txt);
    if (parsed) return parsed;
    if (looksLikeLoginPage(txt)) {
      return { success: false, error: 'Session expired. Please log in again.', session_expired: true };
    }
    return { success: false, error: 'Access denied (HTTP ' + r.status + '): ' + txt.slice(0,200) };
  }

  if (!r.ok) {
    return { success: false, error: 'Server error (HTTP ' + r.status + ').' };
  }

  const text = await r.text();

  // Fast path — clean JSON response
  const direct = tryParseJson(text);
  if (direct) return direct;

  // Slow path — PHP may have emitted a warning/notice/deprecation BEFORE
  // the real JSON (very common on XAMPP with display_errors on). Try to
  // recover by parsing from the first '{' or '[' to the end of the text,
  // so a harmless warning doesn't hide a perfectly valid response.
  const firstBrace = text.search(/[{[]/);
  if (firstBrace > 0) {
    const recovered = tryParseJson(text.slice(firstBrace));
    if (recovered) {
      console.warn('PHP emitted extra output before JSON (likely a warning/notice). Raw prefix:', text.slice(0, firstBrace));
      return recovered;
    }
  }

  // Truly unparseable — surface the real text instead of guessing "session expired"
  if (looksLikeLoginPage(text)) {
    return { success: false, error: 'Session expired. Please log in again.', session_expired: true };
  }
  return { success: false, error: 'Server returned an unexpected response: ' + text.slice(0,300) };
}

// ── Helpers for apiPost ────────────────────────────────────────
function tryParseJson(str) {
  try {
    const v = JSON.parse(str);
    return (v && typeof v === 'object') ? v : null;
  } catch { return null; }
}
function looksLikeLoginPage(text) {
  const t = text.toLowerCase();
  // Real login redirects contain a password field or this app's login form,
  // NOT just any stray "log in" wording inside a PHP warning's file path.
  return t.includes('name="password"') || t.includes('id="login-form"')
      || t.includes('<title>login') || t.includes('medicore — login');
}

// ── Standard "Session Expired" redirect helper ───────────────
function handleApiError(r, tbody, colspan) {
  if (r.session_expired) {
    if (tbody) tbody.innerHTML =
      `<tr><td colspan="${colspan}" class="empty-state" style="color:var(--danger)">
         🔒 Session expired. <a href="${'<?= base_url("login.php") ?>'}" style="color:var(--brand);font-weight:700">Click here to log in again</a>
       </td></tr>`;
    return true;
  }
  return false;
}

// ── CSRF token (read from meta tag set per-page) ─────────────
function csrfToken() {
  return document.querySelector('meta[name="csrf"]')?.content || '';
}

// ── Reusable client-side table pagination ─────────────────────
// Used by the reporting pages (accounting/reports-*.php), whose
// tables are fully server-rendered up front — every row must stay
// in the DOM so the tfoot "Total" rows keep summing the FULL
// dataset, not just one page. But rendering + searching across
// hundreds/thousands of <tr> at once made those tabs slow and hard
// to scan. This keeps every row present, hides all but the current
// page's slice of whatever currently matches the caller's filters,
// and renders Prev/Next controls — a pure display-layer concern
// layered on top of each page's own filter logic (search boxes,
// dropdowns, etc.), not a replacement for it.
//
// Usage:
//   const pager = mediPaginate({
//     rows: Array.from(document.querySelectorAll('.prod-row')),
//     predicate: row => !q || row.dataset.name.includes(q), // optional
//     pagerEl: document.getElementById('prod-pager'),
//     countEl: document.getElementById('prod-count'),       // optional
//     perPage: 25,                                          // optional
//   });
//   // whenever a search/filter input changes: pager.refresh();
function mediPaginate(opts) {
  const {
    rows,
    predicate  = null,
    pagerEl    = null,
    countEl    = null,
    perPage: initialPerPage = 25,
    renumber   = true,
    countLabel = 'result',
  } = opts;

  let page    = 1;
  let perPage = initialPerPage;
  let showAll = false; // when true, pagination is bypassed entirely — every matching row renders at once

  function refresh() {
    const filtered = predicate ? rows.filter(predicate) : rows.slice();
    const epp = showAll ? Math.max(1, filtered.length) : perPage;
    const totalPages = Math.max(1, Math.ceil(filtered.length / epp));
    if (page > totalPages) page = totalPages;
    if (page < 1) page = 1;

    // Use a CSS class (!important, see .mp-hidden in style.css) rather
    // than inline style.display — a plain inline style can silently lose
    // to another script/stylesheet touching the same element later;
    // !important via a dedicated class can't be clobbered that way.
    rows.forEach(r => { r.classList.add('mp-hidden'); });
    const start = showAll ? 0 : (page - 1) * perPage;
    const pageRows = filtered.slice(start, start + epp);
    pageRows.forEach((r, i) => {
      r.classList.remove('mp-hidden');
      if (renumber && r.cells && r.cells[0]) r.cells[0].textContent = start + i + 1;
    });

    if (countEl) {
      countEl.textContent = filtered.length + ' ' + countLabel + (filtered.length === 1 ? '' : 's');
    }
    renderPager(totalPages, filtered.length);
    return filtered;
  }

  function renderPager(totalPages, total) {
    if (!pagerEl) return;
    pagerEl.innerHTML = '';
    if (total === 0) return;

    const info = document.createElement('span');
    info.style.cssText = 'font-size:11px;color:var(--text3);margin-right:10px';

    if (showAll) {
      info.textContent = `Showing all ${total}`;
      pagerEl.appendChild(info);
      return;
    }

    const start = (page - 1) * perPage + 1;
    const end   = Math.min(page * perPage, total);
    info.textContent = `${start}–${end} of ${total}`;
    pagerEl.appendChild(info);

    if (totalPages <= 1) return;

    const mk = (label, target, disabled) => {
      const b = document.createElement('button');
      b.type = 'button';
      b.className = 'btn btn-xs';
      b.textContent = label;
      b.disabled = disabled;
      b.style.marginRight = '4px';
      b.onclick = () => { page = target; refresh(); };
      pagerEl.appendChild(b);
    };
    mk('« First', 1, page <= 1);
    mk('‹ Prev',  page - 1, page <= 1);
    const pInfo = document.createElement('span');
    pInfo.style.cssText = 'font-size:11px;padding:0 8px';
    pInfo.textContent = `Page ${page} / ${totalPages}`;
    pagerEl.appendChild(pInfo);
    mk('Next ›', page + 1, page >= totalPages);
    mk('Last »', totalPages, page >= totalPages);
  }

  const api = {
    refresh,
    goToPage(p) { page = p; refresh(); },
    get page() { return page; },
    // Lets a page wire a "rows per page" <select> (e.g. 100/200/500/1000)
    // to this pager — resets to page 1 so the new page size doesn't land
    // the user on a now-invalid page number.
    setPerPage(n) {
      const v = parseInt(n, 10);
      if (!v || v < 1) return;
      perPage = v;
      page = 1;
      refresh();
    },
    get perPage() { return perPage; },
    // Pagination ON/OFF toggle — OFF renders every matching row at once
    // (no slicing), ON goes back to the current perPage.
    setShowAll(flag) {
      showAll = !!flag;
      page = 1;
      refresh();
    },
    get showAll() { return showAll; },
  };
  refresh();
  return api;
}

// ── Product photo hover-zoom ───────────────────────────────────
// Any <img class="prod-thumb"> — Product List's table, Sale Entry's
// search suggestions, the Alternatives panel — shows an enlarged
// floating preview on hover, so staff can confirm packaging/color at
// a glance without leaving the small inline thumbnail. Delegated on
// `document` (not wired per-page) so it works for rows rendered
// dynamically/after this script runs, with zero per-page setup.
// Reuses the same already-loaded thumbnail src — no extra request.
(function () {
  let zoomEl = null;
  const SIZE = 200;

  function ensureZoomEl() {
    if (zoomEl) return zoomEl;
    zoomEl = document.createElement('img');
    zoomEl.id = 'prod-thumb-zoom';
    zoomEl.style.cssText = `
      position:fixed; z-index:9999; pointer-events:none;
      width:${SIZE}px; height:${SIZE}px; object-fit:cover;
      border-radius:12px; border:3px solid var(--surface);
      box-shadow:0 12px 32px rgba(0,0,0,.35);
      display:none; background:var(--surface2);
    `;
    document.body.appendChild(zoomEl);
    return zoomEl;
  }

  function positionZoom(e) {
    const el  = ensureZoomEl();
    const pad = 18;
    let x = e.clientX + pad, y = e.clientY + pad;
    if (x + SIZE > window.innerWidth)  x = e.clientX - SIZE - pad;
    if (y + SIZE > window.innerHeight) y = e.clientY - SIZE - pad;
    el.style.left = x + 'px';
    el.style.top  = y + 'px';
  }

  document.addEventListener('mouseover', e => {
    const img = e.target.closest && e.target.closest('img.prod-thumb');
    if (!img || !img.src) return;
    const el = ensureZoomEl();
    el.src = img.src;
    el.style.display = 'block';
    positionZoom(e);
  });
  document.addEventListener('mousemove', e => {
    if (zoomEl && zoomEl.style.display === 'block') positionZoom(e);
  });
  document.addEventListener('mouseout', e => {
    if (zoomEl && e.target.closest && e.target.closest('img.prod-thumb')) {
      zoomEl.style.display = 'none';
    }
  });
})();
</script>
</body>
</html>
