mechanic

🧩 Syntax:
// backend/dateUtil.js
// Small, safe date & string helpers shared across backend files.
// - Adds safeTrim() to centralize string trimming
// - Hardened toDate() to tolerate several input shapes without throwing
// - normalizeDates() writes only when parsing succeeds; preserves null/undefined
// - Utility helpers: isValidDate, toEpochMs, toIsoString
//
// NOTE: This file contains **no** Wix imports; it's safe to use in backend and public modules.

/**
 * Returns true if `d` is a Date with a valid time value.
 * @param {*} d
 * @returns {boolean}
 */
export function isValidDate(d) {
  return d instanceof Date && !Number.isNaN(d.getTime());
}

/**
 * Trim a value safely. Always returns a string. Null/undefined -> ''.
 * @param {*} v
 * @returns {string}
 */
export function safeTrim(v) {
  if (v == null) return '';
  return String(v).trim();
}

/**
 * Coerce many shapes (Date, number, ISO string, quoted string, epoch seconds/ms)
 * to a valid Date.
 *
 * Behavior:
 * - Returns the original `null`/`undefined` if input is nullish (non-destructive).
 * - Returns a Date when parsing succeeds.
 * - Returns `undefined` when it can’t parse (so callers can decide to write or skip).
 *
 * @param {*} v
 * @returns {Date | null | undefined}
 */
export function toDate(v) {
  // Already a valid Date
  if (isValidDate(v)) return v;

  // Preserve explicit null/undefined
  if (v === null) return null;
  if (v === undefined) return undefined;

  try {
    // Numbers: treat as epoch ms
    if (typeof v === 'number') {
      const d = new Date(v);
      return isValidDate(d) ? d : undefined;
    }

    if (typeof v === 'string') {
      // Tolerate values like "\"2025-09-04T09:05:23.000Z\"" or numeric strings
      const s = v.trim().replace(/^"+|"+$/g, '');

      // Pure digits β†’ epoch seconds (10) or ms (>=11)
      if (/^\d+$/.test(s)) {
        const n = s.length === 10 ? Number(s) * 1000 : Number(s);
        const d = new Date(n);
        return isValidDate(d) ? d : undefined;
      }

      // Fallback: ISO-ish string
      const d = new Date(s);
      return isValidDate(d) ? d : undefined;
    }

    // Anything else Date() can handle
    const d = new Date(v);
    return isValidDate(d) ? d : undefined;
  } catch {
    return undefined;
  }
}

/**
 * In-place normalize for a set of keys on a plain object.
 * Only writes when `toDate` returns a valid Date (not `undefined`).
 * Leaves null/undefined as-is.
 *
 * @template T extends Record<string, any>
 * @param {T} obj
 * @param {string[]} keys
 * @returns {T}
 */
export function normalizeDates(obj, keys) {
  if (!obj || !Array.isArray(keys) || keys.length === 0) return obj;
  keys.forEach((k) => {
    if (Object.prototype.hasOwnProperty.call(obj, k)) {
      const coerced = toDate(obj[k]);
      if (coerced === null) {
        obj[k] = null;
      } else if (coerced instanceof Date) {
        obj[k] = coerced;
      }
      // if undefined (unparseable), leave the original value untouched
    }
  });
  return obj;
}

/**
 * Convert a value to epoch milliseconds if parsable.
 * Returns:
 *   - number on success
 *   - null if input was null
 *   - undefined if input was undefined or unparseable
 * @param {*} v
 * @returns {number | null | undefined}
 */
export function toEpochMs(v) {
  const d = toDate(v);
  if (d === null) return null;
  if (d === undefined) return undefined;
  // From here d is a Date | (unreachable)
  return isValidDate(d) ? d.getTime() : undefined;
}

/**
 * Convert a value to ISO string if parsable.
 * Returns:
 *   - string on success
 *   - null if input was null
 *   - undefined if input was undefined or unparseable
 * @param {*} v
 * @returns {string | null | undefined}
 */
export function toIsoString(v) {
  const d = toDate(v);
  if (d === null) return null;
  if (d === undefined) return undefined;
  return isValidDate(d) ? d.toISOString() : undefined;
}

// backend/jobNumberModule.jsw
import wixData from 'wix-data';
import wixUsersBackend from 'wix-users-backend';

/**
 * Collection: JobNumberCounter (LIVE)
 * Fields (field keys):
 *   _id: "currentJobNumber" (fixed id for single counter row)
 *   currentJobNumber: Number
 *   lastUpdated: Date/Time
 *   totalTickets: Number
 */

const CN = {
  COLL: 'JobNumberCounter',
  ID: 'currentJobNumber',
  F: { CUR: 'currentJobNumber', UPDATED: 'lastUpdated', TOTAL: 'totalTickets' },
};

const FORMAT = { prefix: 'Job Cart ', padLength: 4, usePadding: true };

const CFG = {
  maxAttempts: 5,
  baseDelayMs: 100,
  // Set this to the *next* number you want the system to return.
  // Example: if you want the first new ticket to be 5001, set 5001 here.
  INITIAL_VALUE: 5001,
};

// IMPORTANT: lets backend code read/write regardless of collection permissions.
const OPTS = { suppressAuth: true, suppressHooks: true };

/* -------------------- Utilities -------------------- */
const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
const jitter = (ms) => Math.floor(ms * (0.75 + Math.random() * 0.5)); // 75%..125%

function toSafeBool(v, fallback = false) {
  if (typeof v === 'boolean') return v;
  if (v === 'true') return true;
  if (v === 'false') return false;
  return fallback;
}

function toSafePrefix(v) {
  const s = typeof v === 'string' ? v : '';
  return s || FORMAT.prefix;
}

function toSafePadLength(v) {
  const n = Number(v);
  if (!Number.isFinite(n) || n <= 0) return FORMAT.padLength;
  return Math.floor(n);
}

const pad = (n, len) => {
  const safeLen = toSafePadLength(len);
  const s = String(n);
  return s.length >= safeLen ? s : '0'.repeat(safeLen - s.length) + s;
};

// Accepts overrides so getNextJobNumber options actually work.
const formatJobNumber = (
  n,
  { prefix = FORMAT.prefix, padLength = FORMAT.padLength, usePadding = FORMAT.usePadding } = {}
) => {
  const px = toSafePrefix(prefix);
  const usePad = toSafeBool(usePadding, FORMAT.usePadding);
  const len = toSafePadLength(padLength);
  return `${px}${usePad ? pad(n, len) : n}`;
};

// Add small randomness to reduce fallback collision risk under heavy concurrency
function fallbackRaw() {
  // 9-digit base from epoch ms + 3 random digits
  const base = Number(String(Date.now()).slice(-9));
  const rand = Math.floor(Math.random() * 1000); // 0..999
  return Number(String(base) + String(rand).padStart(3, '0'));
}

function fallbackFormatted(prefix = FORMAT.prefix /* padLength unused intentionally */) {
  // Human-readable timestamp fallback with random suffix to avoid same-second collisions
  const px = toSafePrefix(prefix);
  const ts = new Date();
  const y = ts.getFullYear();
  const m = String(ts.getMonth() + 1).padStart(2, '0');
  const d = String(ts.getDate()).padStart(2, '0');
  const h = String(ts.getHours()).padStart(2, '0');
  const mi = String(ts.getMinutes()).padStart(2, '0');
  const s = String(ts.getSeconds()).padStart(2, '0');
  const r = Math.random().toString(36).slice(2, 5); // 3-char base36
  return `${px}${y}${m}${d}-${h}${mi}${s}-${r}`;
}

async function assertAdmin() {
  const u = wixUsersBackend.currentUser;
  if (!u?.loggedIn) throw new Error('Not logged in');
  const roles = await u.getRoles().catch(() => []);
  if (!roles.some(r => (r?.name || '').toLowerCase() === 'admin')) {
    throw new Error('Only Admin can perform this action');
  }
}

/* -------------------- Initialization -------------------- */
async function ensureCounterDoc() {
  let doc;
  try {
    doc = await wixData.get(CN.COLL, CN.ID, OPTS);
  } catch (_) {
    // Document doesn't exist, will create below
  }
  if (doc) return doc;

  const initDoc = {
    _id: CN.ID,
    [CN.F.CUR]: CFG.INITIAL_VALUE - 1, // next increment returns INITIAL_VALUE
    [CN.F.UPDATED]: new Date(),
    [CN.F.TOTAL]: 0,
  };

  try {
    return await wixData.insert(CN.COLL, initDoc, OPTS);
  } catch (insertErr) {
    // Handle race condition - another request might have created it
    try {
      return await wixData.get(CN.COLL, CN.ID, OPTS);
    } catch (getErr) {
      console.error('[Counter] ensureCounterDoc failed:', { insertErr, getErr });
      throw new Error('Failed to initialize job number counter.');
    }
  }
}

/* -------------------- Increment Logic -------------------- */
async function nextNumberWithRetry() {
  await ensureCounterDoc();

  for (let attempt = 1; attempt <= CFG.maxAttempts; attempt++) {
    let doc;
    try {
      doc = await wixData.get(CN.COLL, CN.ID, OPTS);
    } catch (e) {
      console.error(`[Counter] get failed (attempt ${attempt}/${CFG.maxAttempts}):`, e);
      if (attempt === CFG.maxAttempts) throw new Error('Could not read counter.');
      await sleep(jitter(CFG.baseDelayMs * Math.pow(2, attempt - 1)));
      continue;
    }

    const currentRaw = doc?.[CN.F.CUR];
    const current = Number.isFinite(Number(currentRaw)) ? Number(currentRaw) : (CFG.INITIAL_VALUE - 1);
    const next = current + 1;

    const updatedDoc = {
      ...doc, // keeps _id & _rev for optimistic concurrency
      [CN.F.CUR]: next,
      [CN.F.UPDATED]: new Date(),
      [CN.F.TOTAL]: Number.isFinite(Number(doc?.[CN.F.TOTAL])) ? Number(doc[CN.F.TOTAL]) + 1 : 1,
    };

    try {
      const saved = await wixData.update(CN.COLL, updatedDoc, OPTS);
      const val = Number(saved?.[CN.F.CUR]);
      return Number.isFinite(val) ? val : next;
    } catch (e) {
      const msg = (e && e.message) || '';
      const code = (e && e.code) || '';
      const isConflict =
        msg.includes('version') ||
        msg.includes('revision') ||
        code === 'WDE0026' ||
        e?.httpStatus === 412;

      console.warn(
        `[Counter] update failed (attempt ${attempt}/${CFG.maxAttempts})${isConflict ? ' [conflict]' : ''}:`,
        { error: e, current, next }
      );

      if (attempt === CFG.maxAttempts) {
        throw new Error('Failed to increment job number after multiple attempts.');
      }
      await sleep(jitter(CFG.baseDelayMs * Math.pow(2, attempt - 1)));
    }
  }

  throw new Error('Unexpected counter failure.');
}

/* -------------------- Public Exports -------------------- */

/**
 * Get the next raw integer from the counter.
 * If the counter fails and `allowFallback` is true, returns a timestamp-derived integer.
 */
export async function getNextJobNumberRaw(options = {}) {
  const { allowFallback = true } = options;

  try {
    const n = await nextNumberWithRetry();
    console.log('Generated job number:', n);
    return n;
  } catch (err) {
    console.error('[jobNumberModule] getNextJobNumberRaw failed:', {
      message: err?.message,
      stack: err?.stack
    });

    if (!allowFallback) throw err;

    const fallback = fallbackRaw();
    console.warn('Using RAW fallback job number:', fallback);
    return fallback;
  }
}

/**
 * Get the next formatted job number string from the counter.
 * If the counter fails and `allowFallback` is true, returns a timestamp-based fallback.
 */
export async function getNextJobNumber(options = {}) {
  const {
    allowFallback = true,
    prefix = FORMAT.prefix,
    padLength = FORMAT.padLength,
    usePadding = FORMAT.usePadding
  } = options;

  try {
    const raw = await getNextJobNumberRaw({ allowFallback: false });
    const formatted = formatJobNumber(raw, { prefix, padLength, usePadding });
    console.log('Generated formatted job number:', formatted);
    return formatted;
  } catch (err) {
    console.error('[jobNumberModule] getNextJobNumber failed:', {
      message: err?.message,
      stack: err?.stack
    });

    if (!allowFallback) throw err;

    const fallback = fallbackFormatted(prefix);
    console.warn('Using FORMATTED fallback job number:', fallback);
    return fallback;
  }
}

/**
 * Peek at the counter document (for debugging).
 */
export async function peekCounter() {
  try {
    return await wixData.get(CN.COLL, CN.ID, OPTS);
  } catch (e) {
    return { error: String(e) };
  }
}

/**
 * Reset counter to a specific value (Admin only).
 */
export async function resetCounter(newValue) {
  await assertAdmin();

  if (typeof newValue !== 'number' || !Number.isFinite(newValue)) {
    throw new Error('Counter value must be a finite number');
  }
  newValue = Math.floor(newValue);
  if (newValue < 0) {
    throw new Error('Counter value must be a non-negative number');
  }

  try {
    await ensureCounterDoc();
    const doc = await wixData.get(CN.COLL, CN.ID, OPTS);

    const updatedDoc = {
      ...doc,
      [CN.F.CUR]: newValue,
      [CN.F.UPDATED]: new Date()
      // Intentionally leaving totalTickets unchanged
    };

    const saved = await wixData.update(CN.COLL, updatedDoc, OPTS);
    console.log('Counter reset to:', newValue);
    return saved;
  } catch (e) {
    console.error('Failed to reset counter:', e);
    throw new Error('Failed to reset counter');
  }
}


// backend/lru.js
// Lightweight in-memory LRU cache with TTL for Velo backend instances.
// NOTE: Cache is per-instance and ephemeral. Use short TTLs (5–30s) for API result caching.

/**
 * Stable stringify that sorts object keys recursively so that
 * logically identical objects produce identical strings.
 */
export function stableStringify(value) {
  const seen = new WeakSet();
  const serialize = (v) => {
    if (v === null || typeof v !== 'object') return JSON.stringify(v);
    if (seen.has(v)) return '"[Circular]"';
    seen.add(v);
    if (Array.isArray(v)) return '[' + v.map(serialize).join(',') + ']';
    const keys = Object.keys(v).sort();
    return '{' + keys.map(k => JSON.stringify(k) + ':' + serialize(v[k])).join(',') + '}';
  };
  return serialize(value);
}

// Simple LRU with TTL
export function createLRU({ max = 200, ttl = 10000 } = {}) {
  const store = new Map(); // key -> { value, expireAt }
  function isExpired(rec) {
    return rec && rec.expireAt !== 0 && Date.now() > rec.expireAt;
  }
  function get(key) {
    const rec = store.get(key);
    if (!rec) return undefined;
    if (isExpired(rec)) {
      store.delete(key);
      return undefined;
    }
    // bump recency
    store.delete(key);
    store.set(key, rec);
    return rec.value;
  }
  function set(key, value) {
    const exp = ttl > 0 ? Date.now() + ttl : 0;
    if (store.has(key)) store.delete(key);
    store.set(key, { value, expireAt: exp });
    // evict oldest if over max
    while (store.size > max) {
      const oldestKey = store.keys().next().value;
      if (oldestKey === undefined) break;
      store.delete(oldestKey);
    }
  }
  function clear() { store.clear(); }
  function size() { return store.size; }
  return { get, set, clear, size };
}

// Singleton caches by namespace (per backend instance)
const __caches = new Map();
export function getCache(namespace = 'default', options = {}) {
  if (__caches.has(namespace)) return __caches.get(namespace);
  const cache = createLRU(options);
  __caches.set(namespace, cache);
  return cache;
}

// Helper to make a stable key from multiple inputs
export function stableKey(...parts) {
  return parts.map(p => (typeof p === 'string' ? p : stableStringify(p))).join('|');
}

// dont edit this (frank) this purges collection > submit log once a week, sunday 2am

// backend/maintenance.jsw
import wixData from 'wix-data';

const COLLECTION = 'SubmitLog';
const OPTS = { suppressAuth: true, suppressHooks: true };

/**
 * Purge SubmitLog rows older than `days` (default 3 days).
 *
 * SCHEDULING (set in Wix β†’ Dev Mode β†’ Backend β†’ Scheduled Jobs UI):
 * - Frequency: Weekly
 * - Day: Sunday
 * - Time: 02:00 (site timezone)
 * - Parameters (JSON): { "days": 3, "batchSize": 200 }
 *
 * NOTE: The schedule itself is configured in the Scheduled Jobs UI, not in code.
 *
 * @param {{ days?: number, batchSize?: number }} [options]
 * @returns {Promise<{deleted:number, cutoff: Date}>}
 */
export async function purgeOldSubmitLogs(options = {}) {
  const days = Number.isFinite(options.days) ? Number(options.days) : 3;
  const batchSize = Number.isFinite(options.batchSize) ? Math.min(Math.max(1, options.batchSize), 1000) : 200;

  const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
  let deleted = 0;

  let page = await wixData.query(COLLECTION)
    .lt('ts', cutoff)
    .ascending('ts')
    .limit(batchSize)
    .find(OPTS);

  while (page.items.length) {
    const ids = page.items.map(i => i._id).filter(Boolean);
    if (ids.length) {
      const chunkSize = 50;
      for (let i = 0; i < ids.length; i += chunkSize) {
        const chunk = ids.slice(i, i + chunkSize);
        await Promise.all(chunk.map(id => wixData.remove(COLLECTION, id, OPTS).catch(() => null)));
      }
      deleted += ids.length;
    }

    if (page.hasNext()) {
      page = await page.next();
    } else {
      break;
    }
  }

  console.log(`[maintenance] Purged ${deleted} SubmitLog rows older than ${days} days (cutoff ${cutoff.toISOString()})`);
  return { deleted, cutoff };
}

/**
 * Helper: returns the recommended Scheduled Job configuration.
 * This is informational; you still set it in the Scheduled Jobs UI.
 */
export function getRecommendedPurgeJobConfig() {
  return {
    frequency: 'WEEKLY',
    day: 'SUNDAY',
    time: '02:00',
    params: { days: 3, batchSize: 200 }
  };
}


// backend/members.jsw
// Robust members helpers: current member details, role checks, and mechanics listing
// Patches:
//  - Adds tiny in-memory TTL cache for getMechanics() to reduce DB scans
//  - Returns both `id` and `_id` from getMemberDetails() for downstream compatibility
//  - Small defensive fixes & consistent trimming

import wixData from 'wix-data';
import wixUsersBackend from 'wix-users-backend';
import { contacts } from 'wix-crm-backend';

/* -------------------- small utils -------------------- */
function firstStr(...candidates) {
  for (const c of candidates) {
    if (typeof c === 'string' && c.trim()) return c.trim();
  }
  return '';
}
function s(v) { return (typeof v === 'string' ? v : '').trim(); }

function extractPhoneFromContact(contact) {
  const arr = Array.isArray(contact?.info?.phones) ? contact.info.phones : [];
  if (!arr.length) return '';

  // Prefer a "mobile/cell" tagged entry if available
  const mobile = arr.find(
    (p) => typeof p === 'object' && typeof p.tag === 'string' && /mobile|cell/i.test(p.tag)
  );
  if (mobile && typeof mobile.phone === 'string' && mobile.phone.trim()) {
    return mobile.phone.trim();
  }

  // Normalize various possible shapes
  const normalized = arr
    .map((p) => {
      if (typeof p === 'string') return p;
      if (p && typeof p.phone === 'string') return p.phone;
      if (p && typeof p.number === 'string') return p.number;
      if (p && typeof p.value === 'string') return p.value;
      return '';
    })
    .filter(Boolean);

  return normalized[0] || '';
}

function normalizeDisplayName({ contact, loginEmail, userRecord }) {
  const contactFirst = contact?.info?.name?.first;
  const contactLast = contact?.info?.name?.last;
  const contactFull = contact?.info?.name?.formattedName;

  const fullFromContact =
    firstStr(contactFull) || firstStr([contactFirst, contactLast].filter(Boolean).join(' '));

  // Avoid using userRecord.profile (not on RetrievedUser). Stick to generic fields if present.
  const userNick = firstStr(userRecord?.nickname);
  const userName = firstStr(userRecord?.name);
  const fullFromUser = firstStr(userNick, userName);

  return firstStr(fullFromContact, fullFromUser, loginEmail);
}

/* -------------------- current member details -------------------- */
/**
 * Returns current member details or null.
 * Shape: { id, _id, firstName, lastName, fullName, phone, email, roles: [{ id, name, description }] }
 */
export async function getMemberDetails() {
  try {
    const user = wixUsersBackend.currentUser;
    if (!user?.loggedIn) return null;

    // Base user record (for loginEmail + generic fallbacks)
    let userRecord = null;
    try {
      userRecord = await wixUsersBackend.getUser(user.id);
    } catch (_) {
      userRecord = null;
    }
    const loginEmail = userRecord?.loginEmail || '';

    // Resolve CRM contact via contacts API (by email)
    let contact = null;
    try {
      if (loginEmail) {
        const q = await contacts
          .queryContacts()
          .eq('info.emails.email', loginEmail)
          .limit(1)
          .find();
        contact = q?.items?.[0] || null;
      }
    } catch (_) {
      contact = null;
    }

    const firstName = firstStr(contact?.info?.name?.first);
    const lastName = firstStr(contact?.info?.name?.last);
    const fullName = normalizeDisplayName({ contact, loginEmail, userRecord });
    const phone = extractPhoneFromContact(contact);

    // Roles for the current user (safe mapping)
    let memberRoles = [];
    try {
      memberRoles = await user.getRoles();
    } catch (_) {
      memberRoles = [];
    }

    // Stable role identifier: use role name
    const rolesOut = (memberRoles || []).map((r) => ({
      id: String(r?.name || ''),
      name: String(r?.name || ''),
      description: String(r?.description || ''),
    }));

    // Include both id and _id for downstream code compatibility
    return {
      id: user.id,
      _id: user.id,
      firstName,
      lastName,
      fullName,
      phone,
      email: loginEmail,
      roles: rolesOut,
    };
  } catch (err) {
    console.error('[members.jsw] getMemberDetails error:', err);
    return null;
  }
}

export async function userHasRole(roleName) {
  try {
    const user = wixUsersBackend.currentUser;
    if (!user?.loggedIn) return false;

    let memberRoles = [];
    try {
      memberRoles = await user.getRoles();
    } catch (_) {
      memberRoles = [];
    }

    const want = String(roleName || '').trim().toLowerCase();
    if (!want) return false;

    return (memberRoles || []).some((r) => String(r?.name || '').trim().toLowerCase() === want);
  } catch (err) {
    console.error(`[members.jsw] userHasRole("${roleName}") error:`, err);
    return false;
  }
}

export async function getUserRoles() {
  try {
    const user = wixUsersBackend.currentUser;
    if (!user?.loggedIn) return [];
    let memberRoles = [];
    try {
      memberRoles = await user.getRoles();
    } catch (_) {
      memberRoles = [];
    }
    // Stable role identifier: use role name
    return (memberRoles || []).map((r) => ({
      id: String(r?.name || ''),
      name: String(r?.name || ''),
      description: String(r?.description || ''),
    }));
  } catch (err) {
    console.error('[members.jsw] getUserRoles error:', err);
    return [];
  }
}

/* -------------------- Mechanics listing -------------------- */
/**
 * Returns mechanics in the site as [{ id, name }].
 * Strategy:
 * 1) Try PrivateMembersData heuristics (fields some sites keep, e.g. "role" or "isMechanic")
 * 2) Fallback: scan Tickets assignee history (id/name pairs)
 * 3) If still nothing, return []
 */
const MEMBERS_COLLECTION = 'Members/PrivateMembersData';
const TICKETS_COLLECTION = 'Tickets';
const DATA_OPTS = { suppressAuth: true };

// Tiny per-instance cache to avoid repeated scans (TTL 120s)
const cache = {
  mechanics: { ts: 0, data: [] }
};

function getNow() { return Date.now(); }
function cacheGet(k) {
  const ent = cache[k];
  if (!ent) return undefined;
  if (getNow() - ent.ts > 120000) return undefined; // 2 minutes
  return ent.data;
}
function cacheSet(k, data) {
  cache[k] = { ts: getNow(), data };
}

export async function getMechanics() {
  const cached = cacheGet('mechanics');
  if (cached) return cached;

  // 1) Heuristic: PrivateMembersData
  try {
    let res = await wixData.query(MEMBERS_COLLECTION).limit(1000).find(DATA_OPTS);
    const items = [...(res.items || [])];

    while (res.hasNext()) {
      res = await res.next();
      items.push(...(res.items || []));
      if (items.length >= 5000) break;
    }

    const looksLikeMechanic = (m) => {
      const roleStr = firstStr(
        m?.role,
        Array.isArray(m?.roles) ? m.roles.join(',') : '',
        Array.isArray(m?.roleNames) ? m.roleNames.join(',') : ''
      ).toLowerCase();
      const boolFlag = m?.isMechanic === true;
      return boolFlag || /mechanic/i.test(roleStr);
    };

    const out = [];
    const seen = new Set();

    for (const m of items) {
      if (!looksLikeMechanic(m)) continue;

      const id = String(m?._id || m?.memberId || m?.userId || '').trim();
      const name =
        firstStr(
          [m?.firstName, m?.lastName].filter(Boolean).join(' '),
          m?.fullName,
          m?.nickname,
          m?.name
        ) || '';

      if (id && name && !seen.has(id)) {
        out.push({ id, name });
        seen.add(id);
      }
    }

    if (out.length) {
      out.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
      cacheSet('mechanics', out);
      return out;
    }
  } catch (err) {
    // Log and continue to fallback
    console.warn('[members.jsw] PrivateMembersData scan failed; falling back:', err?.message);
  }

  // 2) Fallback: scan Tickets for historical assignees
  try {
    let res = await wixData.query(TICKETS_COLLECTION).limit(1000).find(DATA_OPTS);
    const items = [...(res.items || [])];

    while (res.hasNext()) {
      res = await res.next();
      items.push(...(res.items || []));
      if (items.length >= 5000) break;
    }

    const map = new Map();
    for (const t of items) {
      const id = (t?.assignedMechanicId || '').trim();
      const nm = (t?.assignedMechanicName || '').trim();
      if (!id) continue;
      if (!map.has(id)) map.set(id, nm || id);
    }

    const out = Array.from(map.entries())
      .map(([id, name]) => ({ id, name }))
      .filter((m) => m.id && m.name);

    out.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
    if (out.length) {
      cacheSet('mechanics', out);
      return out;
    }
  } catch (err) {
    console.warn('[members.jsw] Tickets scan fallback failed:', err?.message);
  }

  // 3) Nothing found
  cacheSet('mechanics', []);
  return [];
}

// Optional: explicit alias expected by some dashboard code
export async function listMechanicsByRole() {
  return getMechanics();
}

// backend/tickets.jsw
// Tickets service (validation, assignments, status changes, helpers)
//
// This version is aligned with your current file and adds:
//  - Optional rate limiting (SubmitLog collection) for createTicket (toggle RATE_LIMIT below)
//  - Typed httpError() helper to attach httpStatus/code safely (fixes 'status does not exist on Error' warning)
//  - Small comments/formatting; functional logic preserved
//
// Prereqs (if RATE_LIMIT=true):
//  - Data collection: SubmitLog (fields: userId: text, kind: text, ts: datetime)
//  - Index: (userId ASC, kind ASC, ts DESC)
//
import wixData from 'wix-data';
import wixUsersBackend from 'wix-users-backend';
import { getMemberDetails, getMechanics as getMechanicsFromMembers } from 'backend/members.jsw';
import { getNextJobNumberRaw } from 'backend/jobNumberModule.jsw';
import { toDate, normalizeDates } from 'backend/dateUtil.js';

const TICKETS = 'Tickets';
const OPTS = { suppressAuth: true, suppressHooks: false };

const STATUSES = [
  'Not Yet Started',
  'Underway',
  'Parts Required',
  'Ordered',
  'Order Required', // legacy alias retained, normalized in validateStatus()
  'Parts Ready',
  'Ready for Review',
  'Complete'
];

const VALID_PRIORITIES = ['Low', 'Medium', 'Urgent'];
const ALLOWED_LOCATIONS = ['Adare', 'Berrings', 'Celtic', 'Glanmire', 'Killybegs', 'Fossa', 'Ringaskiddy'];

// ---- Error helper (avoids 'status does not exist on Error') ----
/**
 * Create an Error and attach httpStatus/statusCode/code in a way Velo accepts.
 * @param {string} message
 * @param {number} statusCode
 * @param {string} [code]
 * @returns {Error & { httpStatus?: number, statusCode?: number, code?: string }}
 */
function httpError(message, statusCode, code) {
  const err = new Error(message);
  /** @type {any} */ (err).httpStatus = statusCode;
  /** @type {any} */ (err).statusCode = statusCode;
  if (code) { /** @type {any} */ (err).code = code; }
  return err;
}

// ---- Optional lightweight rate limiting (set to false to disable) ----
const RATE_LIMIT = true;
const SUBMITLOG = 'SubmitLog';
async function rateLimitGuard(kind = 'createTicket', windowMs = 60000, max = 5) {
  if (!RATE_LIMIT) return;
  try {
    const cur = wixUsersBackend.currentUser;
    const userId = cur?.id || 'anonymous';
    const since = new Date(Date.now() - windowMs);
    await wixData.insert(SUBMITLOG, { userId, kind, ts: new Date() }, OPTS).catch(() => {});
    const count = await wixData
      .query(SUBMITLOG)
      .eq('userId', userId)
      .eq('kind', kind)
      .gt('ts', since)
      .count()
      .catch(() => 0);
    if (count > max) {
      throw httpError('Too many requests, please slow down.', 429, 'RATE_LIMIT');
    }
  } catch (_) {
    // Never block on limiter errors
  }
}

// -------------------- validators & utils --------------------
function validateString(value, fieldName, required = true, maxLength = null) {
  const str = typeof value === 'string' ? value.trim() : '';
  if (required && !str) throw new Error(`${fieldName} is required`);
  if (str && maxLength && str.length > maxLength) throw new Error(`${fieldName} must be ${maxLength} characters or less`);
  return str;
}

// Canonicalize a few legacy/alias inputs
function validateStatus(status) {
  let s = typeof status === 'string' ? status.trim() : status;

  if (typeof s === 'string') {
    const low = s.toLowerCase();
    if (low === 'order required') s = 'Parts Required'; // legacy alias -> canonical
    if (low === 'ready') s = 'Parts Ready'; // chip label alias -> canonical
  }

  if (s && !STATUSES.includes(s)) throw new Error(`Invalid status. Must be one of: ${STATUSES.join(', ')}`);
  return s || 'Not Yet Started';
}

function validatePriority(priority) {
  // Return undefined for blank so callers can be non-destructive.
  if (priority === undefined || priority === null || String(priority).trim() === '') return undefined;
  const p = String(priority).trim();
  if (!VALID_PRIORITIES.includes(p)) throw new Error(`Invalid priority. Must be one of: ${VALID_PRIORITIES.join(', ')}`);
  return p;
}

function validateLocation(value) {
  const str = validateString(value, 'Plant Location', true, 100);
  const match = ALLOWED_LOCATIONS.find((v) => v.toLowerCase() === str.toLowerCase());
  if (!match) throw new Error(`Invalid plant location. Choose one of: ${ALLOWED_LOCATIONS.join(', ')}`);
  return match;
}

async function getUserContext() {
  const user = wixUsersBackend.currentUser;
  const loggedIn = !!user?.loggedIn;

  let rolesArr = [];
  if (loggedIn) {
    try { rolesArr = await user.getRoles(); } catch (_) { rolesArr = []; }
  }
  const names = rolesArr.map((r) => (r?.name || '').toLowerCase());
  const isAdmin = names.includes('admin');
  const isMechanic = names.includes('mechanic');

  let id = user?.id || '';
  let name = '';
  try {
    const m = await getMemberDetails();
    name = m?.fullName || m?.email || '';
    if (!id && m?.id) id = m.id;
  } catch (_) {}

  return { loggedIn, id, name, isAdmin, isMechanic, roleNames: names };
}

/** Admins OR collaborator/manager/editor/owner type roles can edit PO */
async function canEditPOUser() {
  const u = wixUsersBackend.currentUser;
  if (!u?.loggedIn) return false;

  let rolesArr = [];
  try { rolesArr = await u.getRoles(); } catch (_) { rolesArr = []; }

  const names = (rolesArr || [])
    .map(r => String(r?.name || '').trim().toLowerCase())
    .filter(Boolean);

  if (names.includes('admin')) return true;

  const allowExact = new Set([
    'collaborator','site collaborator','website manager','content manager','store manager',
    'customer service manager','marketing manager','seo manager','blog editor',
    'back office manager','billing manager','editor','owner','site owner','co-owner',
    'site admin','administrator'
  ]);
  const broad = (n) => n.includes('collab') || n.includes('manager') || n.includes('editor') || n.includes('owner') || n.includes('admin');

  return names.some(n => allowExact.has(n) || broad(n));
}

export async function canUserEditPO() {
  return !!(await canEditPOUser());
}

// -------------------- idempotency helpers --------------------
async function findByIdempotencyKey(key) {
  const k = typeof key === 'string' ? key.trim() : '';
  if (!k) return null;
  try {
    const res = await wixData.query(TICKETS)
      .eq('idempotencyKey', k)
      .descending('_createdDate')
      .limit(1)
      .find();
    return (res.items && res.items[0]) || null;
  } catch (e) {
    // Don't break create on read failure
    return null;
  }
}

// -------------------- create ticket --------------------
export async function createTicket(payload) {
  if (!payload || typeof payload !== 'object') throw new Error('Invalid ticket payload');

  // Fast idempotency check (before number allocation)
  const existing = await findByIdempotencyKey(payload.idempotencyKey);
  if (existing) return existing;

  // Optional rate limit
  await rateLimitGuard('createTicket', 60000, 5);

  const u = await getUserContext();

  const title = validateString(payload.title, 'Title', true, 200);
  const plantLocation = validateLocation(payload.plantLocation);
  const vehicleSerial = validateString(payload.vehicleSerial, 'Vehicle Serial', false, 50);
  const vehicleModel = validateString(payload.vehicleModel, 'Vehicle Model', false, 100);
  const plantNumber = validateString(payload.plantNumber, 'Plant Number', false, 50);
  const complaintNotes = validateString(payload.complaintNotes, 'Complaint', false, 5000);
  let status = validateStatus(payload.status);
  const priorityMaybe = validatePriority(payload.priority);
  const poNumber = validateString(payload.poNumber, 'PO Number', false, 50);

  let briefDescription = validateString(payload.briefDescription, 'Brief Description', false, 500);
  if (!briefDescription) briefDescription = 'Brief Description';

  const wantsSelfAssign = !!payload.assignToMe;

  const PROTECTED_KEYS = new Set([
    'submissionDate','createdByID','createdByName','createdByRole',
    'jobCartNumber','lastStatusChangeAt','finishDate','completedById','completedByName',
    'assignedMechanicId','assignedMechanicName','assignedAt','_id','_rev','assignToMe'
  ]);

  const SAFE_EXTRA = Object.fromEntries(
    Object.entries(payload).filter(([key]) =>
      ![
        'title','plantLocation','vehicleSerial','vehicleModel','plantNumber',
        'complaintNotes','priority','status','briefDescription','poNumber','assignToMe',
        'reporterName'
      ].includes(key) && !PROTECTED_KEYS.has(key)
    )
  );

  const nowDt = new Date();

  let assignedMechanicId = '';
  let assignedMechanicName = '';
  let assignedAt = null;

  // If user self-assigns and status is NYS / Parts Ready (alias "ready"), flip to Underway
  if (u.isMechanic && wantsSelfAssign) {
    assignedMechanicId = u.id;
    assignedMechanicName = u.name || 'Mechanic';
    assignedAt = nowDt;
    const stLow = String(status || '').toLowerCase();
    if (status === 'Not Yet Started' || status === 'Parts Ready' || stLow === 'ready') {
      status = 'Underway';
    }
  }

  /** @type {any} */
  const item = {
    title,
    plantLocation,
    vehicleSerial,
    vehicleModel,
    plantNumber,
    complaintNotes,
    priority: priorityMaybe ?? 'Medium',
    status,
    briefDescription,
    poNumber,
    submissionDate: nowDt,
    createdByID: u.id || '',
    createdByName: (u.name || validateString(payload.reporterName, 'Reporter Name', false, 200) || ''),
    createdByRole: u.isAdmin ? 'Admin' : (u.isMechanic ? 'Mechanic' : 'Driver'),
    lastStatusChangeAt: nowDt,

    assignedMechanicId,
    assignedMechanicName,
    assignedAt,

    ...SAFE_EXTRA
  };

  // If created directly as Ready for Review or Complete:
  // - set finishDate + completedBy fields
  // - ensure it's assigned to the creator if nobody is assigned yet (mech OR admin)
  if (item.status === 'Ready for Review' || item.status === 'Complete') {
    item.finishDate = nowDt;
    item.completedById = u.id || '';
    item.completedByName = u.name || '';

    if (!item.assignedMechanicId && (u.isMechanic || u.isAdmin)) {
      item.assignedMechanicId = u.id || '';
      item.assignedMechanicName = u.name || '';
      item.assignedAt = nowDt;
    }
  }

  normalizeDates(item, ['submissionDate','lastStatusChangeAt','assignedAt','finishDate']);

  try {
    const num = await getNextJobNumberRaw();
    item.jobCartNumber = Number(num);
  } catch (_) {
    item.jobCartNumber = Number(String(Date.now()).slice(-7));
  }

  if (!Array.isArray(item.attachment)) item.attachment = [];

  // Store idempotencyKey if provided (already included via SAFE_EXTRA)
  try {
    const saved = await wixData.insert(TICKETS, item, OPTS);

    const needsFix =
      !(saved.submissionDate instanceof Date) ||
      typeof saved.submissionDate === 'string' ||
      (saved.lastStatusChangeAt && (!(saved.lastStatusChangeAt instanceof Date) || typeof saved.lastStatusChangeAt === 'string')) ||
      (saved.assignedAt && (!(saved.assignedAt instanceof Date) || typeof saved.assignedAt === 'string')) ||
      (saved.finishDate && (!(saved.finishDate instanceof Date) || typeof saved.finishDate === 'string'));

    if (needsFix) {
      /** @type {any} */
      const patch = {
        _id: saved._id,
        submissionDate: toDate(saved.submissionDate) || nowDt,
        lastStatusChangeAt: toDate(saved.lastStatusChangeAt) || nowDt
      };
      if (saved.assignedAt != null) patch.assignedAt = toDate(saved.assignedAt) || null;
      if (saved.finishDate != null) patch.finishDate = toDate(saved.finishDate) || null;

      await wixData.update(TICKETS, patch, OPTS);
    }

    return saved;
  } catch (e) {
    // If we failed to insert (possible race), try to return the existing idempotent doc
    const dup = await findByIdempotencyKey(item.idempotencyKey);
    if (dup) return dup;

    console.error('Failed to create ticket:', {
      message: e?.message,
      name: e?.name,
      httpStatus: e?.httpStatus,
      status: e?.status,
      stack: e?.stack
    });
    throw new Error(`createTicket failed: ${e?.message || 'Unknown error'}`);
  }
}

// -------------------- accept assignment --------------------
// Flip Parts Ready/Ready -> Underway on accept (in addition to Not Yet Started)
export async function acceptTicket(ticketId) {
  if (!ticketId || typeof ticketId !== 'string') throw new Error('Valid ticket ID is required');
  const u = await getUserContext();
  if (!u.isMechanic) throw new Error('Only Mechanics can accept tickets');

  let ticket;
  try {
    ticket = await wixData.get(TICKETS, ticketId, OPTS);
  } catch (e) {
    console.error('Failed to get ticket:', e);
    throw new Error('Ticket not found');
  }

  if (ticket.assignedMechanicId) throw new Error('Ticket is already assigned');
  if (ticket.status === 'Complete') throw new Error('Cannot accept completed tickets');

  const curr = String(ticket.status || '').trim();
  const currLow = curr.toLowerCase();
  const shouldFlip = curr === 'Not Yet Started' || curr === 'Parts Ready' || currLow === 'ready';

  /** @type {any} */
  const updates = {
    ...ticket,
    assignedMechanicId: u.id,
    assignedMechanicName: u.name || 'Mechanic',
    assignedAt: new Date()
  };
  if (shouldFlip) {
    updates.status = 'Underway';
    updates.lastStatusChangeAt = new Date();
  }

  normalizeDates(updates, ['assignedAt','lastStatusChangeAt']);

  try {
    return await wixData.update(TICKETS, updates, OPTS);
  } catch (e) {
    console.error('Failed to accept ticket:', e);
    throw new Error('Failed to accept ticket. Please try again.');
  }
}

// -------------------- admin assign/unassign --------------------
export async function assignTicketAdmin(ticketId, mechanicId, mechanicName) {
  if (!ticketId || typeof ticketId !== 'string') throw new Error('Valid ticket ID is required');
  if (!mechanicId || typeof mechanicId !== 'string') throw new Error('Valid mechanic ID is required');

  const u = await getUserContext();
  if (!u.isAdmin) throw new Error('Only Admin can assign tickets');

  let ticket;
  try {
    ticket = await wixData.get(TICKETS, ticketId, OPTS);
  } catch (e) {
    console.error('Failed to get ticket:', e);
    throw new Error('Ticket not found');
  }

  if (ticket.status === 'Complete') throw new Error('Cannot reassign completed tickets');

  /** @type {any} */
  const updates = {
    ...ticket,
    assignedMechanicId: mechanicId,
    assignedMechanicName: validateString(mechanicName, 'Mechanic Name', false, 100) || 'Mechanic',
    assignedAt: new Date()
  };
  normalizeDates(updates, ['assignedAt']);

  try {
    return await wixData.update(TICKETS, updates, OPTS);
  } catch (e) {
    console.error('Failed to assign ticket:', e);
    throw new Error('Failed to assign ticket. Please try again.');
  }
}

export async function unassignTicketAdmin(ticketId) {
  if (!ticketId || typeof ticketId !== 'string') throw new Error('Valid ticket ID is required');
  const u = await getUserContext();
  if (!u.isAdmin) throw new Error('Only Admin can unassign tickets');

  let ticket;
  try {
    ticket = await wixData.get(TICKETS, ticketId, OPTS);
  } catch (e) {
    console.error('Failed to get ticket:', e);
    throw new Error('Ticket not found');
  }

  if (ticket.status === 'Complete') throw new Error('Cannot unassign completed tickets');
  if (!ticket.assignedMechanicId && !ticket.assignedMechanicName) return ticket;

  /** @type {any} */
  const updates = { ...ticket, assignedMechanicId: '', assignedMechanicName: '', assignedAt: null };

  try {
    return await wixData.update(TICKETS, updates, OPTS);
  } catch (e) {
    console.error('Failed to unassign ticket:', e);
    throw new Error('Failed to unassign ticket. Please try again.');
  }
}

// -------------------- status updates --------------------
export async function updateTicketStatus(ticketId, newStatus) {
  if (!ticketId || typeof ticketId !== 'string') throw new Error('Valid ticket ID is required');
  const validatedStatus = validateStatus(newStatus);
  const u = await getUserContext();

  const isCompleting = validatedStatus === 'Complete';
  const isReadyForReview = validatedStatus === 'Ready for Review';

  if (isCompleting && !u.isAdmin) throw new Error('Only Admin can mark tickets as Complete');
  if (!u.isAdmin && !u.isMechanic) throw new Error('Only Admin or Mechanic can update ticket status');

  let ticket;
  try {
    ticket = await wixData.get(TICKETS, ticketId, OPTS);
  } catch (e) {
    console.error('Failed to get ticket:', e);
    throw new Error('Ticket not found');
  }

  if (ticket.status === 'Complete' && !u.isAdmin) throw new Error('Cannot modify completed tickets');
  if (!u.isAdmin && ticket.assignedMechanicId !== u.id) throw new Error('Can only update status of tickets assigned to you');

  /** @type {any} */
  const updates = { ...ticket };

  if (validatedStatus !== ticket.status) {
    updates.status = validatedStatus;
    updates.lastStatusChangeAt = new Date();
  }

  if ((isReadyForReview || isCompleting) && !ticket.finishDate) {
    updates.finishDate = new Date();
    updates.completedById = u.id || '';
    updates.completedByName = u.name || '';
  }

  normalizeDates(updates, ['lastStatusChangeAt','finishDate']);

  try {
    return await wixData.update(TICKETS, updates, OPTS);
  } catch (e) {
    console.error('Failed to update ticket status:', e);
    throw new Error('Failed to update ticket status. Please try again.');
  }
}

// -------------------- PO updates --------------------
/**
 * Admin/Collaborator full control of PO (add/edit/clear).
 */
export async function updatePurchaseOrderAdmin(ticketId, poNumber) {
  if (!ticketId || typeof ticketId !== 'string') throw new Error('Valid ticket ID is required');
  const ok = await canEditPOUser();
  if (!ok) throw new Error('Only Admin or Collaborator can update the PO Number');

  let ticket;
  try {
    ticket = await wixData.get(TICKETS, ticketId, OPTS);
  } catch (e) {
    console.error('Failed to get ticket:', e);
    throw new Error('Ticket not found');
  }

  const cleanedPO = validateString(poNumber, 'PO Number', false, 50);

  /** @type {any} */
  const updates = { ...ticket, poNumber: cleanedPO };

  try {
    return await wixData.update(TICKETS, updates, OPTS);
  } catch (e) {
    console.error('Failed to update PO Number:', e);
    throw new Error('Failed to update PO Number. Please try again.');
  }
}

// -------------------- helpers --------------------
export async function getUniqueLocations() {
  try {
    const result = await wixData.query(TICKETS)
      .isNotEmpty('plantLocation')
      .ascending('plantLocation')
      .limit(1000)
      .find();
    const locations = [...new Set(
      (result.items || [])
        .map((item) => item.plantLocation)
        .filter((loc) => typeof loc === 'string' && loc.trim())
        .map((loc) => loc.trim())
    )].sort();
    return locations;
  } catch (e) {
    console.error('Failed to get unique locations:', e);
    return [];
  }
}

export async function getTicket(ticketId) {
  if (!ticketId || typeof ticketId !== 'string') throw new Error('Valid ticket ID is required');
  try {
    return await wixData.get(TICKETS, ticketId, OPTS);
  } catch (e) {
    console.error('Failed to get ticket:', e);
    throw new Error('Ticket not found');
  }
}

/**
 * General update with validation & permission checks.
 * Admins: can edit fields incl. priority and PO.
 * Mechanics: must be assigned to edit. When ticket is Complete, they may edit ONLY the briefDescription.
 * PO rule: may ADD a PO if the current PO is blank; may NOT change or clear an existing PO.
 * Cannot mark Complete.
 */
export async function updateTicket(ticketId, updates) {
  if (!ticketId || typeof ticketId !== 'string') throw new Error('Valid ticket ID is required');
  if (!updates || typeof updates !== 'object') throw new Error('Updates object is required');

  const u = await getUserContext();
  if (!u.loggedIn) throw new Error('User must be logged in');

  let ticket;
  try {
    ticket = await wixData.get(TICKETS, ticketId, OPTS);
  } catch (e) {
    console.error('Failed to get ticket:', e);
    throw new Error('Ticket not found');
  }

  const isComplete = ticket.status === 'Complete';

  // Base permission: Admins can edit; non-admins must be assigned to the ticket.
  if (!u.isAdmin && ticket.assignedMechanicId !== u.id) {
    throw new Error('Insufficient permissions to update this ticket');
  }

  const ALLOWED_FIELDS = new Set([
    'status','title','plantLocation','vehicleSerial','vehicleModel','plantNumber',
    'complaintNotes','briefDescription','priority','attachment','poNumber'
  ]);

  const filteredUpdates = Object.fromEntries(
    Object.entries(updates).filter(([k]) => ALLOWED_FIELDS.has(k))
  );

  // When ticket is Complete, only Admin can edit all fields; mechanic can edit only briefDescription.
  if (isComplete && !u.isAdmin) {
    const allowedOnComplete = new Set(['briefDescription']);
    const attempted = Object.keys(filteredUpdates);
    if (attempted.length === 0) {
      // nothing to do
    } else if (attempted.some(k => !allowedOnComplete.has(k))) {
      throw new Error('This ticket is Complete. Only the Brief Description can be edited.');
    }
  }

  // Validate & enforce status rules
  if (filteredUpdates.status !== undefined) {
    filteredUpdates.status = validateStatus(filteredUpdates.status);
    if (filteredUpdates.status === 'Complete' && !u.isAdmin) {
      throw new Error('Only Admin can mark tickets as Complete');
    }
  }

  // Normalize location
  if (filteredUpdates.plantLocation !== undefined) {
    filteredUpdates.plantLocation = validateLocation(filteredUpdates.plantLocation);
  }

  // String fields
  const stringFields = {
    title: { maxLength: 200 },
    plantLocation: { maxLength: 100 },
    vehicleSerial: { maxLength: 50 },
    vehicleModel: { maxLength: 100 },
    plantNumber: { maxLength: 50 },
    complaintNotes: { maxLength: 5000 },
    briefDescription: { maxLength: 500 },
    poNumber: { maxLength: 50 }
  };
  for (const [field, cfg] of Object.entries(stringFields)) {
    if (filteredUpdates[field] !== undefined && field !== 'plantLocation') {
      filteredUpdates[field] = validateString(filteredUpdates[field], field, false, cfg.maxLength);
    }
  }

  // Priority (non-destructive when blank)
  if (filteredUpdates.priority !== undefined) {
    const pMaybe = validatePriority(filteredUpdates.priority);
    if (pMaybe === undefined) {
      delete filteredUpdates.priority; // don't overwrite existing priority with blank
    } else {
      filteredUpdates.priority = pMaybe;
    }
  }

  // Attachments
  if (filteredUpdates.attachment !== undefined && !Array.isArray(filteredUpdates.attachment)) {
    filteredUpdates.attachment = [];
  }

  // ---------- PO Number permission logic ----------
  if (filteredUpdates.poNumber !== undefined) {
    const desiredPO = filteredUpdates.poNumber; // already validated/trimmed
    const hadPOBefore = !!(ticket.poNumber && String(ticket.poNumber).trim());
    if (u.isAdmin) {
      // Admins can always set/clear via updateTicket as well
    } else {
      // Non-admin path (mechanic assigned due to base check above)
      // Allow ADD only if previously blank AND new value is non-empty.
      if (hadPOBefore) {
        throw new Error('Only Admin can edit an existing PO Number');
      }
      if (!desiredPO) {
        throw new Error('PO Number cannot be blank when adding');
      }
    }
  }
  // -----------------------------------------------

  /** @type {any} */
  const next = { ...ticket, ...filteredUpdates };

  if (filteredUpdates.status !== undefined && filteredUpdates.status !== ticket.status) {
    next.lastStatusChangeAt = new Date();
    if ((next.status === 'Ready for Review' || next.status === 'Complete') && !ticket.finishDate) {
      next.finishDate = new Date();
      next.completedById = u.id || '';
      next.completedByName = u.name || '';
    }
  }

  normalizeDates(next, ['lastStatusChangeAt','finishDate']);

  try {
    return await wixData.update(TICKETS, next, OPTS);
  } catch (e) {
    console.error('Failed to update ticket:', e);
    throw new Error('Failed to update ticket. Please try again.');
  }
}

// -- optional passthrough for legacy callers --
export async function getNextTicketNumberRaw() {
  return getNextJobNumberRaw();
}

// -------------------- mechanics list (no wix-members-backend) --------------------
export async function listMechanicsByRole() {
  try {
    const fromHelper = await getMechanicsFromMembers();
    if (Array.isArray(fromHelper) && fromHelper.length) {
      const seen = new Set();
      const out = fromHelper
        .map((m) => ({ id: String(m.id || m._id || ''), name: String(m.name || m.fullName || '').trim() }))
        .filter((m) => m.id && m.name && !seen.has(m.id) && seen.add(m.id))
        .sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
      if (out.length) return out;
    }
  } catch (e) {
    console.warn('getMechanicsFromMembers failed:', e);
  }

  // Fallback: scan Tickets for historical assignees
  try {
    const MAX = 1000;
    let res = await wixData.query(TICKETS).limit(MAX).find();
    const items = [...(res.items || [])];
    while (res.hasNext()) {
      res = await res.next();
      items.push(...(res.items || []));
      if (items.length >= 5000) break;
    }
    const map = new Map();
    for (const t of items) {
      const id = (t.assignedMechanicId || '').trim();
      const nm = (t.assignedMechanicName || '').trim();
      if (!id) continue;
      if (!map.has(id)) map.set(id, nm || id);
    }
    return Array.from(map.entries())
      .map(([id, name]) => ({ id, name }))
      .sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
  } catch (e) {
    console.error('listMechanicsByRole fallback failed:', e);
    return [];
  }
}



// Server-side filtering + sorting + paging for Tickets (with LRU caching)
// backend/tickets.read.jsw

import wixData from 'wix-data';
import wixUsersBackend from 'wix-users-backend';
import { getCache, stableKey, stableStringify } from 'backend/lru.js';

/** ============================ Config ============================ */
const COLLECTION = 'Tickets';

// Windowed scan for custom sorts (status/priority/myFirst/unassignedFirst)
const CHUNK_SIZE = 200;
const MAX_SCAN   = 3000;
const DEFAULT_LIMIT = 200;

// LRU cache config (per backend instance)
const CACHE = getCache('tickets.read', { max: 200, ttl: 10000 }); // 10s default

const ALLOWED_LOCATIONS = ['Adare','Berrings','Celtic','Glanmire','Killybegs','Fossa','Ringaskiddy'];
const LEGACY_LOCATIONS_VALUE = '__legacy__';

const WORKFLOW_ORDER = {
  'Parts Ready': 0,
  'Not Yet Started': 1,
  'Underway': 2,
  'Ordered': 3,
  'Parts Required': 4,
  'Ready for Review': 5,
  'Complete': 6
};

const PRIORITY_ORDER = { Urgent: 0, Medium: 1, Low: 2 };

const SEARCH_PATTERNS = {
  FIELD_QUERY: /^([a-z#]{1,10})\s*(?::|\s)\s*(.+)$/i,
  NUMERIC: /^\d+$/,
  HOURS_RANGE: /^(\d+(?:\.\d+)?)\s*\.\.\s*(\d+(?:\.\d+)?)/,
  HOURS_COMPARISON: /^(>=|<=|>|<|=)\s*(\d+(?:\.\d+)?)/i,
  WHITESPACE_NORMALIZE: /\s+/g
};

const STATUS_ALIASES = {
  nys: 'Not Yet Started',
  rfr: 'Ready for Review',
  ord: 'Ordered',
  review: 'Ready for Review',
  'parts ordered': 'Ordered',
  ready: 'Parts Ready',
  'parts req': 'Parts Required',
  pr: 'Parts Required',
  'order required': 'Parts Required'
};

const NORMALIZABLE_FIELDS = new Set([
  'vehicleSerial',
  'vehicleModel',
  'plantNumber',
  'poNumber',
  'briefDescription',
  'complaintNotes',
  'plantLocation',
  'status'
]);

/** ============================ Helpers (normalize-aware search) ============================ */
function canonicalStatus(text) {
  if (!text) return null;
  const t = String(text).trim().toLowerCase();
  const alias = STATUS_ALIASES[t];
  if (alias) return alias;
  const statuses = Object.keys(WORKFLOW_ORDER);
  const found = statuses.find((s) => s.toLowerCase() === t);
  return found || null;
}

function safeTokens(raw) {
  return String(raw || '')
    .split(/[,\n]+/)
    .map((t) => t.replace(/[^\w\s:#.<>=\-\/]/g, ''))
    .map((t) => t.replace(SEARCH_PATTERNS.WHITESPACE_NORMALIZE, ' ').trim())
    .filter(Boolean)
    .slice(0, 10);
}

function norm(v) {
  return String(v || '')
    .toLowerCase()
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .trim();
}

function containsSmart(field, rawTerm) {
  const raw = String(rawTerm || '').trim();
  if (!raw) return wixData.query(COLLECTION);
  const hasNorm = NORMALIZABLE_FIELDS.has(field);
  if (!hasNorm) {
    return wixData.query(COLLECTION).contains(field, raw);
  }
  const fieldNorm = `${field}_norm`;
  const normalized = norm(raw);
  return wixData
    .query(COLLECTION)
    .contains(fieldNorm, normalized)
    .or(wixData.query(COLLECTION).contains(field, raw));
}

function containsAnySmart(fields, term) {
  if (!fields || !fields.length) return wixData.query(COLLECTION);
  let qq = containsSmart(fields[0], term);
  for (let i = 1; i < fields.length; i++) {
    qq = qq.or(containsSmart(fields[i], term));
  }
  return qq;
}

/** ============================ Core API ============================ */
export async function queryTickets(filters = {}, sort = 'newest', skipArg = 0, limitArg = DEFAULT_LIMIT) {
  const fallbackSkip = Number(filters.skip ?? filters.offset ?? 0);
  const fallbackLimit = Number(filters.limit ?? filters.pageSize ?? 0);

  let skip = Number.isFinite(skipArg) && skipArg >= 0 ? skipArg : fallbackSkip;
  let limit = Number.isFinite(limitArg) && limitArg > 0 ? limitArg : fallbackLimit;

  skip = Math.max(0, Number(skip) || 0);
  limit = Math.max(1, Math.min(200, Number(limit) || DEFAULT_LIMIT));

  const viewerIdFromFilters = String(filters.viewerId || '').trim();
  const cu = wixUsersBackend.currentUser;
  const userId = viewerIdFromFilters || (cu && cu.loggedIn ? cu.id : '');

  // Build a stable cache key (include userId because assignment='my' depends on it)
  const cacheKey = stableKey('qt', { filters, sort, skip, limit, userId });

  const cached = CACHE.get(cacheKey);
  if (cached) return cached;

  let q = wixData.query(COLLECTION);

  const selectedStatusRaw = filters.selectedStatus || 'all';
  const selectedStatus = selectedStatusRaw === 'all'
    ? 'all'
    : (canonicalStatus(selectedStatusRaw) || selectedStatusRaw);

  const rawSearch = String(filters.search || '').trim();

  if (selectedStatus === 'all') {
    const wantCompleted = !!rawSearch || !!filters.includeCompleted || filters.assignment === 'allPlusCompleted';
    if (!wantCompleted) q = q.ne('status', 'Complete');
  } else {
    q = q.eq('status', selectedStatus);
  }

  const AL = ALLOWED_LOCATIONS;
  const loc = filters.location || 'all';
  if (loc !== 'all') {
    if (loc === LEGACY_LOCATIONS_VALUE) {
      let sub = wixData.query(COLLECTION).isNotEmpty('plantLocation');
      for (const val of AL) sub = sub.ne('plantLocation', val);
      q = q.and(sub);
    } else {
      q = q.eq('plantLocation', loc);
    }
  }

  const assignment = String(filters.assignment || 'all');
  if (assignment === 'my' && userId) {
    q = q.eq('assignedMechanicId', userId);
  } else if (assignment === 'unassigned') {
    const emptyQ = wixData.query(COLLECTION).isEmpty('assignedMechanicId');
    const blankQ = wixData.query(COLLECTION).eq('assignedMechanicId', '');
    q = q.and(emptyQ.or(blankQ));
  } else if (assignment.startsWith('mech:')) {
    const mechId = assignment.slice(5);
    if (mechId) q = q.eq('assignedMechanicId', mechId);
  }

  if (rawSearch) {
    const tokens = safeTokens(rawSearch);
    for (const tok of tokens) {
      const m = /^([a-z#]{1,10})\s*(?::|\s)\s*(.+)$/i.exec(tok);
      if (m) {
        const key = m[1].toLowerCase();
        const val = (m[2] || '').trim();
        switch (key) {
          case 'jc':
          case 'job':
          case 'job#': {
            const num = Number(val);
            q = q.and(
              Number.isFinite(num)
                ? wixData.query(COLLECTION).eq('jobCartNumber', num)
                : wixData.query(COLLECTION).eq('jobCartNumber', -1)
            );
            break;
          }
          case 'po':
            q = q.and(containsSmart('poNumber', val));
            break;
          case 'pn':
          case 'plant':
          case 'plant#': {
            if (/^\d+$/.test(val)) {
              const pnNum = Number(val);
              q = q.and(
                Number.isFinite(pnNum)
                  ? wixData.query(COLLECTION).eq('plantNumber', pnNum)
                  : wixData.query(COLLECTION).eq('plantNumber', val)
              );
            } else {
              q = q.and(containsSmart('plantNumber', val));
            }
            break;
          }
          case 'lo':
          case 'loc':
          case 'location':
            q = q.and(containsSmart('plantLocation', val));
            break;
          case 'br':
          case 'brief':
          case 'notes':
          case 'desc':
            q = q.and(containsAnySmart(['briefDescription', 'complaintNotes'], val));
            break;
          case 'se':
          case 'serial':
          case 'sn':
          case 'ser':
            q = q.and(containsSmart('vehicleSerial', val));
            break;
          case 'mo':
          case 'model':
            q = q.and(containsSmart('vehicleModel', val));
            break;
          case 'st':
          case 'status':
          case 's': {
            const st = canonicalStatus(val);
            q = q.and(st ? wixData.query(COLLECTION).eq('status', st) : wixData.query(COLLECTION).eq('_id', '__none__'));
            break;
          }
          case 'as':
          case 'assigned':
          case 'asg':
          case 'mech':
          case 'tech':
            q = q.and(wixData.query(COLLECTION).contains('assignedMechanicName', val));
            break;
          case 'ho':
          case 'hours':
          case 'hour':
          case 'hrs':
            q = q.and(parseHoursQuery(val));
            break;
          case 'priority':
          case 'prio': {
            const p = ['urgent', 'medium', 'low'].includes(val.toLowerCase())
              ? val[0].toUpperCase() + val.slice(1).toLowerCase()
              : null;
            if (p) q = q.and(wixData.query(COLLECTION).eq('priority', p));
            break;
          }
          default:
            q = q.and(
              containsAnySmart(
                [
                  'poNumber',
                  'vehicleSerial',
                  'vehicleModel',
                  'complaintNotes',
                  'briefDescription',
                  'plantNumber',
                  'plantLocation',
                  'assignedMechanicName'
                ],
                val
              )
            );
        }
      } else {
        if (/^\d+$/.test(tok)) {
          const num = Number(tok);
          let qq = wixData
            .query(COLLECTION)
            .eq('jobCartNumber', num)
            .or(wixData.query(COLLECTION).eq('machineHours', num));
          qq = qq
            .or(wixData.query(COLLECTION).eq('plantNumber', tok))
            .or(wixData.query(COLLECTION).eq('poNumber', tok));
          q = q.and(qq);
        } else {
          const st = canonicalStatus(tok);
          if (st) {
            q = q.and(wixData.query(COLLECTION).eq('status', st));
          } else {
            q = q.and(
              containsAnySmart(
                [
                  'poNumber',
                  'vehicleSerial',
                  'vehicleModel',
                  'complaintNotes',
                  'briefDescription',
                  'plantNumber',
                  'plantLocation',
                  'assignedMechanicName'
                ],
                tok
              )
            );
          }
        }
      }
    }
  }

  const dbSortable = new Set(['newest', 'oldest', 'jobHigh', 'jobLow']);
  if (dbSortable.has(sort)) {
    if (sort === 'newest')  q = q.descending('submissionDate');
    if (sort === 'oldest')  q = q.ascending('submissionDate');
    if (sort === 'jobHigh') q = q.descending('jobCartNumber');
    if (sort === 'jobLow')  q = q.ascending('jobCartNumber');

    const res = await q.skip(skip).limit(limit).find();
    const payload = { items: res.items || [], hasMore: res.hasNext() };
    CACHE.set(cacheKey, payload);
    return payload;
  }

  const windowItems = [];
  let scanned = 0;

  let res = await q.limit(CHUNK_SIZE).descending('submissionDate').find();
  windowItems.push(...(res.items || []));
  scanned += (res.items || []).length;

  while (windowItems.length < skip + limit && res.hasNext() && scanned < MAX_SCAN) {
    res = await res.next();
    const batch = res.items || [];
    windowItems.push(...batch);
    scanned += batch.length;
  }

  const sorted = serverSortItems(windowItems, sort, userId);
  const page = sorted.slice(skip, skip + limit);
  const hasMore = sorted.length > skip + limit || res.hasNext();

  const payload = { items: page, hasMore };
  CACHE.set(cacheKey, payload);
  return payload;
}

/** ============================ Helpers ============================ */
function toTime(v) {
  if (!v) return 0;
  if (v instanceof Date) return v.getTime();
  if (typeof v === 'string') return new Date(v).getTime() || 0;
  return 0;
}

function serverSortItems(items, sort, userId) {
  if (sort === 'status') {
    return items.slice().sort((a, b) => {
      const ao = WORKFLOW_ORDER[a.status] ?? 999;
      const bo = WORKFLOW_ORDER[b.status] ?? 999;
      if (ao !== bo) return ao - bo;
      const at = toTime(a.lastStatusChangeAt || a.submissionDate);
      const bt = toTime(b.lastStatusChangeAt || b.submissionDate);
      return bt - at;
    });
  }

  if (sort === 'priority') {
    return items.slice().sort((a, b) => {
      const ao = PRIORITY_ORDER[(a.priority || '').trim()] ?? 999;
      const bo = PRIORITY_ORDER[(b.priority || '').trim()] ?? 999;
      if (ao !== bo) return ao - bo;
      return toTime(b.submissionDate) - toTime(a.submissionDate);
    });
  }

  if (sort === 'unassignedFirst') {
    return items.slice().sort((a, b) => {
      const au = a.assignedMechanicId ? 1 : 0;
      const bu = b.assignedMechanicId ? 1 : 0;
      if (au !== bu) return au - bu;
      return toTime(b.submissionDate) - toTime(a.submissionDate);
    });
  }

  if (sort === 'myFirst' && userId) {
    return items.slice().sort((a, b) => {
      const am = a.assignedMechanicId === userId ? 0 : 1;
      const bm = b.assignedMechanicId === userId ? 0 : 1;
      if (am !== bm) return am - bm;
      return toTime(b.submissionDate) - toTime(a.submissionDate);
    });
  }

  return items.slice().sort((a, b) => toTime(b.submissionDate) - toTime(a.submissionDate));
}

function parseHoursQuery(val) {
  const v = String(val).replace(/,/g, '').trim();

  const r = /^(\d+(?:\.\d+)?)\s*\.\.\s*(\d+(?:\.\d+)?)/.exec(v);
  if (r) {
    const a = Number(r[1]), b = Number(r[2]);
    if (Number.isFinite(a) && Number.isFinite(b)) {
      return wixData
        .query(COLLECTION)
        .ge('machineHours', Math.min(a, b))
        .le('machineHours', Math.max(a, b));
    }
  }

  const c = /^(>=|<=|>|<|=)\s*(\d+(?:\.\d+)?)/i.exec(v.replace(/\s+/g, ''));
  if (c) {
    const op = c[1];
    const n = Number(c[2]);
    if (Number.isFinite(n)) {
      if (op === '>')  return wixData.query(COLLECTION).gt('machineHours', n);
      if (op === '>=') return wixData.query(COLLECTION).ge('machineHours', n);
      if (op === '<')  return wixData.query(COLLECTION).lt('machineHours', n);
      if (op === '<=') return wixData.query(COLLECTION).le('machineHours', n);
      return wixData.query(COLLECTION).eq('machineHours', n);
    }
  }

  const n = Number(v);
  if (Number.isFinite(n)) return wixData.query(COLLECTION).eq('machineHours', n);
  return wixData.query(COLLECTION);
}


// backend/data.js
// Collection hooks & normalization for Tickets
// - Reuses shared date helpers from backend/dateUtil.js
// - Publishes realtime ticks
// - Mirrors searchable fields into *_norm

import wixData from 'wix-data';
import { members } from 'wix-members-backend';
import { publish } from 'wix-realtime-backend';
import { userHasRole } from 'backend/members.jsw';
import { toDate as coerceDate } from 'backend/dateUtil.js';

/**
 * @typedef {{ name: string, resourceId?: string }} Channel
 */

/* -------------------- small helpers -------------------- */
function now() { return new Date(); }

/** Coerce with fallback-to-provided (or now). Uses shared date util. */
function toDateSafe(v, fallback) {
  const d = coerceDate(v);
  return d !== undefined ? d : (fallback || now());
}

/** Ensure typical ticket date fields are valid Date objects */
function sanitizeTicketDates(out, original) {
  // Best fallback for "submission-like" moments
  const originalSubmission =
    original?.submissionDate instanceof Date && !Number.isNaN(original?.submissionDate?.getTime?.())
      ? original.submissionDate
      : null;

  const originalCreated =
    original?._createdDate instanceof Date && !Number.isNaN(original?._createdDate?.getTime?.())
      ? original._createdDate
      : null;

  const createdFallback = originalSubmission || originalCreated || now();

  // Core workflow dates
  out.submissionDate = toDateSafe(out.submissionDate ?? originalSubmission ?? originalCreated, createdFallback);
  out.lastStatusChangeAt = toDateSafe(out.lastStatusChangeAt ?? original?.lastStatusChangeAt, out.submissionDate);

  // Assignment timestamp (can be absent if not assigned)
  if (out.assignedMechanicId) {
    out.assignedAt = toDateSafe(out.assignedAt ?? original?.assignedAt, now());
  } else {
    if ('assignedAt' in out) delete out.assignedAt; // keep schema clean
  }

  // Finish date only if present (we also stamp this in transitions)
  if (out.finishDate) {
    out.finishDate = toDateSafe(out.finishDate, now());
  }

  return out;
}

/* -------------------- normalization helpers -------------------- */
/** Normalize string for case/diacritic-insensitive matching. */
function normalizeValue(v) {
  return String(v || '')
    .toLowerCase()
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .trim();
}

/** Mirror searchable fields into *_norm copies (insert/update-safe). */
function applyTicketNormalization(out, original) {
  const src = Object.assign({}, original || {}, out || {});
  const setNorm = (field) => {
    if (src[field] === undefined || src[field] === null) {
      out[`${field}_norm`] = '';
    } else {
      out[`${field}_norm`] = normalizeValue(src[field]);
    }
  };

  setNorm('vehicleSerial');
  setNorm('vehicleModel');
  setNorm('plantNumber');
  setNorm('poNumber');
  setNorm('briefDescription');
  setNorm('complaintNotes');
  setNorm('plantLocation');
  setNorm('status');

  return out;
}

/** Safe string trim for unknown types */
function s(v) { return (typeof v === 'string' ? v : '').trim(); }

/** Resolve a human-friendly member name from a Wix member/user id */
async function getMemberName(userId) {
  if (!userId) return '';

  // Preferred: Members API
  try {
    const m = await members.getMember(userId);
    const first = s(m?.contactDetails?.firstName);
    const last = s(m?.contactDetails?.lastName);
    const nick = s(m?.profile?.nickname);
    const email = s(m?.loginEmail);
    const full = `${first} ${last}`.trim();
    return full || nick || email;
  } catch (_) { /* ignore */ }

  return '';
}

/** Role check for the CURRENT user (hooks run in the caller’s context) */
const userHasRoleAsync = /** @type {any} */ (userHasRole);
async function hasRole(roleLower) {
  try {
    return await userHasRoleAsync(roleLower);
  } catch (_) {
    return false;
  }
}

/* -------------------- realtime tick helper -------------------- */
async function tick(kind, id) {
  try {
    /** @type {Channel} */
    const channel = { name: 'tickets' }; // or { name: 'tickets', resourceId: String(id || '') }
    await publish(channel, { type: kind, id: String(id || '') });
  } catch (e) {
    // never block writes because of realtime
    console.warn('Realtime publish failed:', e?.message || e);
  }
}

/* -------------------- beforeInsert / beforeUpdate -------------------- */
async function beforeInsertCommon(item, context) {
  const userId = context?.userId || '';
  const out = { ...item };

  // defaults
  if (!out.status) out.status = 'Not Yet Started';
  if (!out.priority) out.priority = 'Medium';
  // Always coerce to a valid Date (even if provided)
  out.submissionDate = toDateSafe(out.submissionDate, now());

  // created-by stamps (standardize on createdByID + createdByName)
  out.createdByID = out.createdByID || userId || '';

  // If logged in, prefer account name; otherwise fall back to form's reporter name
  if (!out.createdByName) {
    if (userId) {
      out.createdByName = await getMemberName(userId);
    }
    // guests/drivers β†’ use the typed reporter name (or creatorDisplayName)
    if (!out.createdByName) {
      const reporterTyped = s(out.reporterName || out.creatorDisplayName);
      if (reporterTyped) out.createdByName = reporterTyped;
    }
  }

  if (!out.createdByRole) {
    const admin = await hasRole('admin');
    const mech = await hasRole('mechanic');
    out.createdByRole = admin ? 'Admin' : (mech ? 'Mechanic' : 'Driver');
  }

  // Respect assignment coming from backend/tickets.jsw (e.g., Assign to me).
  // Do not auto-assign here; just normalize when present.
  if (out.assignedMechanicId) {
    out.assignedAt = toDateSafe(out.assignedAt, now());
    if (!out.assignedMechanicName) {
      out.assignedMechanicName = await getMemberName(out.assignedMechanicId);
    }
  } else {
    out.assignedMechanicId = '';
    out.assignedMechanicName = '';
    delete out.assignedAt;
  }

  // We don't store the transient flag
  if ('assignToMe' in out) delete out.assignToMe;

  // initial status timestamp
  out.lastStatusChangeAt = toDateSafe(out.lastStatusChangeAt, now());

  // If inserted already as Ready for Review or Complete, stamp finish meta once
  const nextS = s(out.status);
  if (nextS === 'Ready for Review' || nextS === 'Complete') {
    if (!out.finishDate) out.finishDate = now();
    if (!out.completedById) out.completedById = userId || '';
    if (!out.completedByName && userId) out.completedByName = await getMemberName(userId);
  }

  // Final hardening of all date fields
  sanitizeTicketDates(out, /* original */ null);

  // Normalized mirrors last
  applyTicketNormalization(out, null);

  return out;
}

async function beforeUpdateCommon(item, context) {
  const col = context?.collectionName;
  const id = item?._id;
  if (!col || !id) return item;

  const original = await wixData.get(col, id).catch(() => null);
  if (!original) {
    // No original? Still sanitize what we can.
    const outNoOrig = sanitizeTicketDates({ ...item }, null);
    applyTicketNormalization(outNoOrig, null);
    return outNoOrig;
  }

  const out = { ...item };

  // status β†’ lastStatusChangeAt (+ finish meta when entering RfR/Complete)
  const prevS = s(original.status);
  const nextS = s(out.status);
  if (nextS && prevS !== nextS) {
    out.lastStatusChangeAt = now();

    // When moving into Ready for Review, stamp finish once (do not overwrite later)
    if (nextS === 'Ready for Review') {
      if (!out.finishDate) out.finishDate = now();
      const userId = context?.userId || '';
      if (!out.completedById) out.completedById = userId || '';
      if (!out.completedByName && userId) out.completedByName = await getMemberName(userId);
    }

    // Safety: if jumping straight to Complete, also stamp if missing
    if (nextS === 'Complete') {
      if (!out.finishDate) out.finishDate = now();
      const userId = context?.userId || '';
      if (!out.completedById) out.completedById = userId || '';
      if (!out.completedByName && userId) out.completedByName = await getMemberName(userId);
    }
  }

  // assignment change β†’ assignedAt & ensure name
  const prevA = s(original.assignedMechanicId);
  const nextA = s(out.assignedMechanicId);
  if (prevA !== nextA) {
    if (nextA) {
      out.assignedAt = now();
      if (!out.assignedMechanicName) out.assignedMechanicName = await getMemberName(nextA);
    } else {
      delete out.assignedAt;
      out.assignedMechanicName = '';
    }
  }

  // Always coerce all date fields to real Date objects (handles user edits / bad strings)
  sanitizeTicketDates(out, original);

  // Normalized mirrors last (use merged snapshot)
  applyTicketNormalization(out, original);

  return out;
}

/* -------------------- collection hooks (must match ID "Tickets") -------------------- */
export async function Tickets_beforeInsert(item, context) {
  return beforeInsertCommon(item, context);
}
export async function Tickets_beforeUpdate(item, context) {
  return beforeUpdateCommon(item, context);
}

/* -------------------- realtime ticks -------------------- */
export async function Tickets_afterInsert(item /*, context */) {
  await tick('insert', item?._id);
}
export async function Tickets_afterUpdate(item /*, context */) {
  await tick('update', item?._id);
}
export async function Tickets_afterRemove(item /*, context */) {
  await tick('remove', item?._id);
}

/*******************
 backend/jobs.config
 *******************

 'backend/jobs.config' is a Velo configuration file that lets you schedule code to run repeatedly at specified intervals. 

 Using scheduled jobs you can run backend code without any user interaction. For example, you could generate a weekly 
 report or run nightly data cleansing.

 You schedule code to run by creating a job.
 Each job defines which function to run and when to run it.

 ---
 More about Scheduled Jobs: 
 https://support.wix.com/en/article/velo-scheduling-recurring-jobs
 
 Online tool for building Velo cron expressions
 https://wix.to/NDAQn6c

*******************/

// Job deletes CMS > Collection > SubmitLog once a week - cleans up the logs
{
  "jobs": [
    {
      "functionLocation": "/backend/maintenance.jsw",
      "functionName": "purgeOldSubmitLogs",
      "description": "Purge SubmitLog entries older than 3 days (runs weekly).",
      "executionConfig": {
        "time": "02:00",
        "dayOfWeek": "Sunday"
      },
      "parameters": {
        "days": 3,
        "batchSize": 200
      }
    }
  ]
}

// public/app.constants.js
// Single Source of Truth (SSOT) for shared enums & lists used across frontend and backend.
// Import this file from both sides to prevent drift.
//
// Usage (frontend):
//   import { STATUSES, WORKFLOW_ORDER, STATUS_ALIASES, VALID_PRIORITIES, ALLOWED_LOCATIONS, CHIP_LABELS, PRIORITY_ORDER } from 'public/app.constants.js';
//
// Usage (backend):
//   import { STATUSES, WORKFLOW_ORDER, STATUS_ALIASES, VALID_PRIORITIES, ALLOWED_LOCATIONS, PRIORITY_ORDER } from 'public/app.constants.js';
//
// NOTE: Avoid importing UI-only color tokens from backend. Keep colors in public/colors.js.

export const APP_CONSTANTS_VERSION = '2025-09-17-01';

/** Statuses in canonical workflow order */
export const STATUSES = [
  'Not Yet Started',
  'Underway',
  'Parts Required',
  'Ordered',
  'Parts Ready',
  'Ready for Review',
  'Complete'
];

/** Workflow sort order (lower number = higher priority on sort 'status') */
export const WORKFLOW_ORDER = {
  'Parts Ready': 0,
  'Not Yet Started': 1,
  'Underway': 2,
  'Ordered': 3,
  'Parts Required': 4,
  'Ready for Review': 5,
  'Complete': 6
};

/** Aliases accepted on input/search that map to canonical statuses */
export const STATUS_ALIASES = {
  nys: 'Not Yet Started',
  rfr: 'Ready for Review',
  ord: 'Ordered',
  review: 'Ready for Review',
  'parts ordered': 'Ordered',
  ready: 'Parts Ready',
  'parts req': 'Parts Required',
  pr: 'Parts Required',
  'order required': 'Parts Required'
};

/** Ticket priorities (canonical) */
export const VALID_PRIORITIES = ['Low', 'Medium', 'Urgent'];

/** Plant/yard/site list */
export const ALLOWED_LOCATIONS = ['Adare','Berrings','Celtic','Glanmire','Killybegs','Fossa','Ringaskiddy'];

/** Priority sort order for the dashboard (visual + backend) */
export const PRIORITY_ORDER = { Urgent: 0, Medium: 1, Low: 2 };

/** UI-only helper: compact chip labels (frontend may import) */
export const CHIP_LABELS = {
  'Not Yet Started': 'NYS',
  'Parts Required': 'Parts Req',
  'Ordered': 'Ordered',
  'Parts Ready': 'Ready',
  'Underway': 'Underway',
  'Ready for Review': 'Review',
  'Complete': 'Complete'
};

// public/colors.js
// Centralized UI tokens & styles for the Tickets page, aligned with SSOT constants.

import {
  STATUSES,
  CHIP_LABELS,
  WORKFLOW_ORDER,
  PRIORITY_ORDER
} from 'public/app.constants.js';

export const UI_VERSION = '1.2.0';

/* ========================= Base Palette ========================= */
export const COLORS = {
  YELLOW: '#FEF301',
  GREEN: '#2ECC71',
  BLACK: '#000000',
  WHITE: '#ffffff',
  BLUE: '#3A6AFD',

  // neutrals / accents used by UI
  NEUTRAL_100: '#f6f6f6',
  NEUTRAL_700: '#3f3f3f',
  NEUTRAL_800: '#3d4450',
  NEUTRAL_900: '#111827',

  ERROR_DARK: '#db0000',
  SUCCESS_DARK: '#FFFFFF' // white text on dark bg for success toasts
};

export const THEME = {
  GREEN: COLORS.GREEN,
  WHITE: COLORS.NEUTRAL_100,
  BORDER: COLORS.BLACK
};

/* ========================= Status / Chip / Pill Styles ========================= */
export const STATUS_STYLES = {
  'Not Yet Started': { bg: '#d9d2d4', border: '#d9d2d4', text: '#7F1D1D' },
  'Underway': { bg: '#b9d0fd', border: '#b9d0fd', text: '#184aa7' },
  'Parts Required': { bg: '#d9d2d4', border: '#d9d2d4', text: '#7F1D1D' },
  'Ordered': { bg: '#e1f5cc', border: '#e1f5cc', text: '#2b4412' },
  'Parts Ready': { bg: '#e1f5cc', border: '#e1f5cc', text: '#2b4412' },
  'Ready for Review': { bg: COLORS.BLACK, border: COLORS.BLACK, text: THEME.WHITE },
  'Complete': { bg: COLORS.NEUTRAL_100, border: '#3c3d42', text: '#3c3d42' }
};

// Chips
export const ALL_CHIP_STYLE = { bg: COLORS.NEUTRAL_700, text: THEME.WHITE };
export const INACTIVE_CHIP_STYLE = { bg: COLORS.BLACK, text: THEME.WHITE };
export const INACTIVE_ALL_CHIP_STYLE = { bg: COLORS.NEUTRAL_700, text: THEME.WHITE };

// Job priority pill (job number)
export const PRIORITY_STYLES = {
  Urgent: { bg: '#2f3438', border: '#2f3438', text: '#ff8080' },
  Medium: { bg: '#2f3438', border: '#2f3438', text: '#ffffff' },
  Low: { bg: '#2f3438', border: '#2f3438', text: '#9dc473' }
};

// Assignment pill (not the status pill)
export const ASSIGNED_PILL_STYLES = {
  assigned: { bg: '#000000', border: '#000000', text: '#FFFFFF' },
  unassigned: { bg: '#929599', border: '#929599', text: '#000000' }
};

/* ========================= Glass (row tint) ========================= */
export const GLASS = {
  BASE_BG: '#16202d',
  HOVER_BORDER: '#16202d',

  UNDERWAY_BG: '#5482d9',
  UNDERWAY_BORDER: '#5482d9',

  PARTS_READY_BG: '#62883b',
  PARTS_READY_BORDER: '#62883b',

  COMPLETE_BG: '#545563',
  COMPLETE_BORDER: '#565657'
};

export function getGlassBaseStyle(status) {
  const s = String(status || '').trim();
  let bg = GLASS.BASE_BG;
  let borderColor = GLASS.HOVER_BORDER;
  let borderWidth = 0;
  if (s === 'Underway') {
    bg = GLASS.UNDERWAY_BG; borderColor = GLASS.UNDERWAY_BORDER; borderWidth = 0;
  } else if (s === 'Parts Ready') {
    bg = GLASS.PARTS_READY_BG; borderColor = GLASS.PARTS_READY_BORDER; borderWidth = 0;
  } else if (s === 'Complete') {
    bg = GLASS.COMPLETE_BG; borderColor = GLASS.COMPLETE_BORDER; borderWidth = 0;
  }
  return { bg, borderColor, borderWidth };
}

export function getGlassHoverStyle(_status) {
  return { borderColor: GLASS.HOVER_BORDER, borderWidth: 1 };
}

export function getStatusPillStyle(status) {
  const s = STATUS_STYLES[status] || STATUS_STYLES['Not Yet Started'];
  return { bg: s.bg, border: 'transparent', borderWidth: 0, text: s.text };
}

/* ========================= Controls (dropdowns/inputs/buttons) ========================= */
export const CONTROL = {
  FILLED_BG: COLORS.NEUTRAL_800,
  FILLED_BORDER: COLORS.NEUTRAL_800,
  FILLED_TEXT: THEME.WHITE,

  CLEAR_BG: COLORS.NEUTRAL_700,
  CLEAR_BORDER: COLORS.NEUTRAL_700,
  CLEAR_TEXT: THEME.WHITE,

  REFRESH_BG: COLORS.NEUTRAL_700,
  REFRESH_BORDER: COLORS.NEUTRAL_700,
  REFRESH_TEXT: THEME.WHITE,

  MYJOB_IDLE_BG: COLORS.NEUTRAL_700,
  MYJOB_IDLE_BORDER: COLORS.NEUTRAL_700
};

export const TOAST = {
  ERROR_TEXT: THEME.WHITE,
  SUCCESS_TEXT: THEME.WHITE,
  INFO_TEXT: THEME.WHITE
};

/* ========================= Re-exports (to keep existing imports working) ========================= */
// STATUS_DISPLAY is a convenient alias for CHIP_LABELS (single source of truth for display labels)
export const STATUS_DISPLAY = CHIP_LABELS;

export { STATUSES, CHIP_LABELS, WORKFLOW_ORDER, PRIORITY_ORDER };

/* ========================= Helpers ========================= */
export function getStatusStyle(status) {
  return STATUS_STYLES[status] || null;
}

export function getChipStyle(value, active) {
  if (active) {
    if (value === 'all') return ALL_CHIP_STYLE;
    return STATUS_STYLES[value] || { bg: THEME.GREEN, border: THEME.BORDER, text: THEME.WHITE };
  }
  if (value === 'all') return INACTIVE_ALL_CHIP_STYLE;
  return INACTIVE_CHIP_STYLE;
}

export function getMyJobsStyle(selectedStatus, preferBlue = false) {
  if (preferBlue) return { bg: COLORS.BLUE, text: THEME.WHITE, border: THEME.BORDER };
  const st = getStatusStyle(selectedStatus);
  return st
    ? { bg: st.bg, text: st.text, border: st.border || st.bg }
    : { bg: CONTROL.MYJOB_IDLE_BG, text: THEME.WHITE, border: CONTROL.MYJOB_IDLE_BORDER };
}

export function getControlFilledStyle(kind = 'default') {
  if (kind === 'clear') return { bg: CONTROL.CLEAR_BG, text: CONTROL.CLEAR_TEXT, border: CONTROL.CLEAR_BORDER };
  if (kind === 'refresh') return { bg: CONTROL.REFRESH_BG, text: CONTROL.REFRESH_TEXT, border: CONTROL.REFRESH_BORDER };
  return { bg: CONTROL.FILLED_BG, text: CONTROL.FILLED_TEXT, border: CONTROL.FILLED_BORDER };
}

// convenient namespace default
const UI = {
  UI_VERSION,
  COLORS, THEME,
  STATUSES, CHIP_LABELS, WORKFLOW_ORDER, PRIORITY_ORDER,
  STATUS_STYLES, ALL_CHIP_STYLE, INACTIVE_CHIP_STYLE, INACTIVE_ALL_CHIP_STYLE,
  PRIORITY_STYLES, ASSIGNED_PILL_STYLES,
  GLASS, CONTROL, TOAST,
  STATUS_DISPLAY,
  getGlassBaseStyle, getGlassHoverStyle, getStatusPillStyle,
  getStatusStyle, getChipStyle, getMyJobsStyle, getControlFilledStyle
};

export default UI;


/* global addEventListener, removeEventListener */
// public/realtimeTickets.js
// Client-side helper to receive realtime updates for Tickets.
// Backend must publish with: publish({ name: 'tickets' }, { type, id })
//
// Usage:
//   import { subscribeToTickets, unsubscribeTickets, ticketsRealtimeConnected } from 'public/realtimeTickets.js';
//   const stop = subscribeToTickets(evt => { ... }, { dedupeTtlMs: 20000, onStatus: (s) => {} });
//   // later: stop() or unsubscribeTickets();

import { subscribe } from 'wix-realtime';

const CHANNEL = { name: 'tickets' };

// single active subscription per page
let _unsubscribe = null;
let _connected = false;

// store an optional status callback provided by the caller
/** @type {null | ((s: 'connected'|'connecting'|'disconnected') => void)} */
let _statusCb = null;

// beforeunload cleanup handler
let _onBeforeUnload = null;

// light de-dupe for bursty duplicate events
const _recent = new Set();
let RECENT_TTL_MS = 20000; // default 20s

function _remember(key) {
  _recent.add(key);
  setTimeout(() => _recent.delete(key), RECENT_TTL_MS);
}

/**
 * Start listening for ticket events.
 * Call once per page. Call `unsubscribeTickets()` on unload/navigation.
 *
 * @param {(evt: {type:'insert'|'update'|'remove', id:string}) => void} onEvent
 * @param {{ dedupeTtlMs?: number, onStatus?: (s: 'connected'|'connecting'|'disconnected') => void }} [options]
 * @returns {() => void | Promise<void>} unsubscribe function
 */
export function subscribeToTickets(onEvent, options = {}) {
  if (typeof onEvent !== 'function') {
    console.warn('subscribeToTickets: onEvent must be a function');
    return () => {};
  }
  if (_unsubscribe) {
    // already subscribed; return an unsubscribe that removes the existing one
    return () => unsubscribeTickets();
  }

  // record optional status callback
  _statusCb = (options && typeof options.onStatus === 'function') ? options.onStatus : null;

  if (options && typeof options.dedupeTtlMs === 'number' && options.dedupeTtlMs >= 0) {
    RECENT_TTL_MS = options.dedupeTtlMs;
  }

  const handler = (msg) => {
    // Velo sends { channel, payload, ... }
    const payload = (msg && typeof msg === 'object') ? (msg.payload ?? msg) : null;
    const type = payload && payload.type;
    const id = payload && payload.id != null ? String(payload.id) : '';

    if (!type || !id) return;

    const key = type + ':' + id;
    if (_recent.has(key)) return; // drop duplicates within TTL
    _remember(key);

    try {
      onEvent({ type, id });
    } catch (err) {
      console.warn('subscribeToTickets onEvent error:', (err && err.message) || err);
    }
  };

  // subscribe signature: (channel, messageHandler)
  const ret = subscribe(CHANNEL, handler);

  const markConnected = () => {
    _connected = true;
    if (_statusCb) { try { _statusCb('connected'); } catch (_) {} }
  };

  if (ret && typeof ret.then === 'function') {
    // promise case
    ret.then((fn) => {
      _unsubscribe = fn;
      markConnected();
    }).catch((e) => {
      console.warn('Realtime subscribe failed:', (e && e.message) || e);
    });
  } else {
    // direct function case
    _unsubscribe = ret;
    markConnected();
  }

  // clean up on page unload; avoid referencing 'window'. Use global functions instead.
  const hasAdd = typeof addEventListener === 'function';
  if (hasAdd) {
    _onBeforeUnload = () => unsubscribeTickets();
    try { addEventListener('beforeunload', _onBeforeUnload, { once: true }); } catch (_) {}
  }

  return () => unsubscribeTickets();
}

/**
 * Unsubscribe if subscribed.
 */
export function unsubscribeTickets() {
  const fn = _unsubscribe;
  _unsubscribe = null;

  // remove the unload listener if we added it
  const hasRemove = typeof removeEventListener === 'function';
  if (hasRemove && _onBeforeUnload) {
    try { removeEventListener('beforeunload', _onBeforeUnload); } catch (_) {}
    _onBeforeUnload = null;
  }

  if (!_connected && !fn) return;
  _connected = false;
  if (_statusCb) { try { _statusCb('disconnected'); } catch (_) {} }

  if (typeof fn !== 'function') return;

  try {
    const maybePromise = fn(); // some runtimes return a promise
    if (maybePromise && typeof maybePromise.then === 'function') {
      return maybePromise.catch((e) =>
        console.warn('Realtime unsubscribe failed:', (e && e.message) || e)
      );
    }
  } catch (e) {
    console.warn('Realtime unsubscribe failed:', (e && e.message) || e);
  }
}

/**
 * Returns true when the realtime connection is reported as connected.
 */
export function ticketsRealtimeConnected() {
  return _connected;
}

/**
 * Returns true if a realtime subscription is currently active.
 */
export function isSubscribed() {
  return typeof _unsubscribe === 'function';
}





/* global $w */
// Page Code > Dashboard
// FM 2025 may the force be with you..
// tickets-page.js
// /tickets page code β€” centralized colors via public/colors.js

import wixData from 'wix-data';
import wixUsers from 'wix-users';
import wixWindow from 'wix-window';
import wixLocation from 'wix-location';
import { local as storage, session as sessionStorage } from 'wix-storage';
import { timeline } from 'wix-animations'; // spin animation

// BACKEND calls (mutations & helpers)
import {
  acceptTicket,
  assignTicketAdmin,
  updateTicketStatus,
  unassignTicketAdmin,
  listMechanicsByRole,
  updatePurchaseOrderAdmin,
  updateTicket
} from 'backend/tickets.jsw';
import { getMemberDetails } from 'backend/members.jsw';

// server-side ticket querying (filters + sorting + paging)
import { queryTickets } from 'backend/tickets.read.jsw';

// CENTRALIZED THEME/TOKENS (from public)
import {
  COLORS, THEME, TOAST,
  STATUSES, CHIP_LABELS, STATUS_DISPLAY,
  STATUS_STYLES,
  PRIORITY_STYLES, ASSIGNED_PILL_STYLES,
  getChipStyle, getControlFilledStyle, getMyJobsStyle,
  // central visual helpers:
  getStatusPillStyle, getGlassBaseStyle, getGlassHoverStyle
} from 'public/colors.js';

// Realtime helper (debounced + Velo-safe)
import {
  subscribeToTickets,
  unsubscribeTickets
} from 'public/realtimeTickets.js';

/** ============================ JSDoc Types ============================ */
/**
 * @typedef {Object} PageFilters
 * @property {string} selectedStatus
 * @property {string} location
 * @property {string} assignment
 * @property {boolean} includeCompleted
 * @property {string} search
 * @property {string} sort
 */
/**
 * @typedef {Object} Paging
 * @property {number} skip
 * @property {boolean} hasMore
 */
/**
 * @typedef {Object} PageState
 * @property {PageFilters} filters
 * @property {Paging} paging
 * @property {boolean} loading
 */

/** ============================ Config & Constants ============================ */

const CONFIG = {
  AUTH_CHECK_INTERVAL: 15000,
  REALTIME_DEBOUNCE_MS: 250,
  REALTIME_SUB_FALLBACK_DELAY: 3000,
  SEARCH_DEBOUNCE: 500,
  POLL_START_MS: 60000,
  POLL_MAX_MS: 5 * 60 * 1000,
  MAX_RETRY_ATTEMPTS: 3,
  RETRY_BASE_DELAY: 600,
  ELEMENT_CACHE_SIZE: 100,
  CONNECTION_HEALTH_CHECK_INTERVAL: 30000,
  ERROR_TOAST_DURATION: 5000,
  SUCCESS_TOAST_DURATION: 3000,
  INFO_TOAST_DURATION: 1200,
  INFINITE_SCROLL_THROTTLE: 300,
  USER_ROLE_CACHE_TTL: 5 * 60 * 1000 // 5 minutes
};

const LIGHTBOX_NAME = 'AssignMechanicLightbox';
const STATUS_LIGHTBOX_NAME = 'UpdateStatusLightbox';
const PRIORITY_LIGHTBOX_NAME = 'UpdatePriorityLightbox';
const UPDATE_PO_LIGHTBOX_NAME = 'UpdatePOLightbox';
const UPDATE_BRIEF_LIGHTBOX_NAME = 'UpdateBriefLightbox';
const DETAILS_LIGHTBOX_NAME = 'TicketDetailsLightbox';
const INFO_LIGHTBOX_NAME = 'MoreInfoLightBox'; // NEW
const FORM_URL = '/form';
const DETAILS_PAGE_URL = '/ticket';
const UI_VERSION = 'tickets-2025-09-17'; // bumped to bust cache automatically

const TICKETS = 'Tickets';

const isMobile = wixWindow.formFactor === 'Mobile';

// PAGE SIZE (backend allows up to ~200 via paging; we load in chunks)
const PAGE_SIZE = isMobile ? 10 : 50;

// fixed hover bg for the glass card
const GLASS_HOVER_BG = '#2c3848';

/** ============================ Logging Helpers ============================ */
function logScope(scope, msg, extra) {
  const payload = extra ? ' | ' + JSON.stringify(extra) : '';
  console.log('[' + scope + '] ' + msg + payload);
}
function logWarn(scope, msg, extra) {
  const payload = extra ? ' | ' + JSON.stringify(extra) : '';
  console.warn('[' + scope + '] ' + msg + payload);
}
function logError(scope, err, extra) {
  const payload = extra ? ' | ' + JSON.stringify(extra) : '';
  const message = (err && err.message) ? err.message : String(err);
  console.error('[' + scope + '] ' + message + payload, err);
}

/** ============================ Resource Tracking & Cleanup ============================ */

const RES = {
  intervals: new Set(),
  timeouts: new Set(),
  subscriptions: new Set()
};
function setTrackedInterval(fn, ms) {
  const id = setInterval(fn, ms);
  RES.intervals.add(id);
  return id;
}
function clearTrackedInterval(id) { try { clearInterval(id); } finally { RES.intervals.delete(id); } }
function setTrackedTimeout(fn, ms) {
  const id = setTimeout(fn, ms);
  RES.timeouts.add(id);
  return id;
}
function clearTrackedTimeout(id) { try { clearTimeout(id); } finally { RES.timeouts.delete(id); } }
function trackSubscription(unsubFn) {
  if (typeof unsubFn === 'function') RES.subscriptions.add(unsubFn);
  return unsubFn;
}
function cleanupResources() {
  for (const id of Array.from(RES.timeouts)) { try { clearTimeout(id); } catch(_) {} RES.timeouts.delete(id); }
  for (const id of Array.from(RES.intervals)) { try { clearInterval(id); } catch(_) {} RES.intervals.delete(id); }
  for (const unsub of Array.from(RES.subscriptions)) { try { unsub(); } catch(_) {} RES.subscriptions.delete(unsub); }
}

/** ============================ State ============================ */

let user = { id: null, isMechanic: false, isAdmin: false };
/** @type {PageState} */
let state = {
  filters: {
    selectedStatus: 'all',
    location: 'all',
    assignment: 'all',
    includeCompleted: false,
    search: '',
    sort: 'newest'
  },
  paging: { skip: 0, hasMore: false },
  loading: false
};

// Race-condition guard
let cancelToken = 0;

// search debounce handle
let searchTimeout = null;

// realtime unsubscribe holder
let realtimeUnsub = null;

// ---- Mutation/refresh coordination ----
let realtimeSuspendedUntil = 0;               // suppress realtime callbacks during/after mutations
let refreshInFlight = false;                  // ensure only one refresh & gate realtime during refresh

// op lock for lightbox mutations
let operationInProgress = false;

// polling (fallback if realtime not available)
let pollTimer = null;
let lastSeen = null;
let pollIntervalMs = CONFIG.POLL_START_MS;

/** ============================ Refresh spin state ============================ */
let isRefreshing = false;
let refreshSpinTl = null;
function startRefreshSpin() {
  if (isRefreshing) return;
  const btn = el('#refreshButton');
  if (!btn) return;
  try {
    refreshSpinTl = timeline({ repeat: -1 });
    refreshSpinTl.add(btn, { rotate: 360, duration: 600, easing: 'easeInOutCubic' });
    refreshSpinTl.play();
    isRefreshing = true;
  } catch (e) {
    logWarn('Anim', 'Failed to start refresh spin', { err: e && e.message });
  }
}
function stopRefreshSpin() {
  if (!refreshSpinTl) { isRefreshing = false; return; }
  try {
    refreshSpinTl.pause();
    refreshSpinTl.seek(0);
  } catch (_) {}
  isRefreshing = false;
  refreshSpinTl = null;
}

/** ============================ Utilities ============================ */

// Element cache with size limit
const __elCache = new Map();
function el(selector) {
  if (__elCache.has(selector)) return __elCache.get(selector);
  if (__elCache.size >= CONFIG.ELEMENT_CACHE_SIZE) {
    const firstKey = __elCache.keys().next().value;
    if (firstKey !== undefined) __elCache.delete(firstKey);
  }
  try {
    const ref = $w(selector);
    __elCache.set(selector, ref || null);
    return ref || null;
  } catch (_) {
    __elCache.set(selector, null);
    return null;
  }
}
function elOneOf() { for (let i=0;i<arguments.length;i++){ const e = el(arguments[i]); if (e) return e; } return null; }
function itemEl($item, id) { try { return $item(id); } catch (_) { return null; } }
function canCall(x, m) { return !!(x && typeof x[m] === 'function'); }
function tryShow(x){ try { if (canCall(x,'show')) x.show(); if (canCall(x,'expand')) x.expand(); } catch(e){ logWarn('UI','tryShow failed',{id:x && x.id,err:e && e.message}); } }
function tryHide(x){ try { if (canCall(x,'hide')) x.hide(); if (canCall(x,'collapse')) x.collapse(); } catch(e){ logWarn('UI','tryHide failed',{id:x && x.id,err:e && e.message}); } }
function setTextOrLabel(x, txt){
  try {
    if (!x) return;
    if ('text' in x) x.text = txt;
    else if ('label' in x) x.label = txt;
    if ('ariaLabel' in x && !x.ariaLabel) x.ariaLabel = String(txt || '');
  } catch(e) { logWarn('UI','setTextOrLabel failed',{id:x && x.id,err:e && e.message}); }
}

// Stronger sanitization
function sanitizeInput(input) {
  if (input == null) return '';
  const s = String(input);
  const decoded = s.replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>')
                   .replace(/&quot;/g,'"').replace(/&#x27;|&#39;/g,"'");
  const cleaned = decoded
    .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
    .replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '')
    .replace(/\son\w+\s*=\s*["'][^"']*["']/gi, '')
    .replace(/javascript:/gi, '')
    .replace(/data:/gi, '')
    .replace(/vbscript:/gi, '');
  return cleaned.replace(/<\/?[^>]+(>|$)/g, '').trim();
}
const clean = (v) => sanitizeInput(v);

// Clamp & formatting
const CLAMP = {
  brief:    isMobile ? 14 : 18,
  title:    isMobile ? 14 : 18,
  po:       isMobile ? 8  : 10,
  pn:       isMobile ? 5  : 6,
  serial:   isMobile ? 10 : 12,
  model:    isMobile ? 8  : 10,
  location: isMobile ? 8  : 10,
  hours:    isMobile ? 8  : 10
};
function clampText(str, max) {
  const s = (str || '').replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ').trim();
  if (!max || s.length <= max) return s;
  return s.slice(0, Math.max(0, max - 1)) + '…';
}
function setOneLine(elx, raw, max) {
  if (!elx) return;
  const sanitized = clean(String(raw || '').replace(/\u200B/g, ''));
  const val = clampText(sanitized, max);
  setTextOrLabel(elx, val);
  try {
    if (elx.style) {
      elx.style.whiteSpace = 'nowrap';
      elx.style.overflow = 'hidden';
      elx.style.textOverflow = 'ellipsis';
      elx.style.wordBreak = 'normal';
    }
  } catch (e) { logWarn('UI','setOneLine style failed',{id:elx && elx.id,err:e && e.message}); }
}
function formatHours(v) {
  const n = Number(v);
  if (!Number.isFinite(n)) return '';
  return n.toLocaleString();
}

/** ===== Color helpers ===== */
function normalizeColor(v, { allowTransparent = true } = {}) {
  if (v == null) return allowTransparent ? 'rgba(0, 0, 0, 0)' : COLORS.BLACK;
  const s = String(v).trim();
  if (/^#([0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(s)) return s;
  if (/^rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+(?:\s*,\s*(0|1|0?\.\d+))?\s*\)$/.test(s)) return s;
  if (/^[a-zA-Z]+$/.test(s)) return s;
  return allowTransparent ? 'rgba(0, 0, 0, 0)' : COLORS.BLACK;
}
function toRgbNoAlpha(color) {
  const s = String(color || '').trim();
  const m = s.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i);
  if (m) return 'rgb(' + m[1] + ', ' + m[2] + ', ' + m[3] + ')';
  if (/^#([0-9a-fA-F]{8})$/.test(s)) return '#' + s.slice(1, 7);
  if (s.toLowerCase() === 'transparent') return COLORS.BLACK;
  return s;
}
function isFullyTransparent(color) {
  const s = String(color || '').trim().toLowerCase();
  if (s === 'transparent') return true;
  const m = s.match(/^rgba\((\s*\d+\s*),(\s*\d+\s*),(\s*\d+\s*),\s*(0|0?\.\d+)\s*\)$/i);
  if (m) return Number(m[4]) === 0;
  if (/^#([0-9a-f]{8})$/i.test(s)) return s.slice(7, 9).toLowerCase() === '00';
  return false;
}
function addAlpha(color, alpha = 1) {
  const s = String(color || '').trim();
  if (/^rgba?\(/i.test(s)) {
    const m = s.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i);
    if (m) return 'rgba(' + m[1] + ', ' + m[2] + ', ' + m[3] + ', ' + Math.max(0, Math.min(1, alpha)) + ')';
  }
  if (/^#([0-9a-fA-F]{6})$/.test(s)) {
    const r = parseInt(s.slice(1,3),16);
    const g = parseInt(s.slice(3,5),16);
    const b = parseInt(s.slice(5,7),16);
    return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + Math.max(0, Math.min(1, alpha)) + ')';
  }
  if (/^#([0-9a-fA-F]{8})$/.test(s)) {
    const r = parseInt(s.slice(1,3),16);
    const g = parseInt(s.slice(3,5),16);
    const b = parseInt(s.slice(5,7),16);
    const a = parseInt(s.slice(7,9),16)/255;
    const mixed = a * Math.max(0, Math.min(1, alpha));
    return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + mixed + ')';
  }
  return /^([a-zA-Z]+)$/.test(s) ? s : 'rgba(0, 0, 0, ' + Math.max(0, Math.min(1, alpha)) + ')';
}

/** ===== Width/border helpers (PATCHED) ===== */
function toPxWidth(val) {
  if (val === undefined || val === null) return null;
  const s = String(val).trim().toLowerCase();
  if (s === '0' || s === '0px' || s === '') return null;
  const n = Number(s.replace(/px$/, ''));
  if (!Number.isFinite(n) || n <= 0) return null;
  return String(Math.round(n)) + 'px';
}
function setBorder(elx, color, widthPx) {
  if (!elx || !elx.style) return;
  try {
    const hasColor = !!color && !isFullyTransparent(color);
    const w = toPxWidth(widthPx);
    if (hasColor && w) {
      elx.style.borderColor = toRgbNoAlpha(color);
      elx.style.borderStyle = 'solid';
      elx.style.borderWidth = w;
    } else {
      elx.style.borderStyle = 'none';
    }
  } catch (_) {}
}

// Snapshot & restore without writing empty strings (PATCHED)
function snapshotDesign(elx) {
  if (!elx || !elx.style || elx.__designSnapshot) return;
  try {
    elx.__designSnapshot = {
      backgroundColor: elx.style.backgroundColor || null,
      color: elx.style.color || null,
      borderColor: elx.style.borderColor || null,
      borderWidth: toPxWidth(elx.style.borderWidth || null),
      borderStyle: elx.style.borderStyle || null
    };
  } catch (_) {}
}
function restoreDesign(elx) {
  if (!elx || !elx.style || !elx.__designSnapshot) return;
  try {
    const s = elx.__designSnapshot;
    if (s.backgroundColor) elx.style.backgroundColor = s.backgroundColor;
    if (s.color)           elx.style.color = s.color;
    if (s.borderColor && s.borderWidth) {
      setBorder(elx, s.borderColor, s.borderWidth);
    } else {
      setBorder(elx, 'transparent', null);
    }
  } catch (_) {}
}

/** ===== Button style helpers (PATCHED) ===== */
function applyOutline(elx, {
  bg = 'rgba(0,0,0,0)',
  text = THEME.WHITE,
  border = 'rgba(0,0,0,0)'
} = {}) {
  if (!elx || !elx.style) return;
  try {
    elx.style.backgroundColor = normalizeColor(bg);
    elx.style.color = normalizeColor(text, { allowTransparent: false });
    const hasBorder = !isFullyTransparent(border);
    setBorder(elx, border, hasBorder ? '1px' : null);
    elx.style.boxShadow = 'none';
    elx.style.outline = 'none';
  } catch (_) {}
}
function applyFilled(elx, {
  bg = THEME.GREEN,
  text = THEME.WHITE,
  border = THEME.BORDER
} = {}) {
  if (!elx || !elx.style) return;
  try {
    elx.style.backgroundColor = normalizeColor(bg);
    elx.style.color = normalizeColor(text, { allowTransparent: false });
    const hasBorder = !isFullyTransparent(border);
    setBorder(elx, border, hasBorder ? '1px' : null);
    elx.style.boxShadow = 'none';
    elx.style.outline = 'none';
  } catch (_) {}
}
function clearFocus(elx) {
  try {
    if (elx && typeof elx.blur === 'function') elx.blur();
    if (elx && elx.style) {
      elx.style.boxShadow = 'none';
      elx.style.outline = 'none';
    }
  } catch (_) {}
}

/** ============================ Toasts / Loading ============================ */

let __toastTimer = null;
function getToastParts() {
  const box = el('#errorBoxToast');
  const txt = elOneOf('#errorToast', '#errorBoxToastText');
  return { box, txt };
}
function showToast(kind, message, durationMs) {
  const parts = getToastParts();
  const box = parts.box, txt = parts.txt;
  if (!box && !txt) return;

  const prefix = (kind === 'success') ? 'βœ“ ' : '';
  if (txt) setTextOrLabel(txt, prefix + String(message));

  try {
    const color =
      (kind === 'error')   ? TOAST.ERROR_TEXT :
      (kind === 'success') ? TOAST.SUCCESS_TEXT :
                             TOAST.INFO_TEXT;
    if (txt && txt.style) txt.style.color = normalizeColor(color, { allowTransparent: false });
  } catch (_) {}

  tryShow(box);
  tryShow(txt);

  if (__toastTimer) clearTrackedTimeout(__toastTimer);
  __toastTimer = setTrackedTimeout(function () {
    tryHide(txt);
    tryHide(box);
  }, durationMs);
}

function showError(message, duration) {
  showToast('error', message, duration || CONFIG.ERROR_TOAST_DURATION);
}
function showSuccess(message, duration) {
  showToast('success', message, duration || CONFIG.SUCCESS_TOAST_DURATION);
}
function showInfo(message, duration) {
  showToast('info', message, duration || CONFIG.INFO_TOAST_DURATION);
}

function showLoading(show) {
  const overlay = el('#loadingOverlay');
  if (show) { if (overlay) tryShow(overlay); }
  else { if (overlay) tryHide(overlay); }
}
function forceHideOverlay() {
  const overlay = el('#loadingOverlay');
  if (overlay) tryHide(overlay);
}

const WIRED = { chips: new Set(), buttons: new Set(), inputs: new Set() };

/** ============================ Coalesced refresh & realtime suppression ============================ */

function suspendRealtime(ms) {
  realtimeSuspendedUntil = Date.now() + Math.max(0, ms || 2000);
}

/** ============================ Search helpers ============================ */

const SEARCH_PREFIX = 'βŒ• ';
function ensureSearchPrefix(inp) {
  if (!inp) return;
  const v = String(inp.value || '');
  if (v.indexOf(SEARCH_PREFIX) !== 0) {
    inp.value = SEARCH_PREFIX + v.replace(/^βŒ•\s*/, '');
  }
}
function stripSearchPrefix(v) {
  const s = String(v || '');
  return s.indexOf(SEARCH_PREFIX) === 0 ? s.slice(SEARCH_PREFIX.length) : s;
}

/** ============================ Chips ============================ */

function getStatusChipItemsForHtml() {
  const base = [{ label: 'All', value: 'all' }];
  const list = Array.isArray(STATUSES) ? STATUSES : [];
  const rest = list.map(function (s) { return { label: (CHIP_LABELS && CHIP_LABELS[s]) ? CHIP_LABELS[s] : s, value: s }; });
  return base.concat(rest);
}
function initScrollableChipsHtml() {
  const html = el('#chipsHtml'); if (!html) return;
  tryShow(html);

  const items = getStatusChipItemsForHtml();
  if (typeof html.postMessage === 'function') {
    html.postMessage({ type: 'init', items: items, active: state.filters.selectedStatus || 'all' });
  }

  if (typeof html.onMessage === 'function') {
    html.onMessage(function (event) {
      const msg = event && event.data || {};
      if (msg.type === 'chipClick' && msg.value) {
        setActiveStatus(String(msg.value));
        preferWorkflowSort('chipsHtml', true);
        highlightQuickButtons();
        requestRefresh(true);
        saveFilters();
      }
    });
  }
}
function syncChipsHtmlActive() {
  const html = el('#chipsHtml'); if (!html) return;
  if (typeof html.postMessage === 'function') {
    html.postMessage({ type: 'setActive', active: state.filters.selectedStatus || 'all' });
  }
}

function paintChip(btn, active) {
  if (!btn) return;
  const st = getChipStyle(btn.__chipValue, !!active);
  const isPrimary = !!btn.__inPrimaryChips;

  const bg = (!active && isPrimary) ? addAlpha(st.bg, 0.3) : st.bg;
  const border = isPrimary ? 'transparent' : (st.border || 'transparent');

  applyFilled(btn, { bg: bg, text: st.text, border: border });

  try { if ('ariaPressed' in btn) btn.ariaPressed = !!active; } catch(_) {}
}
function wireChip(btn) {
  if (!btn || btn.__chipWired) return;
  btn.__chipWired = true;
  const repaint = function () { paintChip(btn, !!btn.__isActive); };

  if (canCall(btn, 'onMouseIn')) {
    const st = getChipStyle(btn.__chipValue, true);
    const border = btn.__inPrimaryChips ? 'transparent' : (st.border || 'transparent');
    applyFilled(btn, { bg: st.bg, text: st.text, border: border });
  }
  if (canCall(btn, 'onMouseOut')) btn.onMouseOut(function () { clearFocus(btn); repaint(); });
  if (canCall(btn, 'onFocus')) btn.onFocus(repaint);
  if (canCall(btn, 'onBlur')) btn.onBlur(repaint);
  if (canCall(btn, 'onClick')) {
    btn.onClick(function () {
      WIRED.chips.forEach(function (c) { c.__isActive = false; paintChip(c, false); });
      btn.__isActive = true;
      repaint();
      setActiveStatus(btn.__chipValue);
      highlightQuickButtons();
      requestRefresh(true);
      saveFilters();
    });
  }
  WIRED.chips.add(btn);
  repaint();
}
function buildStatusChips() {
  const r = elOneOf('#statusChipsRepeater', '#statusChipRepeater') || null;
  if (!r) { logWarn('Chips','status chips repeater not found'); return; }
  WIRED.chips.clear();
  const list = Array.isArray(STATUSES) ? STATUSES : [];
  r.data = [{ _id: 'all', title: 'All', value: 'all' }]
    .concat(list.map(function (s) { return { _id: 'status-' + s.toLowerCase().replace(/\s+/g,'-'), title: CHIP_LABELS[s] || s, value: s }; }));
  tryShow(r);
  if (canCall(r, 'onItemReady')) {
    r.onItemReady(function ($item, itemData) {
      const btn = itemEl($item, '#statusChipBtn');
      if (!btn) return;
      btn.label = itemData.title;
      btn.__chipValue = itemData.value;
      try { btn.__inPrimaryChips = (r.id === 'statusChipsRepeater'); } catch (_) { btn.__inPrimaryChips = false; }
      try { if ('ariaLabel' in btn) btn.ariaLabel = 'Filter by ' + itemData.title; } catch(_) {}
      wireChip(btn);
      btn.__isActive = (itemData.value === state.filters.selectedStatus);
      paintChip(btn, btn.__isActive);
    });
  }
  setActiveStatus(state.filters.selectedStatus);
}

/** ============================ Buttons & Inputs ============================ */

function paintClearFiltersBase(btn) {
  const st = getControlFilledStyle('clear');
  applyFilled(btn, { bg: st.bg, text: st.text, border: st.border });
}
function wireButtonTheme(btn) {
  if (!btn || btn.__btnWired) return;
  btn.__btnWired = true;
  const paint = function () {
    const activeStyle = btn.__activeStyle || getControlFilledStyle();
    if (btn.__isActive) applyFilled(btn, activeStyle);
    else applyOutline(btn, {});
  };
  if (btn.id === 'clearFiltersBtn') {
    const hoverPaint = function () {
      const st = getControlFilledStyle();
      applyFilled(btn, { bg: st.bg, text: st.text, border: st.border });
    };
    const basePaint  = function () { paintClearFiltersBase(btn); };
    if (canCall(btn, 'onMouseIn'))  btn.onMouseIn(hoverPaint);
    if (canCall(btn, 'onMouseOut')) btn.onMouseOut(function () { clearFocus(btn); basePaint(); });
    if (canCall(btn, 'onFocus'))    btn.onFocus(hoverPaint);
    if (canCall(btn, 'onBlur'))     btn.onBlur(basePaint);
    if (canCall(btn, 'onClick'))    btn.onClick(function () { setTrackedTimeout(function(){ clearFocus(btn); basePaint(); }, 0); });
    WIRED.buttons.add(btn);
    basePaint();
    return;
  }
  if (canCall(btn, 'onMouseIn'))  btn.onMouseIn(paint);
  if (canCall(btn, 'onMouseOut')) btn.onMouseOut(function () { clearFocus(btn); paint(); });
  if (canCall(btn, 'onFocus'))    btn.onFocus(paint);
  if (canCall(btn, 'onBlur'))     btn.onBlur(paint);
  if (canCall(btn, 'onClick'))    btn.onClick(function () { setTrackedTimeout(function(){ clearFocus(btn); paint(); }, 0); });
  WIRED.buttons.add(btn);
  paint();
}

function initMyJobsSnapshot() {
  const myBtn = el('#myJobsQuickBtn');
  if (myBtn && !myBtn.__editorSnapshotTaken) {
    snapshotDesign(myBtn);
    myBtn.__editorSnapshotTaken = true;
  }
}

function paintMyJobsQuickBtnVisuals() {
  const myBtn = el('#myJobsQuickBtn');
  if (!myBtn) return;

  setTextOrLabel(myBtn, 'My Jobs');

  if (!myBtn.__isActive || !myBtn.style) {
    if (myBtn.__editorSnapshotTaken && myBtn.__designSnapshot) {
      restoreDesign(myBtn);
    } else {
      applyOutline(myBtn, {});
    }
    return;
  }

  const status = state.filters.selectedStatus || 'all';
  let st = null;

  if (status && status !== 'all') {
    st = (typeof getChipStyle === 'function' && getChipStyle(status, true))
      || getStatusPillStyle(status);
  } else {
    st = (typeof getMyJobsStyle === 'function' && getMyJobsStyle())
      || { bg: COLORS.BLUE, text: THEME.WHITE, border: COLORS.BLUE };
  }

  const bg = st && st.bg != null ? st.bg : COLORS.BLUE;
  const text = st && st.text != null ? st.text : THEME.WHITE;
  const border = st && st.border != null ? st.border : 'transparent';

  applyFilled(myBtn, { bg: bg, text: text, border: border });
}

function wireInputTheme(inp) {
  if (!inp || inp.__inpWired) return;
  inp.__inpWired = true;
  const repaint = function () {
    try {
      const st = getControlFilledStyle();
      if (inp.style) {
        inp.style.backgroundColor = normalizeColor(st.bg);
        setBorder(inp, st.border, isFullyTransparent(st.border) ? null : '1px');
        inp.style.color = normalizeColor(st.text, { allowTransparent: false });
        inp.style.boxShadow = 'none';
        inp.style.outline = 'none';
      }
    } catch(e){ logWarn('Theme','input repaint failed',{id:inp && inp.id,err:e && e.message}); }
  };
  if (canCall(inp, 'onMouseIn'))  inp.onMouseIn(repaint);
  if (canCall(inp, 'onMouseOut')) inp.onMouseOut(repaint);
  if (canCall(inp, 'onFocus'))    inp.onFocus(repaint);
  if (canCall(inp, 'onBlur'))     inp.onBlur(repaint);
  WIRED.inputs.add(inp);
  repaint();
}

/**
 * Quick buttons: update state + ARIA; style My Jobs dynamically.
 */
function highlightQuickButtons() {
  const myBtn = el('#myJobsQuickBtn');
  const histBtn = el('#historyQuickBtn');

  if (myBtn) {
    const active = (state.filters.assignment === 'my' && state.filters.selectedStatus !== 'Complete');
    myBtn.__isActive = active;
    try { if ('ariaPressed' in myBtn) myBtn.ariaPressed = !!active; } catch(_) {}
    paintMyJobsQuickBtnVisuals();
  }
  if (histBtn) {
    const isHist = (state.filters.assignment === 'my' && state.filters.selectedStatus === 'Complete');
    histBtn.__isActive = isHist;
    try { if ('ariaPressed' in histBtn) histBtn.ariaPressed = !!isHist; } catch(_) {}
  }
}

/** ============================ Banners & Sort ============================ */

async function setLoginBanner() {
  const banner = el('#loginBannerText');
  if (!banner) return;
  const cu = wixUsers.currentUser;
  const loggedIn = !!(cu && cu.loggedIn);
  let name = loggedIn ? 'Member' : 'Guest';
  try {
    if (loggedIn) {
      const d = await getMemberDetails();
      name = clean(d && (d.fullName || d.firstName || d.email) || 'Member');
    }
  } catch (err) {
    logWarn('Members','Could not get member details',{err:err && err.message});
  }
  const roleLabel = loggedIn ? (user.isAdmin ? 'Admin' : (user.isMechanic ? 'Technician' : 'Member')) : 'Guest';
  const text = loggedIn ? (roleLabel + ' Logged In: ' + name) : 'Not logged in';
  setTextOrLabel(banner, text);
}
function setDashBannerText() {
  const dash = el('#banDashText'); if (!dash) return;
  const loggedIn = !!(wixUsers.currentUser && wixUsers.currentUser.loggedIn);
  if (loggedIn && user.isAdmin) setTextOrLabel(dash, 'Admin Dashboard');
  else if (loggedIn && user.isMechanic) setTextOrLabel(dash, 'Technician Dashboard');
  else setTextOrLabel(dash, 'Service Desk');
}
function initSortDropdown() {
  const dd = el('#sortDropdown'); if (!dd) return;
  dd.options = [
    { label: 'Newest first', value: 'newest' },
    { label: 'Oldest first', value: 'oldest' },
    { label: 'Job # high β†’ low', value: 'jobHigh' },
    { label: 'Job # low β†’ high', value: 'jobLow' },
    { label: 'Status (workflow)', value: 'status' },
    { label: 'Priority (Urgent β†’ Low)', value: 'priority' },
    { label: 'Unassigned first', value: 'unassignedFirst' },
    { label: 'My jobs first', value: 'myFirst' }
  ];
  dd.value = state.filters.sort || 'newest';
  try { if ('placeholder' in dd) dd.placeholder = 'Sort'; } catch(_) {}
}
function preferWorkflowSort(_trigger, force) {
  const current = state.filters.sort || 'newest';
  if (force || current === 'newest') {
    state.filters.sort = 'status';
    const sortDd = el('#sortDropdown'); if (sortDd) sortDd.value = 'status';
  }
}
function forceNewestSort() {
  state.filters.sort = 'newest';
  const sortDd = el('#sortDropdown'); if (sortDd) sortDd.value = 'newest';
}

/** ============================ Assignment ============================ */

function setAssignmentValue(v) {
  const dd  = el('#assignmentDd'); if (dd) dd.value = v;
  const rbg = el('#assignmentFilter'); if (rbg) rbg.value = (v === 'my' || v === 'all' || v === 'unassigned' || v === 'allPlusCompleted') ? v : 'all';
}
async function initAssignmentControl() {
  const rbg = el('#assignmentFilter');
  if (rbg) {
    rbg.options = [
      { label: 'All Assigned',     value: 'all' },
      { label: 'All + Completed',  value: 'allPlusCompleted' },
      { label: 'My jobs',          value: 'my' },
      { label: 'Unassigned',       value: 'unassigned' }
    ];
    rbg.value = (['my','unassigned','allPlusCompleted','all'].includes(state.filters.assignment)) ? state.filters.assignment : 'all';
    if (canCall(rbg, 'onChange')) {
      rbg.onChange(function () {
        state.filters.assignment = rbg.value || 'all';
        state.filters.includeCompleted = (rbg.value === 'allPlusCompleted');
        if (state.filters.includeCompleted) setActiveStatus('all');
        preferWorkflowSort('assignmentFilter');
        highlightQuickButtons();
        requestRefresh(true);
        saveFilters();
      });
    }
  }
  const dd = el('#assignmentDd');
  if (dd) {
    let options = [
      { label: 'All Assigned',     value: 'all' },
      { label: 'All + Completed',  value: 'allPlusCompleted' },
      { label: 'My jobs',          value: 'my' },
      { label: 'Unassigned',       value: 'unassigned' }
    ];
    try {
      const mechOptions = await fetchMechanicOptions();
      if (mechOptions.length) options = options.concat(mechOptions);
    } catch (e) {
      logWarn('Assignment','Could not build mechanic list',{err:e && e.message});
    }
    dd.options = options;
    try { if ('placeholder' in dd) dd.placeholder = 'Assignment'; } catch(_) {}
    const validValues = new Set(options.map(function(o){ return o.value; }));
    dd.value = validValues.has(state.filters.assignment) ? state.filters.assignment : 'all';
    if (canCall(dd, 'onChange')) {
      dd.onChange(function () {
        const val = dd.value || 'all';
        state.filters.assignment = val;
        state.filters.includeCompleted = (val === 'allPlusCompleted');
        if (state.filters.includeCompleted) setActiveStatus('all');
        setAssignmentValue(val);
        preferWorkflowSort('assignmentDd');
        highlightQuickButtons();
        requestRefresh(true);
        saveFilters();
      });
    }
  }
}
async function fetchMechanicOptions() {
  let roleOpts = [];
  try {
    const list = await listMechanicsByRole();
    if (Array.isArray(list) && list.length) {
      const seen = new Set();
      const out = list
        .map(function (m) { return { id: String(m.id || m._id || ''), name: String(m.name || m.fullName || '').trim() }; })
        .filter(function (m) { return m.id && m.name && !seen.has(m.id) && seen.add(m.id); })
        .map(function (m) { return { label: m.name, value: 'mech:' + m.id }; });
      roleOpts = out;
    }
  } catch (e) {
    logWarn('Assignment','listMechanicsByRole failed; will fallback',{err:e && e.message});
  }
  if (roleOpts.length) return dedupeAndSortOptions(roleOpts);
  const ticketOpts = await fetchMechanicOptionsFromTickets();
  return dedupeAndSortOptions(ticketOpts);
}
async function fetchMechanicOptionsFromTickets() {
  const MAX = 1000;
  const items = [];
  let res = await wixData.query(TICKETS).limit(MAX).find();
  items.push.apply(items, (res.items || []));
  while (res.hasNext()) {
    res = await res.next();
    items.push.apply(items, (res.items || []));
    if (items.length >= 5000) break;
  }
  const seen = new Map();
  for (let i=0;i<items.length;i++) {
    const t = items[i];
    const id = (t.assignedMechanicId || '').trim();
    const nm = (t.assignedMechanicName || '').trim();
    if (!id) continue;
    if (!seen.has(id)) seen.set(id, nm || id);
  }
  return Array.from(seen.entries()).map(function (kv) { return { label: kv[1], value: 'mech:' + kv[0] }; });
}
function dedupeAndSortOptions(opts) {
  const map = new Map();
  for (let i=0;i<(opts || []).length;i++) {
    const o = opts[i];
    if (!o || !o.value) continue;
    if (!map.has(o.value)) map.set(o.value, { label: o.label || o.value, value: o.value });
  }
  return Array.from(map.values()).sort(function (a, b) { return a.label.localeCompare(b.label); });
}

/** ============================ Inputs & Refresh ============================ */

function initSearchAndRefresh() {
  const search = el('#searchInput');
  if (search) {
    try { if ('placeholder' in search) { search.placeholder = 'βŒ•'; } } catch(_) {}
    search.value = SEARCH_PREFIX + (state.filters.search || '');
    if (canCall(search, 'onInput')) search.onInput(function () {
      ensureSearchPrefix(search);
      if (searchTimeout) clearTrackedTimeout(searchTimeout);
      searchTimeout = setTrackedTimeout(function () {
        const inp = el('#searchInput');
        if (inp) {
          state.filters.search = stripSearchPrefix((inp.value || '')).trim();
          preferWorkflowSort('search', true);
          highlightQuickButtons();
          requestRefresh(true);
          saveFilters();
        }
      }, CONFIG.SEARCH_DEBOUNCE);
    });
    if (canCall(search, 'onKeyPress')) search.onKeyPress(function (ev) {
      ensureSearchPrefix(search);
      if (ev.key === 'Enter') {
        if (searchTimeout) clearTrackedTimeout(searchTimeout);
        const inp = el('#searchInput');
        if (inp) {
          state.filters.search = stripSearchPrefix((inp.value || '')).trim();
          preferWorkflowSort('search', true);
          highlightQuickButtons();
          requestRefresh(true);
          saveFilters();
        }
      }
    });
    if (canCall(search, 'onFocus')) search.onFocus(function () { ensureSearchPrefix(search); });
    wireInputTheme(search);
  }
  const refresh = el('#refreshButton'); if (refresh) { try { refresh.label = refresh.label || 'Refresh'; } catch(_) {} }
}

/** ============================ Locations ============================ */

const ALLOWED_LOCATIONS = ['Adare','Berrings','Celtic','Glanmire','Killybegs','Fossa','Ringaskiddy'];
const LEGACY_LOCATIONS_VALUE = '__legacy__';
async function loadLocations() {
  const dd = el('#locationDropdown'); if (!dd) return;
  try {
    const res = await wixData.query(TICKETS).limit(1000).find();
    const allowedSet = new Set(ALLOWED_LOCATIONS);
    let hasLegacy = false;
    (res.items || []).forEach(function(i){
      const v = clean(i.plantLocation || '').trim();
      if (v && !allowedSet.has(v)) hasLegacy = true;
    });
    const options = [{ label: 'All locations', value: 'all' }]
      .concat(ALLOWED_LOCATIONS.map(function(v){ return { label: v, value: v }; }));
    if (hasLegacy) options.push({ label: 'Other (legacy)', value: LEGACY_LOCATIONS_VALUE });
    dd.options = options;
    const validValues = new Set(options.map(function(o){ return o.value; }));
    dd.value = validValues.has(state.filters.location) ? state.filters.location : 'all';
  } catch (e) {
    logWarn('Locations','Could not load locations',{err:e && e.message});
    dd.options = [{ label: 'All locations', value: 'all' }].concat(ALLOWED_LOCATIONS.map(function(v){ return { label: v, value: v }; }));
    dd.value = 'all';
  }
}
function saveFilters() {
  try { storage.setItem('ticketFilters', JSON.stringify(state.filters)); }
  catch (e) { logWarn('Storage','Could not save filters',{err:e && e.message}); }
}

/** ============================ Handlers ============================ */

function wireHandlers() {
  const dd = el('#locationDropdown');
  if (dd && canCall(dd, 'onChange')) {
    dd.onChange(function () {
      state.filters.location = dd.value;
      preferWorkflowSort('locationDropdown');
      highlightQuickButtons();
      requestRefresh(true);
      saveFilters();
    });
  }

  const sortDd = el('#sortDropdown');
  if (sortDd && canCall(sortDd, 'onChange')) {
    sortDd.onChange(function () {
      state.filters.sort = sortDd.value || 'newest';
      highlightQuickButtons();
      requestRefresh(true);
      saveFilters();
    });
  }

  const refreshBtn = el('#refreshButton');
  if (refreshBtn) {
    try { if (typeof refreshBtn.tooltip !== 'undefined') refreshBtn.tooltip = 'Refresh'; } catch(_) {}
  }
  if (refreshBtn && canCall(refreshBtn, 'onClick')) {
    refreshBtn.onClick(async function () {
      if (isRefreshing) return;
      startRefreshSpin();
      try {
        showLoading(true);
        const inp = el('#searchInput'); if (inp) state.filters.search = stripSearchPrefix((inp.value || '')).trim();
        await requestRefresh(true);
        showSuccess('Tickets refreshed');
        saveFilters();
      } catch (error) {
        showError('Failed to refresh tickets');
      } finally {
        stopRefreshSpin();
        showLoading(false);
        forceHideOverlay();
      }
    });
  }

  const newBtn = el('#newTicketBtn');
  if (newBtn && canCall(newBtn, 'onClick')) newBtn.onClick(function () {
    cleanupResources();
    try { wixLocation.to(FORM_URL); } catch (e) { logWarn('Nav','Navigation failed',{err:e && e.message}); }
  });

  const clearBtn = el('#clearFiltersBtn');
  if (clearBtn && canCall(clearBtn, 'onClick')) {
    clearBtn.onClick(function () {
      state.filters = { selectedStatus:'all', location:'all', assignment:'all', includeCompleted:false, search:'', sort:'newest' };
      const loc  = el('#locationDropdown'); if (loc) loc.value = 'all';
      setAssignmentValue('all');
      const sort = el('#sortDropdown');     if (sort) sort.value = 'newest';
      const inp  = el('#searchInput');      if (inp) inp.value = SEARCH_PREFIX;

      buildStatusChips();
      setActiveStatus('all');

      const my = el('#myJobsQuickBtn');
      if (my) {
        my.__isActive = false;
        try { if ('ariaPressed' in my) my.ariaPressed = false; } catch(_) {}
        if (my.__editorSnapshotTaken && my.__designSnapshot) restoreDesign(my);
        else applyOutline(my, {});
      }

      highlightQuickButtons();
      requestRefresh(true);
      saveFilters();
    });
  }

  const myBtn = el('#myJobsQuickBtn');
  const histBtn = el('#historyQuickBtn');

  if (myBtn && canCall(myBtn, 'onClick')) {
    myBtn.onClick(function () {
      state.filters.assignment = 'my';
      state.filters.includeCompleted = false;
      if (state.filters.selectedStatus === 'Complete') setActiveStatus('all');
      setAssignmentValue('my');
      preferWorkflowSort('myJobs', true);
      highlightQuickButtons();
      requestRefresh(true);
      saveFilters();
    });
  }
  if (histBtn && canCall(histBtn, 'onClick')) {
    histBtn.onClick(function () {
      state.filters.assignment = 'my';
      state.filters.includeCompleted = true;
      setActiveStatus('Complete');
      setAssignmentValue('my');
      forceNewestSort();
      highlightQuickButtons();
      requestRefresh(true);
      saveFilters();
    });
  }
}

/** ============================ Sticky & Infinite Scroll ============================ */

function ensureSentinelReady() {
  const sentinel = el('#loadMoreSentinel');
  if (!sentinel) return;
  try { if (typeof sentinel.height === 'number' && sentinel.height < 40) sentinel.height = 60; } catch (_) {}
}

function setupLoadMoreFallback() {
  const btn = el('#loadMoreButton');
  if (!btn || !canCall(btn, 'onClick')) return;
  setTextOrLabel(btn, 'Load more');
  const paint = function () {
    const st = getControlFilledStyle();
    applyFilled(btn, { bg: st.bg, text: st.text, border: st.border });
  };
  if (canCall(btn, 'onMouseIn'))  btn.onMouseIn(paint);
  if (canCall(btn, 'onMouseOut')) btn.onMouseOut(paint);
  if (canCall(btn, 'onFocus'))    btn.onFocus(paint);
  if (canCall(btn, 'onBlur'))     btn.onBlur(paint);
  paint();

  btn.onClick(async function () {
    if (state.loading || !state.paging.hasMore) return;
    try {
      showLoading(true);
      const added = await requestRefresh(false);
      if (added > 0) showInfo('Loaded ' + added + ' more');
    } catch {
      showError('Failed to load more tickets. Try again.');
    } finally {
      showLoading(false);
      forceHideOverlay();
    }
  });
}

function setupPagingUI() {
  const btn = el('#mobileFiltersButton');
  const container = el('#filtersContainer');
  if (btn) { tryHide(btn); }
  if (container) {
    try { if (container.collapsed) container.expand && container.expand(); } catch(_) {}
  }
  ensureSentinelReady();
  setupLoadMoreFallback();

  const sentinel = el('#loadMoreSentinel');
  if (sentinel && canCall(sentinel, 'onViewportEnter')) {
    let throttle = false;
    const fire = async function () {
      if (throttle || state.loading || !state.paging.hasMore) return;
      throttle = true;
      try {
        showLoading(true);
        const added = await requestRefresh(false);
        if (added > 0) showInfo('Loaded ' + added + ' more');
      } catch (e) {
        showError('Failed to load more tickets. Scroll again to retry.');
      } finally {
        showLoading(false);
        forceHideOverlay();
        setTrackedTimeout(function () { throttle = false; }, CONFIG.INFINITE_SCROLL_THROTTLE);
      }
    };
    sentinel.onViewportEnter(function () { fire(); });
  }
}
function setupSticky(sentinelSel, sourceSel, pinnedSel, options) {
  const desktopOnly = options && options.desktopOnly;
  const sentinel = el(sentinelSel);
  const source = el(sourceSel);
  const pinned = el(pinnedSel);
  if (!pinned) return;
  if (sentinel) { try { if (typeof sentinel.height === 'number' && sentinel.height < 8) sentinel.height = 8; } catch(_) {} }
  try { if (pinned.hide) pinned.hide(); } catch(_) {}
  const showPinned = function () { if (!(desktopOnly && isMobile)) tryShow(pinned); };
  const hidePinned = function () { tryHide(pinned); };
  if (sentinel) {
    if (canCall(sentinel, 'onViewportLeave')) sentinel.onViewportLeave(showPinned);
    if (canCall(sentinel, 'onViewportEnter')) sentinel.onViewportEnter(hidePinned);
  }
  if (source) {
    if (canCall(source, 'onViewportLeave')) source.onViewportLeave(showPinned);
    if (canCall(source, 'onViewportEnter')) source.onViewportEnter(hidePinned);
  }
  hidePinned();
}

/** ============================ Realtime with Backoff ============================ */

function startPolling(initialMs) {
  pollIntervalMs = initialMs || CONFIG.POLL_START_MS;
  if (pollTimer) { clearTrackedInterval(pollTimer); pollTimer = null; }
  wixData.query(TICKETS).limit(1).descending('_updatedDate').find()
    .then(function (r) { if (r.items && r.items[0]) lastSeen = r.items[0]._updatedDate; })
    .finally(function () { if (!pollTimer) pollTimer = setTrackedInterval(checkForChanges, pollIntervalMs); });
}
function checkForChanges() {
  wixData.query(TICKETS)
    .gt('_updatedDate', lastSeen || new Date(0))
    .limit(1).descending('_updatedDate')
    .find()
    .then(function (res) {
      if (res.items && res.items.length) {
        lastSeen = res.items[0]._updatedDate;
        pollIntervalMs = CONFIG.POLL_START_MS;
        requestRefresh(true);
      }
    })
    .catch(function (e) {
      logWarn('Polling','Polling error',{err:e && e.message});
      pollIntervalMs = Math.min(CONFIG.POLL_MAX_MS, Math.round(pollIntervalMs * 1.5));
      if (pollTimer) { clearTrackedInterval(pollTimer); }
      pollTimer = setTrackedInterval(checkForChanges, pollIntervalMs);
    });
}
function stopPolling() { if (pollTimer) clearTrackedInterval(pollTimer); pollTimer = null; }

function startRealtimeRefresh() {
  if (typeof realtimeUnsub === 'function') { try { realtimeUnsub(); } catch (_) {} realtimeUnsub = null; }
  const debounced = (function () {
    let t;
    return function () {
      if (Date.now() < realtimeSuspendedUntil) return;
      if (refreshInFlight || state.loading) return;
      if (t) clearTrackedTimeout(t);
      t = setTrackedTimeout(function () { requestRefresh(true); }, CONFIG.REALTIME_DEBOUNCE_MS);
    };
  })();

  try {
    const stop = subscribeToTickets(function () {
      debounced();
    }, {
      dedupeTtlMs: 20000
    });
    realtimeUnsub = trackSubscription(stop);
  } catch (e) {
    logWarn('Realtime','subscribe failed; using polling',{err:e && e.message});
    startPolling(90000);
  }
}

/** ============================ Auth Watcher (throttled) ============================ */

function startAuthWatcher() {
  let last = !!(wixUsers.currentUser && wixUsers.currentUser.loggedIn);
  setTrackedInterval(async function () {
    const now = !!(wixUsers.currentUser && wixUsers.currentUser.loggedIn);
    if (now !== last) {
      last = now;
      await initUser();
      await setLoginBanner();
      setDashBannerText();
      await initAssignmentControl();
      highlightQuickButtons();
      await requestRefresh(true);
    }
  }, CONFIG.AUTH_CHECK_INTERVAL);
}

/** ============================ Retry wrapper for backend ============================ */

async function callWithRetry(fn, args, options) {
  const tries = (options && options.tries) || CONFIG.MAX_RETRY_ATTEMPTS;
  const baseDelay = (options && options.delay) || CONFIG.RETRY_BASE_DELAY;
  let lastErr;
  for (let i = 0; i < tries; i++) {
    try { return await fn.apply(null, args || []); }
    catch (e) {
      lastErr = e;
      const transient = /timeout|network|fetch|500|502|503|504/i.test(String((e && e.message) || '')) || [408,429,500,502,503,504].includes(Number((e && e.status) || (e && e.httpStatus)));
      if (!transient || i === tries - 1) throw e;
      await new Promise(function (r) { setTimeout(r, baseDelay * Math.pow(1.6, i)); });
    }
  }
  throw lastErr;
}

/** ============================ Init ============================ */

function resetUiStyles() {
  ['#mobileFiltersButton','#clearFiltersBtn',
   '#locationDropdown','#assignmentDd','#sortDropdown','#searchInput','#refreshButton',
   '#statusChipsRepeater','#statusChipRepeater'
  ]
    .forEach(function (sel) { const x = el(sel); try { if (x && x.style) { x.style.boxShadow = 'none'; x.style.outline = 'none'; } } catch (_) {} });
}
function applyThemeStyles() {
  ['#mobileFiltersButton','#clearFiltersBtn'].forEach(function (id) {
    const b = el(id); if (!b) return;
    wireButtonTheme(b);
    if (id === '#clearFiltersBtn') {
      paintClearFiltersBase(b);
    }
  });

  const histBtn = el('#historyQuickBtn'); if (histBtn) setTextOrLabel(histBtn, 'My History');

  wireInputTheme(el('#searchInput'));
}

const roleCache = { data: null, expiry: 0, TTL: CONFIG.USER_ROLE_CACHE_TTL };

async function initUser() {
  const cu = wixUsers.currentUser;
  user.id = (cu && cu.loggedIn) ? cu.id : null;
  try {
    if (cu && cu.loggedIn) {
      const now = Date.now();
      if (roleCache.data && now < roleCache.expiry) {
        const names = roleCache.data;
        user.isMechanic = names.includes('mechanic');
        user.isAdmin = names.includes('admin');
      } else {
        const roles = await cu.getRoles();
        const names = (roles || []).map(function(r){ return String((r && r.name) || '').toLowerCase(); });
        roleCache.data = names;
        roleCache.expiry = now + roleCache.TTL;
        user.isMechanic = names.includes('mechanic');
        user.isAdmin = names.includes('admin');
      }
    } else {
      user.isMechanic = false;
      user.isAdmin = false;
    }
  } catch (err) { logWarn('Auth','Could not get user roles',{err:err && err.message}); }
}

function maybeBustCacheOnUiVersionChange() {
  try {
    const CURRENT = String(UI_VERSION || '');
    if (!CURRENT) return;
    const LAST = storage.getItem('uiVersion') || '';
    const FLAG = 'reloaded:' + CURRENT;
    if (LAST !== CURRENT && !sessionStorage.getItem(FLAG)) {
      storage.setItem('uiVersion', CURRENT);
      sessionStorage.setItem(FLAG, '1');
      const url = wixLocation.url;
      const sep = url.indexOf('?') >= 0 ? '&' : '?';
      wixLocation.to(url + sep + 'v=' + Date.now());
    }
  } catch (_) {}
}

$w.onReady(async function () {
  maybeBustCacheOnUiVersionChange();

  try {
    forceHideOverlay();
    showLoading(true);

    const dash = el('#banDashText'); if (dash) setTextOrLabel(dash, 'Service Desk');

    const saved = storage.getItem('ticketFilters');
    if (saved) { try { state.filters = Object.assign({}, state.filters, JSON.parse(saved)); } catch { logWarn('Storage','Invalid saved filters'); } }

    await initUser();
    await setLoginBanner();
    setDashBannerText();

    resetUiStyles();
    applyThemeStyles();

    initMyJobsSnapshot();

    buildStatusChips();

    if (isMobile) {
      tryShow(el('#chipsHtml'));
      tryHide(elOneOf('#statusChipsRepeater', '#statusChipRepeater'));
      initScrollableChipsHtml();
    } else {
      tryHide(el('#chipsHtml'));
      tryShow(elOneOf('#statusChipsRepeater', '#statusChipRepeater'));
    }

    await loadLocations();
    await initAssignmentControl();
    initSortDropdown();
    initSearchAndRefresh();
    wireHandlers();

    const rep = el('#ticketsRepeater');
    if (rep && canCall(rep, 'onItemReady')) rep.onItemReady(function ($item, t) { renderItem($item, t); });

    await requestRefresh(true);
    startRealtimeRefresh();
    try { wixUsers.onLogin(async function () { await initUser(); await setLoginBanner(); setDashBannerText(); await initAssignmentControl(); highlightQuickButtons(); await requestRefresh(true); }); } catch (_) {}
    startAuthWatcher();

    try { const pin = el('#headerRowPinned'); if (pin && pin.hide) pin.hide(); } catch(_) {}
    setupSticky('#headerSentinel', '#headerRowBox', '#headerRowPinned', { desktopOnly: false });

    setupPagingUI();
  } catch (error) {
    showError('Failed to initialize tickets page. Please refresh and try again.');
    logError('Init', error);
  } finally {
    showLoading(false);
    forceHideOverlay();
  }
});

/** ============================ Status selection ============================ */

function setActiveStatus(value) {
  state.filters.selectedStatus = value;
  WIRED.chips.forEach(function (chip) {
    chip.__isActive = (chip.__chipValue === value);
    paintChip(chip, chip.__isActive);
  });
  const r = elOneOf('#statusChipsRepeater', '#statusChipRepeater') || null;
  if (r && canCall(r, 'forEachItem')) {
    r.forEachItem(function ($item, itemData) {
      const btn = itemEl($item, '#statusChipBtn');
      if (!btn) return;
      btn.__chipValue = itemData.value;
      btn.__isActive  = (itemData.value === value);
      paintChip(btn, btn.__isActive);
    });
  }
  highlightQuickButtons();
  syncChipsHtmlActive();
}

/** ============================ Re-render helpers (stable) ============================ */

function forceRepeaterRerender(reason) {
  const rep = el('#ticketsRepeater');
  if (!rep || !('data' in rep) || !Array.isArray(rep.data)) return;
  rep.data = rep.data.slice();
  logScope('UI', 'Repeater re-render forced', { reason: reason || 'manual', count: rep.data.length });
}

function nextTick() { return new Promise(function(resolve){ setTimeout(resolve, 0); }); }
async function setRepeaterData(items, opts) {
  const rep = el('#ticketsRepeater');
  if (!rep || !('data' in rep)) return;
  const hard = opts && opts.hard;
  const reason = (opts && opts.reason) || '';
  const data = Array.isArray(items) ? items.map(function(i){ return Object.assign({}, i); }) : [];
  if (hard) {
    rep.data = [];
    await nextTick();
  }
  rep.data = data;
  await nextTick();
  logScope('UI', 'setRepeaterData applied', { hard: !!hard, reason: reason, count: data.length });
}

function patchWithUndo(ticketId, patch) {
  const rep = el('#ticketsRepeater');
  if (!rep || !('data' in rep) || !Array.isArray(rep.data)) return function(){};
  const idx = rep.data.findIndex(function(it){ return it && it._id === ticketId; });
  if (idx < 0) return function(){};
  const before = rep.data[idx] || {};
  const after = Object.assign({}, before, patch);
  const newData = rep.data.slice();
  newData[idx] = after;
  rep.data = newData;
  forceRepeaterRerender('patchWithUndo');
  return function () {
    const rep2 = el('#ticketsRepeater');
    if (!rep2 || !Array.isArray(rep2.data)) return;
    const idx2 = rep2.data.findIndex(function(it){ return it && it._id === ticketId; });
    if (idx2 < 0) return;
    const rollback = rep2.data.slice();
    rollback[idx2] = before;
    rep2.data = rollback;
    forceRepeaterRerender('undoPatch');
  };
}

/** ============================ MoreInfo Lightbox helpers (NEW) ============================ */

function buildInfoSnapshot(t) {
  t = t || {};
  return {
    _id: t._id,
    jobCartNumber: t.jobCartNumber,
    title: t.title,
    briefDescription: t.briefDescription,
    complaintNotes: t.complaintNotes,
    correctiveAction: t.correctiveAction,
    causeOfFailure: t.causeOfFailure,
    complicationRepair: t.complicationRepair,
    poNumber: t.poNumber,
    status: t.status,
    priority: t.priority,

    plantLocation: t.plantLocation,
    plantNumber: t.plantNumber,
    vehicleSerial: t.vehicleSerial,
    vehicleModel: t.vehicleModel,
    vehicleType: t.vehicleType,
    machineHours: t.machineHours,

    assignedMechanicId: t.assignedMechanicId,
    assignedMechanicName: t.assignedMechanicName,

    submissionDate: t.submissionDate,
    createdByID: t.createdByID,
    createdByName: t.createdByName,
    createdByRole: t.createdByRole,

    lastStatusChangeAt: t.lastStatusChangeAt,

    finishDate: t.finishDate,
    completedById: t.completedById,
    completedByName: t.completedByName,

    lastStatusChangedById: t.lastStatusChangedById,
    lastStatusChangedByName: t.lastStatusChangedByName,
    completedAt: t.completedAt
  };
}

async function openInfoLightbox(t) {
  try {
    await wixWindow.openLightbox(INFO_LIGHTBOX_NAME, {
      ticketId: (t && t._id) || '',
      snapshot: buildInfoSnapshot(t || {})
    });
  } catch (e) {
    logWarn('InfoLightbox', 'Could not open MoreInfoLightBox', { err: e && e.message });
  }
}

/** ============================ Rendering ============================ */

function paintGlassFromStatus($item, _style, label) {
  const glass = itemEl($item, '#Glass');
  if (!glass || !glass.style) return;

  const base = getGlassBaseStyle(String(label || '').trim());
  const bw = Number(base.borderWidth || 0);

  try {
    glass.style.backgroundColor = normalizeColor(base.bg);
    setBorder(glass, base.borderColor, bw > 0 ? (String(bw) + 'px') : null);
  } catch (_) {}
}

function wireGlassHover($item, _style, label) {
  const glass = itemEl($item, '#Glass'); if (!glass) return;
  if (glass.__hoverWired) return;
  glass.__hoverWired = true;

  const reset = function () { paintGlassFromStatus($item, null, label); };

  if (canCall(glass, 'onMouseIn')) {
    const hov = getGlassHoverStyle(label);
    const bw  = Number(hov.borderWidth || 1);
    glass.onMouseIn(function () {
      try {
        if (glass.style) {
          glass.style.backgroundColor = normalizeColor(GLASS_HOVER_BG);
          setBorder(glass, hov.borderColor, bw > 0 ? (String(bw) + 'px') : null);
        }
      } catch(_) {}
    });
  }

  if (canCall(glass, 'onMouseOut')) {
    glass.onMouseOut(function () { reset(); });
  }

  if (canCall(glass, 'onFocus')) {
    const hov = getGlassHoverStyle(label);
    const bw  = Number(hov.borderWidth || 1);
    glass.onFocus(function () {
      try {
        if (glass.style) {
          glass.style.backgroundColor = normalizeColor(GLASS_HOVER_BG);
          setBorder(glass, hov.borderColor, bw > 0 ? (String(bw) + 'px') : null);
        }
      } catch(_) {}
    });
  }
  if (canCall(glass, 'onBlur')) {
    glass.onBlur(function () { reset(); });
  }

  if (canCall(glass, 'onViewportLeave')) {
    glass.onViewportLeave(reset);
  }
}

function showPill($box) { try { if ($box && $box.show) $box.show(); if ($box && $box.expand) $box.expand(); } catch (_) {} }
function paintPill($box, $text, label, style) {
  if (!$box || !$text || !style) return;
  try {
    if ($box.style) {
      $box.style.backgroundColor = normalizeColor(style.bg);
      const hasB = style.border && !isFullyTransparent(style.border);
      setBorder($box, style.border || 'transparent', hasB ? '1px' : null);
    }
  } catch (_) {}
  try { $text.text = label; } catch (_) {}
  try { if ($text.style) { $text.style.color = normalizeColor(style.text, { allowTransparent: false }); } } catch (_) {}
}

function paintStatusPill($box, $text, label, statusKey) {
  if (!$box || !$text) return;
  const st = getStatusPillStyle(statusKey);
  try {
    if ($box.style) {
      $box.style.backgroundColor = normalizeColor(st.bg);
      const bw = Number(st.borderWidth || 0);
      setBorder($box, st.border || 'transparent', bw > 0 ? (String(bw) + 'px') : null);
    }
  } catch (_) {}
  try { $text.text = label; } catch (_) {}
  try { if ($text.style) { $text.style.color = normalizeColor(st.text || THEME.WHITE, { allowTransparent: false }); } } catch (_) {}
}

function bindRowClick($item, t, skipStatusPillClick) {
  const clickTargets = ['#rowClickArea','#jobNumberText','#titleText','#plantNumberText','#locationText','#plantText','#serialText','#modelText','#vehicleTypeText','#machineHoursText'];
  for (let i=0;i<clickTargets.length;i++) {
    const sel = clickTargets[i];
    const elx = itemEl($item, sel);
    if (!elx) continue;
    try { if (elx.style) elx.style.cursor = 'pointer'; } catch(_) {}
    if (canCall(elx, 'onClick')) elx.onClick(function(){ openTicketDetails(t); });
  }
  if (!skipStatusPillClick) {
    const sText = itemEl($item, '#statusText');
    if (sText && canCall(sText, 'onClick')) {
      try { if (sText.style) sText.style.cursor = 'pointer'; } catch(_) {}
      sText.onClick(function(){ openTicketDetails(t); });
    }
  }
}
async function openTicketDetails(t) {
  try {
    await wixWindow.openLightbox(DETAILS_LIGHTBOX_NAME, { ticketId: t._id });
  } catch (e) {
    try { wixLocation.to(DETAILS_PAGE_URL + '?id=' + encodeURIComponent(t._id)); }
    catch (err) { logWarn('Nav','Navigation failed',{err:err && err.message}); }
  }
}

/** ---------- Optimistic update helpers (legacy shim) ---------- */

function patchRepeaterItem(ticketId, patch) {
  const rep = el('#ticketsRepeater');
  if (!rep || !('data' in rep) || !Array.isArray(rep.data)) return false;

  const idx = rep.data.findIndex(function(it){ return it && it._id === ticketId; });
  if (idx < 0) return false;

  const before = rep.data[idx] || {};
  const after  = Object.assign({}, before, patch);
  const newData = rep.data.slice();
  newData[idx] = after;
  rep.data = newData;
  forceRepeaterRerender('patchRepeaterItem');
  return true;
}

function resultIndicatesSave(res) {
  if (!res) return false;
  const a = String((res.action || '')).toLowerCase();
  if (['save','saved','update','updated','apply','applied','ok','done'].includes(a)) return true;
  if (typeof res.status === 'string' && res.status.trim()) return true;
  if (typeof res.priority === 'string' && res.priority.trim()) return true;
  if (typeof res.poNumber === 'string') return true;
  if (typeof res.brief === 'string') return true;
  if (res.changed || res.updated) return true;
  return false;
}

async function handleLightboxUpdate(updateFn, opts) {
  const successMessage = (opts && opts.successMessage) || 'Updated';
  const errorMessage = (opts && opts.errorMessage) || 'Update failed. Refreshing…';
  const suspendMs = (opts && opts.suspendMs) || 2000;

  if (operationInProgress) {
    showInfo('Please wait for current operation to complete');
    return;
  }
  operationInProgress = true;
  try {
    suspendRealtime(suspendMs);
    await updateFn();
    showSuccess(successMessage);
    await requestRefresh(true);
  } catch (e) {
    logError('LightboxUpdate', e);
    showError(errorMessage);
    await requestRefresh(true);
  } finally {
    operationInProgress = false;
  }
}

async function openStatusLightboxAndUpdate(t) {
  if (operationInProgress) { showInfo('Please wait for current operation to complete'); return; }
  const originalStatus = t.status;
  try {
    const res = await wixWindow.openLightbox(STATUS_LIGHTBOX_NAME, {
      ticketId: t._id,
      currentStatus: t.status || 'Not Yet Started',
      isAdmin: user.isAdmin,
      isAssignedToUser: !!(user.id && t.assignedMechanicId === user.id)
    });
    if (!resultIndicatesSave(res)) return;

    const nextStatus = String((res && res.status) || '').trim();
    if (!nextStatus) return;

    await handleLightboxUpdate(async function () {
      const undo = patchWithUndo(t._id, { status: nextStatus });
      try {
        await callWithRetry(updateTicketStatus, [t._id, nextStatus]);
        forceRepeaterRerender('status updated');
      } catch (err) {
        undo();
        throw err;
      }
    }, {
      successMessage: 'Status updated to ' + nextStatus,
      errorMessage: 'Failed to update status. Refreshing…'
    });
  } catch (e) {
    patchRepeaterItem(t._id, { status: originalStatus });
    logError('StatusLightbox', e);
    showError('Failed to update status. Refreshing…');
    await requestRefresh(true);
  }
}

async function openPriorityLightboxAndUpdate(t) {
  const before = String(t.priority || 'Medium');
  try {
    const res = await wixWindow.openLightbox(PRIORITY_LIGHTBOX_NAME, {
      ticketId: t._id,
      currentPriority: t.priority || 'Medium',
      jobCartNumber: t.jobCartNumber,
      title: t.title
    });
    if (!resultIndicatesSave(res)) return;

    const nextPriority = String((res && res.priority) || '').trim();
    const allowed = new Set(['Urgent', 'Medium', 'Low']);
    if (!allowed.has(nextPriority)) return;

    await handleLightboxUpdate(async function () {
      const undo = patchWithUndo(t._id, { priority: nextPriority });
      try {
        await callWithRetry(updateTicket, [t._id, { priority: nextPriority }]);
        forceRepeaterRerender('priority updated');
      } catch (err) {
        undo();
        throw err;
      }
    }, {
      successMessage: 'Priority updated to ' + nextPriority,
      errorMessage: 'Failed to update priority. Refreshing…'
    });
  } catch (e) {
    patchRepeaterItem(t._id, { priority: before });
    logError('PriorityLightbox', e);
    showError('Failed to update priority. Refreshing…');
    await requestRefresh(true);
  }
}

function renderItem($item, t) {
  const jobNumberText = itemEl($item, '#jobNumberText');
  const jobPill      = itemEl($item, '#jobNumberPill');
  const jobPillText  = itemEl($item, '#jobNumberPillText');
  const poText       = itemEl($item, '#poNumberText');
  const dateText     = itemEl($item, '#dateText');
  const titleText    = itemEl($item, '#titleText');
  const statusText   = itemEl($item, '#statusText');
  const serialText   = itemEl($item, '#serialText');
  const modelText    = itemEl($item, '#modelText');
  const vehicleTypeText = itemEl($item, '#vehicleTypeText');
  const machineHoursText = itemEl($item, '#machineHoursText');

  const plantNumberText = itemEl($item, '#plantNumberText') || itemEl($item, '#plantText');
  const locationText    = itemEl($item, '#locationText') || itemEl($item, '#plantLocationText');

  const isAssignedToUser = !!(user.id && t.assignedMechanicId === user.id);

  const sLabel = t.status || 'Not Yet Started';
  const sStyle = STATUS_STYLES[sLabel] || STATUS_STYLES['Not Yet Started'];
  const displayStatus = STATUS_DISPLAY[sLabel] || sLabel;

  const jobLabel = (t.jobCartNumber !== undefined && t.jobCartNumber !== null && t.jobCartNumber !== '') ? String(t.jobCartNumber) : '-';

  if (poText) {
    const hasPO = !!(t.poNumber && String(t.poNumber).trim());
    const poVal = hasPO ? t.poNumber : 'Add';
    setOneLine(poText, poVal, CLAMP.po);
    const isAssignedToUserNow = isAssignedToUser;
    const mechanicCanAdd = user.isMechanic && isAssignedToUserNow && !hasPO && sLabel !== 'Complete';
    const adminCanEdit   = user.isAdmin && sLabel !== 'Complete';
    if (mechanicCanAdd || adminCanEdit) {
      try { if (poText.style) { poText.style.cursor = 'pointer'; poText.style.textDecoration = 'underline'; } } catch(_) {}
      if (canCall(poText, 'onClick')) {
        poText.onClick(async function () {
          try {
            if (mechanicCanAdd) {
              const res = await wixWindow.openLightbox(UPDATE_PO_LIGHTBOX_NAME, { ticketId: t._id, currentPO: '', collectOnly: true });
              if (!resultIndicatesSave(res)) return;
              const proposed = (res.poNumber || '').trim();

              await handleLightboxUpdate(async function () {
                const undo = patchWithUndo(t._id, { poNumber: proposed });
                try {
                  await callWithRetry(updateTicket, [t._id, { poNumber: proposed }]);
                  forceRepeaterRerender('po added');
                } catch (err) {
                  undo();
                  throw err;
                }
              }, { successMessage: 'PO number added', errorMessage: 'Failed to update PO. Refreshing…' });

              return;
            }
            const res = await wixWindow.openLightbox(UPDATE_PO_LIGHTBOX_NAME, { ticketId: t._id, currentPO: t.poNumber || '' });
            if (!resultIndicatesSave(res)) return;

            const newPO = typeof res.poNumber === 'string' ? res.poNumber : (t.poNumber || '');

            await handleLightboxUpdate(async function () {
              const undo = patchWithUndo(t._id, { poNumber: newPO });
              try {
                if (res.action !== 'saved') {
                  await callWithRetry(updatePurchaseOrderAdmin, [t._id, newPO || '']);
                }
                forceRepeaterRerender('po updated');
              } catch (err) {
                undo();
                throw err;
              }
            }, { successMessage: 'PO number updated', errorMessage: 'Failed to update PO. Refreshing…' });

          } catch (e) {
            logError('PO', e);
            showError('Failed to update PO. Refreshing…');
            await requestRefresh(true);
          }
        });
      }
    } else {
      try { if (poText.style) { poText.style.cursor = 'default'; poText.style.textDecoration = 'none'; } } catch(_) {}
    }
  }

  const pLabel = (t.priority || 'Medium').trim();
  const pStyle = PRIORITY_STYLES[pLabel] || PRIORITY_STYLES['Medium'];

  if (jobPill && jobPillText) {
    showPill(jobPill);
    paintPill(jobPill, jobPillText, jobLabel, pStyle);
    const canOpenPriority = (user.isAdmin || (user.isMechanic && isAssignedToUser)) && sLabel !== 'Complete';
    try { if (jobPill.style) jobPill.style.cursor = canOpenPriority ? 'pointer' : 'default'; } catch(_) {}
    if (canOpenPriority && canCall(jobPill, 'onClick')) jobPill.onClick(function(){ openPriorityLightboxAndUpdate(t); });
    if (jobNumberText) { try { jobNumberText.text = ''; if (jobNumberText.collapse) jobNumberText.collapse(); } catch(_) {} }
  } else if (jobNumberText) {
    jobNumberText.text = jobLabel;
    try { if (jobNumberText.style) jobNumberText.style.color = normalizeColor(pStyle.text, { allowTransparent: false }); } catch(_) {}
  }

  if (dateText) {
    const s = t.submissionDate;
    let d = null;
    if (s instanceof Date) d = s;
    else if (typeof s === 'string') d = new Date(s);
    dateText.text = (d && !Number.isNaN(d.getTime()))
      ? d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: '2-digit' })
      : '';
  }

  if (statusText) statusText.text = clean(t.status || '');
  if (serialText) setOneLine(serialText, t.vehicleSerial, CLAMP.serial);
  if (modelText)  setOneLine(modelText,  t.vehicleModel,  CLAMP.model);
  if (vehicleTypeText) setOneLine(vehicleTypeText, t.vehicleType || t.vehicleModel || '', CLAMP.model);
  if (machineHoursText) setOneLine(machineHoursText, formatHours(t.machineHours), CLAMP.hours);

  const pnRaw = (t.plantNumber !== undefined && t.plantNumber !== null) ? String(t.plantNumber) : '';
  if (plantNumberText) { const pnToShow = pnRaw.trim() ? pnRaw.trim() : 'β€”'; setOneLine(plantNumberText, pnToShow, CLAMP.pn); try { if (plantNumberText.show) plantNumberText.show(); if (plantNumberText.expand) plantNumberText.expand(); } catch(_) {} }
  if (locationText) { setOneLine(locationText, (t.plantLocation || '').trim(), CLAMP.location); try { if (locationText.show) locationText.show(); if (locationText.expand) locationText.expand(); } catch(_) {} }

  const statusPill     = itemEl($item, '#statusPill');
  const statusPillText = itemEl($item, '#statusPillText');

  if (statusPill && statusPillText) { showPill(statusPill); paintStatusPill(statusPill, statusPillText, displayStatus, sLabel); }
  else if (statusText) { try { if (statusText.style) statusText.style.color = normalizeColor(sStyle.text, { allowTransparent: false }); } catch(_) {} }

  paintGlassFromStatus($item, sStyle, sLabel);
  wireGlassHover($item, sStyle, sLabel);

  const canOpenStatusLightbox =
    (user.isAdmin || (user.isMechanic && !!(user.id && t.assignedMechanicId === user.id))) &&
    sLabel !== 'Complete';
  const clickableStatusEl = statusPill || statusText;
  if (clickableStatusEl && canOpenStatusLightbox) {
    try { if (clickableStatusEl.style) clickableStatusEl.style.cursor = 'pointer'; } catch(_) {}
    if (canCall(clickableStatusEl, 'onClick')) {
      clickableStatusEl.onClick(function(){ openStatusLightboxAndUpdate(t); });
    }
  }

  const priorityPill = itemEl($item, '#priorityPill'); if (priorityPill) { try { if (priorityPill.collapse) priorityPill.collapse(); } catch(_) {} }

  const briefEl = itemEl($item, '#briefText');
  const briefSource =
    (t.briefDescription && String(t.briefDescription).trim()) ? t.briefDescription :
    (t.title && String(t.title).trim())                       ? t.title :
    (t.complaintNotes && String(t.complaintNotes).trim())     ? t.complaintNotes : '';
  if (briefEl) {
    const showText = briefSource || 'Brief Description';
    setOneLine(briefEl, showText, CLAMP.brief);
    tryShow(briefEl);
    if (titleText) { try { titleText.text = ''; if (titleText.collapse) titleText.collapse(); } catch(_) {} }
    const canEditBrief = user.isAdmin || (user.isMechanic && isAssignedToUser);
    if (canEditBrief) {
      try { if (briefEl.style) { briefEl.style.cursor = 'pointer'; briefEl.style.textDecoration = 'underline'; } } catch (_) {}
      if (canCall(briefEl, 'onClick')) {
        briefEl.onClick(async function () {
          try {
            const res = await wixWindow.openLightbox(UPDATE_BRIEF_LIGHTBOX_NAME, { ticketId: t._id, currentBrief: t.briefDescription || '' });
            if (!resultIndicatesSave(res)) return;
            const newBrief = (res.brief || '').trim();

            await handleLightboxUpdate(async function () {
              const undo = patchWithUndo(t._id, { briefDescription: newBrief });
              try {
                await callWithRetry(updateTicket, [t._id, { briefDescription: newBrief }]);
                forceRepeaterRerender('brief updated');
              } catch (err) {
                undo();
                throw err;
              }
            }, { successMessage: 'Brief updated', errorMessage: 'Failed to update brief. Refreshing…' });
          } catch (e) {
            logError('Brief', e);
            showError('Failed to update brief. Refreshing…');
            await requestRefresh(true);
          }
        });
      }
    } else {
      try { if (briefEl.style) { briefEl.style.cursor = 'default'; briefEl.style.textDecoration = 'none'; } } catch(_) {}
    }
  } else if (titleText) {
    const showText = briefSource || t.title || '';
    setOneLine(titleText, showText, CLAMP.title);
    tryShow(titleText);
  }

  const assignedText    = itemEl($item, '#assignedText');
  const assignedPill    = itemEl($item, '#assignedPill');
  const assignedPillTxt = itemEl($item, '#assignedPillText');
  const isAssigned      = !!t.assignedMechanicId;
  const rawAssignee     = isAssigned ? clean(t.assignedMechanicName || '') : '';
  const firstNameOnly   = rawAssignee ? rawAssignee.split(' ')[0] : '';
  const isAssignedToCurrentUser = !!(user.id && t.assignedMechanicId === user.id);
  const canAssign       = user.isAdmin && sLabel !== 'Complete';
  const canSelfUnassign = user.isMechanic && isAssignedToCurrentUser && sLabel !== 'Complete';

  if (assignedPill && assignedPillTxt) {
    const style = (STATUS_STYLES[sLabel]) ||
                  (isAssigned ? ASSIGNED_PILL_STYLES.assigned : ASSIGNED_PILL_STYLES.unassigned);

    showPill(assignedPill);
    paintPill(assignedPill, assignedPillTxt, isAssigned ? (firstNameOnly || 'β€”') : 'Unassigned', style);
    if (assignedText) { try { if (assignedText.collapse) assignedText.collapse(); } catch(_) {} }
    try { if (assignedPill.style) assignedPill.style.cursor = (canAssign || canSelfUnassign) ? 'pointer' : 'default'; } catch(_) {}
    if ((canAssign || canSelfUnassign) && canCall(assignedPill, 'onClick')) {
      assignedPill.onClick(async function () {
        try { if (assignedPill.style) assignedPill.style.opacity = '0.1'; } catch(_) {}
        try {
          if (canAssign) {
            const selection = await wixWindow.openLightbox(LIGHTBOX_NAME, {
              ticketId: t._id,
              currentAssignee: isAssigned ? { id: t.assignedMechanicId, name: t.assignedMechanicName } : null,
              allowAssign: true,
              allowUnassign: true
            });
            if (!selection) return;

            if (selection.action === 'unassign' || !selection.mechanicId) {
              await handleLightboxUpdate(async function () {
                const undo = patchWithUndo(t._id, { assignedMechanicId: '', assignedMechanicName: '' });
                try {
                  await callWithRetry(unassignTicketAdmin, [t._id]);
                  forceRepeaterRerender('unassigned');
                } catch (err) {
                  undo();
                  throw err;
                }
              }, { successMessage: 'Job unassigned', errorMessage: 'Failed to update assignment. Refreshing…' });
              return;
            }

            await handleLightboxUpdate(async function () {
              const undo = patchWithUndo(t._id, { assignedMechanicId: selection.mechanicId, assignedMechanicName: selection.mechanicName });
              try {
                await callWithRetry(assignTicketAdmin, [t._id, selection.mechanicId, selection.mechanicName]);
                forceRepeaterRerender('assigned');
              } catch (err) {
                undo();
                throw err;
              }
            }, { successMessage: 'Job assigned to ' + selection.mechanicName, errorMessage: 'Failed to update assignment. Refreshing…' });

          } else if (canSelfUnassign) {
            const selection = await wixWindow.openLightbox(LIGHTBOX_NAME, {
              ticketId: t._id,
              currentAssignee: isAssigned ? { id: t.assignedMechanicId, name: t.assignedMechanicName } : null,
              allowAssign: false,
              allowUnassign: true
            });
            if (!selection) return;
            if (selection.action === 'unassign') {
              await handleLightboxUpdate(async function () {
                const undo = patchWithUndo(t._id, { assignedMechanicId: '', assignedMechanicName: '' });
                try {
                  await callWithRetry(unassignTicketAdmin, [t._id]);
                  forceRepeaterRerender('self-unassigned');
                } catch (err) {
                  undo();
                  throw err;
                }
              }, { successMessage: 'You have been unassigned', errorMessage: 'Failed to update assignment. Refreshing…' });
            }
          }
        } catch (e) {
          logError('Assign', e);
          showError('Failed to update assignment. Refreshing…');
          await requestRefresh(true);
        } finally {
          try { if (assignedPill.style) assignedPill.style.opacity = '1'; } catch(_) {}
        }
      });
    }
  } else if (assignedText) {
    assignedText.text = isAssigned ? 'Assigned: ' + (firstNameOnly || rawAssignee) : 'Unassigned';
    try { if (assignedText.style) assignedText.style.cursor = (canAssign || canSelfUnassign) ? 'pointer' : 'default'; } catch(_) {}
    if ((canAssign || canSelfUnassign) && canCall(assignedText, 'onClick')) {
      assignedText.onClick(async function () {
        try {
          if (canAssign) {
            const selection = await wixWindow.openLightbox(LIGHTBOX_NAME, {
              ticketId: t._id,
              currentAssignee: isAssigned ? { id: t.assignedMechanicId, name: t.assignedMechanicName } : null,
              allowAssign: true,
              allowUnassign: true
            });
            if (!selection) return;

            if (selection.action === 'unassign' || !selection.mechanicId) {
              await handleLightboxUpdate(async function () {
                const undo = patchWithUndo(t._id, { assignedMechanicId: '', assignedMechanicName: '' });
                try {
                  await callWithRetry(unassignTicketAdmin, [t._id]);
                  forceRepeaterRerender('unassigned (text click)');
                } catch (err) {
                  undo();
                  throw err;
                }
              }, { successMessage: 'Job unassigned', errorMessage: 'Failed to update assignment. Refreshing…' });
              return;
            }

            await handleLightboxUpdate(async function () {
              const undo = patchWithUndo(t._id, { assignedMechanicId: selection.mechanicId, assignedMechanicName: selection.mechanicName });
              try {
                await callWithRetry(assignTicketAdmin, [t._id, selection.mechanicId, selection.mechanicName]);
                forceRepeaterRerender('assigned (text click)');
              } catch (err) {
                undo();
                throw err;
              }
            }, { successMessage: 'Job assigned to ' + selection.mechanicName, errorMessage: 'Failed to update assignment. Refreshing…' });

          } else if (canSelfUnassign) {
            const selection = await wixWindow.openLightbox(LIGHTBOX_NAME, {
              ticketId: t._id,
              currentAssignee: isAssigned ? { id: t.assignedMechanicId, name: t.assignedMechanicName } : null,
              allowAssign: false,
              allowUnassign: true
            });
            if (!selection) return;
            if (selection.action === 'unassign') {
              await handleLightboxUpdate(async function () {
                const undo = patchWithUndo(t._id, { assignedMechanicId: '', assignedMechanicName: '' });
                try {
                  await callWithRetry(unassignTicketAdmin, [t._id]);
                  forceRepeaterRerender('self-unassigned (text click)');
                } catch (err) {
                  undo();
                  throw err;
                }
              }, { successMessage: 'You have been unassigned', errorMessage: 'Failed to update assignment. Refreshing…' });
            }
          }
        } catch (e) {
          logError('Assign', e);
          showError('Failed to update assignment. Refreshing…');
          await requestRefresh(true);
        }
      });
    }
  }

  const acceptBtn = itemEl($item, '#acceptJobButton');
  if (acceptBtn) {
    const isUnassigned = !t.assignedMechanicId;
    const canAccept = user.isMechanic && isUnassigned && sLabel !== 'Complete';
    const isMineButton = user.isMechanic && sLabel !== 'Complete' && isAssignedToUser;
    const paintBtn = function (bg, fg, border) {
      try {
        if (acceptBtn.style) {
          acceptBtn.style.backgroundColor = normalizeColor(bg);
          acceptBtn.style.color = normalizeColor(fg, { allowTransparent: false });
          setBorder(acceptBtn, border, null);
          acceptBtn.style.boxShadow = 'none';
          acceptBtn.style.outline = 'none';
        }
      } catch (_) {}
    };
    if (canAccept) {
      setTextOrLabel(acceptBtn, 'Accept');
      tryShow(acceptBtn);
      paintBtn(COLORS.BLACK, THEME.WHITE, COLORS.BLACK);
      if (canCall(acceptBtn, 'onClick')) {
        acceptBtn.onClick(async function () {
          try {
            if (acceptBtn.disable) acceptBtn.disable();
            await handleLightboxUpdate(async function () {
              const current = (t.status || '').trim();
              const optimistic = { assignedMechanicId: user.id || t.assignedMechanicId };
              if (['Not Yet Started', 'Ready', 'Parts Ready'].includes(current)) optimistic.status = 'Underway';
              const undo = patchWithUndo(t._id, optimistic);
              try {
                await callWithRetry(acceptTicket, [t._id]);
                forceRepeaterRerender('accepted job');
              } catch (err) {
                undo();
                throw err;
              }
            }, { successMessage: 'Job accepted successfully!', errorMessage: 'Failed to accept job. Refreshing…' });
          } finally { try { if (acceptBtn.enable) acceptBtn.enable(); } catch(_) {} }
        });
      }
    } else if (isMineButton) {
      setTextOrLabel(acceptBtn, 'My Job');
      tryShow(acceptBtn);

      const st = getStatusPillStyle(sLabel) || STATUS_STYLES[sLabel] || { bg: COLORS.BLUE, text: THEME.WHITE, border: COLORS.BLUE };
      paintBtn(st.bg, st.text || THEME.WHITE, st.border || st.bg);

      if (canCall(acceptBtn, 'onClick')) acceptBtn.onClick(function(){ openTicketDetails(t); });
    } else {
      tryHide(acceptBtn);
    }
  }

  const infoBtn = itemEl($item, '#infoButton');
  if (infoBtn) {
    setTextOrLabel(infoBtn, 'Info');
    tryShow(infoBtn);
    try { if (infoBtn.style) infoBtn.style.cursor = 'pointer'; } catch (_) {}
    if (canCall(infoBtn, 'onClick')) {
      infoBtn.onClick(function(){ openInfoLightbox(t); });
    }
  }

  const statusDd = itemEl($item, '#statusActionDropdown');
  if (statusDd) { try { if (statusDd.collapse) statusDd.collapse(); } catch(_) {} }

  const canOpenStatus = (user.isAdmin || (user.isMechanic && !!(user.id && t.assignedMechanicId === user.id))) && sLabel !== 'Complete';
  bindRowClick($item, t, canOpenStatus);
}

/** ============================ List refresh (server-side) ============================ */

async function refreshList(reset, tokenAtCall) {
  state.loading = true;
  const refreshBtn = el('#refreshButton'); if (refreshBtn && refreshBtn.disable) refreshBtn.disable();

  if (reset) showSkeleton(true);

  let newlyLoaded = 0;
  try {
    const searching = !!(state.filters.search && state.filters.search.trim());
    const wantCompleted =
      !!state.filters.includeCompleted ||
      state.filters.selectedStatus === 'Complete' ||
      searching;

    const filtersToSend = Object.assign({}, state.filters, { includeCompleted: wantCompleted });

    const res = await callWithRetry(
      queryTickets,
      [{ viewerId: user.id || '', selectedStatus: filtersToSend.selectedStatus, location: filtersToSend.location, assignment: filtersToSend.assignment, includeCompleted: filtersToSend.includeCompleted, search: filtersToSend.search }, state.filters.sort || 'newest', reset ? 0 : state.paging.skip, PAGE_SIZE]
    );
    const items = (res && res.items) || [];
    const hasMore = !!(res && res.hasMore);

    if (tokenAtCall !== cancelToken) return 0;

    state.paging.skip = (reset ? 0 : state.paging.skip) + items.length;
    state.paging.hasMore = hasMore;

    const rep = el('#ticketsRepeater');
    if (rep && 'data' in rep) {
      if (reset) {
        newlyLoaded = items.length;
        await setRepeaterData(items, { hard: true, reason: 'reset' });
      } else {
        const existing = (rep.data || []);
        const existingIds = new Set(existing.map(function(i){ return i._id; }));
        const newItems = items.filter(function(i){ return !existingIds.has(i._id); });
        const combined = existing.concat(newItems);
        newlyLoaded = newItems.length;
        await setRepeaterData(combined, { hard: false, reason: 'append' });
      }
    }
  } catch (e) {
    logError('Refresh', e);
    showError('Failed to load tickets. Please check your connection and try again.');
  } finally {
    if (tokenAtCall === cancelToken) {
      showSkeleton(false);
      updateEmptyState();
      if (refreshBtn && refreshBtn.enable) refreshBtn.enable();
      forceHideOverlay();
    }
    state.loading = false;

    const s = el('#loadMoreSentinel');
    if (s && canCall(s,'collapse') && canCall(s,'expand')) {
      try { s.collapse(); setTrackedTimeout(function () { try { s.expand(); } catch(_){} }, 0); } catch(_) {}
    }
  }
  return newlyLoaded;
}

async function requestRefresh(reset) {
  if (refreshInFlight) return 0;
  refreshInFlight = true;
  suspendRealtime(1200);
  cancelToken++;
  const myToken = cancelToken;
  try {
    return await refreshList(reset, myToken);
  } finally {
    refreshInFlight = false;
  }
}

/** ============================ Skeleton & Empty ============================ */

function showSkeleton(show) {
  const rep = el('#ticketsRepeater');
  const skel = el('#skeletonRepeater');
  const wrap = el('#listWrap');
  try {
    if (show) {
      if (skel) {
        if ('data' in skel && (!skel.data || skel.data.length === 0)) {
          skel.data = Array.from({ length: isMobile ? 4 : 6 }).map(function(_, i){ return { _id: 'skel-' + i }; });
        }
        tryShow(skel);
      }
      if (rep) tryHide(rep);
      if (wrap) tryShow(wrap);
    } else {
      if (skel) tryHide(skel);
      if (rep) tryShow(rep);
      if (wrap) tryShow(wrap);
    }
  } catch (_) {}
}
function updateEmptyState() {
  const rep = el('#ticketsRepeater');
  const box = el('#emptyStateBox');
  const txt = el('#emptyStateText');
  if (!rep || !('data' in rep) || !box) return;
  const hasItems = Array.isArray(rep.data) && rep.data.length > 0;
  if (hasItems) { tryHide(box); }
  else {
    if (txt) {
      const hasActive = state.filters.selectedStatus !== 'all' ||
        state.filters.location !== 'all' ||
        (state.filters.assignment !== 'all' && state.filters.assignment !== 'allPlusCompleted') ||
        !!state.filters.search;
      const message = hasActive
        ? 'No tickets match your current filters. Try adjusting your search criteria.'
        : 'No tickets found. Click "New Ticket" to create one.';
      setTextOrLabel(txt, message);
    }
    tryShow(box);
  }
}



// FM 2025 may the force be with you..
// ***** pages/form.js (Form page) *****
import wixUsers from 'wix-users';
import wixData from 'wix-data';
import wixWindow from 'wix-window';
import { local as storage } from 'wix-storage';
import wixLocation from 'wix-location';
import { fetch as wixFetch } from 'wix-fetch';
import { getMemberDetails } from 'backend/members.jsw';
import { createTicket } from 'backend/tickets.jsw'; // backend API

/**********************  FAST $w CACHE + SAFE HELPERS  **********************/
const __elCache = new Map();
function qw(selector) {
  if (__elCache.has(selector)) return __elCache.get(selector);
  try {
    const ref = $w(selector);
    __elCache.set(selector, ref || null);
    return ref || null;
  } catch (_) {
    __elCache.set(selector, null);
    return null;
  }
}
function call(selector, method, ...args) {
  try {
    const el = qw(selector);
    if (el && typeof el[method] === 'function') return el[method](...args);
  } catch (_) {}
  return undefined;
}
function set(selector, prop, value) {
  try {
    const el = qw(selector);
    if (el && prop in el) el[prop] = value;
  } catch (_) {}
}
function exists(selector) { return !!qw(selector); }
function val(sel) {
  try {
    const el = qw(sel);
    const v = (el && el.value != null) ? el.value : '';
    return String(v).trim();
  } catch (_) { return ''; }
}
/** prevent duplicate listeners on dynamic re-init */
const __wired = new Set();
function bindOnce(selector, event, handler) {
  const key = `${selector}|${event}`;
  if (__wired.has(key)) return false;
  const el = qw(selector);
  if (!el || typeof el[event] !== 'function') return false;
  try { el[event](handler); } catch (_) { return false; }
  __wired.add(key);
  return true;
}
function bindClick(selector, handler) { return bindOnce(selector, 'onClick', handler); }

/**********************  PO HELPERS (support alt IDs) **********************/
const PO_IDS = ['#poNumberInput', '#noNumberInput', '#poAltInput'];
const getPOValue = () => {
  for (const sel of PO_IDS) {
    const v = val(sel);
    if (v) return v;
  }
  return '';
};

/**********************  CONSTANTS  **********************/
const ROLES = { MECHANIC: "mechanic", ADMIN: "admin" };

const STATUS_DISPLAY_MAP = { 'Parts Required': 'Parts Req' };
export function formatStatusForDisplay(status) {
  return STATUS_DISPLAY_MAP[status] || status;
}

const STORAGE = {
  FORM_DATA_KEY: "ticketFormData",
  MAX_SIZE: 15000,
  CONFIRM_DEBOUNCE_MS: 1500,
  AUTO_SAVE_DEBOUNCE_MS: 800,
  DATA_VERSION: "2.1",
  EXPIRY_MS: 48 * 60 * 60 * 1000,
  QUEUE_KEY: "pendingSubmissions",
  LAST_TEMPLATE_KEY: "lastTicketTemplate",
  TEMPLATE_EXPIRY_MS: 14 * 24 * 60 * 60 * 1000 // 14 days
};

const VALIDATION = {
  PHONE_MIN_DIGITS: 7,
  PHONE_MAX_DIGITS: 20,
  SERIAL_MIN_LENGTH: 4,
  SERIAL_MAX_LENGTH: 25,
  MAX_FILE_SIZE: 25 * 1024 * 1024,
  MIN_MACHINE_HOURS: 0,
  MAX_RETRY_ATTEMPTS: 5,
  RETRY_DELAY_BASE: 800,
  MAX_FILES_PER_UPLOAD: 15,
  MAX_TOTAL_FILES: 20,
  SUPPORTED_IMAGE_TYPES: ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/bmp', 'image/tiff', 'image/webp', 'image/heic'],
  SUPPORTED_VIDEO_TYPES: ['video/mp4', 'video/mov', 'video/quicktime', 'video/avi', 'video/wmv', 'video/flv', 'video/mkv', 'video/webm', 'video/3gp'],
  SUPPORTED_DOC_TYPES: ['application/pdf', 'text/plain', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document']
};

const UI_TIMING = {
  ERROR_DISPLAY: 8000,
  CONFIRMATION_DISPLAY: 4000,
  DRAFT_BANNER_DISPLAY: 6000,
  CONNECTION_TIMEOUT: 5000,
  PROGRESS_UPDATE_INTERVAL: 150
};

const FILE_EXTENSIONS = {
  IMAGE: ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp', 'heic'],
  VIDEO: ['mp4', 'mov', 'avi', 'wmv', 'flv', 'mkv', 'webm', '3gp'],
  DOCUMENT: ['pdf', 'txt', 'doc', 'docx']
};

const ERROR_TYPES = {
  NETWORK: 'network',
  VALIDATION: 'validation',
  SERVER: 'server',
  FILE: 'file',
  PERMISSION: 'permission'
};

/**********************  VEHICLE COLLECTION (QR PREFILL) **********************/
const VEHICLES_COLLECTION = "Vehicles";
const VEHICLE_FIELDS = {
  serial: "serialNumber",
  plant:  "plantNumber",
  model:  "vehicleModel",
  type:   "vehicleType",
  site:   "title_fld",
  reg:    "reg",
  serialLower: "serialLower",
  plantLower:  "plantLower"
};
const normalizeLower = (s) => String(s || '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'').trim();

/**********************  SIMPLE LRU CACHE (for vehicle lookups) **********************/
class LRU {
  constructor({ max = 100, ttlMs = 10_000 } = {}) {
    this.max = Math.max(1, max);
    this.ttl = Math.max(0, ttlMs);
    this.map = new Map();
  }
  _now() { return Date.now(); }
  _isExpired(entry) { return this.ttl > 0 && (this._now() - entry.t) > this.ttl; }
  get(key) {
    if (!this.map.has(key)) return undefined;
    const entry = this.map.get(key);
    if (this._isExpired(entry)) { this.map.delete(key); return undefined; }
    this.map.delete(key);
    this.map.set(key, entry);
    return entry.v;
  }
  set(key, value) {
    if (this.map.has(key)) this.map.delete(key);
    this.map.set(key, { v: value, t: this._now() });
    if (this.map.size > this.max) {
      const firstKey = this.map.keys().next().value;
      if (firstKey !== undefined) this.map.delete(firstKey);
    }
  }
  clear() { this.map.clear(); }
}
const vehicleCache = new LRU({ max: 128, ttlMs: 10_000 }); // 10s TTL tuned for type-ahead

/**********************  APP STATE  **********************/
class AppState {
  constructor() {
    this.upload1InProgress = false;
    this.upload2InProgress = false;
    this.upload1Files = [];
    this.upload2Files = [];
    this.cachedUserData = null;
    this.isOnline = true;
    this.lastStorageConfirmTime = 0;
    this.submitAttempts = 0;
    this.autoSaveTimeout = null;
    this.uploadProgress = { 1: 0, 2: 0 };
    this.draftRestored = false;
    this.pendingSubmissions = [];
    this.activeIntervals = new Set();
    this.lastConnectivityCheck = 0;

    this.formValid = true;
    this.jumpHandlerBound = false;
    this.jumpHandlerTried = false;

    // role-driven UI state
    this.hideReporter = false;

    // QR/prefill state
    this.qrPrefillActive = false;
    this.prefilledVehicleFields = new Set();

    // validation UI
    this.touched = new Set();
    this.showAllErrors = false;

    // watchers
    this.authWatchTimer = null;
    this.connWatchTimer = null;
    this.lastLoggedIn = !!wixUsers.currentUser?.loggedIn;
  }
  setUploadState(n, inProgress) { if (n === 1) this.upload1InProgress = inProgress; if (n === 2) this.upload2InProgress = inProgress; }
  setUploadFiles(n, files) { if (n === 1) this.upload1Files = files; if (n === 2) this.upload2Files = files; }
  setUploadProgress(n, progress) { this.uploadProgress[n] = Math.max(0, Math.min(100, progress)); }
  isUploading() { return this.upload1InProgress || this.upload2InProgress; }
  getAllFiles() { return [...this.upload1Files, ...this.upload2Files]; }
  addInterval(id) { if (id) this.activeIntervals.add(id); }
  clearInterval(id) { if (id) { try { clearInterval(id); } catch(_){} this.activeIntervals.delete(id); } }
  clearAllIntervals() { this.activeIntervals.forEach(id => { try { clearInterval(id); } catch(_){} }); this.activeIntervals.clear(); }
  reset() {
    this.upload1InProgress = false;
    this.upload2InProgress = false;
    this.upload1Files = [];
    this.upload2Files = [];
    this.submitAttempts = 0;
    this.uploadProgress = { 1: 0, 2: 0 };
    this.draftRestored = false;
    this.formValid = true;
    this.qrPrefillActive = false;
    this.prefilledVehicleFields = new Set();
    this.touched = new Set();
    this.showAllErrors = false;
    this.clearAllIntervals();
    if (this.autoSaveTimeout) { try { clearTimeout(this.autoSaveTimeout); } catch(_){} this.autoSaveTimeout = null; }
  }
}
const appState = new AppState();

/**********************  UTILS  **********************/
function debounce(fn, wait) {
  let t;
  return function (...args) {
    const later = () => { try { fn.apply(this, args); } catch(_){} clearTimeout(t); };
    clearTimeout(t);
    t = setTimeout(later, wait);
  };
}
function sanitizeInput(text) {
  if (!text) return '';
  return text.replace(/[<>&"']/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;',"'":'&#x27;'}[c]));
}
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
function formatFileSize(bytes){ if(bytes===0) return '0 B'; const k=1024,s=['B','KB','MB','GB']; const i=Math.floor(Math.log(bytes)/Math.log(k)); return parseFloat((bytes/Math.pow(k,i)).toFixed(1))+' '+s[i]; }
function generateErrorId(){ return `err_${Date.now()}_${Math.random().toString(36).slice(2,11)}`; }

/** Retry only on transient errors */
function isTransientError(e) {
  const msg = String(e?.message || '').toLowerCase();
  const code = e?.status || e?.response?.status || e?.httpStatus;
  if (msg.includes('network') || msg.includes('timeout') || msg.includes('fetch')) return true;
  if ([408, 429, 500, 502, 503, 504].includes(Number(code))) return true;
  return false;
}

/**********************  SCHEDULED REVALIDATE (coalesced) **********************/
let __revalidateQueued = false;
function scheduleRevalidate() {
  if (__revalidateQueued) return;
  __revalidateQueued = true;
  try {
    const raf = (typeof requestAnimationFrame === 'function')
      ? requestAnimationFrame
      : (fn) => setTimeout(fn, 0);
    raf(() => {
      __revalidateQueued = false;
      const fields = [
        '#reporterNameInput','#reporterPhoneInput',
        '#vehicleTypeInput','#vehicleModelInput','#vehicleSerialInput',
        '#plantLocationInput','#plantNumberInput',
        '#machineHoursInput','#complaintNotesInput','#briefDescriptionInput',
        '#causeOfFailureInput','#correctiveActionInput','#complicationRepairInput'
      ].filter(exists);
      fields.forEach(sel => { try { updateValidityIfNeeded(sel); } catch(_) {} });
      checkFormValidityAndToggleSubmit();
    });
  } catch(_) {}
}

/**********************  VALIDITY UI (touched-only) **********************/
function updateValidityIfNeeded(sel) {
  try {
    if (!exists(sel)) return;
    if (appState.showAllErrors || appState.touched.has(sel)) {
      qw(sel).updateValidityIndication?.();
    } else {
      qw(sel).resetValidityIndication?.();
    }
  } catch(_) {}
}
function bindTouchedOnBlur(sel) {
  try {
    const el = qw(sel);
    const attach =
      (typeof el?.onBlur === 'function') ? 'onBlur' :
      (typeof el?.onFocusOut === 'function') ? 'onFocusOut' :
      (typeof el?.onChange === 'function') ? 'onChange' : null;
    if (attach) {
      el[attach](() => {
        appState.touched.add(sel);
        updateValidityIfNeeded(sel);
      });
    }
  } catch(_) {}
}

/**********************  SCROLL/FOCUS HELPER  **********************/
async function scrollToAndFocus(selector) {
  try {
    const el = qw(selector);
    if (!el) return;
    if ((selector === '#poNumberInput' || selector === '#noNumberInput' || selector === '#poAltInput') && exists('#mechanicFieldsBox')) {
      try { await qw('#mechanicFieldsBox')?.expand?.(); } catch(_) {}
    }
    if (typeof el.scrollTo === 'function') { await el.scrollTo(); }
    if (typeof el.focus === 'function') { el.focus(); }
    if (typeof el.updateValidityIndication === 'function') { updateValidityIfNeeded(selector); }
  } catch (_) {}
}

/**********************  WELCOME MESSAGE HELPER  **********************/
function updateWelcomeMsg({ loggedIn, isMechanic, isAdmin, member }) {
  if (!exists('#welcomeMsg')) return;
  if (!loggedIn) {
    set('#welcomeMsg', 'text', 'Hello Plant Operator');
    return;
  }
  const showPersonal = !!(isAdmin || isMechanic);
  if (showPersonal) {
    const first =
      String(
        (member?.firstName) ||
        ((member?.fullName || '').split(' ')[0]) ||
        ''
      ).trim();
    const text = first ? `Hello ${first}` : 'Hello';
    set('#welcomeMsg', 'text', text);
  } else {
    set('#welcomeMsg', 'text', 'Hello Plant Operator');
  }
}

/**********************  VALIDITY HELPERS  **********************/
function revalidateRequired(sel) {
  try {
    const el = qw(sel);
    if (!el) return;
    const wasReq = !!el.required;
    el.required = false;
    el.resetValidityIndication?.();
    setTimeout(() => {
      try { el.required = wasReq; } catch (_) {}
      try { updateValidityIfNeeded(sel); } catch(_) {}
      checkFormValidityAndToggleSubmit();
    }, 0);
  } catch (_) {}
}
function ensurePhoneValidity() {
  try {
    if (!exists('#reporterPhoneInput')) return;
    if (val('#reporterPhoneInput')) revalidateRequired('#reporterPhoneInput');
  } catch(_) {}
}

/**********************  ERRORS & VALIDATION  **********************/
class ValidationError extends Error {
  constructor(message, selector = null, type = ERROR_TYPES.VALIDATION) {
    super(message);
    this.name='ValidationError';
    this.type=type;
    this.selector = selector;
    this.id=generateErrorId();
  }
}
class NetworkError extends Error {
  constructor(message, original = null){ super(message); this.name='NetworkError'; this.type=ERROR_TYPES.NETWORK; this.originalError=original; this.id=generateErrorId(); }
}

class FormValidator {
  static validateRequired(fields) {
    const missing = fields.filter(sel => {
      try { const el = qw(sel); return !el || !el.value || !String(el.value).trim(); }
      catch (_) { return true; }
    });
    if (missing.length) {
      missing.forEach(sel => { try { updateValidityIfNeeded(sel); } catch(_) {} });
      throw new ValidationError('Please fill in all required fields.', missing[0]);
    }
  }
  static validatePhone(phone, selector = '#reporterPhoneInput') {
    if (!phone) throw new ValidationError('Phone number is required', selector);
    const cleaned = String(phone).replace(/[\s\-\(\)\+\.]/g,'');
    const re = new RegExp(`^\\d{${VALIDATION.PHONE_MIN_DIGITS},${VALIDATION.PHONE_MAX_DIGITS}}$`);
    if (!re.test(cleaned)) throw new ValidationError(`Phone must contain ${VALIDATION.PHONE_MIN_DIGITS}-${VALIDATION.PHONE_MAX_DIGITS} digits`, selector);
    return cleaned;
  }
  static validateMachineHours(hours, selector = '#machineHoursInput') {
    const str = String(hours ?? '').trim();
    if (!str) throw new ValidationError('Machine hours is required', selector);
    const n = parseFloat(str);
    if (isNaN(n) || n < VALIDATION.MIN_MACHINE_HOURS) throw new ValidationError('Machine hours must be a valid positive number', selector);
    if (n > 99999) throw new ValidationError('Machine hours seems unusually high. Please verify.', selector);
    return n;
  }
  static validateVehicleSerial(serial, selector = '#vehicleSerialInput') {
    const str = String(serial ?? '').trim();
    if (!str) throw new ValidationError('Serial number is required', selector);
    const cleaned = str.replace(/[\s\-]/g,'');
    const re = new RegExp(`^[a-zA-Z0-9]{${VALIDATION.SERIAL_MIN_LENGTH},${VALIDATION.SERIAL_MAX_LENGTH}}$`);
    if (!re.test(cleaned)) throw new ValidationError(`Serial number must be ${VALIDATION.SERIAL_MIN_LENGTH}-${VALIDATION.SERIAL_MAX_LENGTH} alphanumeric characters`, selector);
    return cleaned;
  }
  static validateFile(file, selector = null) {
    if (!file || !file.name) throw new ValidationError('Invalid file selected', selector || ERROR_TYPES.FILE);
    if (file.size > VALIDATION.MAX_FILE_SIZE) {
      const maxMB = Math.round(VALIDATION.MAX_FILE_SIZE/(1024*1024));
      const sizeMB = Math.round((file.size/(1024*1024)) * 10) / 10;
      throw new ValidationError(`File "${file.name}" (${sizeMB}MB) exceeds ${maxMB}MB limit`, selector || ERROR_TYPES.FILE);
    }
    const allTypes = [...VALIDATION.SUPPORTED_IMAGE_TYPES, ...VALIDATION.SUPPORTED_VIDEO_TYPES, ...VALIDATION.SUPPORTED_DOC_TYPES];
    if (!allTypes.includes(file.type)) {
      // Fallback on extension
      const raw = (file.fileUrl || file.name || '');
      const path = raw.split('?')[0];
      const ext = (path.split('.').pop() || '').toLowerCase();
      const allExt = [...FILE_EXTENSIONS.IMAGE, ...FILE_EXTENSIONS.VIDEO, ...FILE_EXTENSIONS.DOCUMENT];
      if (!ext || !allExt.includes(ext)) throw new ValidationError(`File "${file.name}" type not supported`, selector || ERROR_TYPES.FILE);
    }
    return true;
  }
}

/**********************  CUSTOM FIELD VALIDATION (INLINE MESSAGES)  **********************/
const REQUIRED_MESSAGES = {
  '#reporterNameInput':  'Name is required',
  '#reporterPhoneInput': 'Phone number is required',
  '#plantNumberInput':   'Plant number is required',
  '#vehicleTypeInput':   'Vehicle type is required',
  '#vehicleModelInput':  'Model is required',
  '#vehicleSerialInput': 'Serial number is required',
  '#machineHoursInput':  'Machine hours is required',
  '#plantLocationInput': 'Plant location is required',
  '#complaintNotesInput':'Complaint is required',
  '#briefDescriptionInput':'Brief description is required'
};

/************** PLANT LOCATION (Dropdown) – options + validation **************/
const PLANT_LOCATIONS = [
  'Adare','Berrings','Celtic','Glanmire','Killybegs','Fossa','Ringaskiddy'
];
function isValidPlantLocation(v) {
  return PLANT_LOCATIONS.includes(String(v || '').trim());
}
function populatePlantLocationDropdown() {
  if (!exists('#plantLocationInput')) return;
  try {
    const el = qw('#plantLocationInput');
    if (!('options' in el)) return;
    const current = String(el.value || '').trim();
    el.options = PLANT_LOCATIONS.map(name => ({ label: name, value: name }));
    if ('placeholder' in el) el.placeholder = 'Select location…';
    if ('required' in el) el.required = true;

    if (current && isValidPlantLocation(current)) el.value = current;
    else el.value = '';

    bindTouchedOnBlur('#plantLocationInput');

    if (typeof el.onChange === 'function') {
      bindOnce('#plantLocationInput', 'onChange', () => {
        try { updateValidityIfNeeded('#plantLocationInput'); } catch(_) {}
        checkFormValidityAndToggleSubmit();
      });
    }

    // Prevent first-paint red flicker after setting options/required
    try { revalidateRequired('#plantLocationInput'); } catch(_) {}
  } catch (_) {}
}

/**********************/
function setupCustomValidation() {
  // Reporter name
  if (exists('#reporterNameInput')) {
    qw('#reporterNameInput').onCustomValidation?.((value, reject) => {
      const el = qw('#reporterNameInput');
      const requiredNow = !!el?.required;
      if (!requiredNow && !String(value || '').trim()) return;
      if (!String(value || '').trim()) reject(REQUIRED_MESSAGES['#reporterNameInput']);
    });
    bindTouchedOnBlur('#reporterNameInput');
    const nameEl = qw('#reporterNameInput');
    if (typeof nameEl?.onInput === 'function') {
      bindOnce('#reporterNameInput','onInput', () => { try { updateValidityIfNeeded('#reporterNameInput'); } catch(_) {} checkFormValidityAndToggleSubmit(); });
    } else if (typeof nameEl?.onChange === 'function') {
      bindOnce('#reporterNameInput','onChange', () => { try { updateValidityIfNeeded('#reporterNameInput'); } catch(_) {} checkFormValidityAndToggleSubmit(); });
    }
  }

  // Phone
  if (exists('#reporterPhoneInput')) {
    qw('#reporterPhoneInput').onCustomValidation?.((value, reject) => {
      const el = qw('#reporterPhoneInput');
      const requiredNow = !!el?.required;
      const hasText = !!String(value || '').trim();
      if (!requiredNow && !hasText) return;
      try { FormValidator.validatePhone(value, '#reporterPhoneInput'); }
      catch (e) { reject(e.message || 'Invalid phone number'); }
    });
    bindTouchedOnBlur('#reporterPhoneInput');
    const phoneEl = qw('#reporterPhoneInput');
    if (typeof phoneEl?.onInput === 'function') {
      bindOnce('#reporterPhoneInput','onInput', () => { try { updateValidityIfNeeded('#reporterPhoneInput'); } catch(_) {} checkFormValidityAndToggleSubmit(); });
    } else if (typeof phoneEl?.onChange === 'function') {
      bindOnce('#reporterPhoneInput','onChange', () => { try { updateValidityIfNeeded('#reporterPhoneInput'); } catch(_) {} checkFormValidityAndToggleSubmit(); });
    }
  }

  // Machine hours
  if (exists('#machineHoursInput')) {
    qw('#machineHoursInput').onCustomValidation?.((value, reject) => {
      try { FormValidator.validateMachineHours(value, '#machineHoursInput'); }
      catch (e) { reject(e.message || 'Invalid number'); }
    });
    bindTouchedOnBlur('#machineHoursInput');
    const mhEl = qw('#machineHoursInput');
    if (typeof mhEl?.onInput === 'function') {
      bindOnce('#machineHoursInput','onInput', () => { try { updateValidityIfNeeded('#machineHoursInput'); } catch(_) {} checkFormValidityAndToggleSubmit(); });
    } else if (typeof mhEl?.onChange === 'function') {
      bindOnce('#machineHoursInput','onChange', () => { try { updateValidityIfNeeded('#machineHoursInput'); } catch(_) {} checkFormValidityAndToggleSubmit(); });
    }
  }

  // Vehicle serial
  if (exists('#vehicleSerialInput')) {
    qw('#vehicleSerialInput').onCustomValidation?.((value, reject) => {
      try { FormValidator.validateVehicleSerial(value, '#vehicleSerialInput'); }
      catch (e) { reject(e.message || 'Invalid serial'); }
    });
    bindTouchedOnBlur('#vehicleSerialInput');
    const vsEl = qw('#vehicleSerialInput');
    if (typeof vsEl?.onInput === 'function') {
      bindOnce('#vehicleSerialInput','onInput', () => { try { updateValidityIfNeeded('#vehicleSerialInput'); } catch(_) {} checkFormValidityAndToggleSubmit(); });
    } else if (typeof vsEl?.onChange === 'function') {
      bindOnce('#vehicleSerialInput','onChange', () => { try { updateValidityIfNeeded('#vehicleSerialInput'); } catch(_) {} checkFormValidityAndToggleSubmit(); });
    }
  }

  // Simple requireds
  const requiredSimple = ['#vehicleTypeInput','#vehicleModelInput','#plantNumberInput','#complaintNotesInput','#briefDescriptionInput'];
  requiredSimple.forEach(sel => {
    if (!exists(sel)) return;
    qw(sel).onCustomValidation?.((value, reject) => {
      if (!String(value || '').trim()) reject(REQUIRED_MESSAGES[sel] || 'This field is required');
    });
    bindTouchedOnBlur(sel);
    const el = qw(sel);
    if (typeof el?.onInput === 'function') {
      bindOnce(sel,'onInput', () => { try { updateValidityIfNeeded(sel); } catch(_) {} checkFormValidityAndToggleSubmit(); });
    } else if (typeof el?.onChange === 'function') {
      bindOnce(sel,'onChange', () => { try { updateValidityIfNeeded(sel); } catch(_) {} checkFormValidityAndToggleSubmit(); });
    }
  });
}

/**********************  FORM-LEVEL VALIDITY + JUMP LINK  **********************/
function currentRequiredSelectors() {
  // PRIORITY ORDER for "Jump to error": Plant number should come first
  const base = [
    '#plantNumberInput',
    '#vehicleTypeInput','#vehicleModelInput','#vehicleSerialInput',
    '#plantLocationInput',
    '#machineHoursInput','#complaintNotesInput','#briefDescriptionInput'
  ];
  if (!appState.hideReporter) base.unshift('#reporterNameInput', '#reporterPhoneInput');
  return base.filter(exists);
}
function isFieldValidByRules(sel) {
  try {
    const value = val(sel);
    switch (sel) {
      case '#reporterPhoneInput': FormValidator.validatePhone(value, sel); break;
      case '#machineHoursInput':  FormValidator.validateMachineHours(value, sel); break;
      case '#vehicleSerialInput': FormValidator.validateVehicleSerial(value, sel); break;
      case '#plantLocationInput':
        if (!isValidPlantLocation(value)) throw new Error('invalid');
        break;
      default:
        if (!value) throw new Error('required');
    }
    return true;
  } catch { return false; }
}
function listInvalidFields() {
  const invalid = [];
  for (const sel of currentRequiredSelectors()) {
    if (!isFieldValidByRules(sel)) invalid.push(sel);
  }
  return invalid;
}
function checkFormValidityAndToggleSubmit() {
  const invalid = listInvalidFields();
  appState.formValid = invalid.length === 0;
  UIManager.updateSubmitButtonState();
  UIManager.updateJumpToError(invalid);
  return invalid;
}

/**********************  CONNECTIVITY  **********************/
class ConnectivityManager {
  static async verifyConnection() {
    try {
      const timeout = new Promise((_, rej) =>
        setTimeout(() => rej(new Error('timeout')), UI_TIMING.CONNECTION_TIMEOUT)
      );
      await Promise.race([
        wixFetch('/favicon.ico', { method: 'get', cache: 'no-cache' }),
        timeout
      ]);
      return true;
    } catch (_) {
      return false;
    }
  }
  static async ensureConnection() {
    try {
      const now = Date.now();
      if (now - appState.lastConnectivityCheck < 3000) return appState.isOnline;
      appState.lastConnectivityCheck = now;
      const ok = await this.verifyConnection();
      appState.isOnline = ok;
      return ok;
    } catch (_) {
      appState.isOnline = false;
      return false;
    }
  }
  static startConnectivityWatcher(intervalMs = 7000) {
    if (appState.connWatchTimer) { try { clearInterval(appState.connWatchTimer); } catch(_){} appState.connWatchTimer = null; }
    let last = appState.isOnline;
    appState.connWatchTimer = setInterval(async () => {
      try {
        const ok = await this.ensureConnection();
        if (ok !== last) {
          last = ok;
          UIManager.updateConnectivityState();
          if (ok) {
            if (StorageManager.restoreFormData(true)) UIManager.showDraftRestoredBanner();
            await StorageManager.processPendingSubmissions();
            UIManager.hideError();
            UIManager.showStorageConfirmation("Connection restored");
            scheduleRevalidate();
            checkFormValidityAndToggleSubmit();
          } else {
            StorageManager.saveFormData();
            UIManager.updateConnectivityState();
            UIManager.showError("No internet connection. Form data saved. Will retry when back online.", ERROR_TYPES.NETWORK);
            checkFormValidityAndToggleSubmit();
          }
        }
      } catch(_) {}
    }, intervalMs);
  }
}

/**********************  STORAGE  **********************/
let __lastSavedJSON = '';
class StorageManager {
  static getFormFields() {
    return [
      "#reporterNameInput", "#reporterPhoneInput", "#vehicleTypeInput", "#vehicleModelInput",
      "#vehicleSerialInput", "#plantLocationInput", "#plantNumberInput",
      "#machineHoursInput", "#complaintNotesInput",
      "#poNumberInput", "#noNumberInput", "#poAltInput",
      "#causeOfFailureInput", "#correctiveActionInput", "#complicationRepairInput",
      "#briefDescriptionInput"
    ];
  }

  // template helpers
  static saveLastSubmissionTemplate(payload) {
    try {
      const tpl = {
        plantNumber: payload.plantNumber || '',
        vehicleType: payload.vehicleType || '',
        vehicleSerial: payload.vehicleSerial || '',
        vehicleModel: payload.vehicleModel || '',
        plantLocation: payload.plantLocation || '',
        machineHours: (payload.machineHours ?? '').toString()
      };
      const pkg = { data: tpl, timestamp: Date.now(), version: '1' };
      storage.setItem(STORAGE.LAST_TEMPLATE_KEY, JSON.stringify(pkg));
    } catch (e) {
      console.warn('Failed to save last ticket template:', e);
    }
  }
  static getLastSubmissionTemplate() {
    try {
      const raw = storage.getItem(STORAGE.LAST_TEMPLATE_KEY);
      if (!raw) return null;
      const pkg = JSON.parse(raw);
      if (!pkg || !pkg.data) return null;
      if (pkg.timestamp && (Date.now() - pkg.timestamp > STORAGE.TEMPLATE_EXPIRY_MS)) {
        storage.removeItem(STORAGE.LAST_TEMPLATE_KEY);
        return null;
      }
      return pkg.data;
    } catch (e) {
      console.warn('Failed to read last ticket template:', e);
      return null;
    }
  }
  static applyLastSubmissionTemplate() {
    const tpl = this.getLastSubmissionTemplate();
    if (!tpl) return false;

    const apply = (sel, v) => {
      if (!exists(sel)) return;
      const valStr = String(v || '').trim();
      if (!valStr) return;
      set(sel, 'value', valStr);
      try { updateValidityIfNeeded(sel); } catch (_) {}
    };

    apply('#plantNumberInput', tpl.plantNumber);
    apply('#vehicleTypeInput', tpl.vehicleType);
    apply('#vehicleModelInput', tpl.vehicleModel);
    apply('#vehicleSerialInput', tpl.vehicleSerial);
    if (tpl.plantLocation && isValidPlantLocation(tpl.plantLocation)) {
      apply('#plantLocationInput', tpl.plantLocation);
    }
    apply('#machineHoursInput', tpl.machineHours);

    UIManager.showStorageConfirmation("Copied details from last ticket");
    scheduleRevalidate();
    checkFormValidityAndToggleSubmit();
    return true;
  }

  static saveFormData() {
    try {
      const data = {};
      let total = 0;
      for (const sel of StorageManager.getFormFields()) {
        const el = qw(sel);
        if (!el || el.value == null) continue;
        const v = String(el.value).trim();
        if (v) { data[sel] = v; total += v.length; }
      }
      if (total < 10) return false;
      const payload = JSON.stringify({ data, t: Date.now(), v: STORAGE.DATA_VERSION });
      if (payload === __lastSavedJSON) return false;
      if (payload.length > STORAGE.MAX_SIZE) {
        UIManager.showError("Form contains too much text to auto-save. Please submit soon.");
        return false;
      }
      storage.setItem(STORAGE.FORM_DATA_KEY, payload);
      __lastSavedJSON = payload;
      const now = Date.now();
      if (total > 50 && now - appState.lastStorageConfirmTime > STORAGE.CONFIRM_DEBOUNCE_MS) {
        UIManager.showStorageConfirmation(`Auto-saved (${formatFileSize(payload.length)})`);
        appState.lastStorageConfirmTime = now;
      }
      return true;
    } catch (_) { return false; }
  }

  static restoreFormData(isDraft = false) {
    try {
      const raw = storage.getItem(STORAGE.FORM_DATA_KEY);
      if (!raw) return false;
      let pkg;
      try { pkg = JSON.parse(raw); } catch(_) { storage.removeItem(STORAGE.FORM_DATA_KEY); return false; }
      const { t, v, data } = pkg || {};
      if (!data) return false;
      if (t && Date.now()-t > STORAGE.EXPIRY_MS) { this.clearFormData(); return false; }
      if (v && v !== STORAGE.DATA_VERSION) { this.clearFormData(); return false; }
      let count = 0;
      Object.keys(data).forEach(sel => {
        try {
          const el = qw(sel);
          if (!el) return;
          if (data[sel] != null && !el.value) {
            el.value = data[sel];
            count++;
          }
          try { el.resetValidityIndication?.(); } catch(_) {}
          try { updateValidityIfNeeded(sel); } catch(_) {}
        } catch (_) {}
      });
      if (isDraft && count>0) appState.draftRestored = true;

      scheduleRevalidate();
      __lastSavedJSON = raw;
      return count>0;
    } catch (e) {
      console.error("Restore error", e);
      this.clearFormData();
      return false;
    }
  }
  static clearFormData(){ try{ storage.removeItem(STORAGE.FORM_DATA_KEY); __lastSavedJSON=''; } catch(_){} }

  static getPendingSubmissions(){
    try{ const raw = storage.getItem(STORAGE.QUEUE_KEY); return raw? JSON.parse(raw):[]; }
    catch(e){ console.error('Queue load error', e); return []; }
  }
  static savePendingSubmission(payload){
    try{
      const list = this.getPendingSubmissions();
      const sig = hashPayload(payload);
      if (list.some(i => i.sig === sig)) return true; // already queued
      list.push({ id: generateErrorId(), sig, data: payload, timestamp: Date.now(), attempts: 0 });
      if (list.length > 10) list.splice(0, list.length-10);
      storage.setItem(STORAGE.QUEUE_KEY, JSON.stringify(list));
      return true;
    } catch(e){ console.error('Queue save error', e); return false; }
  }
  static async processPendingSubmissions(){
    if (!appState.isOnline) return;
    const q = this.getPendingSubmissions();
    if (!q.length) return;
    UIManager.showStorageConfirmation(`Processing ${q.length} pending submission(s)...`);
    const remain = []; let ok=0;
    for (const item of q){
      try{
        if (Date.now()-item.timestamp > 24*60*60*1000) continue;
        await createTicket(item.data);
        ok++;
      } catch(e){
        item.attempts = (item.attempts||0)+1;
        if (item.attempts < 3) remain.push(item);
      }
    }
    try{
      if (remain.length) storage.setItem(STORAGE.QUEUE_KEY, JSON.stringify(remain));
      else storage.removeItem(STORAGE.QUEUE_KEY);
    }catch(e){ console.error('Queue update error', e); }
    if (ok) UIManager.showStorageConfirmation(`Submitted ${ok} pending ticket(s)`);
  }
}
function hashPayload(obj){
  try { return String(Object.values(obj).join('|')).slice(0, 2000); } catch(_) { return String(Date.now()); }
}

/**********************  UI MANAGER  **********************/
let __typing = false;
function markTyping(on){ __typing = !!on; UIManager.updateSubmitButtonState(); }

class UIManager {
  static resetAllValidityIndicators() {
    [
      '#reporterNameInput','#reporterPhoneInput','#vehicleTypeInput','#vehicleModelInput',
      '#vehicleSerialInput','#plantLocationInput','#plantNumberInput',
      '#machineHoursInput','#complaintNotesInput','#briefDescriptionInput'
    ].filter(exists).forEach(sel => { try { qw(sel).resetValidityIndication?.(); } catch (_) {} });
  }

  static initializeUI() {
    call("#thankYouBox","hide");
    call("#manualForm","show");
    call("#errorMessage","hide");
    call("#storageConfirmation","hide");
    call("#mechanicFieldsBox","collapse");
    call("#uploadStatusText","hide");
    call("#loadingSpinner","hide");
    call("#submitButton","enable");
    set("#submitButton","label","Submit Ticket");
    if (exists("#uploadProgress1")) call("#uploadProgress1","hide");
    if (exists("#uploadProgress2")) call("#uploadProgress2","hide");
    if (exists("#draftRestoredBanner")) call("#draftRestoredBanner","hide");
    if (exists("#jumpToErrorLink")) call("#jumpToErrorLink","hide");
    if (exists("#vehicleDetailsGroup")) call("#vehicleDetailsGroup","expand");

    // Brief description always visible & required (prevent initial flicker)
    if (exists('#briefDescriptionInput')) {
      call('#briefDescriptionInput', 'show');
      try { qw('#briefDescriptionInput').required = true; } catch(_) {}
      try { revalidateRequired('#briefDescriptionInput'); } catch(_) {}
    }

    // Checkboxes default hidden; show for mechanics/admins later
    this.toggleAssignToMeCheckbox(false);
    this.toggleOrderRequiredCheckbox(false);
    this.toggleReadyForReviewCheckbox(false);

    this.togglePOVisibility(false);
    this.toggleReporterFields(true);

    // Prevent initial flicker for reporter fields on first paint
    ['#reporterNameInput', '#reporterPhoneInput'].forEach(sel => {
      if (exists(sel)) { try { revalidateRequired(sel); } catch(_) {} }
    });

    this.updateConnectivityIndicator(appState.isOnline);

    appState.touched = new Set();
    appState.showAllErrors = false;

    UIManager.resetAllValidityIndicators();
  }

  static togglePOVisibility(show) {
    const method = show ? 'show' : 'hide';
    ['#poNumberInput','#poNumberLabel','#poNumberBox','#poNumberRow',
     '#noNumberInput','#noNumberLabel','#noNumberBox','#noNumberRow',
     '#poAltInput','#poAltLabel','#poAltRow']
      .forEach(sel => { if (exists(sel)) call(sel, method); });
    if (exists('#mechanicFieldsBox')) {
      if (show) call('#mechanicFieldsBox','expand');
      else call('#mechanicFieldsBox','collapse');
    }
  }

  static toggleAssignToMeCheckbox(show) {
    if (!exists('#assignToMeCheckbox')) return;
    const method = show ? 'show' : 'hide';
    call('#assignToMeCheckbox', method);
    try { qw('#assignToMeCheckbox').checked = false; } catch(_) {}
  }

  static toggleOrderRequiredCheckbox(show) {
    if (!exists('#orderRequiredCheckbox')) return;
    const method = show ? 'show' : 'hide';
    call('#orderRequiredCheckbox', method);
    try { qw('#orderRequiredCheckbox').checked = false; } catch(_) {}
  }

  static toggleReadyForReviewCheckbox(show) {
    if (!exists('#readyForReview')) return;
    const method = show ? 'show' : 'hide';
    call('#readyForReview', method);
    try { qw('#readyForReview').checked = false; } catch(_) {}
  }

  static toggleReporterFields(show) {
    if (exists('#reporterSection')) {
      if (show) call('#reporterSection', 'expand'); else call('#reporterSection', 'collapse');
    }
    const showMethod = show ? 'show' : 'hide';
    ['#reporterNameLabel','#reporterNameInput','#reporterPhoneLabel','#reporterPhoneInput']
      .forEach(sel => { if (exists(sel)) call(sel, showMethod); });

    if (exists('#reporterNameInput')) { try { qw('#reporterNameInput').required = !!show; } catch(_) {} }
    if (exists('#reporterPhoneInput')) { try { qw('#reporterPhoneInput').required = !!show; } catch(_) {} }

    checkFormValidityAndToggleSubmit();
  }

  static applyDriverQRHide() {
    if (!exists('#vehicleDetailsGroup')) return;

    const d = appState.cachedUserData;
    const loggedIn = wixUsers.currentUser && wixUsers.currentUser.loggedIn;
    const isMech  = !!(d && d.isMechanic);
    const isAdmin = !!(d && d.isAdmin);
    const isDriverView = !loggedIn || (!isMech && !isAdmin);

    const fieldIds = ['#plantNumberInput','#vehicleTypeInput','#vehicleModelInput','#vehicleSerialInput'];
    const allFilled = fieldIds.every(sel => exists(sel) && !!val(sel));

    if (isDriverView && appState.qrPrefillActive && allFilled) {
      call('#vehicleDetailsGroup', 'collapse');
      fieldIds.forEach(sel => { if (exists(sel)) { try { qw(sel).enable?.(); } catch(_) {} }});
    } else {
      call('#vehicleDetailsGroup', 'expand');
    }
  }

  static forceShowManualVehicleFields() {
    if (exists('#vehicleDetailsGroup')) call('#vehicleDetailsGroup', 'expand');
    ['#plantNumberInput','#vehicleTypeInput','#vehicleModelInput','#vehicleSerialInput']
      .forEach(sel => { if (exists(sel)) { call(sel, 'show'); try { qw(sel).enable?.(); } catch(_) {} }});
  }

  static showDraftRestoredBanner() {
    if (!exists("#draftRestoredBanner")) return;
    set("#draftRestoredBanner","text","A saved draft was restored. You can continue where you left off.");
    call("#draftRestoredBanner","show");
    setTimeout(()=>call("#draftRestoredBanner","hide"), UI_TIMING.DRAFT_BANNER_DISPLAY);
  }

  static async updateConnectivityState() {
    const connected = await ConnectivityManager.ensureConnection();
    const method = connected ? "enable" : "disable";
    if (exists("#uploadButton1")) call("#uploadButton1", method);
    if (exists("#uploadButton2")) call("#uploadButton2", method);
    this.updateConnectivityIndicator(connected);
    this.updateSubmitButtonState();
    return connected;
  }

  static updateConnectivityIndicator(connected) {
    if (!exists("#connectivityIndicator")) return;
    set("#connectivityIndicator", "text", connected ? "Online" : "Offline");
  }

  static updateSubmitButtonState() {
    const baseOk = !appState.isUploading() && appState.isOnline && appState.formValid;
    const canSubmit = baseOk && !__typing;
    if (canSubmit) {
      call("#submitButton","enable");
      call("#uploadStatusText","hide");
    } else {
      call("#submitButton","disable");
      const msgs = [];
      if (appState.upload1InProgress) {
        const p1 = appState.uploadProgress[1];
        msgs.push(`Media upload ${p1 >= 10 ? '(' + p1 + '%)' : 'starting...'}`);
      }
      if (appState.upload2InProgress) {
        const p2 = appState.uploadProgress[2];
        msgs.push(`Document upload ${p2 >= 10 ? '(' + p2 + '%)' : 'starting...'}`);
      }
      if (!appState.isOnline) msgs.push("No internet connection");
      if (!appState.formValid) msgs.push("Fix required fields");
      if (__typing) msgs.push("Checking input…");
      set("#uploadStatusText","text", (msgs.join(" β€’ ") || "Please wait..."));
      call("#uploadStatusText","show");
    }
  }

  static updateJumpToError(invalidSelectors = []) {
    if (!exists('#jumpToErrorLink')) return;
    if (invalidSelectors.length === 0) { call('#jumpToErrorLink','hide'); return; }
    const label = `Jump to first error (${invalidSelectors.length})`;
    const el = qw('#jumpToErrorLink');
    if ('label' in el) el.label = label; else if ('text' in el) el.text = label;
    call('#jumpToErrorLink','show');

    if (!appState.jumpHandlerBound && !appState.jumpHandlerTried) {
      appState.jumpHandlerTried = true;
      const ok = bindClick('#jumpToErrorLink', async () => {
        const invalidNow = listInvalidFields();
        if (!invalidNow.length) return;
        await scrollToAndFocus(invalidNow[0]);
      });
      appState.jumpHandlerBound = !!ok;
    }
  }

  static showError(message, type = ERROR_TYPES.VALIDATION, duration = UI_TIMING.ERROR_DISPLAY) {
    if (!exists("#errorMessage")) return;
    set("#errorMessage","text", message);
    call("#errorMessage","show");
    setTimeout(()=>call("#errorMessage","hide"), duration);
  }
  static hideError(){ call("#errorMessage","hide"); }

  static showStorageConfirmation(message) {
    if (!exists("#storageConfirmation")) return;
    set("#storageConfirmation","text",message);
    call("#storageConfirmation","show");
    setTimeout(()=>call("#storageConfirmation","hide"), UI_TIMING.CONFIRMATION_DISPLAY);
  }

  static setSubmitButtonState(enabled, label="Submit Ticket") {
    call("#submitButton", enabled? "enable":"disable");
    set("#submitButton","label",label);
    call("#loadingSpinner", enabled? "hide":"show");
  }

  static updateUploadProgress(uploadNum, progress) {
    const progressText = `#uploadProgress${uploadNum}`;
    const progressBar = `#uploadProgressBar${uploadNum}`;
    if (exists(progressText)) {
      if (progress > 0 && progress < 100) {
        set(progressText,"text",`Uploading... ${progress}%`);
        call(progressText,"show");
      } else {
        call(progressText,"hide");
      }
    }
    if (exists(progressBar)) { try { qw(progressBar).value = progress; } catch (_) {} }
  }

  static clearFormFields() {
    StorageManager.getFormFields().forEach(sel => { try { const el = qw(sel); if (el) el.value = ""; } catch(_) {} });
    if (exists('#assignToMeCheckbox')) { try { qw('#assignToMeCheckbox').checked = false; } catch(_) {} }
    if (exists('#orderRequiredCheckbox')) { try { qw('#orderRequiredCheckbox').checked = false; } catch(_) {} }
    if (exists('#readyForReview')) { try { qw('#readyForReview').checked = false; } catch(_) {} }
    this.hideError();
    call("#storageConfirmation","hide");
    appState.touched = new Set();
    appState.showAllErrors = false;

    if (exists('#briefDescriptionInput')) {
      try { qw('#briefDescriptionInput').required = true; } catch(_) {}
      try { revalidateRequired('#briefDescriptionInput'); } catch(_) {}
    }
    this.resetAllValidityIndicators();
  }

  static safeReset(selector){ call(selector,"reset"); }

  static showThankYou() {
    call("#manualForm","hide");
    call("#thankYouBox","show");
    call("#uploadStatusText","hide");
    call("#loadingSpinner","hide");
    try { wixWindow.scrollTo(0, 0); } catch(_) {}
    setTimeout(() => { try { wixWindow.scrollTo(0, 0); } catch(_) {} }, 150);
    this.showStorageConfirmation("Ticket submitted successfully!");
  }

  static showForm() {
    call("#thankYouBox","hide");
    call("#manualForm","show");
    try { wixWindow.scrollTo(0, 0); } catch(_) {}
    setTimeout(() => { try { wixWindow.scrollTo(0, 0); } catch(_) {} }, 150);
  }
}

/**********************  FILE MANAGER  **********************/
class FileManager {
  static getWixMediaType(file) {
    if (VALIDATION.SUPPORTED_IMAGE_TYPES.includes(file.type)) return "image";
    if (VALIDATION.SUPPORTED_VIDEO_TYPES.includes(file.type)) return "video";
    const raw = file.fileUrl || file.name || "";
    const path = raw.split('?')[0];
    const ext = (path.split('.').pop() || '').toLowerCase();
    if (FILE_EXTENSIONS.IMAGE.includes(ext)) return "image";
    if (FILE_EXTENSIONS.VIDEO.includes(ext)) return "video";
    return null;
  }
  static validateFileCount(files, n) {
    if (files.length > VALIDATION.MAX_FILES_PER_UPLOAD)
      throw new ValidationError(`Too many files selected for upload ${n}. Max ${VALIDATION.MAX_FILES_PER_UPLOAD}.`, ERROR_TYPES.FILE);
  }
  static validateTotalFileCount() {
    const total = appState.getAllFiles().length;
    if (total > VALIDATION.MAX_TOTAL_FILES) {
      throw new ValidationError(`Too many files selected in total. Max ${VALIDATION.MAX_TOTAL_FILES} files across all uploads.`, ERROR_TYPES.FILE);
    }
  }
  static async handleUploadChange(n) {
    if (!appState.isOnline) {
      UIManager.showError("No internet connection. Please select files when you regain a signal.", ERROR_TYPES.NETWORK);
      UIManager.safeReset(n===1 ? "#uploadButton1" : "#uploadButton2");
      return;
    }
    const btn = n===1 ? "#uploadButton1" : "#uploadButton2";
    const files = qw(btn)?.value || [];
    if (files.length === 0) {
      appState.setUploadState(n,false);
      appState.setUploadFiles(n,[]);
      appState.setUploadProgress(n,0);
      UIManager.updateSubmitButtonState();
      return;
    }

    appState.setUploadState(n,true);
    appState.setUploadProgress(n,0);
    UIManager.updateSubmitButtonState();

    let intervalId = null;
    try {
      this.validateFileCount(files, n);
      files.forEach(f => FormValidator.validateFile(f));

      intervalId = setInterval(() => {
        let p = appState.uploadProgress[n];
        p += Math.random()*15 + 5;
        if (p >= 95) { p = 95; clearInterval(intervalId); intervalId = null; }
        p = Math.round(p);
        appState.setUploadProgress(n, p);
        UIManager.updateUploadProgress(n, p);
        UIManager.updateSubmitButtonState();
      }, UI_TIMING.PROGRESS_UPDATE_INTERVAL);
      appState.addInterval(intervalId);

      const results = await qw(btn).uploadFiles();
      const success = results.filter(f => f && f.fileUrl);
      appState.setUploadFiles(n, success);

      this.validateTotalFileCount();

      if (intervalId) { try { clearInterval(intervalId); } catch(_) {} intervalId = null; }

      appState.setUploadProgress(n, 100);
      UIManager.updateUploadProgress(n, 100);
      UIManager.showStorageConfirmation(`${n===1 ? "Media" : "Document"} upload completed: ${success.length} file(s)`);
    } catch (e) {
      console.error("Upload error:", e);
      UIManager.showError(`File upload failed: ${e.message || 'Please try again.'}`, ERROR_TYPES.FILE);
      appState.setUploadFiles(n, []);
    } finally {
      if (intervalId) { try { clearInterval(intervalId); } catch(_) {} }
      appState.setUploadState(n,false);
      appState.setUploadProgress(n,0);
      UIManager.updateUploadProgress(n, 0);
      UIManager.updateSubmitButtonState();
    }
  }
}

/**********************  USER MANAGER  **********************/
class UserManager {
  static async loadUserData() {
    const user = wixUsers.currentUser;

    UIManager.togglePOVisibility(false);
    UIManager.toggleAssignToMeCheckbox(false);
    UIManager.toggleOrderRequiredCheckbox(false);
    UIManager.toggleReadyForReviewCheckbox(false);
    appState.hideReporter = false;
    UIManager.toggleReporterFields(true);

    if (!user.loggedIn) {
      appState.cachedUserData = null;
      set("#reporterNameInput","value","");
      set("#reporterPhoneInput","value","");
      try {
        ['#reporterNameInput','#reporterPhoneInput'].forEach(sel => {
          if (exists(sel)) {
            qw(sel).resetValidityIndication?.();
            revalidateRequired(sel);
            updateValidityIfNeeded(sel);
          }
        });
      } catch(_) {}

      if (exists('#briefDescriptionInput')) {
        call('#briefDescriptionInput', 'show');
        try { qw('#briefDescriptionInput').required = true; } catch(_) {}
        try { revalidateRequired('#briefDescriptionInput'); } catch(_) {}
      }

      updateWelcomeMsg({ loggedIn:false, isMechanic:false, isAdmin:false, member:null });
      UIManager.applyDriverQRHide();
      scheduleRevalidate();
      checkFormValidityAndToggleSubmit();
      return;
    }

    // Fetch roles + member (independent)
    const [rolesRes, memberRes] = await Promise.allSettled([
      user.getRoles(),
      getMemberDetails()
    ]);

    const roles = rolesRes.status === 'fulfilled' ? rolesRes.value : [];
    const member = memberRes.status === 'fulfilled' ? memberRes.value : null;

    const isMechanic = roles.some(r => (r?.name || '').toLowerCase() === ROLES.MECHANIC);
    const isAdmin    = roles.some(r => (r?.name || '').toLowerCase() === ROLES.ADMIN);

    appState.cachedUserData = { roles, isMechanic, isAdmin, memberData: member };

    updateWelcomeMsg({ loggedIn:true, isMechanic, isAdmin, member });

    // Mechanics hide reporter fields
    appState.hideReporter = !!isMechanic;
    UIManager.toggleReporterFields(!appState.hideReporter);

    // PO + checkboxes visible for mechanics/admins
    const showPriv = isMechanic || isAdmin;
    UIManager.togglePOVisibility(showPriv);
    UIManager.toggleAssignToMeCheckbox(isMechanic); // self-assign is mechanic-only in backend
    UIManager.toggleOrderRequiredCheckbox(showPriv);
    UIManager.toggleReadyForReviewCheckbox(showPriv);

    // Hook up ReadyForReview behavior (auto-check Assign to Me for mechanics)
    if (exists('#readyForReview')) {
      bindOnce('#readyForReview','onChange', () => {
        try {
          const rfr = !!qw('#readyForReview').checked;
          const mech = !!appState.cachedUserData?.isMechanic;
          if (rfr && mech && exists('#assignToMeCheckbox')) {
            qw('#assignToMeCheckbox').checked = true;
          }
          scheduleRevalidate();
        } catch(_) {}
      });
    }

    // Brief description always present
    if (exists('#briefDescriptionInput')) {
      call('#briefDescriptionInput', 'show');
      try { qw('#briefDescriptionInput').required = true; } catch(_) {}
      try { revalidateRequired('#briefDescriptionInput'); } catch(_) {}
    }

    // Prefill name/phone if missing/weak
    try {
      const currentName  = exists("#reporterNameInput")  ? val("#reporterNameInput")  : "";
      const currentPhone = exists("#reporterPhoneInput") ? val("#reporterPhoneInput") : "";
      const currentPhoneDigits = currentPhone.replace(/\D/g, '');

      const bestName = String((member?.fullName || `${member?.firstName || ""} ${member?.lastName || ""}`)).trim();
      const bestPhoneRaw = String(member?.phone || "").trim();
      const bestPhoneDigits = bestPhoneRaw.replace(/\D/g, '');

      if (exists("#reporterNameInput") && (!currentName || currentName.length < 2) && bestName) {
        set("#reporterNameInput","value", bestName);
        try { qw('#reporterNameInput').resetValidityIndication?.(); updateValidityIfNeeded('#reporterNameInput'); } catch(_) {}
      }

      if (exists("#reporterPhoneInput") && bestPhoneDigits && (currentPhoneDigits.length < VALIDATION.PHONE_MIN_DIGITS)) {
        const el = qw('#reporterPhoneInput');
        const wasReq = !!el?.required;
        try { if (el) el.required = false; } catch(_) {}
        set("#reporterPhoneInput","value", bestPhoneRaw);
        try {
          el?.resetValidityIndication?.();
          setTimeout(() => {
            try { if (el) el.required = !appState.hideReporter && wasReq; } catch(_) {}
            try { updateValidityIfNeeded('#reporterPhoneInput'); } catch(_) {}
            checkFormValidityAndToggleSubmit();
          }, 0);
        } catch(_) {}
      }
    } catch (_) {}

    UIManager.applyDriverQRHide();
    scheduleRevalidate();
    ensurePhoneValidity();
    checkFormValidityAndToggleSubmit();
  }

  static getUserTitle() {
    const d = appState.cachedUserData;
    if (!d) return "Driver";
    if (d.isAdmin) return "Admin";
    if (d.isMechanic) return "Mechanic";
    return "Driver";
  }
  static startAuthWatcher(intervalMs = 4000) {
    if (appState.authWatchTimer) { try { clearInterval(appState.authWatchTimer); } catch(_){} appState.authWatchTimer = null; }
    appState.authWatchTimer = setInterval(async () => {
      try {
        const nowLogged = !!wixUsers.currentUser?.loggedIn;
        if (nowLogged !== appState.lastLoggedIn) {
          appState.lastLoggedIn = nowLogged;
          await UserManager.loadUserData();
          ensurePhoneValidity();
          const s2 = getQRSerialFromQuery();
          if (s2) { await VehicleManager.prefillFromSerial(s2, __prefillToken); }
          else    { UIManager.forceShowManualVehicleFields(); }
          scheduleRevalidate();
        }
      } catch(_) {}
    }, intervalMs);
  }
}

/**********************  VEHICLE MANAGER (QR + PLANT PREFILL) **********************/
// Prefill cancel token to prevent stale async writes after clearing
let __prefillToken = 0;
function cancelPrefillOperations() { __prefillToken++; }

// Backward-compatible plant prefill helper (kept for watchers)
async function safePrefillFromPlantNumber(pn) {
  const my = __prefillToken; // capture current token
  const ok = await VehicleManager.prefillFromPlantNumber(pn, my);
  return ok && my === __prefillToken;
}

class VehicleManager {
  static async _fetchVehicleBySerial(normSerial, rawSerial) {
    const cacheKey = `serial:${normSerial || ''}|raw:${rawSerial || ''}`;
    const cached = vehicleCache.get(cacheKey);
    if (cached !== undefined) return cached;

    let res = await wixData.query(VEHICLES_COLLECTION)
      .eq(VEHICLE_FIELDS.serialLower, normSerial)
      .limit(1)
      .find();

    if ((!res.items || !res.items[0]) && rawSerial) {
      res = await wixData.query(VEHICLES_COLLECTION)
        .eq(VEHICLE_FIELDS.serial, rawSerial)
        .limit(1)
        .find();
    }

    const item = res.items && res.items[0] ? res.items[0] : null;
    vehicleCache.set(cacheKey, item);
    return item;
  }

  static async _fetchVehicleByPlant(normPlant, rawPlant) {
    const cacheKey = `plant:${normPlant || ''}|raw:${rawPlant || ''}`;
    const cached = vehicleCache.get(cacheKey);
    if (cached !== undefined) return cached;

    let res = await wixData.query(VEHICLES_COLLECTION)
      .eq(VEHICLE_FIELDS.plantLower, normPlant)
      .limit(1)
      .find();

    if ((!res.items || !res.items[0]) && rawPlant) {
      res = await wixData.query(VEHICLES_COLLECTION)
        .eq(VEHICLE_FIELDS.plant, rawPlant)
        .limit(1)
        .find();
    }

    const item = res.items && res.items[0] ? res.items[0] : null;
    vehicleCache.set(cacheKey, item);
    return item;
  }

  static async prefillFromSerial(rawSerial, tokenAtCall = __prefillToken) {
    const serial = String(rawSerial || "").trim();
    if (!serial) return false;

    const norm = normalizeLower(serial);

    try {
      const item = await VehicleManager._fetchVehicleBySerial(norm, serial);

      if (tokenAtCall !== __prefillToken) return false;

      if (!item) { UIManager.showError(`No vehicle found for serial: ${serial}`); return false; }

      const filled = new Set();

      if (tokenAtCall !== __prefillToken) return false;
      if (exists('#plantNumberInput')) {
        const v = String(item[VEHICLE_FIELDS.plant] || '');
        if (v && tokenAtCall === __prefillToken) { set('#plantNumberInput', 'value', v); filled.add('plantNumber'); try { updateValidityIfNeeded('#plantNumberInput'); } catch (_) {} }
      }
      if (tokenAtCall !== __prefillToken) return false;
      if (exists('#vehicleTypeInput')) {
        const v = String(item[VEHICLE_FIELDS.type] || '');
        if (v && tokenAtCall === __prefillToken) { set('#vehicleTypeInput', 'value', v); filled.add('vehicleType'); try { updateValidityIfNeeded('#vehicleTypeInput'); } catch (_) {} }
      }
      if (tokenAtCall !== __prefillToken) return false;
      if (exists('#vehicleModelInput')) {
        const v = String(item[VEHICLE_FIELDS.model] || '');
        if (v && tokenAtCall === __prefillToken) { set('#vehicleModelInput', 'value', v); filled.add('vehicleModel'); try { updateValidityIfNeeded('#vehicleModelInput'); } catch (_) {} }
      }
      if (tokenAtCall !== __prefillToken) return false;
      if (exists('#vehicleSerialInput')) {
        const v = String(item[VEHICLE_FIELDS.serial] || serial);
        if (v && tokenAtCall === __prefillToken) { set('#vehicleSerialInput', 'value', v); filled.add('vehicleSerial'); try { updateValidityIfNeeded('#vehicleSerialInput'); } catch (_) {} }
      }

      if (tokenAtCall !== __prefillToken) return false;

      appState.qrPrefillActive = filled.size > 0;
      appState.prefilledVehicleFields = filled;

      StorageManager.saveFormData();
      UIManager.showStorageConfirmation("Vehicle details prefilled from QR");

      UIManager.applyDriverQRHide();
      scheduleRevalidate();
      checkFormValidityAndToggleSubmit();
      return true;
    } catch (e) {
      console.error("QR prefill error:", e);
      if (tokenAtCall === __prefillToken) UIManager.showError("Could not prefill vehicle details. Please enter manually.");
      return false;
    }
  }

  // Prefill by Plant Number (manual entry) β€” overwrite fields
  static async prefillFromPlantNumber(rawPlantNumber, tokenAtCall = __prefillToken) {
    const plantNumber = String(rawPlantNumber || "").trim();
    if (!plantNumber) return false;

    const norm = normalizeLower(plantNumber);

    try {
      const item = await VehicleManager._fetchVehicleByPlant(norm, plantNumber);

      if (tokenAtCall !== __prefillToken) return false;

      if (!item) {
        UIManager.showError(`No vehicle found for plant number: ${plantNumber}`);
        return false;
      }

      const filled = new Set();
      const forceFill = (sel, value, tag) => {
        if (tokenAtCall !== __prefillToken) return;
        if (!exists(sel)) return;
        const v = String(value || '').trim();
        if (!v) return;
        set(sel, 'value', v);
        filled.add(tag);
        try { updateValidityIfNeeded(sel); } catch(_) {}
      };

      forceFill('#vehicleTypeInput',   item[VEHICLE_FIELDS.type],   'vehicleType');
      forceFill('#vehicleModelInput',  item[VEHICLE_FIELDS.model],  'vehicleModel');
      forceFill('#vehicleSerialInput', item[VEHICLE_FIELDS.serial], 'vehicleSerial');

      if (tokenAtCall !== __prefillToken) return false;

      if (filled.size > 0) {
        appState.qrPrefillActive = true;
        appState.prefilledVehicleFields = new Set([
          ...Array.from(appState.prefilledVehicleFields || []),
          ...Array.from(filled)
        ]);
        StorageManager.saveFormData();
        UIManager.showStorageConfirmation("Vehicle details updated from plant number");
        UIManager.applyDriverQRHide();
      }

      scheduleRevalidate();
      checkFormValidityAndToggleSubmit();
      return true;
    } catch (e) {
      console.error("Plant-number prefill error:", e);
      if (tokenAtCall === __prefillToken) UIManager.showError("Could not prefill from plant number. Please enter details manually.");
      return false;
    }
  }
}

/**********************  DATA MANAGER  **********************/
function getBestMemberName(member) {
  return String((member?.fullName || `${member?.firstName || ''} ${member?.lastName || ''}`)).trim();
}

class DataManager {
  static validateFormData() {
    const required = currentRequiredSelectors();
    FormValidator.validateRequired(required);

    let phone = '';
    if (!appState.hideReporter) phone = FormValidator.validatePhone(val("#reporterPhoneInput"), '#reporterPhoneInput');
    else                         phone = val("#reporterPhoneInput") || '';

    const machineHours  = FormValidator.validateMachineHours(val("#machineHoursInput"), '#machineHoursInput');
    const vehicleSerial = FormValidator.validateVehicleSerial(val("#vehicleSerialInput"), '#vehicleSerialInput');

    const plantLocation = (() => {
      const v = val("#plantLocationInput");
      if (!isValidPlantLocation(v)) throw new ValidationError('Please select a valid plant location', '#plantLocationInput');
      return v;
    })();

    return { phone, machineHours, vehicleSerial, plantLocation };
  }

  static prepareTicketPayload(v) {
    const isMechanic = !!appState.cachedUserData?.isMechanic;
    const isAdmin    = !!appState.cachedUserData?.isAdmin;
    const member     = appState.cachedUserData?.memberData;

    const reporterNameTyped = sanitizeInput(val("#reporterNameInput"));
    const memberName = getBestMemberName(member);
    const creatorDisplayName = memberName || reporterNameTyped || '';

    // Checkbox states
    const assignChecked = exists('#assignToMeCheckbox') && !!qw('#assignToMeCheckbox').checked;
    const partsReqChecked = exists('#orderRequiredCheckbox') && !!qw('#orderRequiredCheckbox').checked;
    const readyForReviewChecked = exists('#readyForReview') && !!qw('#readyForReview').checked;

    // Decide status with precedence: Ready for Review > Parts Required > Not Yet Started
    let status = "Not Yet Started";
    if (readyForReviewChecked) status = "Ready for Review";
    else if (partsReqChecked)  status = "Parts Required";

    const payload = {
      title: UserManager.getUserTitle(),
      status,
      reporterName: reporterNameTyped,
      reporterPhone: v.phone,
      vehicleType: sanitizeInput(val("#vehicleTypeInput")),
      vehicleModel: sanitizeInput(val("#vehicleModelInput")),
      vehicleSerial: v.vehicleSerial,
      plantLocation: v.plantLocation,
      plantNumber: sanitizeInput(val("#plantNumberInput")),
      machineHours: v.machineHours,
      complaintNotes: sanitizeInput(val("#complaintNotesInput")),
      poNumber: sanitizeInput(getPOValue()),
      causeOfFailure: sanitizeInput(val("#causeOfFailureInput")),
      correctiveAction: sanitizeInput(val("#correctiveActionInput")),
      complicationRepair: sanitizeInput(val("#complicationRepairInput")),
      briefDescription: sanitizeInput(val("#briefDescriptionInput") || ""),
      priority: "Medium",
      attachment: [],
      documentFiles: [],
      creatorDisplayName
    };

    // mechanics-only self-assign (backend allows mechanics to self-assign)
    if (isMechanic && (assignChecked || readyForReviewChecked)) {
      payload.assignToMe = true;
    }

    // Explicit marker for parts required (non-breaking)
    if (partsReqChecked) {
      payload.partsRequired = true;
    }

    const allFiles = appState.getAllFiles();
    allFiles.forEach(file => {
      if (!file.fileUrl) return;
      const type = FileManager.getWixMediaType(file);
      if (type) payload.attachment.push({ src: file.fileUrl, type });
      else payload.documentFiles.push(file.fileUrl);
    });

    // Idempotency key for de-dupe
    payload.idempotencyKey = hashPayload(payload);

    return payload;
  }

  static async submitWithRetry(payload, maxRetries = VALIDATION.MAX_RETRY_ATTEMPTS) {
    let lastErr;
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      try {
        appState.submitAttempts = attempt;
        if (attempt > 1) {
          const ok = await ConnectivityManager.ensureConnection();
          if (!ok) throw new NetworkError("No internet connection for retry");
        }
        const res = await createTicket(payload);
        return res;
      } catch (e) {
        if (!isTransientError(e) || attempt === maxRetries) { throw e; }
        lastErr = e;
        UIManager.setSubmitButtonState(false, `Retrying... (${attempt}/${maxRetries})`);
        await sleep(VALIDATION.RETRY_DELAY_BASE * Math.pow(2, attempt - 1));
      }
    }
    throw lastErr;
  }
}

/**********************  AUTOSAVE & LIFECYCLE **********************/
const debouncedAutoSave = debounce(() => {
  if (appState.isOnline) StorageManager.saveFormData();
  else UIManager.showError("Offline: changes will not be saved until you're back online.", ERROR_TYPES.NETWORK);
}, STORAGE.AUTO_SAVE_DEBOUNCE_MS);

/**********************  QUERY UTILS (QR) **********************/
function getQRSerialFromQuery() {
  try {
    const qp = wixLocation.query || {};
    return String(qp.serial || qp.s || '').trim();
  } catch (_) { return ''; }
}

/**********************  PAGE INIT GUARD  **********************/
let __pageInitDone = false;

/**********************  PAGE READY  **********************/
$w.onReady(async function () {
  if (__pageInitDone) return;
  __pageInitDone = true;

  try {
    UIManager.initializeUI();

    ConnectivityManager.startConnectivityWatcher(7000);
    UserManager.startAuthWatcher(4000);

    if (exists('#welcomeMsg')) set('#welcomeMsg', 'text', 'Hello Plant Operator');

    populatePlantLocationDropdown();
    setupCustomValidation();

    // Nudge Wix to clear any initial "required" paint once mounted
    setTimeout(() => {
      ['#plantLocationInput', '#briefDescriptionInput', '#reporterNameInput', '#reporterPhoneInput']
        .filter(exists)
        .forEach(sel => { try { revalidateRequired(sel); } catch(_) {} });
    }, 0);

    const hadDraft = StorageManager.restoreFormData(true);
    if (hadDraft) {
      UIManager.showDraftRestoredBanner();
      scheduleRevalidate();
    }

    await UIManager.updateConnectivityState();

    // typing lock + autosave (bind once per field)
    StorageManager.getFormFields().forEach(sel => {
      try {
        const el = qw(sel);
        if (typeof el?.onChange === 'function') bindOnce(sel,'onChange', () => debouncedAutoSave());
        if (typeof el?.onInput === 'function') {
          bindOnce(sel,'onInput', () => { markTyping(true); setTimeout(() => markTyping(false), 500); });
        }
      } catch(_) {}
    });

    // Wire uploads
    if (exists("#uploadButton1")) bindOnce("#uploadButton1","onChange", () => FileManager.handleUploadChange(1));
    if (exists("#uploadButton2")) bindOnce("#uploadButton2","onChange", () => FileManager.handleUploadChange(2));

    // Buttons
    if (exists("#submitButton")) bindClick("#submitButton", submitButton_click);
    if (exists("#submitAnotherButton")) bindClick("#submitAnotherButton", submitAnotherButton_click);
    if (exists("#clearFormButton")) bindClick("#clearFormButton", clearFormButton_click);

    await UserManager.loadUserData();
    ensurePhoneValidity();

    // ReadyForReview β†’ ensure Assign to Me toggles appropriately while typing
    if (exists('#readyForReview')) {
      bindOnce('#readyForReview','onChange', () => {
        try {
          const rfr = !!qw('#readyForReview').checked;
          const mech = !!appState.cachedUserData?.isMechanic;
          if (rfr && mech && exists('#assignToMeCheckbox')) {
            qw('#assignToMeCheckbox').checked = true;
          }
        } catch(_) {}
      });
    }

    const serialParam = getQRSerialFromQuery();
    if (serialParam) { await VehicleManager.prefillFromSerial(serialParam, __prefillToken); }
    else            { UIManager.forceShowManualVehicleFields(); }

    // Plant-number prefill β€” instant feel via focused watcher + debounce
    if (exists('#plantNumberInput')) {
      const doLookup = async () => {
        const pn = val('#plantNumberInput');
        if (pn) await safePrefillFromPlantNumber(pn);
      };
      const debounced = debounce(doLookup, 200);

      let __plantWatch = null;
      let __lastPn = '';

      const startWatch = () => {
        if (__plantWatch) return;
        __lastPn = val('#plantNumberInput');
        __plantWatch = setInterval(() => {
          try {
            const cur = val('#plantNumberInput');
            if (cur !== __lastPn) {
              __lastPn = cur;
              if (cur.length >= 2) debounced();
            }
          } catch (_) {}
        }, 180);
      };
      const stopWatch = () => { try { clearInterval(__plantWatch); } catch(_){} __plantWatch = null; };

      const plantEl = qw('#plantNumberInput');
      if (typeof plantEl?.onFocus === 'function') bindOnce('#plantNumberInput','onFocus', startWatch);
      if (typeof plantEl?.onBlur === 'function')  bindOnce('#plantNumberInput','onBlur',  () => { stopWatch(); doLookup(); });
      if (typeof plantEl?.onInput === 'function') bindOnce('#plantNumberInput','onInput', debounced);
      if (typeof plantEl?.onChange === 'function') bindOnce('#plantNumberInput','onChange', doLookup);
    }

    try {
      wixUsers.onLogin(async () => {
        await UserManager.loadUserData();
        ensurePhoneValidity();
        const s2 = getQRSerialFromQuery();
        if (s2) { await VehicleManager.prefillFromSerial(s2, __prefillToken); }
        else    { UIManager.forceShowManualVehicleFields(); }
        scheduleRevalidate();
      });
    } catch (_) {}

    scheduleRevalidate();
    setTimeout(() => scheduleRevalidate(), 500);
    checkFormValidityAndToggleSubmit();

    if (exists('#jumpToErrorLink') && !appState.jumpHandlerBound && !appState.jumpHandlerTried) {
      appState.jumpHandlerTried = true;
      const ok = bindClick('#jumpToErrorLink', async () => {
        const invalidNow = listInvalidFields();
        if (!invalidNow.length) return;
        await scrollToAndFocus(invalidNow[0]);
      });
      appState.jumpHandlerBound = !!ok;
    }
  } catch (e) {
    console.error("onReady error:", e);
    UIManager.showError("Form initialization failed. Please refresh the page.");
  }
});

/**********************  SUBMIT & RESET  **********************/
let __submitLock = false;

export async function submitButton_click() {
  if (__submitLock) return;
  __submitLock = true;
  setTimeout(() => { __submitLock = false; }, 600);

  try {
    if (!appState.isOnline) {
      UIManager.showError("No internet connection detected.");
      return;
    }
    if (appState.isUploading()) {
      UIManager.showError("File upload in progress, please wait...");
      return;
    }

    const invalid = listInvalidFields();

    if (invalid.length) {
      appState.showAllErrors = true;
      invalid.forEach(sel => appState.touched.add(sel));
      currentRequiredSelectors().forEach(sel => updateValidityIfNeeded(sel));
      UIManager.updateJumpToError(invalid);
      await scrollToAndFocus(invalid[0]);
      UIManager.showError("Please fill in all required fields.");
      return;
    }

    const okConn = await ConnectivityManager.ensureConnection();
    appState.isOnline = okConn;
    await UIManager.updateConnectivityState();
    if (!okConn) {
      StorageManager.saveFormData();
      UIManager.showError("No internet connection detected. Form data saved. Try again when you have signal.", ERROR_TYPES.NETWORK);
      return;
    }

    const v = DataManager.validateFormData();
    const payload = DataManager.prepareTicketPayload(v);

    UIManager.setSubmitButtonState(false, "Submitting...");

    try {
      await DataManager.submitWithRetry(payload);

      // Invalidate any in-flight prefill to prevent repopulation after we clear
      cancelPrefillOperations();
      // Clear hot vehicle cache so subsequent tickets start fresh
      vehicleCache.clear();

      // Save minimal template for "Submit Another"
      StorageManager.saveLastSubmissionTemplate(payload);

      UIManager.clearFormFields();
      StorageManager.clearFormData();
      appState.reset();
      UIManager.safeReset("#uploadButton1");
      UIManager.safeReset("#uploadButton2");
      UIManager.showThankYou();
      await UserManager.loadUserData();
      setupCustomValidation();
      scheduleRevalidate();
      checkFormValidityAndToggleSubmit();
    } catch (e) {
      console.error("Submit error:", e);
      const http = e?.status || e?.response?.status || e?.httpStatus;
      if (isTransientError(e)) {
        StorageManager.saveFormData();
        StorageManager.savePendingSubmission(payload);
        UIManager.showError("Submission failed due to a temporary connection/server issue. Saved locally and will retry when online.", ERROR_TYPES.NETWORK);
      } else {
        const hint = (exists('#orderRequiredCheckbox') && qw('#orderRequiredCheckbox').checked)
          ? " (Tip: ensure your backend/collection allows the 'Parts Required' status)."
          : "";
        UIManager.showError(`Submission failed: ${e.message || `Server error ${http || ''}`}.${hint}`, ERROR_TYPES.SERVER);
      }
    } finally {
      UIManager.setSubmitButtonState(true, "Submit Ticket");
    }
  } catch (ve) {
    console.error("Validation error:", ve);
    UIManager.showError(ve.message || "Please check your entries.", ERROR_TYPES.VALIDATION);
    if (ve && ve.selector) {
      appState.showAllErrors = true;
      appState.touched.add(ve.selector);
      updateValidityIfNeeded(ve.selector);
      await scrollToAndFocus(ve.selector);
    }
    UIManager.setSubmitButtonState(true, "Submit Ticket");
  }
}

export async function submitAnotherButton_click() {
  try {
    // Invalidate any in-flight prefill before wiping fields
    cancelPrefillOperations();
    // Start with a fresh vehicle cache for the next ticket
    vehicleCache.clear();

    UIManager.clearFormFields();
    StorageManager.clearFormData();

    appState.qrPrefillActive = false;
    appState.prefilledVehicleFields = new Set();

    const serialParam = getQRSerialFromQuery();
    if (serialParam) { await VehicleManager.prefillFromSerial(serialParam, __prefillToken); }
    else            { UIManager.forceShowManualVehicleFields(); }

    appState.reset();
    UIManager.safeReset("#uploadButton1");
    UIManager.safeReset("#uploadButton2");
    UIManager.setSubmitButtonState(true, "Submit Ticket");
    UIManager.showForm();

    try { wixWindow.scrollTo(0, 0); } catch(_) {}
    setTimeout(() => { try { wixWindow.scrollTo(0, 0); } catch(_) {} }, 150);

    await UserManager.loadUserData();

    // Apply last-ticket template after showing the form
    StorageManager.applyLastSubmissionTemplate();

    setupCustomValidation();
    scheduleRevalidate();
    checkFormValidityAndToggleSubmit();
  } catch (e) {
    console.error("Reset error:", e);
    UIManager.showError("Error resetting form. Please try again.");
  }
}

/**********************  CLEAR FORM (TOP BUTTON) **********************/
export async function clearFormButton_click() {
  try {
    if (appState.isUploading()) {
      UIManager.showError("File upload in progress, please wait before clearing.");
      return;
    }

    // Invalidate any in-flight prefill so cleared fields stay cleared on first click
    cancelPrefillOperations();
    // Clear hot cache as part of a full reset
    vehicleCache.clear();

    UIManager.clearFormFields();
    StorageManager.clearFormData();

    appState.qrPrefillActive = false;
    appState.prefilledVehicleFields = new Set();
    appState.reset();

    UIManager.safeReset("#uploadButton1");
    UIManager.safeReset("#uploadButton2");

    UIManager.forceShowManualVehicleFields();
    UIManager.setSubmitButtonState(true, "Submit Ticket");
    UIManager.showForm();

    setupCustomValidation();
    scheduleRevalidate();
    checkFormValidityAndToggleSubmit();

    UIManager.showStorageConfirmation("Form cleared");
    try { wixWindow.scrollTo(0, 0); } catch(_) {}
  } catch (e) {
    console.error("Clear form error:", e);
    UIManager.showError("Could not clear the form. Please try again.");
  }
}


// /ticket page code – displays full job info by ?id=<ticketId>
// Works with your existing backend methods and the dashboard link
// FRANK M 2025

import wixData from 'wix-data';
import wixUsers from 'wix-users';
import wixWindow from 'wix-window';
import wixLocation from 'wix-location';

import {
  acceptTicket,
  assignTicketAdmin,
  updateTicketStatus,
  unassignTicketAdmin,
  updatePurchaseOrderAdmin,
} from 'backend/tickets.jsw';

const TICKETS = 'Tickets';

// Keep names aligned with your dashboard/lightboxes
const LIGHTBOX_NAME = 'AssignMechanicLightbox';
const STATUS_LIGHTBOX_NAME = 'UpdateStatusLightbox';
const UPDATE_PO_LIGHTBOX_NAME = 'UpdatePOLightbox';

// Where the "Back" button goes (your dashboard list page)
const DASHBOARD_URL = '/tickets';

/* ---------------- helpers ---------------- */
function el(id){ try { return $w(id); } catch(_) { return null; } }
function canCall(x, m){ return !!(x && typeof x[m] === 'function'); }
function tryShow(x){ try { if (canCall(x,'show')) x.show(); if (canCall(x,'expand')) x.expand(); } catch(_){} }
function tryHide(x){ try { if (canCall(x,'hide')) x.hide(); if (canCall(x,'collapse')) x.collapse(); } catch(_){} }
function setTextOrLabel(x, txt){
  try {
    if (!x) return;
    if ('text' in x) x.text = String(txt ?? '');
    else if ('label' in x) x.label = String(txt ?? '');
  } catch(_) {}
}
function sanitize(s){
  if (!s) return '';
  return String(s)
    .replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>')
    .replace(/&quot;/g,'"').replace(/&#x27;|&#39;/g,"'")
    .trim();
}
function showLoading(show=true){
  const overlay = el('#loadingOverlay');
  if (!overlay) return;
  try { show ? overlay.show() : overlay.hide(); } catch(_) {}
}
function toast(msg, color){
  const t = el('#errorToast'); if (!t) return;
  setTextOrLabel(t, msg);
  try { if (t.style && color) t.style.color = color; } catch(_) {}
  tryShow(t);
  setTimeout(() => { tryHide(t); }, 3000);
}

/* ---------------- user / state ---------------- */
let user = { id: null, isAdmin: false, isMechanic: false };
let ticket = null;

async function initUser(){
  const cu = wixUsers.currentUser;
  user.id = cu?.loggedIn ? cu.id : null;
  try {
    if (cu?.loggedIn) {
      const roles = await cu.getRoles();
      const names = (roles || []).map(r => String(r?.name || '').toLowerCase());
      user.isAdmin = names.includes('admin');
      user.isMechanic = names.includes('mechanic');
    } else {
      user.isAdmin = false;
      user.isMechanic = false;
    }
  } catch(e){ /* ignore */ }
}

/* ---------------- data load ---------------- */
async function loadTicketById(id){
  if (!id) throw new Error('Missing ticket id.');
  const rec = await wixData.get(TICKETS, id);
  if (!rec) throw new Error('Ticket not found.');
  return rec;
}

/* ---------------- media helpers ---------------- */
function normalizeAttachments(t){
  const media = Array.isArray(t?.attachment) ? t.attachment : [];
  const docs  = Array.isArray(t?.documentFiles) ? t.documentFiles : [];

  // Gallery items for images/videos (Wix Pro Gallery style)
  const galleryItems = [];
  for (const it of media){
    if (!it) continue;
    const src = it.src || it.url || it.fileUrl || it;
    const type = (it.type || '').toLowerCase();
    if (!src) continue;
    if (type === 'video'){
      galleryItems.push({ type: 'video', src });
    } else {
      galleryItems.push({ type: 'image', src });
    }
  }

  // Documents list items (name + link)
  const docItems = docs.map((u, idx) => ({
    _id: 'doc-'+idx,
    url: String(u),
    name: String(u).split('?')[0].split('/').pop() || 'Document'
  }));

  return { galleryItems, docItems };
}

function renderGallery(t){
  const { galleryItems, docItems } = normalizeAttachments(t);

  const gal = el('#ticketGallery');
  if (gal && 'items' in gal){
    if (galleryItems.length){
      try { gal.items = galleryItems; tryShow(gal); } catch(_) {}
    } else {
      tryHide(gal);
    }
  }

  const docRep = el('#documentsRepeater');
  if (docRep && 'data' in docRep){
    if (docItems.length){
      docRep.data = docItems;
      tryShow(docRep);
      if (canCall(docRep, 'onItemReady')){
        docRep.onItemReady(($item, item) => {
          const nameEl = $item('#docName');
          const linkEl = $item('#docLink');
          if (nameEl) setTextOrLabel(nameEl, item.name);
          if (linkEl){
            try {
              // support either Button (link) or Text element with link property
              if ('link' in linkEl) linkEl.link = item.url;
              if ('target' in linkEl) linkEl.target = '_blank';
              if ('label' in linkEl && !linkEl.label) linkEl.label = 'Open';
            } catch(_){}
          }
        });
      }
    } else {
      tryHide(docRep);
    }
  }
}

/* ---------------- render ---------------- */
function renderTicket(t){
  // basic text fields
  setTextOrLabel(el('#titleText'), sanitize(t.title || ''));
  setTextOrLabel(el('#jobNumberText'),
    String((t.jobCartNumber !== undefined && t.jobCartNumber !== null && t.jobCartNumber !== '') ? t.jobCartNumber : '-')
  );
  setTextOrLabel(el('#statusText'), sanitize(t.status || ''));
  setTextOrLabel(el('#poNumberText'), (t.poNumber && String(t.poNumber).trim()) ? sanitize(t.poNumber) : 'Add PO');
  setTextOrLabel(el('#plantNumberText'), sanitize((t.plantNumber ?? '').toString() || ''));
  setTextOrLabel(el('#locationText'), sanitize(t.plantLocation || ''));
  setTextOrLabel(el('#serialText'), sanitize(t.vehicleSerial || ''));
  setTextOrLabel(el('#modelText'), sanitize(t.vehicleModel || ''));
  setTextOrLabel(el('#vehicleTypeText'), sanitize(t.vehicleType || ''));
  setTextOrLabel(el('#hoursText'), Number.isFinite(Number(t.machineHours)) ? String(t.machineHours) : '');
  setTextOrLabel(el('#briefText'), sanitize(t.briefDescription || ''));
  setTextOrLabel(el('#complaintNotesText'), sanitize(t.complaintNotes || ''));
  setTextOrLabel(el('#causeOfFailureText'), sanitize(t.causeOfFailure || ''));
  setTextOrLabel(el('#correctiveActionText'), sanitize(t.correctiveAction || ''));
  setTextOrLabel(el('#complicationRepairText'), sanitize(t.complicationRepair || ''));

  // dates (if your collection has them)
  const subDate = (t.submissionDate instanceof Date) ? t.submissionDate : (t.submissionDate ? new Date(t.submissionDate) : null);
  const finDate = (t.finishDate instanceof Date) ? t.finishDate : (t.finishDate ? new Date(t.finishDate) : null);
  setTextOrLabel(el('#submittedAtText'), subDate && !isNaN(subDate) ? subDate.toLocaleString() : '');
  setTextOrLabel(el('#finishedAtText'), finDate && !isNaN(finDate) ? finDate.toLocaleString() : '');

  // assigned (text fallback)
  const assignedText = el('#assignedText');
  if (assignedText) {
    const name = (t.assignedMechanicName || '').trim();
    setTextOrLabel(assignedText, name ? `Assigned: ${name}` : 'Unassigned');
    if (user.isAdmin) {
      try { if (assignedText.style) assignedText.style.cursor = 'pointer'; } catch(_) {}
      if (canCall(assignedText,'onClick')) assignedText.onClick(() => openAssignLightbox(t));
    }
  }

  // optional assigned pill variant
  const assignedPill = el('#assignedPill');
  const assignedPillText = el('#assignedPillText');
  if (assignedPill && assignedPillText) {
    const name = (t.assignedMechanicName || '').trim();
    setTextOrLabel(assignedPillText, name || 'Unassigned');
    try { if (assignedPill.style) assignedPill.style.cursor = user.isAdmin ? 'pointer' : 'default'; } catch(_) {}
    if (user.isAdmin && canCall(assignedPill,'onClick')) {
      assignedPill.onClick(() => openAssignLightbox(t));
    }
  }

  // PO edits (admin)
  const poEl = el('#poNumberText');
  if (poEl) {
    if (user.isAdmin) {
      try { if (poEl.style) { poEl.style.cursor = 'pointer'; poEl.style.textDecoration = 'underline'; } } catch(_) {}
      if (canCall(poEl,'onClick')) {
        poEl.onClick(async () => {
          try {
            const res = await wixWindow.openLightbox(UPDATE_PO_LIGHTBOX_NAME, {
              ticketId: t._id,
              currentPO: t.poNumber || ''
            });
            if (!res) return;

            if (res.action === 'saved') {
              await refreshTicket();
              toast('PO updated');
            } else if (res.action === 'save') {
              await updatePurchaseOrderAdmin(t._id, res.poNumber || '');
              await refreshTicket();
              toast('PO updated');
            }
          } catch (e) {
            toast('Failed to update PO');
          }
        });
      }
    } else {
      try { if (poEl.style) { poEl.style.cursor = 'default'; poEl.style.textDecoration = 'none'; } } catch(_) {}
    }
  }

  // Update Status button (optional)
  const upBtn = el('#updateStatusButton');
  if (upBtn) {
    const canUpdate = user.isAdmin || (user.isMechanic && user.id && t.assignedMechanicId === user.id);
    if (canUpdate && t.status !== 'Complete') {
      tryShow(upBtn);
      if (canCall(upBtn,'onClick')) {
        upBtn.onClick(async () => {
          try {
            const res = await wixWindow.openLightbox(STATUS_LIGHTBOX_NAME, {
              ticketId: t._id,
              currentStatus: t.status || 'Not Yet Started',
              isAdmin: user.isAdmin,
              isAssignedToUser: !!(user.id && t.assignedMechanicId === user.id)
            });
            if (res && res.action === 'update' && res.status) {
              await updateTicketStatus(t._id, res.status);
              await refreshTicket();
              toast(`Status β†’ ${res.status}`);
            }
          } catch (e) {
            toast('Failed to update status');
          }
        });
      }
    } else {
      tryHide(upBtn);
    }
  }

  // Accept / My Job (mechanic)
  const acceptBtn = el('#acceptJobButton');
  if (acceptBtn) {
    const isAssigned = !!t.assignedMechanicId;
    const isMine = !!(user.isMechanic && user.id && t.assignedMechanicId === user.id);
    const canAccept = !!(user.isMechanic && !isAssigned && t.status !== 'Complete');

    if (canAccept) {
      setTextOrLabel(acceptBtn, 'Accept');
      tryShow(acceptBtn);
      if (canCall(acceptBtn,'onClick')) {
        acceptBtn.onClick(async () => {
          try {
            acceptBtn.disable?.();
            await acceptTicket(t._id);
            await refreshTicket();
            toast('Job accepted');
          } catch (e) {
            toast('Failed to accept job');
          } finally {
            acceptBtn.enable?.();
          }
        });
      }
    } else if (isMine && t.status !== 'Complete') {
      setTextOrLabel(acceptBtn, 'My Job');
      tryShow(acceptBtn);
      if (canCall(acceptBtn,'onClick')) {
        acceptBtn.onClick(() => { /* already here */ });
      }
    } else {
      tryHide(acceptBtn);
    }
  }

  // Media/gallery & documents
  renderGallery(t);
}

async function openAssignLightbox(t){
  try {
    const res = await wixWindow.openLightbox(LIGHTBOX_NAME, {
      ticketId: t._id,
      currentAssignee: t.assignedMechanicId ? { id: t.assignedMechanicId, name: t.assignedMechanicName } : null,
      allowAssign: true,
      allowUnassign: true
    });
    if (!res) return;

    if (res.action === 'unassign' || !res.mechanicId) {
      await unassignTicketAdmin(t._id);
      await refreshTicket();
      toast('Job unassigned');
      return;
    }

    await assignTicketAdmin(t._id, res.mechanicId, res.mechanicName);
    await refreshTicket();
    toast(`Assigned to ${res.mechanicName}`);
  } catch (e) {
    toast('Assignment failed');
  }
}

/* ---------------- refresh ---------------- */
async function refreshTicket(){
  if (!ticket?._id) return;
  showLoading(true);
  try {
    ticket = await wixData.get(TICKETS, ticket._id);
    renderTicket(ticket);
  } finally {
    showLoading(false);
  }
}

/* ---------------- auth watcher (replaces onLogout) ---------------- */
function startAuthWatcher(intervalMs = 4000) {
  let last = !!wixUsers.currentUser?.loggedIn;
  setInterval(async () => {
    try {
      const now = !!wixUsers.currentUser?.loggedIn;
      if (now !== last) {
        last = now;
        await initUser();
        await refreshTicket();
      }
    } catch(_){}
  }, intervalMs);
}

/* ---------------- onReady ---------------- */
$w.onReady(async () => {
  try {
    showLoading(true);

    await initUser();

    const q = wixLocation.query || {};
    const id = q.id || '';
    if (!id) {
      toast('Missing ticket id');
      return;
    }

    ticket = await loadTicketById(id);
    renderTicket(ticket);

    // Back button
    const backBtn = el('#backButton');
    if (backBtn && canCall(backBtn,'onClick')) {
      backBtn.onClick(() => {
        try { wixLocation.to(DASHBOARD_URL); } catch(_) {}
      });
    }

    // Refresh controls/permissions on login
    try { wixUsers.onLogin(async () => { await initUser(); await refreshTicket(); }); } catch(_) {}
    startAuthWatcher();

  } catch (e) {
    console.error(e);
    toast('Unable to load ticket');
  } finally {
    showLoading(false);
  }
});