'use strict'
const cfg = require('./config')
const { log, json } = require('./helpers')
const erp = require('./erp')
const types = require('./types')
const { verifyJwt } = require('./magic-link')
const { extractField } = require('./vision')
const ui = require('./ui')

// ── Tech mobile SPA ──────────────────────────────────────────────────────────
// Server-rendered shell + hash-routed client views (home/hist/cal/profile/job-detail).
// All UI primitives come from ../ui (design.css, components, client.js, scanner.js).
// All jobs pre-loaded and embedded as JSON; detail view pops open instantly from
// the cache and async-refreshes notes / photos / equipment.
//
// Field-targeted scans: each form field has a 📷 that opens a Gemini-backed
// single-field extractor — "find the Wi-Fi password on this label" rather than
// "read everything". Tech approves or retries the value before it fills the input.

// ═════════════════════════════════════════════════════════════════════════════
// Auth + request helpers
// ═════════════════════════════════════════════════════════════════════════════

function authToken (path) {
  const m = path.match(/^\/t\/([A-Za-z0-9_\-\.]+)/)
  return m ? verifyJwt(m[1]) : null
}

async function readBody (req) {
  const chunks = []
  for await (const c of req) chunks.push(c)
  try { return JSON.parse(Buffer.concat(chunks).toString()) } catch { return null }
}

// ═════════════════════════════════════════════════════════════════════════════
// Route dispatcher
// ═════════════════════════════════════════════════════════════════════════════

async function route (req, res, method, path) {
  if (method === 'GET'  && /^\/t\/[A-Za-z0-9_\-\.]+$/.test(path)) return handlePage(req, res, path)
  if (method === 'POST' && path.endsWith('/status'))        return handleStatus(req, res, path)
  if (method === 'POST' && path.endsWith('/note'))          return handleNote(req, res, path)
  if (method === 'POST' && path.endsWith('/photo'))         return handlePhotoUpload(req, res, path)
  if (method === 'GET'  && path.endsWith('/photos'))        return handlePhotoList(req, res, path)
  if (method === 'GET'  && path.endsWith('/photo-serve'))   return handlePhotoServe(req, res, path)
  if (method === 'GET'  && path.endsWith('/job'))           return handleJobDetail(req, res, path)
  if (method === 'POST' && path.endsWith('/field-scan'))    return handleFieldScan(req, res, path)
  if (method === 'POST' && path.endsWith('/scan'))          return handleScan(req, res, path)
  if (method === 'POST' && path.endsWith('/vision'))        return handleVision(req, res, path)
  if (method === 'POST' && path.endsWith('/equip'))         return handleEquipInstall(req, res, path)
  if (method === 'POST' && path.endsWith('/equip-remove'))  return handleEquipRemove(req, res, path)
  if (method === 'GET'  && path.endsWith('/catalog'))       return handleCatalog(req, res, path)
  if (method === 'GET'  && path.endsWith('/equip-list'))    return handleEquipList(req, res, path)
  return json(res, 404, { error: 'Not found' })
}

// ═════════════════════════════════════════════════════════════════════════════
// Page shell + data load
// ═════════════════════════════════════════════════════════════════════════════

async function handlePage (req, res, path) {
  const payload = authToken(path)
  if (!payload) { res.writeHead(200, ui.htmlHeaders()); return res.end(ui.pageExpired()) }

  const techId = payload.sub
  // Montreal local time — UTC midnight rolls over 4-5 h before Montreal midnight,
  // which would mislabel evening jobs as "hier".
  const today     = ui.montrealDate()
  const rearDate  = ui.montrealDate(new Date(Date.now() - 60 * 86400 * 1000))
  const frontDate = ui.montrealDate(new Date(Date.now() + 60 * 86400 * 1000))

  try {
    const tech = await erp.get('Dispatch Technician', techId, {
      fields: ['name', 'full_name', 'phone', 'email', 'assigned_group'],
    }) || { name: techId, full_name: techId }

    // Real time field is `start_time`; customer_name / service_location_name
    // are fetched-from links that v16 blocks in queries, so we pull the link
    // IDs and resolve labels via hydrateLabels.
    const jobs = await erp.list('Dispatch Job', {
      fields: ['name', 'subject', 'status', 'customer', 'service_location', 'start_time', 'scheduled_date', 'duration_h', 'priority', 'job_type', 'notes', 'assigned_group', 'address', 'ticket_id', 'source_issue', 'actual_start', 'actual_end'],
      filters: [
        ['assigned_tech', '=', techId],
        ['scheduled_date', 'between', [rearDate, frontDate]],
      ],
      orderBy: 'scheduled_date desc, start_time asc',
      limit: 300,
    })
    await erp.hydrateLabels(jobs, {
      customer:         { doctype: 'Customer',         out: 'customer_name',         fields: ['name', 'customer_name'] },
      service_location: { doctype: 'Service Location', out: 'service_location_name', fields: ['name', 'address_line_1', 'city'],
                          format: l => [l.address_line_1, l.city].filter(Boolean).join(', ') || l.name },
    })

    const token = path.split('/')[2]
    res.writeHead(200, ui.htmlHeaders())
    res.end(renderPage({ tech, jobs, token, today }))
  } catch (e) {
    log('tech-mobile page error:', e.message)
    res.writeHead(500, ui.htmlHeaders())
    res.end(ui.pageError())
  }
}

// ═════════════════════════════════════════════════════════════════════════════
// API handlers (unchanged behavior — just cleaner layout)
// ═════════════════════════════════════════════════════════════════════════════

async function handleStatus (req, res, path) {
  const payload = authToken(path)
  if (!payload) return json(res, 401, { error: 'expired' })
  const body = await readBody(req)
  if (!body?.job || !body?.status) return json(res, 400, { error: 'job and status required' })
  if (!['In Progress', 'Completed'].includes(body.status)) return json(res, 400, { error: 'Invalid status' })
  try {
    const { setJobStatusWithChain } = require('./dispatch')
    const result = await setJobStatusWithChain(body.job, body.status)
    return json(res, 200, { ...result, tech: payload.sub })
  } catch (e) { return json(res, 500, { error: e.message }) }
}

async function handleNote (req, res, path) {
  const payload = authToken(path)
  if (!payload) return json(res, 401, { error: 'expired' })
  const body = await readBody(req)
  if (!body?.job) return json(res, 400, { error: 'job required' })
  try {
    const r = await erp.update('Dispatch Job', body.job, { notes: body.notes || '' })
    if (!r.ok) return json(res, r.status || 500, { error: r.error })
    return json(res, 200, { ok: true })
  } catch (e) { return json(res, 500, { error: e.message }) }
}

async function handlePhotoUpload (req, res, path) {
  const payload = authToken(path)
  if (!payload) return json(res, 401, { error: 'expired' })
  const body = await readBody(req)
  if (!body?.job || !body?.image) return json(res, 400, { error: 'job and image required' })
  try {
    const base64 = body.image.replace(/^data:image\/[^;]+;base64,/, '')
    const ext = (body.image.match(/^data:image\/([a-z]+);/) || [, 'jpg'])[1].replace('jpeg', 'jpg')
    const fileName = body.file_name || `dj-${body.job}-${Date.now()}.${ext}`
    const r = await erp.create('File', {
      file_name: fileName, is_private: 1, content: base64, decode: 1,
      attached_to_doctype: 'Dispatch Job', attached_to_name: body.job,
    })
    if (!r.ok) return json(res, r.status || 500, { error: r.error })
    return json(res, 200, { ok: true, name: r.data?.name, file_url: r.data?.file_url, file_name: r.data?.file_name })
  } catch (e) { return json(res, 500, { error: e.message }) }
}

async function handlePhotoList (req, res, path) {
  const payload = authToken(path)
  if (!payload) return json(res, 401, { error: 'expired' })
  const url = new URL(req.url, 'http://localhost')
  const jobName = url.searchParams.get('job')
  if (!jobName) return json(res, 400, { error: 'job required' })
  try {
    const items = await erp.list('File', {
      fields: ['name', 'file_name', 'file_url', 'is_private', 'creation'],
      filters: [
        ['attached_to_doctype', '=', 'Dispatch Job'],
        ['attached_to_name', '=', jobName],
        ['file_name', 'like', 'dj-%'],
      ],
      orderBy: 'creation desc',
      limit: 50,
    })
    return json(res, 200, { ok: true, items })
  } catch (e) { return json(res, 500, { error: e.message }) }
}

// Proxy a private file so the tech browser can render without ERPNext creds.
async function handlePhotoServe (req, res, path) {
  const payload = authToken(path)
  if (!payload) { res.writeHead(401); return res.end() }
  const url = new URL(req.url, 'http://localhost')
  const filePath = url.searchParams.get('p')
  if (!filePath || !/^\/(private\/files|files)\//.test(filePath)) { res.writeHead(400); return res.end() }
  try {
    const erpBase = (cfg.ERPNEXT_URL || 'https://erp.gigafibre.ca').replace(/\/$/, '')
    const headers = {}
    if (cfg.ERPNEXT_API_KEY && cfg.ERPNEXT_API_SECRET) {
      headers.Authorization = `token ${cfg.ERPNEXT_API_KEY}:${cfg.ERPNEXT_API_SECRET}`
    }
    const r = await fetch(erpBase + filePath, { headers })
    if (!r.ok) { res.writeHead(404); return res.end() }
    const ct = r.headers.get('content-type') || 'application/octet-stream'
    const buf = Buffer.from(await r.arrayBuffer())
    res.writeHead(200, { 'Content-Type': ct, 'Cache-Control': 'private, max-age=300' })
    res.end(buf)
  } catch (e) {
    log('photo-serve error:', e.message)
    res.writeHead(500); res.end()
  }
}

async function handleJobDetail (req, res, path) {
  const payload = authToken(path)
  if (!payload) return json(res, 401, { error: 'expired' })
  const url = new URL(req.url, 'http://localhost')
  const name = url.searchParams.get('name')
  if (!name) return json(res, 400, { error: 'name required' })
  try {
    const job = await erp.get('Dispatch Job', name)
    if (!job) return json(res, 404, { error: 'not found' })
    return json(res, 200, { ok: true, job })
  } catch (e) { return json(res, 500, { error: e.message }) }
}

async function handleFieldScan (req, res, path) {
  const payload = authToken(path)
  if (!payload) return json(res, 401, { error: 'expired' })
  const body = await readBody(req)
  if (!body?.image || !body?.field) return json(res, 400, { error: 'image and field required' })
  try {
    const base64 = body.image.replace(/^data:image\/[^;]+;base64,/, '')
    const out = await extractField(base64, body.field, {
      hint: body.hint, equipment_type: body.equipment_type,
      brand: body.brand, model: body.model,
    })
    return json(res, 200, { ok: true, ...out })
  } catch (e) {
    log('field-scan error:', e.message)
    return json(res, 500, { error: e.message })
  }
}

async function handleScan (req, res, path) {
  const payload = authToken(path)
  if (!payload) return json(res, 401, { error: 'expired' })
  const body = await readBody(req)
  if (!body?.code) return json(res, 400, { error: 'code required' })
  const q = body.code.replace(/[:\-\s]/g, '').toUpperCase()
  try {
    for (const field of ['serial_number', 'mac_address']) {
      const rows = await erp.list('Service Equipment', {
        fields: ['name', 'serial_number', 'item_name', 'equipment_type', 'mac_address', 'status', 'customer', 'service_location'],
        filters: [[field, 'like', `%${q}%`]],
        limit: 5,
      })
      if (rows.length) return json(res, 200, { ok: true, results: rows, source: 'Service Equipment', query: q })
    }
    const sn = await erp.list('Serial No', {
      fields: ['name', 'serial_no', 'item_code', 'status', 'warehouse'],
      filters: [['serial_no', 'like', `%${q}%`]],
      limit: 5,
    })
    if (sn.length) return json(res, 200, { ok: true, results: sn, source: 'Serial No', query: q })
    return json(res, 200, { ok: true, results: [], query: q })
  } catch (e) { return json(res, 500, { error: e.message }) }
}

async function handleVision (req, res, path) {
  const payload = authToken(path)
  if (!payload) return json(res, 401, { error: 'expired' })
  const body = await readBody(req)
  if (!body?.image) return json(res, 400, { error: 'image required' })
  try {
    const { extractBarcodes } = require('./vision')
    const base64 = body.image.replace(/^data:image\/[^;]+;base64,/, '')
    const result = await extractBarcodes(base64)
    return json(res, 200, { ok: true, ...result })
  } catch (e) { return json(res, 500, { error: e.message }) }
}

async function handleEquipInstall (req, res, path) {
  const payload = authToken(path)
  if (!payload) return json(res, 401, { error: 'expired' })
  const body = await readBody(req)
  if (!body) return json(res, 400, { error: 'body required' })

  const {
    job, barcode, mac_address, gpon_sn, equipment_type, brand, model,
    wifi_ssid, wifi_password, notes, customer, location, action,
  } = body
  const today = new Date().toISOString().slice(0, 10)

  try {
    const installData = {
      request: job || '', barcode: barcode || '',
      equipment_type: equipment_type || '', brand: brand || '', model: model || '',
      notes: [
        notes || '',
        mac_address ? `MAC: ${mac_address}` : '',
        gpon_sn ? `GPON-SN: ${gpon_sn}` : '',
        wifi_ssid ? `SSID: ${wifi_ssid}` : '',
        wifi_password ? `Wi-Fi PWD: ${wifi_password}` : '',
      ].filter(Boolean).join(' | '),
      installation_date: today, technician: payload.sub, action: action || 'install',
    }
    const r = await erp.create('Equipment Install', installData)
    if (!r.ok) return json(res, r.status || 500, { error: r.error })

    if (barcode && (action === 'install' || action === 'replace')) {
      try {
        const q = barcode.replace(/[:\-\s]/g, '').toUpperCase()
        const existing = await erp.list('Service Equipment', {
          fields: ['name'], filters: [['serial_number', 'like', `%${q}%`]], limit: 1,
        })
        const seCommon = {
          status: 'Actif',
          customer: customer || '', service_location: location || '',
          mac_address: mac_address || '', gpon_sn: gpon_sn || '',
          wifi_ssid: wifi_ssid || '', wifi_password: wifi_password || '',
          brand: brand || '', model: model || '', equipment_type: equipment_type || '',
        }
        const seUpdate = Object.fromEntries(Object.entries(seCommon).filter(([, v]) => v !== '' && v != null))
        if (existing.length) {
          await erp.update('Service Equipment', existing[0].name, seUpdate)
        } else {
          await erp.create('Service Equipment', { serial_number: barcode, ...seCommon })
        }
      } catch (e) { log('Equip link/create warn:', e.message) }
    }
    return json(res, 200, { ok: true, name: r.name || '' })
  } catch (e) { return json(res, 500, { error: e.message }) }
}

async function handleEquipRemove (req, res, path) {
  const payload = authToken(path)
  if (!payload) return json(res, 401, { error: 'expired' })
  const body = await readBody(req)
  if (!body?.equipment) return json(res, 400, { error: 'equipment name required' })
  try {
    await erp.update('Service Equipment', body.equipment, {
      status: body.status || 'Retourné', customer: '', service_location: '',
    })
    await erp.create('Equipment Install', {
      request: body.job || '', barcode: body.serial || '',
      equipment_type: body.equipment_type || '',
      notes: `Retrait par ${payload.sub}`,
      installation_date: new Date().toISOString().slice(0, 10),
      technician: payload.sub, action: 'remove',
    })
    return json(res, 200, { ok: true })
  } catch (e) { return json(res, 500, { error: e.message }) }
}

async function handleCatalog (req, res, path) {
  const payload = authToken(path)
  if (!payload) return json(res, 401, { error: 'expired' })
  try {
    const items = await erp.list('Item', {
      fields: ['name', 'item_name', 'item_group', 'standard_rate', 'image', 'description'],
      filters: [['item_group', 'in', ['Network Equipment', 'CPE', 'Équipements réseau', 'Products']]],
      limit: 50,
    })
    if (!items.length) {
      return json(res, 200, { ok: true, items: [
        { name: 'ONT-GPON',     item_name: 'ONT GPON',              item_group: 'CPE',               standard_rate: 0,    description: 'Terminal fibre client' },
        { name: 'ROUTER-WIFI6', item_name: 'Routeur Wi-Fi 6',       item_group: 'CPE',               standard_rate: 9.99, description: 'Routeur sans fil' },
        { name: 'DECODER-IPTV', item_name: 'Décodeur IPTV',         item_group: 'CPE',               standard_rate: 7.99, description: 'Décodeur télévision' },
        { name: 'VOIP-ATA',     item_name: 'Adaptateur VoIP',       item_group: 'CPE',               standard_rate: 4.99, description: 'Adaptateur téléphonie' },
        { name: 'AMP-COAX',     item_name: 'Amplificateur coaxial', item_group: 'Network Equipment', standard_rate: 0,    description: 'Amplificateur signal' },
      ]})
    }
    return json(res, 200, { ok: true, items })
  } catch (e) { return json(res, 500, { error: e.message }) }
}

async function handleEquipList (req, res, path) {
  const payload = authToken(path)
  if (!payload) return json(res, 401, { error: 'expired' })
  const url = new URL(req.url, 'http://localhost')
  const jobName = url.searchParams.get('job')
  if (!jobName) return json(res, 400, { error: 'job param required' })
  try {
    const items = await erp.list('Equipment Install', {
      fields: ['name', 'barcode', 'equipment_type', 'brand', 'model', 'action', 'notes', 'installation_date'],
      filters: [['request', '=', jobName]],
      orderBy: 'creation desc',
      limit: 20,
    })
    return json(res, 200, { ok: true, items })
  } catch (e) { return json(res, 500, { error: e.message }) }
}

// ═════════════════════════════════════════════════════════════════════════════
// Server-side HTML rendering (uses ../ui primitives)
// ═════════════════════════════════════════════════════════════════════════════

function jobCard (j, today) {
  const urgent  = types.isUrgent(j.priority)
  const done    = types.isTerminal(j.status)
  const inProg  = types.isInProgress(j.status)
  const border  = urgent ? 'var(--danger)' : done ? 'var(--text-dim)' : inProg ? 'var(--warning)' : 'var(--brand)'
  const overdue = !done && j.scheduled_date && j.scheduled_date < today
  const dlbl    = ui.dateLabelFr(j.scheduled_date, today)
  const tlbl    = j.start_time ? ui.fmtTime(j.start_time) : ''
  const datePart = [dlbl, tlbl].filter(Boolean).join(' · ')

  const inner = `
    <div class="row">
      <span class="jid">${ui.esc(j.name)}</span>
      ${ui.badge(j.status)}
      <span class="jdt">${ui.esc(datePart)}</span>
    </div>
    <div class="jtt">${ui.esc(j.subject || 'Sans titre')}</div>
    ${j.customer_name         ? `<div class="jsb">👤 ${ui.esc(j.customer_name)}</div>` : ''}
    ${j.service_location_name ? `<div class="jsb">📍 ${ui.esc(j.service_location_name)}</div>` : ''}
    <div class="jmt">${j.duration_h ? `⏱ ${j.duration_h}h` : ''} ${j.job_type ? ui.esc(j.job_type) : ''}${urgent ? ' 🔥' : ''}</div>`

  return ui.card(inner, {
    extraClass: (done ? 'dim ' : '') + (overdue ? 'od' : ''),
    onclick:    `go('#job/${ui.esc(j.name)}')`,
    style:      `border-left:4px solid ${border}`,
  })
}

function renderPage ({ tech, jobs, token, today }) {
  const techName = tech.full_name || tech.name

  // Partition for the home view — rest is client-side filtering
  const inProgress = jobs.filter(j => types.isInProgress(j.status))
  const pending    = jobs.filter(j => !types.isTerminal(j.status) && !types.isInProgress(j.status))
  const overdue    = pending.filter(j => j.scheduled_date && j.scheduled_date < today)
  const todayJobs  = pending.filter(j => j.scheduled_date === today)
  const upcoming   = pending.filter(j => j.scheduled_date && j.scheduled_date > today).slice(0, 20)
  const history    = jobs.filter(j => types.isTerminal(j.status))
  const nodate     = pending.filter(j => !j.scheduled_date)
  const activeCount = inProgress.length + overdue.length + todayJobs.length + nodate.length

  const hub = cfg.EXTERNAL_URL || ''
  const jobsMap = Object.fromEntries(jobs.map(j => [j.name, j]))

  const body = `
${renderHome({ techName, inProgress, overdue, todayJobs, nodate, upcoming, activeCount, historyCount: history.length, today })}
${renderHist({ history, overdue, today })}
${renderCal()}
${renderProfile({ tech, techName })}
<div class="view" id="view-detail"></div>
${renderEquipOverlay()}
${ui.tabBar([
  { id: 'home',    label: "Aujourd'hui", icon: '📅', active: true },
  { id: 'cal',     label: 'Calendrier',  icon: '📆' },
  { id: 'hist',    label: 'Historique',  icon: '📋' },
  { id: 'profile', label: 'Profil',      icon: '👤' },
])}
  `

  return ui.page({
    title: 'Mes tâches — Gigafibre',
    bodyClass: 'pad-body',
    cfg: { hub, base: '/t/' + token, today },
    bootVars: {
      TOKEN: token,
      TODAY: today,
      JOBS: jobsMap,
    },
    includeScanner: true,
    head: `<style>
.view{display:none}.view.active{display:block}
.jid{font-size:10px;font-weight:700;color:var(--brand);letter-spacing:.3px}
.jdt{margin-left:auto;font-size:11px;color:var(--text-muted);font-weight:600;text-align:right;text-transform:capitalize}
.jtt{font-size:15px;font-weight:600;margin:6px 0 2px;color:var(--text);line-height:1.25}
.jsb{font-size:12px;color:var(--text-muted);margin:1px 0}
.jmt{font-size:11px;color:var(--text-dim);margin:4px 0 0}
.sinp{width:100%;padding:10px 14px;border:1.5px solid var(--border);border-radius:12px;font-size:14px;background:var(--surface);outline:none}
.sinp:focus{border-color:var(--brand)}
.hist-filter{display:flex;gap:6px;margin-bottom:10px;overflow-x:auto;padding-bottom:4px}
.hist-filter button{padding:6px 12px;border:1.5px solid var(--border);background:var(--surface);color:var(--text-muted);border-radius:16px;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap;font-family:inherit}
.hist-filter button.on{border-color:var(--brand);background:var(--brand);color:#fff}
/* Detail view */
.dt-hdr{background:linear-gradient(135deg,var(--brand-dark),var(--brand));color:#fff;padding:12px 14px 24px;border-radius:0 0 18px 18px}
.dt-hdr .nav{display:flex;align-items:center;gap:10px;margin-bottom:10px}
.dt-hdr .nav button{background:rgba(255,255,255,.18);border:none;color:#fff;width:36px;height:36px;border-radius:50%;font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center}
.dt-hdr .nav .jn{flex:1;font-size:11px;font-weight:700;opacity:.8;letter-spacing:.5px}
.dt-hdr h1{font-size:19px;font-weight:700;margin-bottom:6px;line-height:1.25}
.dt-hdr .meta{display:flex;gap:8px;flex-wrap:wrap;font-size:12px;opacity:.9}
.dt-hdr .meta span{background:rgba(255,255,255,.18);padding:3px 10px;border-radius:8px}
.cust-line{display:flex;align-items:center;gap:10px;padding:10px 0;border-bottom:1px solid var(--border-soft);font-size:14px;color:#334155}
.cust-line:last-child{border-bottom:none}.cust-line .ico{font-size:18px;width:24px;text-align:center}
.cust-line a{color:var(--brand);margin-left:auto;text-decoration:none;font-weight:600;font-size:13px;padding:6px 10px;background:var(--brand-soft);border-radius:8px}
.desc{font-size:14px;color:#334155;line-height:1.5;white-space:pre-wrap}
.notes-area{width:100%;min-height:120px;padding:10px 12px;border:1.5px solid var(--border);border-radius:10px;font-size:14px;font-family:inherit;resize:vertical;outline:none;line-height:1.4}
.notes-area:focus{border-color:var(--brand)}
.photo-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-top:4px}
.photo-grid .ph{position:relative;aspect-ratio:1;border-radius:8px;overflow:hidden;background:var(--border-soft)}
.photo-grid .ph img{width:100%;height:100%;object-fit:cover;display:block}
.photo-add{aspect-ratio:1;border:2px dashed #cbd5e1;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:26px;color:var(--text-dim);cursor:pointer;background:var(--surface-alt)}
.photo-add:active{background:var(--brand-soft);border-color:var(--brand);color:var(--brand)}
.eq-item{background:var(--surface-alt);border-radius:10px;padding:10px 12px;margin-bottom:6px;display:flex;align-items:center;gap:10px}
.eq-item .eq-info{flex:1}.eq-sn{font-weight:700;color:var(--text);font-size:13px}.eq-type{font-size:11px;color:var(--text-muted);margin-top:2px}
.eq-rm{background:var(--danger-soft);color:var(--danger);border:none;border-radius:6px;padding:6px 10px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit}
.add-eq-btn{width:100%;padding:14px;background:var(--brand-soft);color:var(--brand);border:2px dashed #c7d2fe;border-radius:10px;font-size:14px;font-weight:600;cursor:pointer;margin-top:6px;font-family:inherit}
.add-eq-btn:active{background:#e0e7ff}
.action-bar{position:sticky;bottom:68px;left:0;right:0;background:var(--surface);border-top:1px solid var(--border);padding:10px 12px;display:flex;gap:8px;z-index:30;margin:20px -12px 0}
.action-bar .btn{flex:1;padding:14px}
/* Equipment add form */
.af{background:var(--surface);border-radius:12px;padding:14px;margin-bottom:16px;box-shadow:0 1px 2px rgba(0,0,0,.04)}
.af-row{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}.af-row b{font-size:15px;color:var(--text)}
.af label{display:block;font-size:11px;font-weight:700;color:#475569;margin:10px 0 4px;letter-spacing:.3px;text-transform:uppercase}
.af .fwrap{position:relative;display:flex;gap:6px;align-items:stretch}
.af .fwrap input,.af .fwrap select{flex:1;padding:9px 11px;border:1.5px solid var(--border);border-radius:8px;font-size:14px;background:var(--surface);outline:none;font-family:inherit}
.af .fwrap input:focus,.af .fwrap select:focus{border-color:var(--brand)}
.af .fscan{width:42px;height:auto;border:none;border-radius:8px;background:var(--brand-soft);color:var(--brand);font-size:18px;cursor:pointer;flex-shrink:0}
.af .fscan:active{background:#e0e7ff}
.cat-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px}
.cat-card{background:var(--surface);border:2px solid #e5e7eb;border-radius:10px;padding:12px;text-align:center;cursor:pointer;transition:all var(--t-fast)}
.cat-card:active{border-color:var(--brand);background:var(--brand-soft)}
.cat-card .cn{font-size:13px;font-weight:700;color:#1e293b;margin-bottom:2px}
.cat-card .cd{font-size:10px;color:var(--text-dim)}
.cat-card .cp{font-size:13px;font-weight:700;color:var(--brand);margin-top:4px}
.inp-row{display:flex;gap:8px;margin-bottom:10px}
.inp-row .inp{flex:1}
</style>`,
    body,
    script: CLIENT_SCRIPT,
  })
}

// ═════════════════════════════════════════════════════════════════════════════
// View renderers (home / hist / cal / profile / overlays)
// ═════════════════════════════════════════════════════════════════════════════

function renderHome ({ techName, inProgress, overdue, todayJobs, nodate, upcoming, activeCount, historyCount, today }) {
  const isEmpty = activeCount === 0 && inProgress.length === 0
  const cards = (list) => list.map(j => jobCard(j, today))

  return `<div class="view active" id="view-home">
  <div class="hdr">
    <div class="hdr-d">${ui.esc(ui.todayFr())}</div>
    <div class="hdr-n"><span class="ic">👷</span>${ui.esc(techName)}</div>
    ${ui.statRow([
      { value: activeCount,        label: 'À FAIRE',  id: 'stActive' },
      { value: inProgress.length,  label: 'EN COURS' },
      { value: historyCount,       label: 'TERMINÉS' },
    ])}
  </div>
  <div class="wrap">
    ${isEmpty ? ui.emptyState('🎉', 'Aucune tâche active.<br/>Profitez de la pause !') : ''}
    ${ui.section('En cours',    cards(inProgress))}
    ${ui.section('En retard',   cards(overdue), 'danger')}
    ${ui.section("Aujourd'hui", cards(todayJobs))}
    ${ui.section('Sans date',   cards(nodate))}
    ${ui.section('À venir',     cards(upcoming))}
  </div>
</div>`
}

function renderHist ({ history, overdue, today }) {
  const all = [...overdue, ...history]
  const doneCount = history.filter(j => types.isDone(j.status)).length
  const cancCount = history.filter(j => types.isCancelled(j.status)).length

  return `<div class="view" id="view-hist">
  <div class="hdr">
    <div class="hdr-d">Historique</div>
    <div class="hdr-n"><span class="ic">📋</span>Tâches passées</div>
  </div>
  <div class="wrap">
    <div class="sbar"><input class="sinp" id="histSearch" placeholder="🔍 Rechercher (client, lieu, sujet…)"></div>
    <div class="hist-filter" id="histFilter">
      <button class="on" data-f="all">Tous (${all.length})</button>
      <button data-f="done">Terminés (${doneCount})</button>
      <button data-f="overdue">Manqués (${overdue.length})</button>
      <button data-f="cancelled">Annulés (${cancCount})</button>
    </div>
    <div id="histList">
      ${all.length ? all.map(j => jobCard(j, today)).join('') : ui.emptyState('📭', 'Aucun historique.')}
    </div>
  </div>
</div>`
}

function renderCal () {
  return `<div class="view" id="view-cal">
  <div class="hdr">
    <div class="hdr-d">Calendrier</div>
    <div class="hdr-n"><span class="ic">📆</span>Vue mensuelle</div>
  </div>
  <div class="wrap">
    ${ui.placeholder('🚧', 'Bientôt disponible', `La vue calendrier avec navigation mois/semaine arrive à la phase 4.<br/>Pour l'instant, utilisez <b>Aujourd'hui</b> ou <b>Historique</b>.`)}
  </div>
</div>`
}

function renderProfile ({ tech, techName }) {
  const line = (icon, text, link = '') =>
    `<div class="cust-line"><span class="ico">${icon}</span><span>${ui.esc(text)}</span>${link}</div>`
  return `<div class="view" id="view-profile">
  <div class="hdr">
    <div class="hdr-d">Profil</div>
    <div class="hdr-n"><span class="ic">👤</span>${ui.esc(techName)}</div>
  </div>
  <div class="wrap">
    <div class="panel">
      <h3>Informations</h3>
      ${line('🪪', tech.name)}
      ${tech.phone ? line('📞', tech.phone, `<a href="tel:${ui.esc(tech.phone)}">Appeler</a>`) : ''}
      ${tech.email ? line('✉️', tech.email) : ''}
      ${tech.assigned_group ? line('👥', tech.assigned_group) : ''}
    </div>
    <div class="panel">
      <h3>Support</h3>
      ${line('📞', 'Ligne support', '<a href="tel:4382313838">438-231-3838</a>')}
    </div>
    <div class="panel ta-c" style="padding:20px">
      ${ui.button('↻ Actualiser les données', { kind: 'muted', block: true, onclick: 'location.reload()' })}
    </div>
  </div>
</div>`
}

function renderEquipOverlay () {
  return `<div class="ov" id="eqOv">
  <div class="ov-hdr">
    <button class="ov-x" onclick="closeEquip()">✕</button>
    <h2 id="eqTitle">Équipement</h2>
  </div>
  <div class="ov-body">
    <div class="cam-wrap"><video id="vid" autoplay playsinline muted></video><div class="cam-overlay"></div></div>
    <div class="inp-row">
      <input class="inp" id="codeInp" placeholder="SN, MAC ou code-barre…" autocomplete="off" autocapitalize="off">
      <button class="btn btn-pri btn-sm" onclick="doScan()">Chercher</button>
    </div>
    <div id="scanRes"></div>
    <div class="ov-sec">Installé sur cette tâche</div>
    <div id="eqList"><div class="sr sm">Chargement…</div></div>
    <div class="ov-sec">Catalogue</div>
    <div class="cat-grid" id="catGrid"><div class="sr sm">Chargement…</div></div>
    <div class="af" id="addForm" style="display:none">
      <div class="af-row"><b id="afTitle">Nouvel équipement</b><button class="ov-x" onclick="hideForm()">✕</button></div>
      <label>Type</label>
      <div class="fwrap"><select id="afType"><option>ONT</option><option>Routeur</option><option>Modem</option><option>Décodeur TV</option><option>VoIP</option><option>Switch</option><option>AP</option><option>Amplificateur</option><option>Splitter</option><option>Autre</option></select></div>
      <label>Marque</label>       <div class="fwrap"><input id="afBrand" placeholder="Ex: TP-Link, Huawei, ZTE…"></div>
      <label>Modèle</label>       <div class="fwrap"><input id="afModel" placeholder="Ex: HG8245H"><button class="fscan" onclick="scanInto('model','Modèle','afModel')">📷</button></div>
      <label>Numéro de série</label><div class="fwrap"><input id="afSn" placeholder="SN / Serial"><button class="fscan" onclick="scanInto('serial_number','Numéro de série','afSn')">📷</button></div>
      <label>Adresse MAC</label>  <div class="fwrap"><input id="afMac" placeholder="AA:BB:CC:DD:EE:FF"><button class="fscan" onclick="scanInto('mac_address','Adresse MAC','afMac')">📷</button></div>
      <label>GPON SN</label>      <div class="fwrap"><input id="afGpon" placeholder="HWTC12345678"><button class="fscan" onclick="scanInto('gpon_sn','GPON SN','afGpon')">📷</button></div>
      <label>Wi-Fi SSID</label>   <div class="fwrap"><input id="afSsid" placeholder="Nom du réseau Wi-Fi"><button class="fscan" onclick="scanInto('wifi_ssid','SSID Wi-Fi','afSsid')">📷</button></div>
      <label>Wi-Fi Password</label><div class="fwrap"><input id="afPwd" placeholder="Clé WPA"><button class="fscan" onclick="scanInto('wifi_password','Mot de passe Wi-Fi','afPwd')">📷</button></div>
      <label>Notes</label>        <div class="fwrap"><input id="afNotes" placeholder="Port, emplacement…"></div>
      <label>Action</label>       <div class="fwrap"><select id="afAction"><option value="install">Installer</option><option value="replace">Remplacer</option></select></div>
      <button class="btn btn-pri btn-block mt-2" id="afSubmit" onclick="submitEquip()">Ajouter l'équipement</button>
    </div>
  </div>
</div>`
}

// ═════════════════════════════════════════════════════════════════════════════
// Client-side logic (consumes window.api / window.router / window.scanner / etc.)
// ═════════════════════════════════════════════════════════════════════════════

const CLIENT_SCRIPT = `
${types.CLIENT_TYPES_JS}

// Current detail-view job, customer, location (set by openDetail)
var CJ='',CC='',CL='',CMODEL='',CTYPE='';
// Equipment overlay scanner (separate from field-scan)
var stream=null, bdTimer=null;

// ── Hash routes ──────────────────────────────────────────────────────────
router.on('#home',    function(){ showTab('home') });
router.on('#cal',     function(){ showTab('cal') });
router.on('#hist',    function(){ showTab('hist') });
router.on('#profile', function(){ showTab('profile') });
router.on('#job/:name', function(p){ openDetail(p.name) });
router.dispatch();

function showTab(v){
  closeEquip(); if (window.scanner && window.scanner.close) scanner.close();
  var views=document.querySelectorAll('.view');
  for(var i=0;i<views.length;i++) views[i].classList.remove('active');
  $('view-'+v).classList.add('active');
  var tabs=document.querySelectorAll('.tab');
  for(var k=0;k<tabs.length;k++) tabs[k].classList.toggle('on', tabs[k].dataset.v===v);
}

// ── Status update (start / finish) ───────────────────────────────────────
function doSt(jobName, status, btn){
  if(btn){btn.classList.add('dis');btn.textContent='…'}
  api.post('/status', {job:jobName, status:status}).then(function(r){
    var d=r.data||{};
    if(d.ok||d.status==='In Progress'||d.status==='Completed'){
      toast(status==='Completed' ? 'Terminé ✓' : 'En route ▶', true);
      if(JOBS[jobName]) JOBS[jobName].status=status;
      setTimeout(function(){location.reload()}, 700);
    } else {
      toast('Erreur: '+(d.error||''), false);
      if(btn) btn.classList.remove('dis');
    }
  }).catch(function(){ toast('Erreur réseau', false); if(btn) btn.classList.remove('dis') });
}

// ── Historique search + filter ───────────────────────────────────────────
(function(){
  var s=$('histSearch'); if(s) s.addEventListener('input', applyHistFilter);
  var hf=document.querySelectorAll('#histFilter button');
  for(var i=0;i<hf.length;i++){
    hf[i].addEventListener('click', function(e){
      for(var j=0;j<hf.length;j++) hf[j].classList.remove('on');
      e.target.classList.add('on'); applyHistFilter();
    });
  }
})();
function applyHistFilter(){
  var qEl=$('histSearch'); var q = qEl ? qEl.value.toLowerCase().trim() : '';
  var fbtn=document.querySelector('#histFilter button.on');
  var f = fbtn ? fbtn.dataset.f : 'all';
  var cards=document.querySelectorAll('#histList .card');
  for(var i=0;i<cards.length;i++){
    var c=cards[i]; var txt=c.textContent.toLowerCase();
    var jidEl=c.querySelector('.jid'); if(!jidEl){c.style.display='none';continue}
    var j=JOBS[jidEl.textContent]; if(!j){c.style.display='none';continue}
    var okQ = !q || txt.indexOf(q)>=0;
    var okF = true;
    if(f==='done')      okF = isDone(j.status);
    else if(f==='cancelled') okF = j.status==='Cancelled';
    else if(f==='overdue')   okF = !isTerminal(j.status) && j.scheduled_date && j.scheduled_date<TODAY;
    c.style.display = (okQ&&okF) ? 'block' : 'none';
  }
}

// ── Job detail view ──────────────────────────────────────────────────────
function openDetail(name){
  var j=JOBS[name];
  if(!j){ toast('Tâche introuvable', false); go('#home'); return }
  CJ=j.name; CC=j.customer||''; CL=j.service_location||'';
  var views=document.querySelectorAll('.view');
  for(var i=0;i<views.length;i++) views[i].classList.remove('active');
  $('view-detail').classList.add('active');
  $('view-detail').innerHTML=renderDetail(j);
  loadPhotos(); loadEquipList();
}

function renderDetail(j){
  var done     = isTerminal(j.status);
  var canStart = ['Scheduled','assigned','open'].indexOf(j.status) >= 0;
  var canFinish= isInProgress(j.status);
  var sMeta    = {Scheduled:['Planifié','#818cf8'], assigned:['Assigné','#818cf8'], open:['Ouvert','#818cf8'],
                  'In Progress':['En cours','#f59e0b'], in_progress:['En cours','#f59e0b'],
                  Completed:['Terminé','#22c55e'], done:['Terminé','#22c55e'], Cancelled:['Annulé','#94a3b8']};
  var sm = sMeta[j.status] || [j.status||'—','#94a3b8'];
  var urgent = isUrgent(j.priority);
  var addr   = j.address || j.service_location_name || '';
  var gps    = j.service_location_name ? 'https://www.google.com/maps/dir/?api=1&destination='+encodeURIComponent(j.service_location_name) : '';

  return ''
    +'<div class="dt-hdr">'
    +  '<div class="nav"><button onclick="go(\\'#home\\')">‹</button><div class="jn">'+_esc(j.name)+'</div></div>'
    +  '<h1>'+_esc(j.subject||'Sans titre')+'</h1>'
    +  '<div class="meta">'
    +    '<span style="background:'+sm[1]+'">'+sm[0]+'</span>'
    +    '<span>📅 '+dlbl(j.scheduled_date,TODAY)+(j.start_time?' · '+fmtTime(j.start_time):'')+'</span>'
    +    (j.duration_h ? '<span>⏱ '+j.duration_h+'h</span>' : '')
    +    (urgent ? '<span style="background:#ef4444">🔥 Urgent</span>' : '')
    +    (j.job_type ? '<span>'+_esc(j.job_type)+'</span>' : '')
    +  '</div>'
    +'</div>'
    // Client & location
    +((j.customer_name||addr) ? (
      '<div class="panel"><h3>Client & lieu</h3>'
      +(j.customer_name ? '<div class="cust-line"><span class="ico">👤</span><span>'+_esc(j.customer_name)+'</span></div>' : '')
      +(addr ? '<div class="cust-line"><span class="ico">📍</span><span>'+_esc(addr)+'</span>'+(gps?'<a href="'+gps+'" target="_blank">GPS</a>':'')+'</div>' : '')
      +'</div>') : '')
    // Description
    +(j.description ? '<div class="panel"><h3>Description</h3><div class="desc">'+_esc(j.description)+'</div></div>' : '')
    // Notes
    +'<div class="panel"><h3>Notes du technicien <span class="rt">Éditable</span></h3>'
    +  '<textarea class="notes-area" id="jobNotes" placeholder="Observations, codes, contacts…">'+_esc(j.notes||'')+'</textarea>'
    +  '<button class="btn btn-pri btn-block mt-1" id="saveNotesBtn" onclick="saveNotes()">💾 Enregistrer les notes</button>'
    +'</div>'
    // Photos
    +'<div class="panel"><h3>Photos <span class="rt"><input type="file" id="photoInp" accept="image/*" capture="environment" style="display:none" onchange="onPhotoPick(this)"></span></h3>'
    +  '<div class="photo-grid" id="photoGrid"><div class="photo-add" onclick="$(\\'photoInp\\').click()">+</div></div>'
    +'</div>'
    // Equipment
    +'<div class="panel"><h3>Équipement installé</h3>'
    +  '<div id="dtEquipList"><div class="sr sm">Chargement…</div></div>'
    +  '<button class="add-eq-btn" onclick="openEquip(CJ,CC,CL)">📷 Ajouter / scanner un équipement</button>'
    +'</div>'
    // Action bar
    +(!done ? (
      '<div class="action-bar">'
      +(gps ? '<a href="'+gps+'" target="_blank" class="btn btn-ghost">📍 Naviguer</a>' : '')
      +(canStart  ? '<button class="btn btn-pri" onclick="doSt(\\''+j.name+'\\',\\'In Progress\\',this)">▶ Démarrer</button>' : '')
      +(canFinish ? '<button class="btn btn-ok"  onclick="doSt(\\''+j.name+'\\',\\'Completed\\',this)">✓ Terminer</button>' : '')
      +'</div>') : '');
}

// ── Notes ────────────────────────────────────────────────────────────────
function saveNotes(){
  var el=$('jobNotes'); if(!el) return;
  var b=$('saveNotesBtn'); b.disabled=true; b.textContent='Enregistrement…';
  api.post('/note', {job:CJ, notes:el.value}).then(function(r){
    var d=r.data||{};
    if(d.ok){ toast('Notes enregistrées ✓', true); if(JOBS[CJ]) JOBS[CJ].notes=el.value }
    else toast('Erreur: '+(d.error||''), false);
    b.disabled=false; b.textContent='💾 Enregistrer les notes';
  }).catch(function(){ toast('Erreur réseau', false); b.disabled=false; b.textContent='💾 Enregistrer les notes' });
}

// ── Photos ───────────────────────────────────────────────────────────────
function loadPhotos(){
  var el=$('photoGrid'); if(!el) return;
  api.get('/photos?job='+encodeURIComponent(CJ)).then(function(r){
    var d=r.data||{};
    var add='<div class="photo-add" onclick="$(\\'photoInp\\').click()">+</div>';
    if(!d.ok){ el.innerHTML=add; return }
    el.innerHTML = add + (d.items||[]).map(function(ph){
      var src = api.url('/photo-serve?p='+encodeURIComponent(ph.file_url));
      return '<div class="ph"><a href="'+src+'" target="_blank"><img src="'+src+'" loading="lazy"/></a></div>';
    }).join('');
  }).catch(function(){});
}
function onPhotoPick(input){
  var f=input.files[0]; if(!f) return; input.value='';
  var reader=new FileReader();
  reader.onload=function(e){
    toast('Envoi…', true);
    api.post('/photo', {job:CJ, image:e.target.result, file_name:f.name}).then(function(r){
      var d=r.data||{};
      if(d.ok){ toast('Photo ajoutée ✓', true); loadPhotos() }
      else toast('Erreur: '+(d.error||''), false);
    }).catch(function(){ toast('Erreur réseau', false) });
  };
  reader.readAsDataURL(f);
}

// ── Equipment overlay ────────────────────────────────────────────────────
function openEquip(job,cust,loc){
  CJ=job||CJ; CC=cust||CC; CL=loc||CL;
  $('eqTitle').textContent='📷 '+CJ;
  $('eqOv').classList.add('open');
  $('codeInp').value=''; $('scanRes').innerHTML='';
  hideForm(); startCam(); loadEquipList(); loadCatalog();
}
function closeEquip(){ var o=$('eqOv'); if(o) o.classList.remove('open'); stopCam() }

function startCam(){
  var v=$('vid');
  if(!navigator.mediaDevices||!navigator.mediaDevices.getUserMedia){ v.style.display='none'; return }
  navigator.mediaDevices.getUserMedia({video:{facingMode:'environment',width:{ideal:1280},height:{ideal:720}}})
  .then(function(s){ stream=s; v.srcObject=s; v.style.display='block';
    if('BarcodeDetector' in window){
      try{
        var bd=new BarcodeDetector({formats:['qr_code','code_128','ean_13','ean_8','code_39']});
        bdTimer=setInterval(function(){
          bd.detect(v).then(function(c){ if(c.length){ $('codeInp').value=c[0].rawValue; doScan() } }).catch(function(){});
        }, 500);
      }catch(e){}
    }
  }).catch(function(){ v.style.display='none' });
}
function stopCam(){
  if(bdTimer){ clearInterval(bdTimer); bdTimer=null }
  if(stream){ stream.getTracks().forEach(function(t){t.stop()}); stream=null }
  var v=$('vid'); if(v) v.srcObject=null;
}

function doScan(){
  var code=$('codeInp').value.trim(); if(!code) return;
  var r=$('scanRes'); r.innerHTML='<div class="sr sm">Recherche…</div>';
  api.post('/scan', {code:code}).then(function(x){
    var d=x.data||{};
    if(!d.ok){ r.innerHTML='<div class="sr" style="color:#ef4444">'+(d.error||'erreur')+'</div>'; return }
    if(!d.results.length){
      r.innerHTML='<div class="sr">Aucun résultat pour <b>'+_esc(code)+'</b>. <a href="#" onclick="showForm(&quot;'+_esc(code)+'&quot;);return false" style="color:#5c59a8;font-weight:600;margin-left:6px">Créer nouveau →</a></div>';
      $('afSn').value=code;
      return;
    }
    r.innerHTML=d.results.map(function(it){
      var sn=it.serial_number||it.serial_no||it.name;
      var nm=it.item_name||it.item_code||it.equipment_type||'';
      var st=it.status||'';
      return '<div class="sr"><b>'+_esc(sn)+'</b> '+_esc(nm)+(st?' <span class="bdg" style="background:#818cf820;color:#818cf8">'+_esc(st)+'</span>':'')+'</div>';
    }).join('');
  }).catch(function(){ r.innerHTML='<div class="sr" style="color:#ef4444">Erreur réseau</div>' });
}

function loadEquipList(){
  var targets=[['eqList',true],['dtEquipList',false]];
  api.get('/equip-list?job='+encodeURIComponent(CJ)).then(function(r){
    var d=r.data||{};
    for(var i=0;i<targets.length;i++){
      var el=$(targets[i][0]); if(!el) continue;
      if(!d.ok||!d.items.length){ el.innerHTML='<div class="sr sm">Aucun équipement installé</div>'; continue }
      el.innerHTML = d.items.map(function(it){
        var icon = it.action==='remove' ? '🔴' : '🟢';
        var rm   = (it.action!=='remove' && targets[i][1]) ? '<button class="eq-rm" onclick="removeEquip(&quot;'+_esc(it.name)+'&quot;,&quot;'+_esc(it.barcode||'')+'&quot;,&quot;'+_esc(it.equipment_type||'')+'&quot;)">Retirer</button>' : '';
        return '<div class="eq-item"><div class="eq-info"><div class="eq-sn">'+icon+' '+_esc(it.barcode||'—')+'</div><div class="eq-type">'+_esc([it.equipment_type,it.brand,it.model].filter(Boolean).join(' · '))+'</div>'+(it.notes?'<div class="eq-type" style="margin-top:4px;color:#475569">'+_esc(it.notes)+'</div>':'')+'</div>'+rm+'</div>';
      }).join('');
    }
  }).catch(function(){});
}

function loadCatalog(){
  var g=$('catGrid'); g.innerHTML='<div class="sr sm">Chargement…</div>';
  api.get('/catalog').then(function(r){
    var d=r.data||{};
    if(!d.ok||!d.items.length){ g.innerHTML='<div class="sr sm">Catalogue vide</div>'; return }
    g.innerHTML = d.items.map(function(it){
      return '<div class="cat-card" onclick="showFormCat(&quot;'+_esc(it.item_name||it.name)+'&quot;,&quot;'+_esc(it.name)+'&quot;,&quot;'+_esc(it.item_group||'')+'&quot;)"><div class="cn">'+_esc(it.item_name||it.name)+'</div><div class="cd">'+_esc(it.item_group||'')+'</div>'+(it.standard_rate?'<div class="cp">'+Number(it.standard_rate).toFixed(2)+'$</div>':'')+'</div>';
    }).join('');
  }).catch(function(){ g.innerHTML='<div class="sr sm">Erreur réseau</div>' });
}

function showForm(sn){
  var f=$('addForm'); f.style.display='block';
  $('afSn').value=sn||''; $('afBrand').value=''; $('afModel').value='';
  $('afMac').value=''; $('afGpon').value=''; $('afSsid').value=''; $('afPwd').value=''; $('afNotes').value='';
  $('afTitle').textContent='Nouvel équipement';
  f.scrollIntoView({behavior:'smooth', block:'start'});
}
function showFormCat(name,code,group){
  showForm($('codeInp').value||'');
  $('afTitle').textContent=name;
  CMODEL=code; CTYPE=group||'';
}
function hideForm(){ $('addForm').style.display='none' }

function submitEquip(){
  var b=$('afSubmit'); b.disabled=true; b.textContent='Enregistrement…';
  var data={
    job:CJ, customer:CC, location:CL,
    barcode:$('afSn').value.trim(),
    equipment_type:$('afType').value,
    brand:$('afBrand').value.trim(),
    model:$('afModel').value.trim(),
    mac_address:$('afMac').value.trim().replace(/[:\\-\\.\\s]/g,'').toUpperCase(),
    gpon_sn:$('afGpon').value.trim(),
    wifi_ssid:$('afSsid').value.trim(),
    wifi_password:$('afPwd').value.trim(),
    notes:$('afNotes').value.trim(),
    action:$('afAction').value,
  };
  api.post('/equip', data).then(function(r){
    var d=r.data||{};
    if(d.ok){ toast('Équipement ajouté ✓', true); hideForm(); loadEquipList() }
    else toast('Erreur: '+(d.error||''), false);
    b.disabled=false; b.textContent="Ajouter l'équipement";
  }).catch(function(){ toast('Erreur réseau', false); b.disabled=false; b.textContent="Ajouter l'équipement" });
}

function removeEquip(name,sn,type){
  if(!confirm('Retirer cet équipement ?')) return;
  api.post('/equip-remove', {equipment:name, serial:sn, equipment_type:type, job:CJ}).then(function(r){
    var d=r.data||{};
    if(d.ok){ toast('Retiré ✓', true); loadEquipList() }
    else toast('Erreur', false);
  }).catch(function(){ toast('Erreur réseau', false) });
}

(function(){ var ci=$('codeInp'); if(ci) ci.addEventListener('keydown', function(e){ if(e.key==='Enter') doScan() }) })();

// ── Field-targeted scan (Gemini, via scanner.js) ─────────────────────────
function scanInto(field, label, targetId){
  scanner.open(field, label, function(value){
    var t=$(targetId); if(t){ t.value=value; t.dispatchEvent(new Event('input')) }
    toast(label+': '+value, true);
  }, {
    equipment_type: $('afType') ? $('afType').value : '',
    brand:          $('afBrand') ? $('afBrand').value.trim() : '',
    model:          $('afModel') ? $('afModel').value.trim() : '',
  });
}
`

module.exports = { route }
