<script setup>
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
import { useAuthStore } from 'src/stores/auth'
import { useDispatchStore } from 'src/stores/dispatch'
import { TECH_COLORS, MAPBOX_TOKEN } from 'src/config/erpnext'
import { fetchOpenRequests, updateServiceRequestStatus } from 'src/api/service-request'

const auth  = useAuthStore()
const store = useDispatchStore()

// ── UI state ────────────────────────────────────────────────────────────────
const currentView      = ref(localStorage.getItem('dispatch-view') || 'schedule')
const sidebarCollapsed = ref(localStorage.getItem('dispatch-sidebar') === '1')

function toggleSidebar () {
  sidebarCollapsed.value = !sidebarCollapsed.value
  localStorage.setItem('dispatch-sidebar', sidebarCollapsed.value ? '1' : '0')
  nextTick(() => { _measureSchedule(); if (currentView.value === 'map') map?.resize() })
}
const currentTheme  = ref(localStorage.getItem('dispatch-theme') || 'dark')
const scheduleDate  = ref(new Date())
const modalTicket   = ref(null)
const tooltipTicket = ref(null)
const tooltipPos    = ref({ x: 0, y: 0 })
const tooltipSize   = ref({ w: 0, h: 0 })
let   tooltipTimer  = null

// ── Timeline scroll ref ───────────────────────────────────────────────────────
const schScroll = ref(null)

// ── Job labels A–Z (identifiants visuels cohérents carte ↔ timeline) ─────────
const jobLabelMap = computed(() => {
  const m = {}
  const alpha = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
  store.jobs.forEach((job, i) => { m[job.id] = i < 26 ? alpha[i] : String(i + 1) })
  return m
})

// ── Multi-sélection Ctrl+clic ─────────────────────────────────────────────────
const selectedJobIds = ref({})  // { [jobId]: true }
const selectedCount  = computed(() => Object.keys(selectedJobIds.value).length)
function isJobSelected (id) { return !!selectedJobIds.value[id] }
function clearSelection ()  { selectedJobIds.value = {} }
function handleJobClick (e, jobId) {
  e.stopPropagation()
  if (e.ctrlKey || e.metaKey) {
    // Ctrl/Cmd+clic → toggle dans la multi-sélection
    const s = { ...selectedJobIds.value }
    if (s[jobId]) delete s[jobId]; else s[jobId] = true
    selectedJobIds.value = s
  } else {
    // Clic simple → sélection unique (ou désélection si déjà seul sélectionné)
    const s = selectedJobIds.value
    selectedJobIds.value = (Object.keys(s).length === 1 && s[jobId]) ? {} : { [jobId]: true }
  }
}
async function transferSelected (techId) {
  const tech = store.technicians.find(t => t.id === techId)
  if (!tech) return
  pushHistory()
  for (const jobId of Object.keys(selectedJobIds.value)) {
    await store.assignJobToTech(jobId, techId, tech.queue.length)
  }
  await updateTechnicianRoute(tech)
  clearSelection()
}
async function removeSelected () {
  pushHistory()
  for (const jobId of Object.keys(selectedJobIds.value)) {
    const tech = store.technicians.find(t => t.queue.some(q => q.id === jobId))
    const job  = store.jobs.find(j => j.id === jobId)
    if (tech && job) await removeJobFromTech(tech, job)
  }
  clearSelection()
}

// ── Undo history (Ctrl+Z / Cmd+Z) ────────────────────────────────────────────
const MAX_HISTORY = 30
const undoStack = []

function snapState () {
  return {
    jobs:   store.jobs.map(j => ({ ...j })),
    queues: store.technicians.reduce((acc, t) => { acc[t.id] = t.queue.map(q => q.id); return acc }, {}),
  }
}

function pushHistory () {
  undoStack.push(snapState())
  if (undoStack.length > MAX_HISTORY) undoStack.shift()
}

async function undoLast () {
  const snap = undoStack.pop()
  if (!snap) return

  // Restore job fields
  snap.jobs.forEach(sj => {
    const live = store.jobs.find(j => j.id === sj.id)
    if (live) Object.assign(live, sj)
  })

  // Rebuild tech queues from stored ID order
  store.technicians.forEach(tech => {
    const ids = snap.queues[tech.id]
    if (ids !== undefined)
      tech.queue = ids.map(id => store.jobs.find(j => j.id === id)).filter(Boolean)
  })

  // Sync statuses to ERPNext (best-effort)
  for (const sj of snap.jobs) {
    store.setJobStatus(sj.id, sj.status).catch(() => {})
  }

  clearSelection()
  for (const tech of store.technicians) await updateTechnicianRoute(tech)
  drawAllMarkers()
}

// ── Rubber-band (lasso) selection on timeline ─────────────────────────────────
const lasso = ref(null)  // { x1, y1, x2, y2 } in sch-inner absolute coordinates

const lassoStyle = computed(() => {
  if (!lasso.value) return null
  const { x1, y1, x2, y2 } = lasso.value
  return {
    left:   Math.min(x1, x2) + 'px',
    top:    Math.min(y1, y2) + 'px',
    width:  Math.abs(x2 - x1) + 'px',
    height: Math.abs(y2 - y1) + 'px',
  }
})

function applyLassoSelection () {
  if (!lasso.value) return
  const { x1, y1, x2, y2 } = lasso.value
  const lx1 = Math.min(x1, x2), lx2 = Math.max(x1, x2)
  const ly1 = Math.min(y1, y2), ly2 = Math.max(y1, y2)
  if (lx2 - lx1 < 4 && ly2 - ly1 < 4) return   // ignore tiny clicks
  const HEADER_H = 46, ROW_H = 72, COL_W = 140
  const newSel = {}
  scheduleRows.value.forEach((row, i) => {
    const ry1 = HEADER_H + i * ROW_H
    const ry2 = ry1 + ROW_H
    if (ly2 < ry1 || ly1 > ry2) return
    row.blocks.forEach(block => {
      if (block.type !== 'job') return
      const bx1 = COL_W + block.left
      const bx2 = bx1 + block.width
      if (lx2 < bx1 || lx1 > bx2) return
      newSel[block.job.id] = true
    })
  })
  selectedJobIds.value = newSel
}

function _lassoMouseMove (e) {
  if (!lasso.value) return
  const outer = schScroll.value
  if (!outer) return
  const rect = outer.getBoundingClientRect()
  lasso.value = {
    ...lasso.value,
    x2: e.clientX - rect.left + outer.scrollLeft,
    y2: e.clientY - rect.top  + outer.scrollTop,
  }
  applyLassoSelection()
}

function _lassoMouseUp () {
  if (lasso.value) {
    const { x1, y1, x2, y2 } = lasso.value
    if (Math.abs(x2 - x1) < 4 && Math.abs(y2 - y1) < 4) {
      clearSelection()   // clic simple sur espace vide → désélectionner tout
    } else {
      applyLassoSelection()
    }
    lasso.value = null
  }
  document.removeEventListener('mousemove', _lassoMouseMove)
  document.removeEventListener('mouseup',   _lassoMouseUp)
}

function onSchMouseDown (e) {
  if (e.button !== 0) return
  if (e.target.closest('.sch-block') ||
      e.target.closest('.sch-travel-label') ||
      e.target.closest('.sch-tech-name') ||
      e.target.closest('.sch-nav-bar')) return
  const outer = schScroll.value
  if (!outer) return
  const rect = outer.getBoundingClientRect()
  const x = e.clientX - rect.left + outer.scrollLeft
  const y = e.clientY - rect.top  + outer.scrollTop
  lasso.value = { x1: x, y1: y, x2: x, y2: y }
  e.preventDefault()
  document.addEventListener('mousemove', _lassoMouseMove)
  document.addEventListener('mouseup',   _lassoMouseUp)
}

// ── Évaluations techniciens (localStorage) ───────────────────────────────────
const techRatings = ref(JSON.parse(localStorage.getItem('dispatch-tech-ratings') || '{}'))
// { [techId]: { score: 4.3, count: 17 } }
const ratingTech  = ref(null)   // techId dont le panneau de saisie est ouvert
const hoverRating = ref(0)      // étoile survolée pendant la saisie

function setTechRating (techId, score) {
  const r = techRatings.value[techId] || { score: 0, count: 0 }
  const count = r.count + 1
  const avg   = parseFloat(((r.score * r.count + score) / count).toFixed(1))
  techRatings.value = { ...techRatings.value, [techId]: { score: avg, count } }
  localStorage.setItem('dispatch-tech-ratings', JSON.stringify(techRatings.value))
  ratingTech.value = null
}

// ── Notifications techniciens ─────────────────────────────────────────────────
const notifyingTech = ref(null)
async function notifyTech (tech) {
  notifyingTech.value = tech.id
  const summary = tech.queue.map((j, i) => `${i + 1}. ${j.subject} (${j.duration}h)`).join(' | ')
  try {
    // Essaie n8n webhook si configuré
    await fetch('/api/method/frappe.client.get_value?doctype=Dispatch+Settings&fieldname=n8n_webhook_base', {
      credentials: 'include',
    }).then(async r => {
      const d = await r.json().catch(() => ({}))
      const base = d.message?.n8n_webhook_base
      if (base) {
        await fetch(`${base}/notify-tech`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ tech_name: tech.fullName, jobs: summary }),
        }).catch(() => {})
      }
    }).catch(() => {})
  } finally {
    notifyingTech.value = null
  }
}

// ── Mapbox ───────────────────────────────────────────────────────────────────
let map = null

function initMap () {
  const mapboxgl = window.mapboxgl
  if (!mapboxgl) return
  mapboxgl.accessToken = MAPBOX_TOKEN
  map = new mapboxgl.Map({
    container: 'map',
    style: 'mapbox://styles/mapbox/dark-v11',
    center: [-73.5673, 45.5017],
    zoom: 11,
  })
  map.addControl(new mapboxgl.NavigationControl(), 'bottom-right')
  map.on('load', () => {
    setupMapLayers()
    drawAllMarkers()
  })
}

function setupMapLayers () {
  if (!map) return
  const helmetSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>`
  const helmetUrl = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(helmetSvg)
  const img = new Image()
  img.onload = () => {
    if (!map.hasImage('tech-icon')) map.addImage('tech-icon', img)
  }
  img.src = helmetUrl

  map.addSource('tickets', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } })
  map.addSource('techs',   { type: 'geojson', data: { type: 'FeatureCollection', features: [] } })

  const SEL = ['==', ['get', 'selected'], true]
  map.addLayer({ id: 'tickets-glow',          type: 'circle', source: 'tickets', paint: { 'circle-radius': 16, 'circle-color': ['get', 'color'], 'circle-opacity': 0.22, 'circle-blur': 0.8 } })
  map.addLayer({ id: 'tickets-circle',         type: 'circle', source: 'tickets', paint: { 'circle-radius': 10, 'circle-color': ['get', 'color'], 'circle-stroke-width': 2, 'circle-stroke-color': 'white' } })
  map.addLayer({ id: 'tickets-sel-halo',       type: 'circle', source: 'tickets', filter: SEL, paint: { 'circle-radius': 22, 'circle-color': '#ffffff', 'circle-opacity': 0.25, 'circle-blur': 0.6 } })
  map.addLayer({ id: 'tickets-sel-ring',       type: 'circle', source: 'tickets', filter: SEL, paint: { 'circle-radius': 13, 'circle-color': ['get', 'color'], 'circle-stroke-width': 3, 'circle-stroke-color': '#ffffff' } })
  map.addLayer({ id: 'tickets-labels',         type: 'symbol', source: 'tickets', layout: { 'text-field': ['get', 'label'], 'text-size': 10, 'text-anchor': 'center', 'text-allow-overlap': true, 'text-ignore-placement': true, 'text-font': ['DIN Offc Pro Bold', 'Arial Unicode MS Bold'] }, paint: { 'text-color': 'white' } })
  map.addLayer({ id: 'techs-circle',   type: 'symbol', source: 'techs', layout: { 'icon-image': 'tech-icon', 'icon-size': 1, 'icon-allow-overlap': true } })

  ;['tickets-circle', 'techs-circle'].forEach(layerId => {
    map.on('click', layerId, e => {
      const feat = e.features[0].properties
      const coords = e.features[0].geometry.coordinates
      new window.mapboxgl.Popup().setLngLat(coords).setHTML(
        `<strong>${feat.id}</strong><br>${feat.subject || feat.name}<br><small>${feat.address || ''}</small>`
      ).addTo(map)
    })
    map.on('mouseenter', layerId, () => { map.getCanvas().style.cursor = 'pointer' })
    map.on('mouseleave', layerId, () => { map.getCanvas().style.cursor = '' })
  })
}

function jobIsInView (job) {
  // Unassigned jobs always visible
  const tech = store.technicians.find(t => t.queue.some(q => q.id === job.id))
  if (!tech) return true
  const due = getJobDueDate(tech.id, job.id)
  if (!due) return true  // undated → treat as today (always show)
  const viewStartStr = localDateStr(scheduleDate.value)
  if (zoomView.value === 'day') return due === viewStartStr
  const viewEnd = new Date(scheduleDate.value)
  viewEnd.setDate(viewEnd.getDate() + weekDays.value - 1)
  const viewEndStr = localDateStr(viewEnd)
  return due >= viewStartStr && due <= viewEndStr
}

function drawAllMarkers () {
  if (!map || !map.getSource('tickets')) return

  const ticketFeatures = store.jobs.filter(jobIsInView).map(t => {
    const color = jobColor(t)
    return {
      type: 'Feature',
      geometry: { type: 'Point', coordinates: t.coords },
      properties: { id: t.id, subject: t.subject, address: t.address, color, assigned: !!t.assignedTech, label: jobLabelMap.value[t.id] || '', selected: !!selectedJobIds.value[t.id] },
    }
  }).filter(f => f.geometry.coordinates[0] !== 0)

  const techFeatures = store.technicians.map(tech => ({
    type: 'Feature',
    geometry: { type: 'Point', coordinates: tech.coords },
    properties: { id: tech.id, name: tech.fullName },
  })).filter(f => f.geometry.coordinates[0] !== 0)

  map.getSource('tickets').setData({ type: 'FeatureCollection', features: ticketFeatures })
  map.getSource('techs').setData({ type: 'FeatureCollection', features: techFeatures })
}

async function geocodeAddress (address) {
  if (!address) return null
  try {
    const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(address)}.json?limit=1&access_token=${MAPBOX_TOKEN}`
    const res  = await fetch(url)
    const data = await res.json()
    const feat = data.features?.[0]
    if (feat) return feat.geometry.coordinates  // [lng, lat]
  } catch (_) {}
  return null
}

function isValidCoords (c) {
  return Array.isArray(c) && c.length === 2 && (c[0] !== 0 || c[1] !== 0)
}

async function updateTechnicianRoute (tech) {
  if (!map) return
  const sourceId = `route-${tech.id}`
  const layerId  = `route-layer-${tech.id}`
  if (map.getLayer(layerId + '-outline')) map.removeLayer(layerId + '-outline')
  if (map.getLayer(layerId))              map.removeLayer(layerId)
  if (map.getSource(sourceId))           map.removeSource(sourceId)
  tech.queue.forEach(t => { t.legDist = null; t.legDur = null })
  // Ne tracer la route que pour le tech sélectionné
  if (selectedTechId.value && tech.id !== selectedTechId.value) { drawAllMarkers(); return }
  if (tech.queue.length === 0) { drawAllMarkers(); return }

  // Geocode any job that still has [0,0] coords
  for (const job of tech.queue) {
    if (!isValidCoords(job.coords) && job.address) {
      const c = await geocodeAddress(job.address)
      if (c) job.coords = c
    }
  }

  // Only route jobs with valid coords; record which queue indices are valid
  const validJobs = tech.queue.filter(j => isValidCoords(j.coords))
  if (!isValidCoords(tech.coords) || validJobs.length === 0) { drawAllMarkers(); return }

  const waypoints = [tech.coords, ...validJobs.map(t => t.coords)]
  const coordsStr = waypoints.map(c => c.join(',')).join(';')
  try {
    const res = await fetch(
      `https://api.mapbox.com/directions/v5/mapbox/driving/${coordsStr}?geometries=geojson&overview=full&steps=false&access_token=${MAPBOX_TOKEN}`
    )
    const data = await res.json()
    if (!data.routes?.[0]) { drawAllMarkers(); return }
    const route = data.routes[0]
    route.legs.forEach((leg, i) => {
      if (validJobs[i]) {
        validJobs[i].legDist = (leg.distance / 1000).toFixed(1)
        validJobs[i].legDur  = Math.round(leg.duration / 60)
      }
    })
    const color = TECH_COLORS[tech.colorIdx]
    const beforeId = map.getLayer('tickets-glow') ? 'tickets-glow' : undefined
    map.addSource(sourceId, { type: 'geojson', data: { type: 'Feature', geometry: route.geometry } })
    map.addLayer({ id: layerId + '-outline', type: 'line', source: sourceId, layout: { 'line-join': 'round', 'line-cap': 'round' }, paint: { 'line-color': color, 'line-width': 8, 'line-opacity': 0.25 } }, beforeId)
    map.addLayer({ id: layerId,              type: 'line', source: sourceId, layout: { 'line-join': 'round', 'line-cap': 'round' }, paint: { 'line-color': color, 'line-width': 3.5, 'line-opacity': 0.9 } }, beforeId)
  } catch (e) {
    console.error('Route error:', e)
  }
  drawAllMarkers()
}

// ── Theme ────────────────────────────────────────────────────────────────────
function applyTheme (theme) {
  document.body.classList.toggle('light-mode', theme === 'light')
  localStorage.setItem('dispatch-theme', theme)
}
function toggleTheme () {
  currentTheme.value = currentTheme.value === 'dark' ? 'light' : 'dark'
  applyTheme(currentTheme.value)
}

// ── View toggle ──────────────────────────────────────────────────────────────
function switchView (view) {
  currentView.value = view
  localStorage.setItem('dispatch-view', view)
  if (view === 'map') nextTick(() => map?.resize())
  if (view === 'schedule') nextTick(() => { _measureSchedule(); scrollToShiftStart() })
}

// Double-clic sur un jour de la vue semaine → zoom en vue jour sur cette date
function zoomToDay (date) {
  if (!date) return
  scheduleDate.value = new Date(date)
  zoomView.value = 'day'
  localStorage.setItem('dispatch-zoom', 'day')
  nextTick(() => { _measureSchedule(); scrollToShiftStart() })
}

function scrollToShiftStart () {
  nextTick(() => {
    const el = schScroll.value
    if (!el) return
    // Vue semaine : tout tient à l'écran, pas de scroll horizontal
    el.scrollLeft = zoomView.value === 'week' ? 0 : Math.max(0, SHIFT_START * pxPerMin.value - 40)
  })
}

// ── Assignation des jobs à un jour — stockage par date absolue ISO ────────────
// clé : 'techId||jobId'  →  valeur : 'YYYY-MM-DD' (date absolue)
const jobDueDates = ref(JSON.parse(localStorage.getItem('dispatch-job-dates') || '{}'))
function _dayKey (techId, jobId) { return techId + '||' + jobId }

function getJobDueDate (techId, jobId) {
  return jobDueDates.value[_dayKey(techId, jobId)] || null
}

// Retourne l'offset (0..weekDays-1) par rapport à scheduleDate pour la vue semaine
function getJobDay (techId, jobId) {
  const due = getJobDueDate(techId, jobId)
  if (!due) return 0
  const viewStart = new Date(scheduleDate.value); viewStart.setHours(0, 0, 0, 0)
  const d = new Date(due + 'T00:00:00')
  const diff = Math.round((d - viewStart) / 86400000)
  return Math.max(0, Math.min(diff, (weekDays.value || 5) - 1))
}

async function saveScheduledDate (jobId, dateStr) {
  const csrf = document.cookie.match(/csrftoken=([^;]+)/)?.[1] || ''
  try {
    await fetch(`/api/resource/Issue/${encodeURIComponent(jobId)}`, {
      method: 'PUT', credentials: 'include',
      headers: { 'Content-Type': 'application/json', 'X-Frappe-CSRF-Token': csrf },
      body: JSON.stringify({ expected_resolution_date: dateStr }),
    })
  } catch (_) {}
}

function localDateStr (d) {
  // Format YYYY-MM-DD using LOCAL date parts to avoid UTC offset shifting the date
  const yr = d.getFullYear()
  const mo = String(d.getMonth() + 1).padStart(2, '0')
  const da = String(d.getDate()).padStart(2, '0')
  return `${yr}-${mo}-${da}`
}

function setJobDay (techId, jobId, day) {
  const d = new Date(scheduleDate.value)
  d.setHours(0, 0, 0, 0)   // normalise à minuit local avant d'ajouter des jours
  d.setDate(d.getDate() + day)
  const dateStr = localDateStr(d)
  jobDueDates.value = { ...jobDueDates.value, [_dayKey(techId, jobId)]: dateStr }
  localStorage.setItem('dispatch-job-dates', JSON.stringify(jobDueDates.value))
  saveScheduledDate(jobId, dateStr)
}

// Migre les anciennes données offset (dispatch-job-days) vers dates absolues (dispatch-job-dates)
// Les jobs sans date restent sans date (s'affichent sur tous les jours — comportement voulu)
function migrateJobDays () {
  const oldData = JSON.parse(localStorage.getItem('dispatch-job-days') || '{}')
  if (Object.keys(oldData).length === 0) return
  // Ne migre que si le nouveau stockage est vide pour ne pas écraser des données existantes
  if (Object.keys(jobDueDates.value).length === 0) {
    const base = new Date(scheduleDate.value)
    base.setHours(0, 0, 0, 0)
    const newDates = {}
    for (const [key, offset] of Object.entries(oldData)) {
      const d = new Date(base)
      d.setDate(d.getDate() + (offset || 0))
      newDates[key] = localDateStr(d)
    }
    jobDueDates.value = newDates
    localStorage.setItem('dispatch-job-dates', JSON.stringify(newDates))
  }
  localStorage.removeItem('dispatch-job-days')
}

// ── Demandes en attente (couche translucide sur le planning) ─────────────────
const SLOT_RANGES = {
  morning:   { start: 8 * 60,  end: 12 * 60 },
  afternoon: { start: 12 * 60, end: 17 * 60 },
  evening:   { start: 17 * 60, end: 20 * 60 },
  flexible:  { start: 8 * 60,  end: 20 * 60 },
}
const SVC_ICONS = { internet: '🌐', tv: '📺', telephone: '📞', multi: '🔧' }

const showPendingLayer = ref(localStorage.getItem('dispatch-pending-layer') === '1')
const pendingReqs      = ref([])
const pendingModal     = ref(null)   // demande ouverte dans le modal d'acceptation
const pendingLoading   = ref(false)
const pendingAssignTech = ref('')    // techId sélectionné dans le modal

// ── Field Dispatch UI state ───────────────────────────────────────────────────
const selectedTechId = ref(null)
const woSectionOpen   = ref(true)
const techSectionOpen = ref(true)
const woTab           = ref('unassigned')
const leftPanelW      = ref(parseInt(localStorage.getItem('fd-left-w') || '260'))
const mapPanelW       = ref(parseInt(localStorage.getItem('fd-map-w')  || '320'))
const leftPanelOpen   = ref(true)
const mapPanelOpen    = ref(true)

// Couleurs par type de service (pins sur la carte + légende)
const SVC_COLORS = {
  'Internet':       '#3b82f6',
  'Télévisión':     '#a855f7',
  'Téléphonie':     '#10b981',
  'Multi-service':  '#f59e0b',
}
function jobColor (job) {
  const svc = job.service_type || ''
  if (SVC_COLORS[svc]) return SVC_COLORS[svc]
  const s = (job.subject || '').toLowerCase()
  if (s.includes('internet'))                  return '#3b82f6'
  if (s.includes('tv') || s.includes('télév')) return '#a855f7'
  if (s.includes('téléph'))                    return '#10b981'
  if (s.includes('multi'))                     return '#f59e0b'
  if (job.assignedTech) {
    const t = store.technicians.find(x => x.id === job.assignedTech)
    if (t) return TECH_COLORS[t.colorIdx]
  }
  return '#6b7280'
}

// Sélectionner un tech → affiche uniquement sa route sur la carte
function selectTech (techId) {
  selectedTechId.value = (selectedTechId.value === techId) ? null : techId
  store.technicians.forEach(t => {
    const lid = `route-layer-${t.id}`
    if (map?.getLayer(lid + '-outline')) map.removeLayer(lid + '-outline')
    if (map?.getLayer(lid))              map.removeLayer(lid)
    if (map?.getSource(`route-${t.id}`)) map.removeSource(`route-${t.id}`)
    t.queue.forEach(j => { j.legDist = null; j.legDur = null })
  })
  if (selectedTechId.value) {
    const t = store.technicians.find(x => x.id === selectedTechId.value)
    if (t) updateTechnicianRoute(t)
  }
  drawAllMarkers()
}

function startResizeLeft (e) {
  e.preventDefault()
  const startX = e.clientX; const startW = leftPanelW.value
  const move = ev => { leftPanelW.value = Math.max(180, Math.min(500, startW + ev.clientX - startX)); localStorage.setItem('fd-left-w', String(leftPanelW.value)) }
  const up   = () => { document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up) }
  document.addEventListener('mousemove', move); document.addEventListener('mouseup', up)
}
function startResizeMap (e) {
  e.preventDefault()
  const startX = e.clientX; const startW = mapPanelW.value
  const move = ev => { mapPanelW.value = Math.max(200, Math.min(600, startW - (ev.clientX - startX))); localStorage.setItem('fd-map-w', String(mapPanelW.value)) }
  const up   = () => { document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up) }
  document.addEventListener('mousemove', move); document.addEventListener('mouseup', up)
}

// Statut techniciens (pour resource list + scheduleRows)
const stMap = {
  'available':   { cls: 'status-on-shift',   label: 'Disponible' },
  'en-route':    { cls: 'status-enroute',     label: 'En route'   },
  'busy':        { cls: 'status-in-progress', label: 'En cours'   },
  'in progress': { cls: 'status-in-progress', label: 'En cours'   },
  'off':         { cls: 'status-off',         label: 'Hors shift' },
}

async function loadPendingReqs () {
  pendingLoading.value = true
  try { pendingReqs.value = await fetchOpenRequests() } catch (_) { pendingReqs.value = [] }
  pendingLoading.value = false
}

function togglePendingLayer () {
  showPendingLayer.value = !showPendingLayer.value
  localStorage.setItem('dispatch-pending-layer', showPendingLayer.value ? '1' : '0')
  if (showPendingLayer.value && pendingReqs.value.length === 0) loadPendingReqs()
}

// Retourne la liste des dates préférées d'une demande (format plat ou tableau)
function reqDates (req) {
  if (Array.isArray(req.preferred_dates) && req.preferred_dates.length > 0)
    return req.preferred_dates.filter(d => d.date).map((d, i) => ({
      date: d.date,
      slots: Array.isArray(d.time_slots) && d.time_slots.length ? d.time_slots : [d.time_slot || 'flexible'],
      priority: i + 1,
    }))
  const out = []
  for (let i = 1; i <= 3; i++) {
    const d = req[`preferred_date_${i}`]
    const s = req[`time_slot_${i}`]
    if (d) out.push({ date: d, slots: [s || 'flexible'], priority: i })
  }
  return out
}

// Blocs positionnés sur le planning pour la vue courante
const pendingBlocks = computed(() => {
  if (!showPendingLayer.value) return []
  const blocks = []
  const isWeek = zoomView.value === 'week'
  const viewStart = new Date(scheduleDate.value)
  viewStart.setHours(0, 0, 0, 0)

  pendingReqs.value.forEach((req, ri) => {
    const dates = reqDates(req)
    dates.forEach(({ date, slots, priority }) => {
      const d = new Date(date + 'T00:00:00')
      d.setHours(0, 0, 0, 0)
      let dayOffset = null
      if (isWeek) {
        const diff = Math.round((d - viewStart) / 86400000)
        if (diff >= 0 && diff < weekDays.value) dayOffset = diff
      } else {
        if (d.toDateString() === viewStart.toDateString()) dayOffset = 0
      }
      if (dayOffset === null) return

      slots.forEach(slot => {
        const range = SLOT_RANGES[slot] || SLOT_RANGES.flexible
        const dayBaseMin = isWeek ? dayOffset * 24 * 60 : 0
        const left  = (dayBaseMin + range.start) * pxPerMin.value
        const width = Math.max((range.end - range.start) * pxPerMin.value, 4)
        blocks.push({
          key: `pr-${req.name || ri}-${date}-${slot}`,
          left, width,
          req,
          slotLabel: { morning: 'Matin', afternoon: 'Après-midi', evening: 'Soir', flexible: 'Flexible' }[slot] || slot,
          priority,
          dateLabel: new Date(date + 'T12:00:00').toLocaleDateString('fr-CA', { weekday: 'short', day: 'numeric', month: 'short' }),
          icon: SVC_ICONS[req.service_type] || '🔧',
        })
      })
    })
  })
  return blocks
})

// Drag depuis un bloc en attente → nouveau type 'pending'
function onPendingDragStart (e, req) {
  dragState.value = { type: 'pending', req }
  e.dataTransfer.effectAllowed = 'copy'
  e.stopPropagation()
}

// Accepter et assigner la demande à un tech (depuis modal ou drop)
async function acceptPendingReq (req, techId) {
  const tech = store.technicians.find(t => t.id === techId)
  if (!tech) return
  const reqId = req.name || req.ref || ('SR-' + Date.now().toString(36).toUpperCase())

  // Résoudre les coordonnées GPS : utiliser celles stockées ou géocoder l'adresse
  const address = req.address || ''
  let coords = [0, 0]
  if (isValidCoords(req.coordinates)) {
    coords = req.coordinates
  } else if (req.lng && req.lat) {
    coords = [req.lng, req.lat]
  } else if (address) {
    coords = (await geocodeAddress(address)) || [0, 0]
  }

  // Créer un job local (fonctionne sans Frappe)
  const job = {
    id:          reqId,
    subject:     `${SVC_ICONS[req.service_type] || '🔧'} ${req.problem_type || req.service_type}`,
    address,
    duration:    '2',
    priority:    req.urgency === 'urgent' ? 'high' : 'medium',
    status:      'open',
    assignedTech: null,
    coords,
  }
  if (!store.jobs.find(j => j.id === job.id)) store.jobs.push(job)
  pushHistory()
  await store.assignJobToTech(job.id, tech.id, tech.queue.length)

  // Auto-placer sur le bon jour selon la 1re date préférée du client
  const dates = reqDates(req)
  let confirmedDateStr = localDateStr(new Date())
  if (dates.length > 0) {
    const prefDate = new Date(dates[0].date + 'T00:00:00')
    prefDate.setHours(0, 0, 0, 0)
    const viewStart = new Date(scheduleDate.value)
    viewStart.setHours(0, 0, 0, 0)
    const diff = Math.round((prefDate - viewStart) / 86400000)
    const day = (diff >= 0 && diff < weekDays.value) ? diff : 0
    setJobDay(tech.id, job.id, day)
    confirmedDateStr = dates[0].date
  } else {
    // Pas de date préférée → assigne aujourd'hui
    setJobDay(tech.id, job.id, 0)
  }

  await updateTechnicianRoute(tech)

  // Persister le statut Confirmed dans ERPNext (empêche la réapparition dans les demandes)
  try {
    await updateServiceRequestStatus(reqId, 'Confirmed', confirmedDateStr)
  } catch (_) {}

  // Sauvegarder les dates non retenues en note/commentaire sur le billet ERPNext
  if (dates.length > 1) {
    const unaccepted = dates.slice(1)
      .map(d => `• ${d.date}${d.slot ? ' — ' + d.slot : ''}`)
      .join('\n')
    const noteContent = `Dates proposées non retenues :\n${unaccepted}`
    const csrf = document.cookie.match(/csrftoken=([^;]+)/)?.[1] || ''
    try {
      await fetch('/api/resource/Comment', {
        method: 'POST', credentials: 'include',
        headers: { 'Content-Type': 'application/json', 'X-Frappe-CSRF-Token': csrf },
        body: JSON.stringify({
          comment_type: 'Comment',
          reference_doctype: 'Service Request',
          reference_name: reqId,
          content: noteContent,
        }),
      })
    } catch (_) {}
  }

  // Retirer de la liste (supporte name Frappe ET ref localStorage)
  const uid = req.name || req.ref
  pendingReqs.value = pendingReqs.value.filter(r => (r.name || r.ref) !== uid)
  pendingModal.value = null
  pendingAssignTech.value = ''
}

// ── Schedule (Gantt) ─────────────────────────────────────────────────────────
const SHIFT_START = 8 * 60  // 08:00

const ZOOM_CONFIG = {
  day:  { pxPerMin: 2,     totalMins: 24 * 60,     tickMajor: 60,      tickMinor: 30    },
  week: { pxPerMin: 2 / 5, totalMins: 5 * 24 * 60, tickMajor: 24 * 60, tickMinor: 6 * 60 },
}
const zoomView  = ref(localStorage.getItem('dispatch-zoom') || 'day')
// Nombre de jours affichés : 5 (lun–ven) ou 7 (lun–dim) si volet fermé
const weekDays  = computed(() => (zoomView.value === 'week' && sidebarCollapsed.value) ? 7 : 5)

// En vue semaine, pxPerMin s'adapte pour afficher weekDays sans scroll horizontal
const scheduleW = ref(0)
let _resizeObs  = null
function _measureSchedule () {
  if (schScroll.value) scheduleW.value = schScroll.value.clientWidth
}

const pxPerMin  = computed(() => {
  if (zoomView.value === 'week' && scheduleW.value > 140)
    return (scheduleW.value - 140) / (weekDays.value * 24 * 60)
  return ZOOM_CONFIG[zoomView.value].pxPerMin
})
const totalMins = computed(() => zoomView.value === 'week' ? weekDays.value * 24 * 60 : ZOOM_CONFIG.day.totalMins)
const totalW    = computed(() => totalMins.value * pxPerMin.value)

// Snap a date to the Monday of its week
function toMonday (date) {
  const d = new Date(date)
  const diff = (d.getDay() + 6) % 7   // 0=Mon … 6=Sun
  d.setDate(d.getDate() - diff)
  return d
}

const scheduleDateLabel = computed(() => {
  if (zoomView.value === 'week') {
    const end = new Date(scheduleDate.value)
    end.setDate(end.getDate() + weekDays.value - 1)
    return scheduleDate.value.toLocaleDateString('fr-FR', { weekday: 'short', day: 'numeric', month: 'short' })
      + ' – ' + end.toLocaleDateString('fr-FR', { weekday: 'short', day: 'numeric', month: 'short', year: 'numeric' })
  }
  return scheduleDate.value.toLocaleDateString('fr-FR', { weekday: 'short', day: 'numeric', month: 'short', year: 'numeric' })
})

function prevDay () {
  const d = new Date(scheduleDate.value)
  d.setDate(d.getDate() - (zoomView.value === 'week' ? 7 : 1))
  scheduleDate.value = d
}
function nextDay () {
  const d = new Date(scheduleDate.value)
  d.setDate(d.getDate() + (zoomView.value === 'week' ? 7 : 1))
  scheduleDate.value = d
}

const rulerTicks = computed(() => {
  const cfg = ZOOM_CONFIG[zoomView.value]
  const now = new Date()
  const isToday = scheduleDate.value.toDateString() === now.toDateString()
  const ticks = []
  for (let min = 0; min <= totalMins.value; min += cfg.tickMinor) {
    const x = min * pxPerMin.value
    const isMajor = min % cfg.tickMajor === 0
    let label = null
    let isCurrent = false
    if (zoomView.value === 'week') {
      if (isMajor) {
        const d = new Date(scheduleDate.value)
        d.setDate(d.getDate() + min / cfg.tickMajor)
        label = d.toLocaleDateString('fr-FR', { weekday: 'short', day: 'numeric' })
        isCurrent = d.toDateString() === now.toDateString()
        ticks.push({ x, isMajor, label, isCurrent, date: new Date(d) })
        continue
      }
    } else {
      const h = Math.floor(min / 60)
      if (isMajor) {
        label = String(h).padStart(2, '0') + 'h00'
        isCurrent = isToday && h === now.getHours()
      }
    }
    ticks.push({ x, isMajor, label, isCurrent })
  }
  return ticks
})

const nowLineX = computed(() => {
  const now = new Date()
  if (zoomView.value === 'day') {
    if (scheduleDate.value.toDateString() !== now.toDateString()) return -1
    return (now.getHours() * 60 + now.getMinutes()) * pxPerMin.value
  }
  // week view: position within the N-day window starting at scheduleDate
  const start = new Date(scheduleDate.value)
  start.setHours(0, 0, 0, 0)
  const end = new Date(start)
  end.setDate(end.getDate() + weekDays.value)
  if (now < start || now >= end) return -1
  return ((now - start) / 60000) * pxPerMin.value
})

const scheduleRows = computed(() => {
  return store.technicians.map(tech => {
    const color = TECH_COLORS[tech.colorIdx]
    const st = stMap[(tech.status || '').toLowerCase()] || { cls: 'status-on-shift', label: tech.status || '—' }
    const totalLoad = tech.queue.reduce((a, t) => a + (parseFloat(t.duration) || 0) * 60 + (parseInt(t.legDur) || 0), 0)
    const blocks = []

    if (zoomView.value === 'week') {
      // Regroupe les jobs par jour assigné (0–4), empile depuis 8h00 de chaque jour
      const byDay = {}
      tech.queue.forEach((job, idx) => {
        const d = getJobDay(tech.id, job.id)
        ;(byDay[d] = byDay[d] || []).push({ job, idx })
      })
      for (let dayNum = 0; dayNum < weekDays.value; dayNum++) {
        let currentMin = dayNum * 24 * 60 + SHIFT_START
        // Calcul de la date calendrier du jour
        const dayDate = new Date(scheduleDate.value)
        dayDate.setDate(dayDate.getDate() + dayNum)
        const dayLabel = dayDate.toLocaleDateString('fr-CA', { weekday: 'short', day: 'numeric', month: 'short' })
        const dueDateStr = localDateStr(dayDate)
        for (const { job, idx } of (byDay[dayNum] || [])) {
          const driveMin = parseInt(job.legDur) || 0
          const workMin  = Math.max((parseFloat(job.duration) || 1) * 60, 5)
          if (driveMin > 0) {
            blocks.push({ key: `${job.id}-drive`, type: 'travel', left: currentMin * pxPerMin.value, width: Math.max(driveMin * pxPerMin.value, 4), driveMin })
            currentMin += driveMin
          }
          const sH = Math.floor(currentMin / 60) % 24, sM = currentMin % 60
          const eMin = currentMin + workMin
          const eH = Math.floor(eMin / 60) % 24, eM = eMin % 60
          blocks.push({
            key: `${job.id}-job`, type: 'job',
            left: currentMin * pxPerMin.value, width: Math.max(workMin * pxPerMin.value, 20),
            color, job, idx,
            dueDate: dueDateStr,
            timeRange: `${dayLabel}`,
            timeHours: `${String(sH).padStart(2,'0')}h${String(sM).padStart(2,'0')}–${String(eH).padStart(2,'0')}h${String(eM).padStart(2,'0')}`,
          })
          currentMin += workMin
        }
      }
    } else {
      // Vue jour : n'afficher que les jobs prévues pour la date sélectionnée
      const todayStr = localDateStr(scheduleDate.value)
      let currentMin = SHIFT_START
      tech.queue.forEach((job, idx) => {
        const due = getJobDueDate(tech.id, job.id)
        // Pas de date assignée → visible sur tous les jours (non planifié)
        if (due && due !== todayStr) return
        const driveMin = parseInt(job.legDur) || 0
        const workMin  = Math.max((parseFloat(job.duration) || 1) * 60, 5)
        if (driveMin > 0) {
          blocks.push({ key: `${job.id}-drive`, type: 'travel', left: currentMin * pxPerMin.value, width: Math.max(driveMin * pxPerMin.value, 4), driveMin })
          currentMin += driveMin
        }
        const sH = Math.floor(currentMin / 60), sM = currentMin % 60
        const eMin = currentMin + workMin
        const eH = Math.floor(eMin / 60), eM = eMin % 60
        const timeHours = `${String(sH).padStart(2,'0')}h${String(sM).padStart(2,'0')}–${String(eH).padStart(2,'0')}h${String(eM).padStart(2,'0')}`
        blocks.push({
          key: `${job.id}-job`, type: 'job',
          left: currentMin * pxPerMin.value, width: Math.max(workMin * pxPerMin.value, 20),
          color, job, idx,
          dueDate: todayStr,
          timeRange: timeHours,
          timeHours,
        })
        currentMin += workMin
      })
    }

    return { tech, color, st, loadH: (totalLoad / 60).toFixed(1), blocks }
  })
})

// ── Drag & drop ──────────────────────────────────────────────────────────────
const dragState  = ref(null)  // { type: 'ticket'|'sort', jobId?, techId?, fromIdx? }
const dropTarget = ref(null)  // { techId, insertIdx } — drives the visual indicator

function onTicketDragStart (e, job) {
  if (job.status !== 'open') { e.preventDefault(); return }
  dragState.value = { type: 'ticket', jobId: job.id }
  e.dataTransfer.effectAllowed = 'move'
}

function onJobDragStart (e, tech, idx) {
  dragState.value = { type: 'sort', techId: tech.id, fromIdx: idx }
  e.dataTransfer.effectAllowed = 'move'
  e.stopPropagation()
}

function onDragEnd () {
  dragState.value = null
  dropTarget.value = null
}

// Calculate where to insert based on mouse Y vs item midpoints
function calcInsertIdx (zoneEl, clientY, queueLen) {
  const items = zoneEl.querySelectorAll('.job-item')
  for (let i = 0; i < items.length; i++) {
    const rect = items[i].getBoundingClientRect()
    if (clientY < rect.top + rect.height / 2) return i
  }
  return queueLen
}

function onDragOverZone (e, tech) {
  e.preventDefault()
  e.dataTransfer.dropEffect = 'move'
  const insertIdx = calcInsertIdx(e.currentTarget, e.clientY, tech.queue.length)
  dropTarget.value = { techId: tech.id, insertIdx }
}

function onDragLeaveZone (e, tech) {
  // Only clear when pointer truly leaves the zone (not entering a child element)
  if (!e.currentTarget.contains(e.relatedTarget)) {
    if (dropTarget.value?.techId === tech.id) dropTarget.value = null
  }
}

async function onDropOnZone (e, tech) {
  e.preventDefault()
  if (!dragState.value) return
  const ds = dragState.value
  const insertIdx = dropTarget.value?.techId === tech.id
    ? dropTarget.value.insertIdx
    : tech.queue.length
  dragState.value = null
  dropTarget.value = null
  pushHistory()

  if (ds.type === 'ticket') {
    await store.assignJobToTech(ds.jobId, tech.id, insertIdx)
    await updateTechnicianRoute(tech)
  } else if (ds.type === 'sort') {
    if (ds.techId === tech.id) {
      await store.reorderTechQueue(tech.id, ds.fromIdx, insertIdx)
      await updateTechnicianRoute(tech)
    } else {
      // Cross-tech: move job from one tech's queue to another
      const srcTech = store.technicians.find(t => t.id === ds.techId)
      const job = srcTech?.queue[ds.fromIdx]
      if (job) {
        await store.assignJobToTech(job.id, tech.id, insertIdx)
        if (srcTech) await updateTechnicianRoute(srcTech)
        await updateTechnicianRoute(tech)
      }
    }
  }
}

// ── Timeline (schedule) drag & drop ──────────────────────────────────────────
const schDropTarget = ref(null) // { techId, insertIdx, xPos }

function onSchBlockDragStart (e, row, block) {
  dragState.value = { type: 'sort', techId: row.tech.id, fromIdx: block.idx, jobId: block.job.id }
  e.dataTransfer.effectAllowed = 'move'
  e.stopPropagation()
}

function onSchDragOverRow (e, row) {
  e.preventDefault()
  e.dataTransfer.dropEffect = 'move'
  const timelineEl = e.currentTarget.querySelector('.sch-timeline')
  if (!timelineEl) return
  const rect = timelineEl.getBoundingClientRect()
  const x = e.clientX - rect.left

  if (zoomView.value === 'week') {
    // Snap to day column
    const dayW    = 24 * 60 * pxPerMin.value
    const snapDay = Math.max(0, Math.min(weekDays.value - 1, Math.floor(x / dayW)))
    schDropTarget.value = { techId: row.tech.id, insertIdx: -1, snapDay, xPos: snapDay * dayW, snapWidth: dayW }
    return
  }

  const jobBlocks = row.blocks.filter(b => b.type === 'job')
  let insertIdx = jobBlocks.length
  for (let i = 0; i < jobBlocks.length; i++) {
    if (x < jobBlocks[i].left + jobBlocks[i].width / 2) { insertIdx = i; break }
  }
  let xPos
  if (jobBlocks.length === 0) {
    xPos = SHIFT_START * pxPerMin.value
  } else if (insertIdx === 0) {
    xPos = jobBlocks[0].left - 4
  } else if (insertIdx >= jobBlocks.length) {
    const last = jobBlocks[jobBlocks.length - 1]
    xPos = last.left + last.width + 4
  } else {
    xPos = jobBlocks[insertIdx - 1].left + jobBlocks[insertIdx - 1].width + 4
  }
  schDropTarget.value = { techId: row.tech.id, insertIdx, xPos }
}

function onSchDragLeaveRow (e, row) {
  if (!e.currentTarget.contains(e.relatedTarget)) {
    if (schDropTarget.value?.techId === row.tech.id) schDropTarget.value = null
  }
}

async function onSchDropRow (e, row) {
  e.preventDefault()
  if (!dragState.value) return
  const ds       = dragState.value
  const isWeek   = zoomView.value === 'week'
  const snapDay  = schDropTarget.value?.snapDay ?? 0
  const insertIdx = (!isWeek && schDropTarget.value?.techId === row.tech.id)
    ? schDropTarget.value.insertIdx
    : row.tech.queue.length
  dragState.value  = null
  schDropTarget.value = null
  pushHistory()

  if (ds.type === 'pending') {
    await acceptPendingReq(ds.req, row.tech.id)
    return
  } else if (ds.type === 'ticket') {
    await store.assignJobToTech(ds.jobId, row.tech.id, insertIdx)
    if (isWeek) setJobDay(row.tech.id, ds.jobId, snapDay)
    await updateTechnicianRoute(row.tech)
  } else if (ds.type === 'sort') {
    if (ds.techId === row.tech.id) {
      if (!isWeek) await store.reorderTechQueue(row.tech.id, ds.fromIdx, insertIdx)
      if (isWeek && ds.jobId) setJobDay(row.tech.id, ds.jobId, snapDay)
      await updateTechnicianRoute(row.tech)
    } else {
      const srcTech = store.technicians.find(t => t.id === ds.techId)
      const job     = srcTech?.queue[ds.fromIdx]
      if (job) {
        await store.assignJobToTech(job.id, row.tech.id, insertIdx)
        if (isWeek) setJobDay(row.tech.id, job.id, snapDay)
        if (srcTech) await updateTechnicianRoute(srcTech)
        await updateTechnicianRoute(row.tech)
      }
    }
  }
}

async function removeJobFromTech (tech, job) {
  pushHistory()
  tech.queue = tech.queue.filter(q => q.id !== job.id)
  tech.queue.forEach((q, i) => { q.routeOrder = i })
  job.status = 'open'
  job.assignedTech = null
  await store.setJobStatus(job.id, 'open')
  await updateTechnicianRoute(tech)
}

// ── Modal ────────────────────────────────────────────────────────────────────
function openModal (ticket) {
  clearTooltip()
  modalTicket.value = ticket
}
function closeModal () { modalTicket.value = null }

async function completeTicket (jobId) {
  await store.setJobStatus(jobId, 'completed')
  store.technicians.forEach(t => { t.queue = t.queue.filter(q => q.id !== jobId) })
  drawAllMarkers()
}

// ── Tooltip ──────────────────────────────────────────────────────────────────
function scheduleTooltip (ticket, e) {
  clearTooltip()
  tooltipTimer = setTimeout(() => showTooltip(ticket, e.clientX, e.clientY), 600)
}
function clearTooltip () {
  clearTimeout(tooltipTimer)
  tooltipTimer = null
  tooltipTicket.value = null
}
function showTooltip (ticket, x, y) {
  tooltipTicket.value = ticket
  tooltipPos.value = { x, y }
  nextTick(() => {
    const el = document.getElementById('ticketTooltip')
    if (!el) return
    tooltipSize.value = { w: el.offsetWidth, h: el.offsetHeight }
  })
}
const tooltipStyle = computed(() => {
  const { x, y } = tooltipPos.value
  const { w, h } = tooltipSize.value
  return {
    left: Math.min(x + 14, window.innerWidth  - w - 8) + 'px',
    top:  Math.min(y + 14, window.innerHeight - h - 8) + 'px',
  }
})

// ── Computed lists ────────────────────────────────────────────────────────────
const unassignedJobs = computed(() => store.jobs.filter(j => j.status === 'open'))

function prioClass (p) { return { high: 'priority-high', medium: 'priority-medium', low: 'priority-low' }[p] || '' }
function prioLabel (p) { return { high: 'Urgent', medium: 'Moyen', low: 'Faible' }[p] || p }

// ── ERP status label ──────────────────────────────────────────────────────────
const erpLabel = computed(() => ({
  ok:             'ERPNext Connecté',
  error:          'Erreur API',
  session_expired:'Session expirée',
  pending:        'ERPNext...',
}[store.erpStatus] || 'ERPNext...'))

/// ── Watch: redraw map when data or view period changes ────────────────────────
watch(() => [store.jobs, store.technicians], () => { drawAllMarkers() }, { deep: true })
watch(selectedJobIds, () => { drawAllMarkers() }, { deep: true })
watch([scheduleDate, zoomView, jobDueDates], () => { drawAllMarkers() }, { deep: false })
watch(zoomView, v => {
  localStorage.setItem('dispatch-zoom', v)
  if (v === 'week') scheduleDate.value = toMonday(scheduleDate.value)
  nextTick(() => { _measureSchedule(); scrollToShiftStart() })
})

// ── Keyboard ─────────────────────────────────────────────────────────────────
function onKeyDown (e) {
  if (e.key === 'Escape') { closeModal(); clearSelection() }
  if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) { e.preventDefault(); undoLast() }
  if ((e.key === 'Delete' || e.key === 'Backspace') && selectedCount.value > 0 && !modalTicket.value) {
    e.preventDefault(); removeSelected()
  }
}

// ── Lifecycle ─────────────────────────────────────────────────────────────────
onMounted(async () => {
  applyTheme(currentTheme.value)
  document.addEventListener('keydown', onKeyDown)
  await auth.checkSession()
  await store.loadAll()
  migrateJobDays()
  if (showPendingLayer.value) loadPendingReqs()
  nextTick(() => {
    _measureSchedule()
    _resizeObs = new ResizeObserver(_measureSchedule)
    if (schScroll.value) _resizeObs.observe(schScroll.value)
    scrollToShiftStart()
  })
  // Mapbox script is loaded via CDN in index.html; init after data is ready
  if (window.mapboxgl) {
    initMap()
  } else {
    const s = document.createElement('script')
    s.src = 'https://api.mapbox.com/mapbox-gl-js/v2.15.0/mapbox-gl.js'
    s.onload = initMap
    document.head.appendChild(s)
    const l = document.createElement('link')
    l.rel = 'stylesheet'
    l.href = 'https://api.mapbox.com/mapbox-gl-js/v2.15.0/mapbox-gl.css'
    document.head.appendChild(l)
  }
})

onUnmounted(() => {
  document.removeEventListener('keydown', onKeyDown)
  document.removeEventListener('mousemove', _lassoMouseMove)
  document.removeEventListener('mouseup',   _lassoMouseUp)
  if (_resizeObs) _resizeObs.disconnect()
  clearTooltip()
  if (map) map.remove()
})
</script>

<template>
<div class="fd-root" :class="{ 'light-mode': currentTheme === 'light' }">

  <!-- ══════════════════════════ TOP ACTION BAR ══════════════════════════ -->
  <nav class="fd-topbar">
    <div class="fd-tb-left">
      <div class="fd-logo">
        <div class="fd-logo-icon">
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5"><polygon points="3 11 22 2 13 21 11 13 3 11"/></svg>
        </div>
        <span class="fd-logo-text">Dispatch</span>
      </div>
      <button class="fd-btn" @click="$router.push('/booking')">+ Demande</button>
      <button class="fd-btn fd-btn-green" @click="$router.push('/contractor')">+ Technicien</button>
      <button class="fd-btn fd-btn-pending" :class="{ active: showPendingLayer }" @click="togglePendingLayer">
        📋 Demandes
        <span v-if="pendingReqs.length" class="fd-badge-sm">{{ pendingReqs.length }}</span>
      </button>
    </div>
    <div class="fd-tb-center">
      <button class="fd-nav-btn" @click="prevDay">‹</button>
      <span class="fd-date-label">{{ scheduleDateLabel }}</span>
      <button class="fd-nav-btn" @click="nextDay">›</button>
      <div class="fd-zoom-toggle">
        <button :class="{ active: zoomView === 'day' }"  @click="zoomView = 'day'">Jour</button>
        <button :class="{ active: zoomView === 'week' }" @click="zoomView = 'week'">Semaine</button>
      </div>
    </div>
    <div class="fd-tb-right">
      <div class="fd-erp-dot" :class="{ connected: store.erpStatus === 'ok' }"
           :title="{ ok: 'ERPNext connecté', error: 'ERPNext hors ligne' }[store.erpStatus] || 'ERPNext...'"></div>
      <button class="fd-btn-icon" @click="$router.push('/admin')" title="Paramètres">⚙️</button>
      <button class="fd-btn-icon" @click="toggleTheme">{{ currentTheme === 'dark' ? '🌙' : '☀️' }}</button>
    </div>
  </nav>

  <!-- ══════════════════════════ MAIN AREA (3 colonnes) ══════════════════ -->
  <div class="fd-main">

    <!-- ── LEFT : Techniciens + Work Orders ── -->
    <div class="fd-left" :style="leftPanelOpen ? 'width:'+leftPanelW+'px;min-width:'+leftPanelW+'px' : 'width:36px;min-width:36px'">

      <!-- Collapse toggle strip -->
      <button class="fd-panel-toggle" @click="leftPanelOpen=!leftPanelOpen" :title="leftPanelOpen?'Réduire':'Ouvrir'">
        {{ leftPanelOpen ? '◀' : '▶' }}
      </button>

      <!-- Panel body (hidden when collapsed) -->
      <div v-show="leftPanelOpen" class="fd-left-body">

        <!-- ── Section: Techniciens ── -->
        <div class="fd-acc-hdr" @click="techSectionOpen=!techSectionOpen">
          <span>Techniciens <span class="fd-badge-sm">{{ store.technicians.length }}</span></span>
          <span class="fd-chevron">{{ techSectionOpen ? '▲' : '▼' }}</span>
        </div>
        <div v-show="techSectionOpen" class="fd-acc-body fd-res-list">
          <div v-for="tech in store.technicians" :key="tech.id"
               class="fd-res-row"
               :class="{ 'fd-res-selected': selectedTechId === tech.id }"
               @click="selectTech(tech.id)"
               @dragover="onDragOverZone($event, tech)"
               @dragleave="onDragLeaveZone($event, tech)"
               @drop="onDropOnZone($event, tech)">
            <div class="fd-res-sel-bar" :style="selectedTechId === tech.id ? 'background:' + TECH_COLORS[tech.colorIdx] : ''"></div>
            <div class="fd-res-avatar" :style="'background:' + TECH_COLORS[tech.colorIdx]">
              {{ tech.fullName.split(' ').map(n => n[0]).join('').toUpperCase().slice(0,2) }}
            </div>
            <div class="fd-res-info">
              <div class="fd-res-name">{{ tech.fullName }}</div>
              <div class="fd-res-meta">
                <span class="status-badge" :class="(stMap[(tech.status||'').toLowerCase()] || stMap['available']).cls"
                      style="font-size:0.58rem;padding:0.05rem 0.3rem;border-radius:4px;">
                  {{ (stMap[(tech.status||'').toLowerCase()] || stMap['available']).label }}
                </span>
                <span class="fd-res-jobs">{{ tech.queue.reduce((a,j)=>a+(parseFloat(j.duration)||0),0).toFixed(1) }}h · {{ tech.queue.length }} job{{ tech.queue.length!==1?'s':'' }}</span>
              </div>
              <div class="fd-res-bar">
                <div class="fd-res-bar-fill"
                     :style="{ width: Math.min(100, tech.queue.reduce((a,j)=>a+(parseFloat(j.duration)||0),0)/8*100)+'%', background: TECH_COLORS[tech.colorIdx] }">
                </div>
              </div>
              <div class="fd-res-rating" @click.stop="ratingTech = ratingTech===tech.id?null:tech.id">
                <span class="stars-bg" style="font-size:0.65rem;">★★★★★</span>
                <span class="stars-fg" style="font-size:0.65rem;" :style="{ width: ((techRatings[tech.id]?.score??5)/5*100)+'%' }">★★★★★</span>
                <span style="font-size:0.62rem;color:var(--text-secondary);margin-left:0.2rem;">{{ techRatings[tech.id]?.score.toFixed(1) ?? '5.0' }}</span>
              </div>
              <div v-if="ratingTech===tech.id" class="rating-input" @click.stop>
                <span v-for="n in 5" :key="n" class="star-btn" :class="{ active: n<=(hoverRating||0) }"
                      @mouseenter="hoverRating=n" @mouseleave="hoverRating=0" @click.stop="setTechRating(tech.id,n)">★</span>
              </div>
            </div>
            <div v-if="dropTarget?.techId===tech.id" class="fd-drop-glow"></div>
          </div>
        </div>

        <!-- Multi-selection bar -->
        <div v-if="selectedCount > 0" class="fd-sel-toolbar">
          <span class="fd-sel-count">{{ selectedCount }} sélectionné{{ selectedCount>1?'s':'' }}</span>
          <select class="fd-sel-transfer" @change="transferSelected($event.target.value);$event.target.value=''">
            <option value="">Transférer à…</option>
            <option v-for="tech in store.technicians" :key="tech.id" :value="tech.id">{{ tech.fullName }}</option>
          </select>
          <button class="fd-sel-btn" @click="removeSelected" title="Retirer de la queue">✕</button>
          <button class="fd-sel-btn" @click="clearSelection">↩</button>
        </div>

        <!-- ── Section: Work Orders ── -->
        <div class="fd-acc-hdr fd-acc-hdr-wo">
          <div class="fd-wo-tabs-inline" @click.stop>
            <button class="fd-wo-tab" :class="{ active: woTab==='unassigned' }" @click="woTab='unassigned'">
              Non assignés <span class="fd-badge-sm" :style="unassignedJobs.length?'background:#ef4444':''">{{ unassignedJobs.length }}</span>
            </button>
            <button class="fd-wo-tab" :class="{ active: woTab==='pending' }" @click="woTab='pending'">
              Demandes <span v-if="pendingReqs.length" class="fd-badge-sm">{{ pendingReqs.length }}</span>
            </button>
          </div>
          <span class="fd-chevron" @click="woSectionOpen=!woSectionOpen">{{ woSectionOpen ? '▲' : '▼' }}</span>
        </div>
        <div v-show="woSectionOpen" class="fd-acc-body fd-wo-list">
          <template v-if="woTab==='unassigned'">
            <div v-if="!unassignedJobs.length" class="fd-wo-empty">✓ Tous les tickets sont assignés</div>
            <div v-for="job in unassignedJobs" :key="job.id"
                 class="fd-wo-card-v" :class="{ 'fd-wo-card-sel': isJobSelected(job.id) }"
                 draggable="true"
                 @dragstart="onTicketDragStart($event,job)"
                 @dragend="onDragEnd"
                 @click="handleJobClick($event,job.id)"
                 @dblclick="openModal(job)">
              <div class="fd-wo-stripe" :style="'background:'+jobColor(job)"></div>
              <div class="fd-wo-inner">
                <div class="fd-wo-top">
                  <span class="job-label-badge">{{ jobLabelMap[job.id] }}</span>
                  <span class="ticket-priority" :class="prioClass(job.priority)">{{ prioLabel(job.priority) }}</span>
                </div>
                <div class="fd-wo-subject">{{ job.subject }}</div>
                <div class="fd-wo-addr">📍 {{ job.address }}</div>
              </div>
            </div>
          </template>
          <template v-else>
            <div v-if="!pendingReqs.length" class="fd-wo-empty">Aucune demande en attente</div>
            <div v-for="req in pendingReqs" :key="req.name||req.ref"
                 class="fd-wo-card-v" :class="{ 'fd-wo-urgent': req.urgency==='urgent' }"
                 draggable="true"
                 @dragstart="onPendingDragStart($event,req)"
                 @dragend="onDragEnd"
                 @click="pendingModal=req; pendingAssignTech=''">
              <div class="fd-wo-stripe" :style="'background:'+(req.urgency==='urgent'?'#ef4444':'#f59e0b')"></div>
              <div class="fd-wo-inner">
                <div class="fd-wo-top">
                  <span class="ticket-id">{{ req.name||req.ref }}</span>
                  <span v-if="req.urgency==='urgent'" class="ticket-priority priority-high">🚨</span>
                </div>
                <div class="fd-wo-subject">{{ SVC_ICONS[req.service_type]||'🔧' }} {{ req.problem_type||req.service_type }}</div>
                <div class="fd-wo-addr">📍 {{ req.address }}</div>
                <div v-if="req.budget_label" style="font-size:0.68rem;color:#f59e0b;margin-top:0.1rem;">💰 {{ req.budget_label }}</div>
              </div>
            </div>
          </template>
        </div>

        <!-- Footer -->
        <div class="fd-res-footer">
          <button class="fd-btn fd-btn-sm" @click="undoLast" title="Ctrl+Z">↩ Annuler</button>
          <span class="fd-erp-label">{{ { ok:'ERPNext ✓', error:'ERP hors ligne', loading:'Connexion…' }[store.erpStatus] || '' }}</span>
        </div>

      </div><!-- /.fd-left-body -->

      <!-- Resize handle (right edge of left panel) -->
      <div v-show="leftPanelOpen" class="fd-resize-handle" @mousedown.prevent="startResizeLeft"></div>
    </div>

    <!-- ── CENTER : Gantt timeline ── -->
    <div class="fd-gantt-wrap" ref="schScroll" @mousedown="onSchMouseDown">
      <div class="sch-inner" :style="'width:' + (140 + totalW) + 'px'" :class="{ 'sch-lassoing': lasso }">
        <div v-if="lasso" class="sch-lasso" :style="lassoStyle"></div>

        <!-- Pending overlay -->
        <template v-if="showPendingLayer">
          <div v-for="pb in pendingBlocks" :key="pb.key"
               class="pending-req-block"
               :class="{ 'pending-urgent': pb.req.urgency==='urgent', 'pending-p1': pb.priority===1 }"
               :style="{ left:(140+pb.left)+'px', width:pb.width+'px', top:'46px', height:(scheduleRows.length*72)+'px' }"
               draggable="true"
               @dragstart="onPendingDragStart($event, pb.req)"
               @dragend="onDragEnd"
               @click.stop="pendingModal=pb.req; pendingAssignTech=''">
            <div class="pending-req-inner">
              <span class="pending-icon">{{ pb.icon }}</span>
              <span class="pending-name">{{ pb.req.customer_name || pb.req.name }}</span>
              <span class="pending-slot">{{ pb.slotLabel }}</span>
              <span class="pending-date">{{ pb.dateLabel }}</span>
              <span class="pending-budget" v-if="pb.req.budget_label">💰 {{ pb.req.budget_label }}</span>
            </div>
          </div>
          <div v-if="pendingLoading" class="pending-loading-badge">Chargement demandes…</div>
          <div v-else-if="showPendingLayer && pendingBlocks.length===0 && pendingReqs.length>0"
               class="pending-no-match">Aucune demande pour cette période</div>
        </template>

        <!-- Ruler -->
        <div class="sch-header-row">
          <div class="sch-corner">Technicien</div>
          <div class="sch-ruler" :style="'width:' + totalW + 'px'">
            <div v-for="tick in rulerTicks" :key="tick.x"
                 :class="tick.isMajor ? 'sch-tick-major' : 'sch-tick-minor'"
                 :style="'left:' + tick.x + 'px'">
              <span v-if="tick.label" class="sch-tick-label"
                    :class="{ 'sch-tick-label-current': tick.isCurrent, 'sch-tick-label-zoomable': zoomView==='week' && tick.date }"
                    :title="zoomView==='week' && tick.date ? 'Double-clic pour voir ce jour' : undefined"
                    @dblclick="zoomView==='week' && tick.date && zoomToDay(tick.date)">{{ tick.label }}</span>
            </div>
            <div v-if="nowLineX >= 0" class="sch-now-line" :style="'left:'+nowLineX+'px'"></div>
          </div>
        </div>

        <!-- Tech rows -->
        <div v-for="row in scheduleRows" :key="row.tech.id"
             class="sch-row" :class="{ 'sch-row-active': selectedTechId===row.tech.id }"
             @dragover="onSchDragOverRow($event, row)"
             @dragleave="onSchDragLeaveRow($event, row)"
             @drop="onSchDropRow($event, row)">
          <div class="sch-tech-name" @click="selectTech(row.tech.id)" style="cursor:pointer;"
               :style="selectedTechId===row.tech.id ? 'border-left:3px solid '+row.color+';padding-left:0.6rem;' : ''">
            <div style="font-size:0.82rem;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:100%;">
              {{ row.tech.fullName }}
            </div>
            <div style="display:flex;align-items:center;gap:0.35rem;margin-top:0.2rem;">
              <span class="status-badge" :class="row.st.cls">{{ row.st.label }}</span>
              <span style="font-size:0.62rem;color:var(--text-secondary);font-weight:600;">{{ row.loadH }}h</span>
            </div>
          </div>
          <div class="sch-timeline" :style="'width:' + totalW + 'px'">
            <div v-for="tick in rulerTicks.filter(t => t.isMajor && t.x > 0)"
                 :key="'hg-' + tick.x" class="sch-hour-guide" :style="{ left: tick.x+'px' }"></div>
            <template v-if="zoomView === 'week'">
              <div v-for="d in weekDays" :key="'dbg-'+d"
                   class="sch-day-bg" :class="{ 'sch-day-bg-alt': d%2===0 }"
                   :style="{ left:((d-1)*24*60*pxPerMin)+'px', width:(24*60*pxPerMin)+'px' }"
                   @dblclick.stop="zoomToDay(rulerTicks.find(t=>t.date && Math.round(t.x/(24*60*pxPerMin))===d-1)?.date)"></div>
              <div v-for="d in weekDays" :key="'ds-'+d" class="sch-day-sep"
                   :style="{ left:((d-1)*24*60*pxPerMin)+'px' }"></div>
            </template>
            <div v-if="schDropTarget?.techId===row.tech.id && schDropTarget.snapDay!==undefined"
                 class="sch-day-drop-col" :style="{ left:schDropTarget.xPos+'px', width:schDropTarget.snapWidth+'px' }"></div>
            <div v-else-if="schDropTarget?.techId===row.tech.id"
                 class="sch-drop-indicator" :style="{ left:schDropTarget.xPos+'px' }"></div>
            <div v-for="block in row.blocks" :key="block.key"
                 :class="[block.type==='job'?'sch-block sch-job-block':'sch-travel-label', block.type==='job'&&isJobSelected(block.job.id)?'sch-block-selected':'']"
                 :style="{ left:block.left+'px', width:block.width+'px', background:block.type==='job'?block.color+'ee':undefined }"
                 :draggable="block.type==='job'"
                 @dragstart="block.type==='job' && onSchBlockDragStart($event,row,block)"
                 @dragend="onDragEnd"
                 @click="block.type==='job' && handleJobClick($event,block.job.id)"
                 @dblclick="block.type==='job' && openModal(block.job)">
              <template v-if="block.type==='job'">
                <div class="sch-block-label" style="pointer-events:none;">{{ jobLabelMap[block.job.id] }}</div>
                <div class="sch-block-inner" style="pointer-events:none;">
                  <div class="sch-block-title">{{ block.job.subject }}</div>
                  <div class="sch-block-time">{{ block.timeRange }}</div>
                  <div v-if="block.timeHours && block.timeHours!==block.timeRange" class="sch-block-hours">{{ block.timeHours }}</div>
                </div>
              </template>
              <template v-else>→ {{ block.driveMin }}m</template>
            </div>
          </div>
        </div>
      </div>
    </div>

    <!-- ── RIGHT : Map (resize handle + collapsable) ── -->
    <div class="fd-resize-handle fd-resize-map" v-show="mapPanelOpen" @mousedown.prevent="startResizeMap"></div>
    <div class="fd-mapside" :style="mapPanelOpen ? 'width:'+mapPanelW+'px;min-width:'+mapPanelW+'px' : 'width:36px;min-width:36px'">
      <!-- Collapse toggle -->
      <button class="fd-panel-toggle fd-panel-toggle-map" @click="mapPanelOpen=!mapPanelOpen" :title="mapPanelOpen?'Réduire carte':'Ouvrir carte'">
        {{ mapPanelOpen ? '▶' : '◀' }}
      </button>
      <template v-if="mapPanelOpen">
        <div class="fd-map-legend">
          <div v-for="(col, label) in SVC_COLORS" :key="label" class="fd-legend-item">
            <span class="fd-legend-dot" :style="'background:'+col"></span>
            <span>{{ label }}</span>
          </div>
          <div class="fd-legend-divider" v-if="selectedTechId"></div>
          <div v-if="selectedTechId" class="fd-legend-item">
            <span class="fd-legend-dot" :style="'background:'+TECH_COLORS[store.technicians.find(t=>t.id===selectedTechId)?.colorIdx]"></span>
            <span style="font-weight:600;">{{ store.technicians.find(t=>t.id===selectedTechId)?.fullName }}</span>
          </div>
          <div v-else class="fd-legend-hint">↑ Cliquer un technicien pour son parcours</div>
        </div>
        <div id="map"></div>
      </template>
    </div>

  </div><!-- /.fd-main -->

  <!-- ── Modal demande en attente ── -->
  <div v-if="pendingModal" class="modal-overlay" @click.self="pendingModal=null">
    <div class="modal-card" @click.stop style="max-width:480px;">
      <div class="modal-header">
        <span class="ticket-id" style="font-size:0.9rem;">{{ pendingModal.name }}</span>
        <span v-if="pendingModal.urgency==='urgent'" class="ticket-priority priority-high">🚨 Urgent</span>
        <button class="modal-close" @click="pendingModal=null">✕</button>
      </div>
      <p class="modal-title">
        {{ SVC_ICONS[pendingModal.service_type]||'🔧' }}
        {{ pendingModal.problem_type||pendingModal.service_type }}
      </p>
      <div class="modal-row">👤 <strong>{{ pendingModal.customer_name }}</strong>
        <span v-if="pendingModal.phone" style="color:var(--text-secondary);margin-left:0.5rem;">{{ pendingModal.phone }}</span>
      </div>
      <div class="modal-row">📍 <strong>{{ pendingModal.address }}</strong></div>
      <div v-if="pendingModal.description" class="modal-row" style="font-size:0.82rem;color:var(--text-secondary);">
        {{ pendingModal.description }}
      </div>
      <div v-if="pendingModal.budget_label" class="modal-row">💰 Budget : <strong>{{ pendingModal.budget_label }}</strong></div>
      <div style="margin-bottom:0.75rem;font-size:0.72rem;font-weight:800;text-transform:uppercase;letter-spacing:0.05em;color:var(--text-secondary);">
        Dates préférées
      </div>
      <div style="display:flex;flex-wrap:wrap;gap:0.4rem;margin-bottom:1rem;">
        <div v-for="pd in reqDates(pendingModal)" :key="pd.priority"
             style="background:rgba(99,102,241,0.12);border:1px solid rgba(99,102,241,0.3);border-radius:8px;padding:0.4rem 0.75rem;font-size:0.78rem;">
          <span style="color:var(--text-secondary);font-size:0.68rem;">{{ pd.priority }}e choix · </span>
          <strong>{{ new Date(pd.date+'T12:00:00').toLocaleDateString('fr-CA',{weekday:'short',day:'numeric',month:'short'}) }}</strong>
          <span style="color:#818cf8;margin-left:0.35rem;">{{ pd.slots.map(s=>({morning:'Matin',afternoon:'Après-midi',evening:'Soir',flexible:'Flexible'}[s]||s)).join(' · ') }}</span>
        </div>
      </div>
      <div style="margin-bottom:0.75rem;font-size:0.72rem;font-weight:800;text-transform:uppercase;letter-spacing:0.05em;color:var(--text-secondary);">
        Assigner à un technicien
      </div>
      <div style="display:flex;gap:0.5rem;flex-wrap:wrap;margin-bottom:1rem;">
        <button v-for="tech in store.technicians" :key="tech.id"
                class="sch-zoom-btn"
                :class="{ active: pendingAssignTech===tech.id }"
                :style="pendingAssignTech===tech.id?`border-color:${TECH_COLORS[tech.colorIdx]};background:${TECH_COLORS[tech.colorIdx]}22`:''"
                @click="pendingAssignTech=tech.id">
          {{ tech.fullName }}
        </button>
      </div>
      <div class="modal-footer">
        <button class="btn-complete-modal" :disabled="!pendingAssignTech"
                @click="acceptPendingReq(pendingModal, pendingAssignTech)">
          ✓ Accepter &amp; assigner
        </button>
        <button class="sch-revert-btn" @click="pendingModal=null">Fermer</button>
      </div>
    </div>
  </div>

  <!-- ── Modal ticket ── -->
  <div v-if="modalTicket" class="modal-overlay" @click.self="closeModal">
    <div class="modal-card" @click.stop>
      <div class="modal-header">
        <span class="ticket-id" style="font-size:0.9rem;">{{ modalTicket.id }}</span>
        <span class="ticket-priority" :class="prioClass(modalTicket.priority)">{{ prioLabel(modalTicket.priority) }}</span>
        <span v-if="modalTicket.status==='completed'" class="status-badge status-in-progress">✓ Complété</span>
        <button class="modal-close" @click="closeModal">✕</button>
      </div>
      <p class="modal-title">{{ modalTicket.subject }}</p>
      <div class="modal-row">📍 <strong>{{ modalTicket.address }}</strong></div>
      <div class="modal-row">⏱️ Durée estimée : <strong>{{ modalTicket.duration }}h</strong></div>
      <div v-if="modalTicket.assignedTech" class="modal-row">
        👤 Technicien :
        <strong :style="'color:'+TECH_COLORS[store.technicians.find(t=>t.id===modalTicket.assignedTech)?.colorIdx||0]">
          ■ {{ store.technicians.find(t=>t.id===modalTicket.assignedTech)?.fullName }}
        </strong>
      </div>
      <div v-else class="modal-row" style="color:var(--orange);">⚠️ Non assigné</div>
      <div v-if="modalTicket.legDist" class="modal-row">
        🚗 Trajet : <strong>{{ modalTicket.legDist }} km ({{ modalTicket.legDur }} min)</strong>
      </div>
      <div class="modal-footer">
        <button v-if="modalTicket.status!=='completed'" class="btn-complete-modal"
                @click="completeTicket(modalTicket.id); closeModal()">
          ✓ Marquer comme complété
        </button>
        <span v-else style="color:var(--green);font-size:0.85rem;font-weight:600;">✓ Ticket complété</span>
        <button class="sch-revert-btn" @click="closeModal">Fermer</button>
      </div>
    </div>
  </div>

  <!-- ── Tooltip ── -->
  <div v-if="tooltipTicket" id="ticketTooltip" :style="tooltipStyle">
    <div style="font-weight:700;margin-bottom:0.35rem;color:var(--text-primary);">
      {{ tooltipTicket.id }} — {{ tooltipTicket.subject }}
    </div>
    <div style="color:var(--text-secondary);margin-bottom:0.25rem;">📍 {{ tooltipTicket.address }}</div>
    <div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;font-size:0.72rem;">
      <span>⏱️ {{ tooltipTicket.duration }}h</span>
      <span v-if="tooltipTicket.assignedTech">
        👤 {{ store.technicians.find(t=>t.id===tooltipTicket.assignedTech)?.fullName }}
      </span>
      <span v-if="tooltipTicket.status==='completed'" style="color:var(--green);font-weight:700;">✓ Complété</span>
    </div>
  </div>

</div>
</template>


<style scoped>
/* ═══════════════════════════════════════════════════
   CSS Variables (dark / light)
═══════════════════════════════════════════════════ */
:root {
  --bg: #0f1117; --sidebar-bg: #151820; --card-bg: #1e2130; --card-hover: #252840;
  --border: rgba(255,255,255,0.07); --border-accent: rgba(99,102,241,0.45);
  --text-primary: #e8eaf0; --text-secondary: #8b90a8;
  --accent: #6366f1; --red: #f43f5e; --green: #10b981; --orange: #f59e0b;
}
.light-mode {
  --bg: #f0f2f8; --sidebar-bg: #ffffff; --card-bg: #ffffff; --card-hover: #f5f7ff;
  --border: rgba(0,0,0,0.09); --border-accent: rgba(99,102,241,0.4);
  --text-primary: #1e2130; --text-secondary: #6b7280;
}

/* ═══════════════════════════════════════════════════
   Root layout — 2 rows: topbar | main
═══════════════════════════════════════════════════ */
:global(html), :global(body) { margin: 0; padding: 0; height: 100%; overflow: hidden; }
.fd-root {
  display: grid;
  grid-template-rows: 44px 1fr;
  grid-template-columns: 1fr;
  height: 100vh;
  width: 100vw;
  overflow: hidden;
  background: var(--bg);
  color: var(--text-primary);
  font-family: 'Inter', sans-serif;
}

/* ═══════════════════════════════════════════════════
   Top action bar
═══════════════════════════════════════════════════ */
.fd-topbar {
  grid-row: 1;
  display: flex; align-items: center; gap: 0.75rem;
  padding: 0 1rem;
  background: var(--sidebar-bg);
  border-bottom: 1px solid var(--border);
  box-shadow: 0 1px 8px rgba(0,0,0,0.35);
  z-index: 20;
}
.fd-tb-left  { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }
.fd-tb-center{ display: flex; align-items: center; gap: 0.5rem; flex: 1; justify-content: center; }
.fd-tb-right { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; margin-left: auto; }

.fd-logo { display: flex; align-items: center; gap: 0.4rem; margin-right: 0.5rem; }
.fd-logo-icon { width: 26px; height: 26px; background: linear-gradient(135deg, var(--accent), #a855f7); border-radius: 6px; display: flex; align-items: center; justify-content: center; }
.fd-logo-text { font-size: 1rem; font-weight: 700; }

.fd-btn { background: var(--card-bg); border: 1px solid var(--border); border-radius: 6px; color: var(--text-primary); font-size: 0.75rem; font-weight: 600; padding: 0.3rem 0.65rem; cursor: pointer; transition: border-color 0.15s, background 0.15s; white-space: nowrap; }
.fd-btn:hover { border-color: var(--border-accent); background: var(--card-hover); }
.fd-btn-green { border-color: rgba(16,185,129,0.3); color: var(--green); }
.fd-btn-green:hover { background: rgba(16,185,129,0.1); }
.fd-btn-pending { border-color: rgba(245,158,11,0.3); color: var(--orange); }
.fd-btn-pending.active { background: rgba(245,158,11,0.15); border-color: var(--orange); }
.fd-btn-sm { font-size: 0.68rem; padding: 0.2rem 0.45rem; }
.fd-btn-icon { background: none; border: none; cursor: pointer; font-size: 1rem; padding: 0 0.2rem; opacity: 0.7; }
.fd-btn-icon:hover { opacity: 1; }

.fd-nav-btn { background: none; border: 1px solid var(--border); border-radius: 6px; color: var(--text-primary); font-size: 1rem; padding: 0.15rem 0.6rem; cursor: pointer; }
.fd-nav-btn:hover { background: var(--card-bg); }
.fd-date-label { font-size: 0.82rem; font-weight: 600; color: var(--text-primary); min-width: 160px; text-align: center; }

.fd-zoom-toggle { display: flex; background: var(--card-bg); border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
.fd-zoom-toggle button { background: none; border: none; color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; padding: 0.25rem 0.6rem; cursor: pointer; transition: color 0.15s, background 0.15s; }
.fd-zoom-toggle button.active { background: var(--accent); color: white; }

.fd-badge-sm { background: var(--accent); color: white; font-size: 0.6rem; padding: 0.05rem 0.35rem; border-radius: 8px; font-weight: 700; margin-left: 0.25rem; }
.fd-erp-dot { width: 7px; height: 7px; border-radius: 50%; background: #ef4444; transition: background 0.3s; }
.fd-erp-dot.connected { background: var(--green); }

/* ═══════════════════════════════════════════════════
   Main area — 3 columns: left | gantt | map
═══════════════════════════════════════════════════ */
.fd-main {
  grid-row: 2;
  display: flex;
  overflow: hidden;
  min-height: 0;
}

/* ── Left panel (resizable, collapsable) ── */
.fd-left {
  display: flex;
  flex-direction: row;
  flex-shrink: 0;
  background: var(--sidebar-bg);
  border-right: 1px solid var(--border);
  overflow: hidden;
  transition: width 0.18s, min-width 0.18s;
  position: relative;
}
.fd-left-body {
  flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0;
}

/* Panel collapse toggle (left & map) */
.fd-panel-toggle {
  position: absolute; top: 50%; transform: translateY(-50%);
  right: -1px; z-index: 15;
  width: 16px; min-width: 16px; height: 40px;
  background: var(--card-bg); border: 1px solid var(--border);
  border-radius: 0 6px 6px 0;
  color: var(--text-secondary); font-size: 0.55rem;
  cursor: pointer; display: flex; align-items: center; justify-content: center;
  transition: color 0.15s, background 0.15s;
  padding: 0;
}
.fd-panel-toggle:hover { color: var(--accent); background: var(--card-hover); }
.fd-panel-toggle-map {
  right: auto; left: -1px;
  border-radius: 6px 0 0 6px;
}

/* Accordion headers */
.fd-acc-hdr {
  padding: 0.4rem 0.65rem;
  font-size: 0.6rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.07em;
  color: var(--text-secondary);
  border-bottom: 1px solid var(--border);
  display: flex; align-items: center; justify-content: space-between;
  cursor: pointer; flex-shrink: 0;
  transition: background 0.12s;
}
.fd-acc-hdr:hover { background: var(--card-hover); }
.fd-acc-hdr-wo { padding: 0.25rem 0.4rem 0.25rem 0.5rem; cursor: default; }
.fd-chevron { font-size: 0.55rem; opacity: 0.6; margin-left: 0.3rem; flex-shrink: 0; }

/* Accordion body — tech list */
.fd-acc-body { overflow-y: auto; }
.fd-acc-body::-webkit-scrollbar { width: 3px; }
.fd-acc-body::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); }
.fd-res-list { flex-shrink: 0; max-height: 45vh; }

/* Work orders list */
.fd-wo-list { flex: 1; min-height: 0; padding: 0.3rem 0; }
.fd-wo-tabs-inline { display: flex; gap: 0.15rem; align-items: center; flex: 1; min-width: 0; }
.fd-wo-tab { background: none; border: none; border-bottom: 2px solid transparent; color: var(--text-secondary); font-size: 0.65rem; font-weight: 700; padding: 0.2rem 0.35rem 0.15rem; cursor: pointer; text-transform: uppercase; letter-spacing: 0.04em; transition: color 0.15s, border-color 0.15s; white-space: nowrap; }
.fd-wo-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
.fd-wo-empty { font-size: 0.75rem; color: var(--text-secondary); font-style: italic; padding: 0.75rem 0.75rem; }
.fd-wo-card-v {
  display: flex; overflow: hidden; cursor: grab;
  border-bottom: 1px solid var(--border);
  transition: background 0.12s;
}
.fd-wo-card-v:hover { background: var(--card-hover); }
.fd-wo-card-sel { background: rgba(99,102,241,0.07) !important; }
.fd-wo-urgent { border-left: 2px solid rgba(239,68,68,0.5); }

/* shared wo inner (reused for card-v) */
.fd-section-hdr { padding: 0.5rem 0.75rem; font-size: 0.62rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.07em; color: var(--text-secondary); border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }

.fd-res-row {
  position: relative;
  display: flex; align-items: flex-start; gap: 0.5rem;
  padding: 0.6rem 0.75rem 0.6rem 0.5rem;
  border-bottom: 1px solid var(--border);
  cursor: pointer;
  transition: background 0.15s;
  user-select: none;
}
.fd-res-row:hover { background: var(--card-hover); }
.fd-res-selected { background: rgba(99,102,241,0.06) !important; }

.fd-res-sel-bar { position: absolute; left: 0; top: 0; bottom: 0; width: 3px; border-radius: 0 2px 2px 0; transition: background 0.2s; }
.fd-res-avatar {
  width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
  font-size: 0.7rem; font-weight: 700; color: white; flex-shrink: 0; margin-top: 2px;
}
.fd-res-info { flex: 1; min-width: 0; }
.fd-res-name { font-size: 0.8rem; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.fd-res-meta { display: flex; align-items: center; gap: 0.3rem; margin: 0.15rem 0; flex-wrap: wrap; }
.fd-res-jobs { font-size: 0.62rem; color: var(--text-secondary); }
.fd-res-bar { height: 3px; background: var(--border); border-radius: 2px; margin-top: 0.25rem; overflow: hidden; }
.fd-res-bar-fill { height: 100%; border-radius: 2px; transition: width 0.3s; }
.fd-res-rating { display: flex; align-items: center; position: relative; margin-top: 0.2rem; cursor: pointer; }
.fd-drop-glow { position: absolute; inset: 0; border: 2px dashed var(--accent); border-radius: 4px; background: rgba(99,102,241,0.05); pointer-events: none; }

.fd-sel-toolbar { padding: 0.4rem 0.5rem; background: rgba(99,102,241,0.1); border-top: 1px solid var(--border-accent); display: flex; flex-wrap: wrap; gap: 0.3rem; align-items: center; font-size: 0.7rem; }
.fd-sel-count { font-weight: 700; color: var(--accent); }
.fd-sel-transfer { background: var(--card-bg); border: 1px solid var(--border); border-radius: 4px; color: var(--text-primary); font-size: 0.68rem; padding: 0.15rem 0.3rem; cursor: pointer; }
.fd-sel-btn { background: none; border: 1px solid var(--border); border-radius: 4px; color: var(--text-secondary); font-size: 0.65rem; padding: 0.15rem 0.4rem; cursor: pointer; }
.fd-sel-btn:hover { color: var(--red); border-color: var(--red); }

.fd-res-footer { padding: 0.4rem 0.5rem; border-top: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
.fd-erp-label { font-size: 0.58rem; color: var(--text-secondary); }

/* ── Center: Gantt ── */
.fd-gantt-wrap {
  flex: 1;
  overflow-x: auto;
  overflow-y: auto;
  position: relative;
  min-width: 0;
}
.fd-gantt-wrap::-webkit-scrollbar { height: 5px; width: 5px; }
.fd-gantt-wrap::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 3px; }

/* ── Resize handles ── */
.fd-resize-handle {
  width: 4px; min-width: 4px; flex-shrink: 0;
  background: transparent;
  cursor: col-resize;
  transition: background 0.15s;
  z-index: 10;
}
.fd-resize-handle:hover { background: var(--accent); }
.fd-resize-map { order: -1; }

/* ── Right: Map (resizable, collapsable) ── */
.fd-mapside {
  display: flex; flex-direction: column;
  border-left: 1px solid var(--border);
  overflow: hidden; flex-shrink: 0;
  transition: width 0.18s, min-width 0.18s;
  position: relative;
}
.fd-map-legend {
  padding: 0.4rem 0.6rem;
  background: var(--sidebar-bg);
  border-bottom: 1px solid var(--border);
  display: flex; flex-wrap: wrap; gap: 0.4rem 0.75rem; align-items: center;
  flex-shrink: 0;
}
.fd-legend-item { display: flex; align-items: center; gap: 0.25rem; font-size: 0.65rem; color: var(--text-secondary); }
.fd-legend-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.fd-legend-divider { width: 1px; height: 12px; background: var(--border); margin: 0 0.25rem; }
.fd-legend-hint { font-size: 0.62rem; color: var(--text-secondary); font-style: italic; }
#map { flex: 1; min-height: 0; }

/* shared wo card parts */
.fd-wo-stripe  { width: 4px; flex-shrink: 0; }
.fd-wo-inner   { padding: 0.4rem 0.5rem; flex: 1; min-width: 0; }
.fd-wo-top     { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.15rem; }
.fd-wo-subject { font-size: 0.75rem; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.fd-wo-addr    { font-size: 0.63rem; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ═══════════════════════════════════════════════════
   Gantt inner components (unchanged)
═══════════════════════════════════════════════════ */
.sch-inner { position: relative; display: flex; flex-direction: column; min-height: 100%; }
.sch-lassoing { cursor: crosshair; }
.sch-lasso { position: absolute; z-index: 30; border: 1.5px dashed var(--accent); background: rgba(99,102,241,0.08); pointer-events: none; }
.sch-header-row { position: sticky; top: 0; z-index: 11; display: flex; height: 46px; background: var(--sidebar-bg); border-bottom: 1px solid var(--border); flex-shrink: 0; }
.sch-corner { position: sticky; left: 0; top: 0; z-index: 12; width: 140px; min-width: 140px; flex-shrink: 0; background: var(--sidebar-bg); border-right: 1px solid var(--border); display: flex; align-items: center; padding: 0 0.75rem; font-size: 0.65rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-secondary); }
.sch-ruler { position: relative; flex: 1; height: 100%; }
.sch-tick-major { position: absolute; top: 0; height: 100%; border-left: 1px solid var(--border); }
.sch-tick-minor { position: absolute; bottom: 0; height: 35%; border-left: 1px dashed rgba(255,255,255,0.08); }
.sch-tick-label { position: absolute; top: 5px; left: 4px; font-size: 0.62rem; color: var(--text-secondary); white-space: nowrap; font-weight: 600; user-select: none; }
.sch-tick-label-current { color: #f43f5e; font-weight: 700; }
.sch-tick-label-zoomable { cursor: zoom-in; border-bottom: 1px dashed currentColor; opacity: 0.85; transition: opacity 0.15s; }
.sch-tick-label-zoomable:hover { opacity: 1; color: var(--accent); }
.sch-now-line { position: absolute; top: 0; width: 2px; height: 100%; background: #f43f5e; opacity: 0.8; pointer-events: none; }
.sch-row { display: flex; height: 72px; border-bottom: 1px solid var(--border); transition: background 0.15s; }
.sch-row-active { background: rgba(99,102,241,0.04); }
.sch-tech-name { position: sticky; left: 0; z-index: 5; width: 140px; min-width: 140px; flex-shrink: 0; padding: 0 0.75rem; display: flex; flex-direction: column; justify-content: center; background: var(--sidebar-bg); border-right: 1px solid var(--border); transition: border-left 0.15s; }
.sch-timeline { position: relative; flex: 1; height: 100%; }
.sch-hour-guide { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--border); opacity: 0.5; pointer-events: none; }
.sch-day-bg { position: absolute; top: 0; bottom: 0; pointer-events: auto; }
.sch-day-bg-alt { background: rgba(255,255,255,0.015); }
.sch-day-sep { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--border); opacity: 0.7; pointer-events: none; }
.sch-drop-indicator { position: absolute; top: 4px; bottom: 4px; width: 2px; background: var(--accent); border-radius: 2px; pointer-events: none; z-index: 10; }
.sch-day-drop-col { position: absolute; top: 0; bottom: 0; background: rgba(99,102,241,0.1); border: 1px dashed var(--accent); border-radius: 4px; pointer-events: none; z-index: 8; }

.sch-block {
  position: absolute; top: 8px; bottom: 8px; border-radius: 8px;
  display: flex; align-items: center;
  box-shadow: 0 2px 8px rgba(0,0,0,0.3);
  transition: box-shadow 0.15s, transform 0.15s;
  cursor: grab; overflow: hidden; z-index: 4;
}
.sch-block:hover { box-shadow: 0 4px 16px rgba(0,0,0,0.45); transform: translateY(-1px); z-index: 5; }
.sch-block-selected { outline: 2px solid white; z-index: 6; }
.sch-block-label { width: 22px; min-width: 22px; height: 22px; border-radius: 50%; background: rgba(0,0,0,0.25); display: flex; align-items: center; justify-content: center; font-size: 0.62rem; font-weight: 700; color: white; margin-left: 6px; flex-shrink: 0; }
.sch-block-inner { flex: 1; min-width: 0; padding: 0 6px; }
.sch-block-title { font-size: 0.72rem; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: white; }
.sch-block-time  { font-size: 0.6rem; color: rgba(255,255,255,0.75); white-space: nowrap; }
.sch-block-hours { font-size: 0.58rem; color: rgba(255,255,255,0.6); white-space: nowrap; }
.sch-travel-label { position: absolute; top: 50%; transform: translateY(-50%); font-size: 0.6rem; color: var(--text-secondary); white-space: nowrap; padding: 0 4px; pointer-events: none; }

/* Pending overlay */
.pending-req-block { position: absolute; z-index: 3; pointer-events: auto; cursor: pointer; overflow: hidden; border-radius: 4px; background: repeating-linear-gradient(-45deg, rgba(245,158,11,0.18) 0, rgba(245,158,11,0.18) 6px, transparent 6px, transparent 12px); border: 1px solid rgba(245,158,11,0.35); transition: border-color 0.15s; }
.pending-req-block:hover { border-color: rgba(245,158,11,0.7); }
.pending-urgent { background: repeating-linear-gradient(-45deg, rgba(239,68,68,0.22) 0, rgba(239,68,68,0.22) 6px, transparent 6px, transparent 12px); border-color: rgba(239,68,68,0.4); }
.pending-req-inner { display: flex; flex-direction: column; gap: 1px; padding: 4px 6px; }
.pending-icon { font-size: 0.75rem; }
.pending-name { font-size: 0.65rem; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--orange); }
.pending-slot, .pending-date { font-size: 0.58rem; color: var(--text-secondary); white-space: nowrap; }
.pending-budget { font-size: 0.58rem; color: var(--green); }
.pending-loading-badge { position: sticky; top: 50px; left: 150px; background: var(--accent); color: white; font-size: 0.72rem; padding: 0.3rem 0.75rem; border-radius: 20px; display: inline-block; z-index: 15; }
.pending-no-match { position: sticky; top: 50px; left: 150px; color: var(--text-secondary); font-size: 0.72rem; font-style: italic; padding: 0.3rem 0.75rem; display: inline-block; }

/* Status badges */
.status-badge { font-size: 0.62rem; padding: 0.1rem 0.4rem; border-radius: 6px; font-weight: 600; white-space: nowrap; }
.status-on-shift  { background: rgba(16,185,129,0.15); color: var(--green); }
.status-enroute   { background: rgba(245,158,11,0.15); color: var(--orange); }
.status-in-progress { background: rgba(99,102,241,0.15); color: #818cf8; }
.status-off       { background: rgba(239,68,68,0.1);   color: var(--red); }

/* Priority badges */
.ticket-priority { font-size: 0.68rem; padding: 0.1rem 0.4rem; border-radius: 8px; font-weight: 600; }
.priority-high   { background: rgba(239,68,68,0.2);   color: var(--red); }
.priority-medium { background: rgba(245,158,11,0.2);  color: var(--orange); }
.priority-low    { background: rgba(16,185,129,0.2);  color: var(--green); }
.job-label-badge { background: rgba(99,102,241,0.2); color: #818cf8; font-size: 0.65rem; font-weight: 700; padding: 0.05rem 0.35rem; border-radius: 6px; }
.ticket-id { font-size: 0.72rem; font-weight: 600; color: var(--accent); }

/* Star rating */
.star-display { position: relative; display: inline-block; }
.stars-bg { color: rgba(255,255,255,0.15); letter-spacing: 1px; }
.stars-fg { position: absolute; left: 0; top: 0; overflow: hidden; white-space: nowrap; color: #f59e0b; letter-spacing: 1px; }
.rating-input { display: flex; align-items: center; gap: 0.1rem; margin-top: 0.2rem; }
.star-btn { cursor: pointer; font-size: 1rem; color: rgba(255,255,255,0.2); transition: color 0.1s; }
.star-btn.active { color: #f59e0b; }
.rating-val { font-size: 0.72rem; font-weight: 700; color: #f59e0b; }
.rating-cnt { font-size: 0.65rem; color: var(--text-secondary); }
.rating-hint { font-size: 0.65rem; color: var(--text-secondary); margin-left: 0.25rem; }

/* Zoom btn (reused in pending modal) */
.sch-zoom-btn { background: var(--card-bg); border: 1px solid var(--border); border-radius: 6px; color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; padding: 0.3rem 0.75rem; cursor: pointer; transition: color 0.15s, border-color 0.15s, background 0.15s; }
.sch-zoom-btn.active { border-color: var(--accent); color: var(--accent); background: rgba(99,102,241,0.1); }

/* Modals */
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.65); backdrop-filter: blur(4px); z-index: 100; display: flex; align-items: center; justify-content: center; }
.modal-card { background: var(--sidebar-bg); border: 1px solid var(--border); border-radius: 16px; padding: 1.5rem; min-width: 340px; max-width: 520px; width: 100%; box-shadow: 0 24px 60px rgba(0,0,0,0.55); }
.modal-header { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.75rem; }
.modal-title  { font-size: 1.05rem; font-weight: 700; margin: 0 0 0.75rem; }
.modal-row    { font-size: 0.85rem; margin-bottom: 0.5rem; display: flex; align-items: center; gap: 0.4rem; }
.modal-footer { display: flex; gap: 0.5rem; margin-top: 1rem; align-items: center; }
.modal-close  { margin-left: auto; background: none; border: none; color: var(--text-secondary); font-size: 1.1rem; cursor: pointer; }
.modal-close:hover { color: var(--red); }
.btn-complete-modal { background: var(--green); color: white; border: none; border-radius: 8px; padding: 0.5rem 1rem; font-size: 0.82rem; font-weight: 700; cursor: pointer; }
.btn-complete-modal:disabled { opacity: 0.4; cursor: not-allowed; }
.sch-revert-btn { background: none; border: 1px solid var(--border); border-radius: 8px; color: var(--text-secondary); padding: 0.5rem 0.75rem; font-size: 0.8rem; cursor: pointer; }
.sch-revert-btn:hover { border-color: var(--border-accent); }

/* Tooltip */
#ticketTooltip { position: fixed; z-index: 200; background: var(--sidebar-bg); border: 1px solid var(--border-accent); border-radius: 10px; padding: 0.75rem 1rem; max-width: 280px; box-shadow: 0 8px 24px rgba(0,0,0,0.45); font-size: 0.8rem; pointer-events: none; }
</style>
