// ════════════════════════════════════════════════════════════════════════
// PropMystro · M2 · pm-d-maint.jsx  (faithful port of pm-maint*.jsx)
// Maintenance — portfolio kanban (drag between stages), ticket drawer with
// stage stepper + photos + contractor assign, create-ticket modal with
// damp/mould auto-escalation (Awaab's-Law-aligned 14-day clock), resolve
// flow that books a repairs/compliance expense (SA105 box), contractor
// directory, and the property Maintenance tab with search + filters.
// Tables: tickets · contractors · expenses (RLS write-gated by 'maintenance').
// → window.PMMaintHub, window.PMTabMaintenance
// ════════════════════════════════════════════════════════════════════════
(function () {
  const { useState, useEffect, useCallback } = React;

  // ── model (verbatim from pm-maint-data.jsx) ───────────────────────────
  const STAGES = [
    { key: 'inbox', label: 'Inbox', hint: 'New / triage' },
    { key: 'diagnosing', label: 'Diagnosing', hint: 'Inspecting cause' },
    { key: 'quoted', label: 'Quoted', hint: 'Awaiting go-ahead' },
    { key: 'dispatched', label: 'Dispatched', hint: 'Contractor booked' },
    { key: 'resolved', label: 'Resolved', hint: 'Closed' },
  ];
  const stageIndex = (k) => STAGES.findIndex(s => s.key === k);
  const stageLabel = (k) => (STAGES.find(s => s.key === k) || {}).label || k;
  const nextStage = (k) => { const i = stageIndex(k); return i >= 0 && i < STAGES.length - 1 ? STAGES[i + 1].key : null; };

  const CATS = [
    { key: 'damp_mould', label: 'Damp & mould', icon: '🦠', expenseCat: 'repairs', box: 25, hazard: true },
    { key: 'plumbing', label: 'Plumbing', icon: '🚰', expenseCat: 'repairs', box: 25 },
    { key: 'heating', label: 'Heating / boiler', icon: '🔥', expenseCat: 'repairs', box: 25 },
    { key: 'electrical', label: 'Electrical', icon: '⚡', expenseCat: 'repairs', box: 25 },
    { key: 'structural', label: 'Structural', icon: '🧱', expenseCat: 'repairs', box: 25 },
    { key: 'appliance', label: 'Appliance', icon: '🧺', expenseCat: 'repairs', box: 25 },
    { key: 'pest', label: 'Pest control', icon: '🐀', expenseCat: 'repairs', box: 25 },
    { key: 'compliance', label: 'Compliance fix', icon: '📋', expenseCat: 'compliance', box: 29 },
    { key: 'general', label: 'General / other', icon: '🔧', expenseCat: 'repairs', box: 25 },
  ];
  const cat = (k) => CATS.find(c => c.key === k) || CATS[CATS.length - 1];
  const PRIORITIES = [
    { key: 'low', label: 'Low', tone: 'ok' }, { key: 'normal', label: 'Normal', tone: 'plain' },
    { key: 'high', label: 'High', tone: 'warn' }, { key: 'critical', label: 'Critical', tone: 'crit' },
  ];
  const prioLabel = (k) => (PRIORITIES.find(p => p.key === k) || {}).label || k;
  const prioTone = (k) => (PRIORITIES.find(p => p.key === k) || {}).tone || 'plain';
  const SOURCES = [
    { key: 'landlord', label: 'Logged by landlord' }, { key: 'inspection', label: 'On-site inspection' }, { key: 'tenant', label: 'Reported by tenant' },
  ];
  const srcLabel = (k) => (SOURCES.find(s => s.key === k) || {}).label || k;

  const DAMP_DAYS = 14;
  const today = () => new Date().toISOString().slice(0, 10);
  const addDays = (iso, n) => { const d = new Date(iso); d.setDate(d.getDate() + n); return d.toISOString().slice(0, 10); };
  const daysBetween = (a, b) => Math.round((new Date(b) - new Date(a)) / 864e5);
  const fmt = (d) => d ? new Date(d).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' }) : '—';
  const gbp = (n) => '£' + Number(n || 0).toLocaleString('en-GB');

  const escalated = (t) => t && cat(t.category).hazard && t.status !== 'resolved';
  const deadline = (t) => (!t || !cat(t.category).hazard) ? null : addDays(t.raised_at, DAMP_DAYS);
  function countdown(t) {
    const dl = deadline(t); if (!dl) return null;
    if (t.status === 'resolved') return { resolved: true, deadline: dl, days: 0, overdue: false };
    const days = daysBetween(today(), dl);
    return { days, overdue: days < 0, deadline: dl, resolved: false };
  }
  const effPriority = (t) => escalated(t) ? 'critical' : (t.priority || 'normal');

  function Spin() { return <span className="spin dark" />; }

  // ── shared chips ───────────────────────────────────────────────────────
  function Clock({ ticket, compact }) {
    const cd = countdown(ticket); if (!cd) return null;
    if (cd.resolved) return <span className="mt-clock ok">✓ within deadline</span>;
    const cls = cd.overdue ? 'over' : (cd.days <= 3 ? 'soon' : 'on');
    return <span className={'mt-clock ' + cls} title={'Damp & mould must be resolved by ' + fmt(cd.deadline) + ' (Awaab\u2019s Law-aligned)'}>⏱ {cd.overdue ? Math.abs(cd.days) + 'd overdue' : cd.days + 'd left'}{!compact && ' · due ' + fmt(cd.deadline)}</span>;
  }
  function PrioPill({ ticket }) {
    const p = effPriority(ticket);
    return <span className={'mt-pill ' + prioTone(p)}>{prioLabel(p)}</span>;
  }

  // ── photo slot (faithful localStorage drop) ────────────────────────────
  function PhotoSlot({ id, label, seedLabel, size }) {
    const KEY = 'pm-photo-' + id;
    const [img, setImg] = useState(() => { try { return localStorage.getItem(KEY) || ''; } catch (e) { return ''; } });
    const [over, setOver] = useState(false);
    const take = (file) => {
      if (!file || !/^image\//.test(file.type)) return;
      const r = new FileReader();
      r.onload = e => { try { localStorage.setItem(KEY, e.target.result); } catch (err) {} setImg(e.target.result); };
      r.readAsDataURL(file);
    };
    const dim = size || 84;
    return <label className={'mt-photo' + (over ? ' over' : '') + (img ? ' filled' : '')} style={{ width: dim, height: dim }} title={label || 'Add a photo'}
      onDragOver={e => { e.preventDefault(); setOver(true); }} onDragLeave={() => setOver(false)}
      onDrop={e => { e.preventDefault(); setOver(false); take(e.dataTransfer.files[0]); }}>
      <input type="file" accept="image/*" style={{ display: 'none' }} onChange={e => take(e.target.files[0])} />
      {img ? <img src={img} alt={label || ''} /> : seedLabel ? <span className="mt-photo-seed"><span>▦</span><span className="l">{seedLabel}</span></span> : <span className="mt-photo-add">＋</span>}
    </label>;
  }

  // ── ticket card ────────────────────────────────────────────────────────
  function TicketCard({ ticket, prop, contractor, onOpen }) {
    const c = cat(ticket.category); const esc = escalated(ticket);
    return <div className={'mt-card' + (esc ? ' escalated' : '')} draggable
      onDragStart={e => e.dataTransfer.setData('text/ticket', ticket.id)} onClick={() => onOpen(ticket.id)}>
      <div className="mt-card-top"><span className="mt-cat">{c.icon} {c.label}</span><PrioPill ticket={ticket} /></div>
      <div className="mt-title">{ticket.title}</div>
      <div className="mt-meta">{prop ? prop.address : '—'}</div>
      {esc && <Clock ticket={ticket} compact />}
      <div className="mt-foot">
        <span className="pmd-mono">{fmt(ticket.raised_at)}</span>
        {ticket.photos && ticket.photos.length > 0 && <span className="pmd-mono">▦ {ticket.photos.length}</span>}
        {contractor && <span className="pmd-mono" title={contractor.name}>· {contractor.name.split(' ')[0]}</span>}
      </div>
    </div>;
  }

  // ── resolve modal ──────────────────────────────────────────────────────
  function ResolveModal({ sb, ticket, prop, contractor, onClose, onDone, toast }) {
    const c = cat(ticket.category);
    const [cost, setCost] = useState(String(ticket.cost || ticket.quote_amount || ''));
    const [book, setBook] = useState(true);
    const [busy, setBusy] = useState(false);
    const expCat = c.expenseCat; const box = c.box;
    const save = async () => {
      setBusy(true);
      const amt = Number(cost) || 0;
      let expenseId = ticket.expense_id || null;
      if (book && amt > 0) {
        const { data, error } = await sb.from('expenses').insert({
          property_id: ticket.property_id, date: today(), category: expCat, sa105_box: box,
          amount: amt, vendor: contractor ? contractor.name : 'Contractor', description: ticket.title,
          source: 'ticket', status: 'reconciled',
        }).select('id').single();
        if (!error && data) expenseId = data.id;
      }
      const { error } = await sb.from('tickets').update({ status: 'resolved', cost: amt, resolved_at: today(), expense_id: expenseId }).eq('id', ticket.id);
      setBusy(false);
      if (error) return toast(error.message);
      toast('Ticket resolved' + (expenseId && book && amt > 0 ? ' · expense booked' : ''));
      onDone();
    };
    return <div className="modal-scrim" onClick={onClose}><div className="modal" style={{ maxWidth: 500 }} onClick={e => e.stopPropagation()}>
      <div className="modal-head"><div><div className="pmd-mono" style={{ fontSize: 11, color: 'var(--brand-deep)' }}>RESOLVE TICKET</div><h2>{ticket.title}</h2><p className="modal-sub">Close the job and record what it cost. {c.label}{contractor ? ' · ' + contractor.name : ''}.</p></div><button className="x" onClick={onClose}>✕</button></div>
      <div style={{ padding: '0 24px' }}>
        <div className="field" style={{ maxWidth: 220 }}><label>Final cost (£)</label><input value={cost} autoFocus onChange={e => setCost(e.target.value.replace(/[^\d.]/g, ''))} placeholder="0" /></div>
        <label className="chk" style={{ alignItems: 'flex-start', whiteSpace: 'normal', opacity: Number(cost) > 0 ? 1 : 0.5 }}>
          <input type="checkbox" checked={book && Number(cost) > 0} disabled={!(Number(cost) > 0)} onChange={e => setBook(e.target.checked)} style={{ marginTop: 3 }} />
          <span>Book as an expense — <b>{expCat === 'compliance' ? 'Compliance & certificates' : 'Repairs & maintenance'}</b>, SA105 box {box}, on {prop ? prop.address : 'this property'}.</span>
        </label>
      </div>
      <div className="modal-foot" style={{ justifyContent: 'flex-end', gap: 8 }}>
        <button className="btn btn-ghost" onClick={onClose}>Cancel</button>
        <button className="btn btn-primary" style={{ width: 'auto' }} onClick={save} disabled={busy}>{busy ? <Spin /> : 'Mark resolved'}</button>
      </div>
    </div></div>;
  }

  // ── ticket drawer ──────────────────────────────────────────────────────
  function Drawer({ sb, ticketId, tickets, props, contractors, onClose, reload, toast, onOpenProperty }) {
    const ticket = tickets.find(t => t.id === ticketId);
    const [resolving, setResolving] = useState(false);
    if (!ticket) return null;
    const p = props.find(x => x.id === ticket.property_id);
    const c = cat(ticket.category);
    const ctr = contractors.find(x => x.id === ticket.contractor_id);
    const esc = escalated(ticket);
    const next = nextStage(ticket.status);
    const update = async (patch) => { const { error } = await sb.from('tickets').update(patch).eq('id', ticket.id); if (error) toast(error.message); else reload(); };
    const advance = () => { if (next === 'resolved') setResolving(true); else update({ status: next }); };

    return <div className="drawer-scrim" onClick={onClose}><div className="drawer" onClick={e => e.stopPropagation()}>
      <button className="x drawer-x" onClick={onClose}>✕</button>
      <div className="pmd-mono" style={{ fontSize: 11, color: 'var(--brand-deep)' }}>{c.icon} {c.label.toUpperCase()} · {srcLabel(ticket.source).toUpperCase()}</div>
      <h2 style={{ fontSize: 21, margin: '4px 0 8px' }}>{ticket.title}</h2>
      <div className="mt-drawer-sub">
        {p && <button className="linkbtn" onClick={() => { onClose(); onOpenProperty && onOpenProperty(p); }}>{p.address}</button>}
        <PrioPill ticket={ticket} />
        {esc && <Clock ticket={ticket} />}
      </div>

      {esc && <div className="mt-awaab"><strong>Damp &amp; mould — escalated to critical.</strong><span> Auto-flagged under the Awaab's Law-aligned response window. Must be made safe / resolved by <b>{fmt(deadline(ticket))}</b>.</span></div>}

      <div className="mt-stages">
        {STAGES.map((s, i) => {
          const cur = stageIndex(ticket.status);
          const cls = i < cur ? 'done' : i === cur ? 'on' : '';
          return <button key={s.key} className={'mt-pip ' + cls} title={s.label}
            onClick={() => s.key !== 'resolved' ? update({ status: s.key }) : setResolving(true)}>{s.label}</button>;
        })}
      </div>

      {ticket.description && <p className="mt-desc">{ticket.description}</p>}

      <div className="mt-grid">
        <div><span className="mt-l">RAISED</span><strong>{fmt(ticket.raised_at)}</strong><div className="pmd-mono" style={{ fontSize: 11, color: 'var(--ink-faint)' }}>{ticket.raised_by || '—'}</div></div>
        {Number(ticket.quote_amount) > 0 && <div><span className="mt-l">QUOTE</span><strong>{gbp(ticket.quote_amount)}</strong></div>}
        {ticket.status === 'resolved' && <div><span className="mt-l">RESOLVED</span><strong>{fmt(ticket.resolved_at)}</strong><div className="pmd-mono" style={{ fontSize: 11, color: 'var(--ink-faint)' }}>{gbp(ticket.cost)}{ticket.expense_id ? ' · expensed' : ''}</div></div>}
      </div>

      <div className="mt-block"><span className="mt-l">PHOTOS</span>
        <div className="mt-photo-row">
          {(ticket.photos || []).map(ph => <PhotoSlot key={ph} id={ph} label="Ticket photo" seedLabel="Photo" />)}
          <PhotoSlot id={ticket.id + '-add-' + ((ticket.photos || []).length)} label="Add a photo" />
        </div>
      </div>

      <div className="mt-block"><span className="mt-l">CONTRACTOR</span>
        <select className="pm-input" style={{ width: '100%' }} value={ticket.contractor_id || ''} onChange={e => update({ contractor_id: e.target.value || null })}>
          <option value="">— unassigned —</option>
          {contractors.map(x => <option key={x.id} value={x.id}>{x.name} · {x.trade}</option>)}
        </select>
        {ctr && <div className="pmd-mono" style={{ fontSize: 11, color: 'var(--ink-faint)', marginTop: 6 }}>{[ctr.phone, ctr.email].filter(Boolean).join(' · ')}</div>}
      </div>

      {ticket.status !== 'resolved' && <div style={{ display: 'flex', justifyContent: 'space-between', gap: 8, marginTop: 16 }}>
        <button className="btn btn-primary btn-sm" onClick={() => setResolving(true)}>Resolve…</button>
        {next && <button className="btn btn-ghost btn-sm" onClick={advance}>Move to {stageLabel(next)} →</button>}
      </div>}

      {resolving && <ResolveModal sb={sb} ticket={ticket} prop={p} contractor={ctr} toast={toast} onClose={() => setResolving(false)} onDone={() => { setResolving(false); reload(); }} />}
    </div></div>;
  }

  // ── create-ticket modal ────────────────────────────────────────────────
  function CreateModal({ sb, props, contractors, prefillPropertyId, onClose, onCreated, toast }) {
    const sorted = props.slice().sort((a, b) => (a.address || '').localeCompare(b.address || ''));
    const [f, setF] = useState({ property_id: prefillPropertyId || (sorted[0] || {}).id || '', title: '', category: 'general', priority: 'normal', source: 'landlord', description: '', contractor_id: '' });
    const [busy, setBusy] = useState(false);
    const set = (patch) => setF(s => ({ ...s, ...patch }));
    const isDamp = cat(f.category).hazard;
    const save = async () => {
      if (!f.property_id || !f.title.trim()) return;
      setBusy(true);
      const { data, error } = await sb.from('tickets').insert({
        property_id: f.property_id, title: f.title.trim(), category: f.category,
        priority: isDamp ? 'critical' : f.priority, status: 'inbox', source: f.source,
        raised_by: 'Landlord', raised_at: today(), description: f.description || null,
        contractor_id: f.contractor_id || null, quote_amount: 0, cost: 0, photos: [],
      }).select('id').single();
      setBusy(false);
      if (error) return toast(/maintenance|feature/.test(error.message) ? 'Maintenance needs the Professional plan.' : error.message);
      toast('Ticket created');
      onCreated(data ? data.id : null);
    };
    return <div className="modal-scrim" onClick={onClose}><div className="modal" style={{ maxWidth: 620 }} onClick={e => e.stopPropagation()}>
      <div className="modal-head"><div><div className="pmd-mono" style={{ fontSize: 11, color: 'var(--brand-deep)' }}>NEW TICKET</div><h2>Log a maintenance job</h2><p className="modal-sub">Lands in the Inbox column. Damp &amp; mould auto-escalates to critical with a statutory response clock.</p></div><button className="x" onClick={onClose}>✕</button></div>
      <div className="modal-form">
        <div className="field full"><label>Title</label><input value={f.title} autoFocus placeholder="e.g. No hot water — boiler lockout" onChange={e => set({ title: e.target.value })} /></div>
        <div className="field"><label>Property</label><select value={f.property_id} onChange={e => set({ property_id: e.target.value })}>{sorted.map(p => <option key={p.id} value={p.id}>{p.address}</option>)}</select></div>
        <div className="field"><label>Category</label><select value={f.category} onChange={e => set({ category: e.target.value })}>{CATS.map(c => <option key={c.key} value={c.key}>{c.label}</option>)}</select></div>
        <div className="field"><label>Priority</label><select value={isDamp ? 'critical' : f.priority} disabled={isDamp} onChange={e => set({ priority: e.target.value })}>{PRIORITIES.map(p => <option key={p.key} value={p.key}>{p.label}</option>)}</select></div>
        <div className="field"><label>Source</label><select value={f.source} onChange={e => set({ source: e.target.value })}>{SOURCES.map(s => <option key={s.key} value={s.key}>{s.label}</option>)}</select></div>
        <div className="field full"><label>Description</label><textarea rows={3} value={f.description} placeholder="What's wrong, access notes, who reported it…" onChange={e => set({ description: e.target.value })}></textarea></div>
      </div>
      {isDamp && <div className="mt-awaab" style={{ margin: '12px 24px 0' }}><strong>Damp &amp; mould.</strong><span> Will be flagged critical with a {DAMP_DAYS}-day resolution clock (Awaab's Law-aligned).</span></div>}
      <div className="modal-foot" style={{ justifyContent: 'flex-end', gap: 8 }}>
        <button className="btn btn-ghost" onClick={onClose}>Cancel</button>
        <button className="btn btn-primary" style={{ width: 'auto' }} disabled={!f.property_id || !f.title.trim() || busy} onClick={save}>{busy ? <Spin /> : 'Create ticket'}</button>
      </div>
    </div></div>;
  }

  // ── contractor directory ───────────────────────────────────────────────
  function ContractorsModal({ sb, contractors, onClose, reload, toast }) {
    const [adding, setAdding] = useState(false);
    const [f, setF] = useState({ name: '', trade: '', phone: '', email: '' });
    const [busy, setBusy] = useState(false);
    const add = async () => {
      if (!f.name.trim()) return;
      setBusy(true);
      const { error } = await sb.from('contractors').insert({ name: f.name.trim(), trade: f.trade || null, phone: f.phone || null, email: f.email || null });
      setBusy(false);
      if (error) return toast(error.message);
      setF({ name: '', trade: '', phone: '', email: '' }); setAdding(false); toast('Contractor added'); reload();
    };
    return <div className="modal-scrim" onClick={onClose}><div className="modal" style={{ maxWidth: 560 }} onClick={e => e.stopPropagation()}>
      <div className="modal-head"><div><div className="pmd-mono" style={{ fontSize: 11, color: 'var(--brand-deep)' }}>CONTRACTOR DIRECTORY</div><h2>Contractors</h2><p className="modal-sub">Your trusted trades. Assign any of these to a ticket.</p></div><button className="x" onClick={onClose}>✕</button></div>
      <div style={{ padding: '0 24px' }}>
        {contractors.length === 0 && !adding && <div className="empty" style={{ padding: '20px 0' }}><div className="ico">🛠</div><h3>No contractors yet</h3><p>Add your trusted trades so you can assign them to tickets.</p></div>}
        {contractors.map(c => <div key={c.id} className="mt-ctr-row">
          <div><strong>{c.name}</strong><div className="pmd-mono" style={{ fontSize: 11, color: 'var(--ink-faint)' }}>{c.trade || '—'}</div></div>
          <div className="pmd-mono" style={{ fontSize: 11, color: 'var(--ink-faint)', textAlign: 'right' }}>{c.phone}<div>{c.email}</div></div>
        </div>)}
        {adding && <div className="modal-form" style={{ padding: '12px 0 0' }}>
          <div className="field"><label>Name</label><input value={f.name} autoFocus onChange={e => setF({ ...f, name: e.target.value })} /></div>
          <div className="field"><label>Trade</label><input value={f.trade} placeholder="e.g. Plumbing" onChange={e => setF({ ...f, trade: e.target.value })} /></div>
          <div className="field"><label>Phone</label><input value={f.phone} onChange={e => setF({ ...f, phone: e.target.value })} /></div>
          <div className="field"><label>Email</label><input value={f.email} onChange={e => setF({ ...f, email: e.target.value })} /></div>
        </div>}
      </div>
      <div className="modal-foot" style={{ justifyContent: adding ? 'flex-end' : 'space-between', gap: 8 }}>
        {!adding && <button className="btn btn-ghost btn-sm" onClick={() => setAdding(true)}>+ Add contractor</button>}
        {adding && <button className="btn btn-ghost" onClick={() => setAdding(false)}>Cancel</button>}
        {adding ? <button className="btn btn-primary" style={{ width: 'auto' }} disabled={!f.name.trim() || busy} onClick={add}>{busy ? <Spin /> : 'Save'}</button>
          : <button className="btn btn-primary" style={{ width: 'auto' }} onClick={onClose}>Done</button>}
      </div>
    </div></div>;
  }

  // ── locked state (Professional gate) ───────────────────────────────────
  function Locked({ openBilling }) {
    return <div className="card card-pad"><div className="locked">
      <div className="lock-ico">🔧</div>
      <h3>Maintenance is a Professional feature</h3>
      <p>Track every job from report to resolved on a kanban board, with contractors, photos, and the Awaab's-Law damp &amp; mould response clock.</p>
      <button className="btn btn-primary btn-sm" style={{ width: 'auto' }} onClick={openBilling}>Upgrade to Professional</button>
      <div className="lock-foot">Your data stays exactly as it is — upgrading just unlocks the module.</div>
    </div></div>;
  }

  // ═══ MAINTENANCE HUB — portfolio kanban ══════════════════════════════
  function MaintHub({ sb, account, toast, openBilling, onOpenProperty }) {
    const entitled = account.features && account.features.maintenance === true;
    const [tickets, setTickets] = useState(null);
    const [props, setProps] = useState([]);
    const [contractors, setContractors] = useState([]);
    const [open, setOpen] = useState(null);
    const [creating, setCreating] = useState(false);
    const [showCtrs, setShowCtrs] = useState(false);
    const [catFilter, setCatFilter] = useState('all');
    const [dragOver, setDragOver] = useState(null);
    const [resolveFor, setResolveFor] = useState(null);

    const load = useCallback(async () => {
      if (!entitled) return;
      const [t, p, c] = await Promise.all([
        sb.from('tickets').select('*').order('raised_at', { ascending: false }),
        sb.from('properties').select('id,address'),
        sb.from('contractors').select('*').order('name', { ascending: true }),
      ]);
      setTickets(t.data || []); setProps(p.data || []); setContractors(c.data || []);
    }, [sb, entitled]);
    useEffect(() => { load(); }, [load]);

    if (!entitled) return <React.Fragment>
      <div className="pagehead"><h1>Maintenance</h1><p>Every job from report to resolved.</p></div>
      <Locked openBilling={openBilling} />
    </React.Fragment>;
    if (tickets === null) return <div className="card card-pad"><Spin /></div>;

    const filterFn = (t) => catFilter === 'all' ? true : (catFilter === 'damp' ? cat(t.category).hazard : t.category === catFilter);
    const cols = {}; STAGES.forEach(s => { cols[s.key] = []; });
    tickets.filter(filterFn).forEach(t => { (cols[t.status] || cols.inbox).push(t); });
    Object.keys(cols).forEach(k => cols[k].sort((a, b) => {
      const ea = escalated(a), eb = escalated(b);
      if (ea !== eb) return ea ? -1 : 1;
      return (b.raised_at || '').localeCompare(a.raised_at || '');
    }));
    const openCount = tickets.filter(t => t.status !== 'resolved').length;
    const escCount = tickets.filter(t => escalated(t)).length;
    const propById = Object.fromEntries(props.map(p => [p.id, p]));
    const ctrById = Object.fromEntries(contractors.map(c => [c.id, c]));

    const move = async (ticketId, stage) => {
      const t = tickets.find(x => x.id === ticketId);
      if (!t || t.status === stage) return;
      if (stage === 'resolved') { setResolveFor(t); return; }   // route through resolve flow (cost capture)
      const { error } = await sb.from('tickets').update({ status: stage }).eq('id', ticketId);
      if (error) toast(error.message); else load();
    };

    return <React.Fragment>
      <div className="pagehead" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 14, flexWrap: 'wrap' }}>
        <div><h1>Maintenance</h1><p>Every job from report to resolved. Damp &amp; mould auto-escalates with a statutory response clock. Drag a card between columns.</p></div>
        <div className="summary" style={{ margin: 0 }}>
          <div className="sumcard"><div className="n">{openCount}</div><div className="l">Open</div></div>
          <div className={'sumcard' + (escCount ? ' bad' : '')}><div className="n">{escCount}</div><div className="l">Escalated</div></div>
        </div>
      </div>

      <div className="pm-toolbar">
        <button className="btn btn-primary btn-sm" onClick={() => setCreating(true)}>+ New ticket</button>
        <button className="btn btn-ghost btn-sm" onClick={() => setShowCtrs(true)}>Contractors</button>
        <select className="pm-input" value={catFilter} onChange={e => setCatFilter(e.target.value)} style={{ marginLeft: 'auto' }}>
          <option value="all">All categories</option>
          <option value="damp">Damp &amp; mould</option>
          {CATS.filter(c => !c.hazard).map(c => <option key={c.key} value={c.key}>{c.label}</option>)}
        </select>
      </div>

      <div className="mt-board">
        {STAGES.map(stage => <div key={stage.key} className={'mt-col' + (dragOver === stage.key ? ' drop' : '')}
          onDragOver={e => { e.preventDefault(); setDragOver(stage.key); }}
          onDragLeave={() => setDragOver(d => d === stage.key ? null : d)}
          onDrop={e => { e.preventDefault(); setDragOver(null); const id = e.dataTransfer.getData('text/ticket'); if (id) move(id, stage.key); }}>
          <div className="mt-col-head"><div><strong>{stage.label}</strong> <span className="pmd-mono" style={{ color: 'var(--ink-faint)', fontSize: 11 }}>{cols[stage.key].length}</span></div><span className="pmd-mono mt-hint">{stage.hint}</span></div>
          <div className="mt-col-body">
            {cols[stage.key].length === 0 && <div className="mt-empty">—</div>}
            {cols[stage.key].map(t => <TicketCard key={t.id} ticket={t} prop={propById[t.property_id]} contractor={ctrById[t.contractor_id]} onOpen={setOpen} />)}
          </div>
        </div>)}
      </div>

      {open && <Drawer sb={sb} ticketId={open} tickets={tickets} props={props} contractors={contractors} onClose={() => setOpen(null)} reload={load} toast={toast} onOpenProperty={onOpenProperty} />}
      {creating && <CreateModal sb={sb} props={props} contractors={contractors} toast={toast} onClose={() => setCreating(false)} onCreated={(id) => { setCreating(false); load(); if (id) setOpen(id); }} />}
      {showCtrs && <ContractorsModal sb={sb} contractors={contractors} onClose={() => setShowCtrs(false)} reload={load} toast={toast} />}
      {resolveFor && <ResolveModal sb={sb} ticket={resolveFor} prop={propById[resolveFor.property_id]} contractor={ctrById[resolveFor.contractor_id]} toast={toast} onClose={() => setResolveFor(null)} onDone={() => { setResolveFor(null); load(); }} />}
    </React.Fragment>;
  }

  // ═══ PROPERTY MAINTENANCE TAB ════════════════════════════════════════
  function TabMaintenance({ sb, property, account, toast, openBilling }) {
    const entitled = account.features && account.features.maintenance === true;
    const [tickets, setTickets] = useState(null);
    const [contractors, setContractors] = useState([]);
    const [open, setOpen] = useState(null);
    const [creating, setCreating] = useState(false);
    const [q, setQ] = useState('');
    const [catFilter, setCatFilter] = useState('all');
    const [statusFilter, setStatusFilter] = useState('all');
    const [from, setFrom] = useState('');
    const [to, setTo] = useState('');

    const load = useCallback(async () => {
      if (!entitled) return;
      const [t, c] = await Promise.all([
        sb.from('tickets').select('*').eq('property_id', property.id).order('raised_at', { ascending: false }),
        sb.from('contractors').select('*').order('name', { ascending: true }),
      ]);
      setTickets(t.data || []); setContractors(c.data || []);
    }, [sb, property.id, entitled]);
    useEffect(() => { load(); }, [load]);

    if (!entitled) return <div className="card-pad"><div className="locked">
      <div className="lock-ico">🔧</div><h3>Maintenance is a Professional feature</h3>
      <p>Log jobs against this property, assign contractors and track the damp &amp; mould response clock.</p>
      <button className="btn btn-primary btn-sm" style={{ width: 'auto' }} onClick={openBilling}>Upgrade to Professional</button>
    </div></div>;
    if (tickets === null) return <div className="card-pad"><Spin /></div>;

    const needle = q.trim().toLowerCase();
    const filtered = tickets.filter(t => {
      if (catFilter !== 'all' && t.category !== catFilter) return false;
      if (statusFilter === 'open' && t.status === 'resolved') return false;
      if (statusFilter === 'resolved' && t.status !== 'resolved') return false;
      if (from && (t.raised_at || '') < from) return false;
      if (to && (t.raised_at || '') > to) return false;
      if (needle && !((t.title || '') + ' ' + (t.description || '')).toLowerCase().includes(needle)) return false;
      return true;
    });
    const openCount = tickets.filter(t => t.status !== 'resolved').length;
    const ctrById = Object.fromEntries(contractors.map(c => [c.id, c]));

    return <React.Fragment>
      <div className="card-head"><div><h3>Maintenance</h3><div className="sub">{openCount} open · {tickets.length} total for this property</div></div>
        <button className="btn btn-ghost btn-sm" onClick={() => setCreating(true)}>+ New ticket</button></div>

      <div className="card-pad" style={{ borderBottom: '1px solid var(--line-soft)', display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
        <input className="pm-input" style={{ flex: 1, minWidth: 160 }} placeholder="Search tickets…" value={q} onChange={e => setQ(e.target.value)} />
        <select className="pm-input" value={catFilter} onChange={e => setCatFilter(e.target.value)}><option value="all">All categories</option>{CATS.map(c => <option key={c.key} value={c.key}>{c.label}</option>)}</select>
        <select className="pm-input" value={statusFilter} onChange={e => setStatusFilter(e.target.value)}><option value="all">All statuses</option><option value="open">Open</option><option value="resolved">Resolved</option></select>
        <input className="pm-input" type="date" value={from} onChange={e => setFrom(e.target.value)} title="From" />
        <input className="pm-input" type="date" value={to} onChange={e => setTo(e.target.value)} title="To" />
      </div>

      {filtered.length === 0 ? <div className="empty"><div className="ico">🔧</div><h3>{tickets.length === 0 ? 'No tickets yet' : 'No matches'}</h3><p>{tickets.length === 0 ? 'Log a maintenance job for this property — damp & mould gets a statutory clock automatically.' : 'Try different filters.'}</p>{tickets.length === 0 && <button className="btn btn-primary btn-sm" onClick={() => setCreating(true)}>+ New ticket</button>}</div>
        : filtered.map(t => { const c = cat(t.category); const esc = escalated(t); return <div className="row clickable" key={t.id} onClick={() => setOpen(t.id)}>
          <span className="avatar" style={{ background: esc ? 'var(--red-soft)' : 'var(--surface-2)' }}>{c.icon}</span>
          <div className="main">
            <div className="t">{t.title}</div>
            <div className="s">{c.label} · {srcLabel(t.source)} · raised {fmt(t.raised_at)}{t.status === 'resolved' ? ' · resolved ' + fmt(t.resolved_at) + (Number(t.cost) > 0 ? ' · ' + gbp(t.cost) : '') : ''}</div>
          </div>
          {esc && <Clock ticket={t} compact />}
          <PrioPill ticket={t} />
          <span className={'badge ' + (t.status === 'resolved' ? 'ok' : 'none')}>{stageLabel(t.status)}</span>
        </div>; })}

      {open && <Drawer sb={sb} ticketId={open} tickets={tickets} props={[property]} contractors={contractors} onClose={() => setOpen(null)} reload={load} toast={toast} />}
      {creating && <CreateModal sb={sb} props={[property]} contractors={contractors} prefillPropertyId={property.id} toast={toast} onClose={() => setCreating(false)} onCreated={(id) => { setCreating(false); load(); if (id) setOpen(id); }} />}
    </React.Fragment>;
  }

  window.PMMaintHub = MaintHub;
  window.PMTabMaintenance = TabMaintenance;
})();
