<template>
  <q-page class="tech-surface">
    <!-- Offline banner (fixed top) -->
    <transition name="slide-down">
      <div v-if="!isOnline" class="tg-banner">
        <span class="material-icons" style="font-size:16px">wifi_off</span>
        Hors ligne — les modifications seront synchronisées à la reconnexion
      </div>
    </transition>

    <!-- Hero: greeting + progress arc -->
    <section class="tg-hero">
      <div class="tg-hero-inner">
        <!-- Top row: avatar + name + refresh -->
        <div class="hero-top">
          <div class="hero-left">
            <div class="tg-avatar">{{ initials }}</div>
            <div>
              <div class="t-caption t-ink-on-hero-dim" style="text-transform:capitalize">{{ todayLabel }}</div>
              <div class="hero-name">{{ techName }}</div>
            </div>
          </div>
          <button class="hero-refresh" @click="loadTasks" :disabled="loading" aria-label="Rafraîchir">
            <span class="material-icons" :class="{ spinning: loading }">refresh</span>
          </button>
        </div>

        <!-- Progress arc + count -->
        <div class="hero-progress">
          <div class="tg-arc">
            <svg :width="ARC_SIZE" :height="ARC_SIZE" viewBox="0 0 120 120">
              <circle class="tg-arc-track" cx="60" cy="60" :r="ARC_R" stroke-width="10" />
              <circle class="tg-arc-fill" cx="60" cy="60" :r="ARC_R" stroke-width="10"
                :stroke-dasharray="arcCircumference"
                :stroke-dashoffset="arcOffset" />
            </svg>
            <div class="tg-arc-center">
              <div class="arc-numerator">{{ doneCount }}</div>
              <div class="arc-denom">/ {{ jobs.length || '—' }}</div>
            </div>
          </div>
          <div class="hero-progress-text">
            <div class="t-h2 t-ink-on-hero">{{ heroHeadline }}</div>
            <div class="t-caption t-ink-on-hero-dim">{{ heroSubline }}</div>
          </div>
        </div>
      </div>
    </section>

    <!-- Job list -->
    <div class="tg-page tg-page--content">
      <!-- In progress (always first — most likely what tech is doing) -->
      <div v-if="inProgressJobs.length" class="section-head">
        <span class="t-micro">En cours</span>
        <span class="t-micro t-ink-400">{{ inProgressJobs.length }}</span>
      </div>
      <JobCard v-for="job in inProgressJobs" :key="job.name"
        :job="job" status="work"
        @click="openSheet(job)" />

      <!-- Upcoming -->
      <div v-if="upcomingJobs.length" class="section-head" :class="{ 'section-head--spaced': inProgressJobs.length }">
        <span class="t-micro">À venir</span>
        <span class="t-micro t-ink-400">{{ upcomingJobs.length }}</span>
      </div>
      <JobCard v-for="(job, idx) in upcomingJobs" :key="job.name"
        :job="job" :status="upcomingStatus(job)" :index="idx + 1"
        @click="openSheet(job)" />

      <!-- Completed (collapsible) -->
      <div v-if="completedJobs.length" class="section-head section-head--spaced"
        @click="showCompleted = !showCompleted" style="cursor:pointer">
        <span class="t-micro">Terminées</span>
        <span class="t-micro t-ink-400">{{ completedJobs.length }}</span>
        <span class="material-icons section-head-chevron" :class="{ 'is-open': showCompleted }">expand_more</span>
      </div>
      <template v-if="showCompleted">
        <JobCard v-for="job in completedJobs" :key="job.name"
          :job="job" status="go" done
          @click="openSheet(job)" />
      </template>

      <!-- Empty state -->
      <div v-if="!loading && jobs.length === 0" class="empty-state">
        <div class="empty-icon"><span class="material-icons">event_available</span></div>
        <div class="t-h2 t-ink-900" style="margin-top:16px">Aucune tâche aujourd'hui</div>
        <div class="t-caption" style="margin-top:4px">Profite de la pause ☕</div>
      </div>

      <!-- Group-subscription feed: unassigned jobs the tech can claim. -->
      <!-- Lives below the personal list so techs check it deliberately. -->
      <div v-if="groupJobs.length" class="section-head section-head--spaced"
        @click="showGroupJobs = !showGroupJobs" style="cursor:pointer">
        <span class="material-icons" style="font-size:18px;vertical-align:-4px;margin-right:4px">groups</span>
        <span class="t-micro">Tâches du groupe</span>
        <span class="t-micro t-ink-400">{{ groupJobs.length }} disponible{{ groupJobs.length > 1 ? 's' : '' }}</span>
        <span class="material-icons section-head-chevron" :class="{ 'is-open': showGroupJobs }">expand_more</span>
      </div>
      <template v-if="showGroupJobs">
        <div v-for="job in groupJobs" :key="'grp-' + job.name" class="group-card">
          <div class="group-card-row">
            <div class="col" style="min-width:0">
              <div class="t-caption t-ink-600">{{ job.assigned_group || 'Non assigné' }}
                <span v-if="job.priority === 'urgent' || job.priority === 'high'"
                  class="tg-chip is-compact is-stop"
                  style="margin-left:6px">
                  Urgent
                </span>
              </div>
              <div class="t-body t-ink-900" style="font-weight:600;margin-top:2px">{{ job.subject }}</div>
              <div class="t-caption t-ink-500" style="margin-top:2px">
                <template v-if="job.customer_name">{{ job.customer_name }}</template>
                <template v-if="job.service_location_name"> · {{ job.service_location_name }}</template>
              </div>
              <div class="t-micro t-ink-400" style="margin-top:2px">
                <template v-if="job.scheduled_date">
                  <span class="material-icons" style="font-size:12px;vertical-align:-2px">event</span>
                  {{ fmtDate(job.scheduled_date) }}{{ job.scheduled_time ? ' · ' + fmtTime(job.scheduled_time) : '' }}
                </template>
                <template v-else>Non planifié</template>
              </div>
            </div>
            <button class="tg-btn is-primary is-compact" :disabled="claimingJob === job.name" @click="claimJob(job)">
              <span v-if="claimingJob === job.name" class="material-icons spinning" style="font-size:16px">autorenew</span>
              <span v-else class="material-icons" style="font-size:16px">pan_tool</span>
              Prendre
            </button>
          </div>
        </div>
      </template>
      <div v-if="!loading && jobs.length === 0 && groupJobs.length === 0" class="empty-state-hint">
        <span class="t-caption t-ink-500">Aucune tâche disponible dans vos groupes non plus.</span>
      </div>
    </div>

    <!-- Peek sheet: job detail -->
    <transition name="fade">
      <div v-if="sheetOpen" class="tg-peek-backdrop is-open" @click="sheetOpen = false" />
    </transition>
    <div class="tg-peek" :class="{ 'is-open': sheetOpen }" v-if="sheetJob">
      <div class="tg-peek-handle" />
      <div style="padding: 8px 20px 20px">
        <!-- Title row -->
        <div class="peek-title-row">
          <div>
            <div class="t-caption" style="color:var(--tg-700);font-weight:700">{{ sheetJob.name }}</div>
            <div class="t-h2" style="margin-top:2px">{{ sheetJob.subject || 'Sans titre' }}</div>
          </div>
          <button class="peek-close" @click="sheetOpen = false" aria-label="Fermer">
            <span class="material-icons">close</span>
          </button>
        </div>

        <!-- Priority chip -->
        <div v-if="sheetJob.priority === 'urgent' || sheetJob.priority === 'high'" style="margin-top:8px">
          <span class="tg-chip" :class="sheetJob.priority === 'urgent' ? 'is-stop' : 'is-work'">
            <span class="tg-chip-dot" />
            {{ sheetJob.priority === 'urgent' ? 'Urgent' : 'Haute priorité' }}
          </span>
        </div>

        <!-- Info rows -->
        <div v-if="sheetJob.service_location_name" class="info-row">
          <span class="material-icons info-icon" style="color:var(--sig-stop)">place</span>
          <div class="col">
            <div class="t-caption">Adresse</div>
            <div class="t-body">{{ sheetJob.service_location_name }}</div>
          </div>
          <button class="tg-btn is-ghost is-compact" @click="openGps(sheetJob)">
            <span class="material-icons" style="font-size:18px">navigation</span>
            Carte
          </button>
        </div>

        <div class="info-row-pair">
          <div v-if="sheetJob.scheduled_time" class="info-row">
            <span class="material-icons info-icon">schedule</span>
            <div>
              <div class="t-caption">Heure</div>
              <div class="t-body">{{ fmtTime(sheetJob.scheduled_time) }}</div>
            </div>
          </div>
          <div v-if="sheetJob.customer_name" class="info-row">
            <span class="material-icons info-icon">person</span>
            <div>
              <div class="t-caption">Client</div>
              <div class="t-body">{{ sheetJob.customer_name }}</div>
            </div>
          </div>
        </div>

        <!-- Description -->
        <div v-if="sheetJob.description" class="info-row-stack">
          <div class="t-caption">Notes</div>
          <div class="t-body-sm" style="white-space:pre-wrap">{{ sheetJob.description }}</div>
        </div>

        <!-- Primary action (contextual to status) -->
        <div style="display:flex;flex-direction:column;gap:10px;margin-top:20px">
          <button v-if="isScheduled(sheetJob)"
            class="tg-btn is-primary is-block"
            @click="doStatus(sheetJob, 'In Progress')" :disabled="saving">
            <span class="material-icons">directions_car</span>
            En route
          </button>
          <button v-if="isInProgress(sheetJob)"
            class="tg-btn is-success is-block"
            @click="doStatus(sheetJob, 'Completed')" :disabled="saving">
            <span class="material-icons">check_circle</span>
            Marquer comme terminée
          </button>

          <!-- Testing shortcut: direct Completed from any non-done state -->
          <button v-if="canTerminateDirect(sheetJob)"
            class="tg-btn is-outline is-compact"
            @click="doStatus(sheetJob, 'Completed')" :disabled="saving">
            <span class="material-icons" style="font-size:18px">fast_forward</span>
            Terminer directement (test)
          </button>

          <button v-if="sheetJob.status === 'Completed'"
            class="tg-btn is-ghost is-block"
            @click="doStatus(sheetJob, 'In Progress')" :disabled="saving">
            <span class="material-icons">replay</span>
            Rouvrir
          </button>

          <!-- Secondary actions -->
          <div class="peek-actions-row">
            <button class="tg-btn is-outline is-compact" @click="goScan(sheetJob)">
              <span class="material-icons" style="font-size:18px">qr_code_scanner</span>
              Scanner
            </button>
            <button class="tg-btn is-outline is-compact" @click="goDetail(sheetJob)">
              <span class="material-icons" style="font-size:18px">open_in_full</span>
              Détails
            </button>
          </div>
        </div>
      </div>
    </div>
  </q-page>
</template>

<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { Notify } from 'quasar'
import { BASE_URL } from 'src/config/erpnext'
import { HUB_URL } from 'src/config/hub'
import { fmtClock as fmtTime } from 'src/composables/useFormatters' // « 8:30 »→« 8h30 » (source unique)
import JobCard from '../components/JobCard.vue'

const props = defineProps({
  token: { type: String, default: '' },
})

const router = useRouter()
const loading = ref(false)
const jobs = ref([])
const saving = ref(false)
const isOnline = ref(navigator.onLine)
const showCompleted = ref(false)
const magicTechId = ref('')

// Group subscription feed — unassigned claimable jobs.
// Showing them below the personal list acts as an opt-in subscription:
// techs with idle time scroll down and pick up extra work without a
// dispatcher round-trip.
const groupJobs = ref([])
const showGroupJobs = ref(true)
const claimingJob = ref('')

// Peek sheet state
const sheetOpen = ref(false)
const sheetJob = ref(null)

// Tech identity
const techName = ref('Technicien')
const initials = computed(() => {
  const parts = techName.value.split(' ')
  return parts.length >= 2
    ? (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
    : techName.value.slice(0, 2).toUpperCase()
})

// ── Progress arc math ──────────────────────────────────────────────────────
const ARC_SIZE = 96
const ARC_R = 50
const arcCircumference = 2 * Math.PI * ARC_R
const arcPct = computed(() => jobs.value.length ? doneCount.value / jobs.value.length : 0)
const arcOffset = computed(() => arcCircumference * (1 - arcPct.value))

// ── Date + counts ──────────────────────────────────────────────────────────
const today = new Date().toISOString().slice(0, 10)
const todayLabel = computed(() =>
  new Date().toLocaleDateString('fr-CA', { weekday: 'long', day: 'numeric', month: 'long' })
)

const doneCount = computed(() => jobs.value.filter(j => j.status === 'Completed').length)

const inProgressJobs = computed(() =>
  jobs.value.filter(j => j.status === 'In Progress' || j.status === 'in_progress')
)
const upcomingJobs = computed(() =>
  jobs.value
    .filter(j => ['open', 'Scheduled', 'assigned', 'Open', 'Assigned'].includes(j.status))
    .sort((a, b) => (a.scheduled_time || '').localeCompare(b.scheduled_time || ''))
)
const completedJobs = computed(() => jobs.value.filter(j => j.status === 'Completed'))

const heroHeadline = computed(() => {
  if (!jobs.value.length) return 'Aucune tâche'
  if (doneCount.value === jobs.value.length) return 'Journée terminée 🎉'
  if (doneCount.value === 0) return `${upcomingJobs.value.length + inProgressJobs.value.length} à faire`
  return `${doneCount.value} sur ${jobs.value.length} terminées`
})
const heroSubline = computed(() => {
  const next = inProgressJobs.value[0] || upcomingJobs.value[0]
  if (!next) return 'Profite de la pause ☕'
  return `Prochain · ${fmtTime(next.scheduled_time) || 'dès que possible'}`
})

// ── Card status classification ─────────────────────────────────────────────
function upcomingStatus (job) {
  if (job.priority === 'urgent') return 'stop'
  if (job.priority === 'high') return 'work'
  return 'tg'
}
function isScheduled (j) { return ['Scheduled', 'assigned', 'open', 'Open', 'Assigned'].includes(j.status) }
function isInProgress (j) { return ['In Progress', 'in_progress'].includes(j.status) }
function canTerminateDirect (j) {
  // Raccourci de TEST seulement (dev) — saut direct open→Completed ; jamais livré en prod.
  return import.meta.env.DEV && j.status && j.status !== 'Completed' && j.status !== 'Cancelled'
}

// fmtTime → composables/useFormatters (fmtClock)
function fmtDate (iso) {
  if (!iso) return ''
  try {
    const d = new Date(iso + 'T00:00:00')
    return d.toLocaleDateString('fr-CA', { weekday: 'short', day: 'numeric', month: 'short' })
  } catch { return iso }
}

function openSheet (job) { sheetJob.value = job; sheetOpen.value = true }

// ── API calls ──────────────────────────────────────────────────────────────
async function apiFetch (url) {
  const res = await fetch(BASE_URL + url)
  if (!res.ok) throw new Error('API ' + res.status)
  return (await res.json()).data || []
}

/**
 * Update job status through targo-hub (not direct ERPNext PUT).
 * The /dispatch/job-status endpoint is the canonical write path:
 *   1. PUTs the status change to ERPNext
 *   2. Walks the chain (unblocks depends_on children on Completed)
 *   3. Broadcasts SSE so other clients refresh in real time
 * Going direct to ERPNext would skip steps 2+3 and the chain wouldn't walk.
 */
async function apiSetStatus (jobName, status) {
  const res = await fetch(`${HUB_URL}/dispatch/job-status`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ job: jobName, status }),
  })
  if (!res.ok) {
    const err = await res.text().catch(() => 'Unknown error')
    throw new Error(`Status update failed (${res.status}): ${err}`)
  }
  return res.json()
}

async function verifyMagicToken (token) {
  try {
    const res = await fetch(`${HUB_URL}/magic-link/verify?token=${encodeURIComponent(token)}`)
    if (!res.ok) return null
    const data = await res.json()
    return data.ok ? data.tech_id : null
  } catch { return null }
}

async function loadTasks () {
  loading.value = true
  try {
    let techId = ''

    if (props.token) {
      techId = await verifyMagicToken(props.token)
      if (!techId) {
        Notify.create({ type: 'negative', message: 'Lien expiré ou invalide. Demandez un nouveau lien par SMS.', timeout: 6000 })
        loading.value = false
        return
      }
      magicTechId.value = techId
      try {
        const techDoc = await apiFetch('/api/resource/Dispatch Technician/' + encodeURIComponent(techId) + '?fields=["name","full_name"]')
        if (techDoc && techDoc.full_name) techName.value = techDoc.full_name
        else techName.value = techId
      } catch { techName.value = techId }
    } else {
      // Authentik session fallback
      try {
        const me = await fetch(BASE_URL + '/api/method/frappe.auth.get_logged_user')
        if (me.ok) {
          const u = await me.json()
          const userName = u.message || ''
          if (userName && userName !== 'authenticated') {
            try {
              const techs = await apiFetch('/api/resource/Dispatch Technician?filters=[["user","=","' + userName + '"]]&fields=["name","full_name"]&limit_page_length=1')
              if (techs.length) {
                techId = techs[0].name
                techName.value = techs[0].full_name || prettify(userName)
              } else {
                techName.value = prettify(userName)
              }
            } catch { techName.value = prettify(userName) }
          }
        }
      } catch {}
    }

    // Filter: open/scheduled/in-progress/completed (everything except 'On Hold' which
    // is the chain-gated pending state — those shouldn't show in the active list)
    const statusFilter = ['open', 'Open', 'Scheduled', 'assigned', 'Assigned', 'In Progress', 'in_progress', 'Completed']
    const filterArr = [
      ['scheduled_date', '=', today],
      ['status', 'in', statusFilter],
    ]
    if (techId) filterArr.push(['assigned_tech', '=', techId])

    const params = new URLSearchParams({
      fields: JSON.stringify(['name', 'subject', 'status', 'customer', 'customer_name',
        'service_location', 'service_location_name', 'scheduled_time', 'description',
        'job_type', 'duration_h', 'priority', 'depends_on', 'step_order']),
      filters: JSON.stringify(filterArr),
      limit_page_length: 50,
      order_by: 'scheduled_time asc',
    })
    jobs.value = await apiFetch('/api/resource/Dispatch Job?' + params)

    // Load the group-subscription feed alongside the personal list. Fire-and-
    // forget — if it fails we just hide the section instead of blocking the
    // main UX.
    await loadGroupJobs().catch(() => {})
  } catch (e) {
    Notify.create({ type: 'warning', message: 'Erreur chargement: ' + e.message })
  } finally {
    loading.value = false
  }
}

/**
 * Pull unassigned claimable jobs from the hub. The hub endpoint filters by
 * status ∈ {open, Scheduled} and assigned_tech empty — it does NOT filter by
 * the tech's own group yet (we don't track tech→group membership yet; that's
 * a future tags/skills layer). In the meantime techs see the whole pool with
 * the `assigned_group` badge visible so they can self-select what they can
 * handle.
 */
async function loadGroupJobs () {
  try {
    const res = await fetch(`${HUB_URL}/dispatch/group-jobs`)
    if (!res.ok) { groupJobs.value = []; return }
    const data = await res.json()
    groupJobs.value = (data.jobs || []).filter(j => j.assigned_group)
  } catch {
    groupJobs.value = []
  }
}

async function claimJob (job) {
  const techId = magicTechId.value
  if (!techId) {
    Notify.create({ type: 'warning', message: 'Identité technicien introuvable — connexion requise.' })
    return
  }
  claimingJob.value = job.name
  try {
    const res = await fetch(`${HUB_URL}/dispatch/claim-job`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ job: job.name, tech_id: techId }),
    })
    const data = await res.json().catch(() => ({}))
    if (!res.ok) {
      if (res.status === 409) {
        Notify.create({ type: 'warning', message: data.error || 'Déjà pris.', timeout: 4500 })
      } else {
        Notify.create({ type: 'negative', message: 'Échec: ' + (data.error || res.status) })
      }
      await loadTasks()
      return
    }
    Notify.create({ type: 'positive', message: `Tâche prise en charge: ${job.subject}`, icon: 'check' })
    // Optimistic: remove from the group list, reload personal list
    groupJobs.value = groupJobs.value.filter(j => j.name !== job.name)
    await loadTasks()
  } catch (e) {
    Notify.create({ type: 'negative', message: 'Erreur réseau: ' + e.message })
  } finally {
    claimingJob.value = ''
  }
}

function prettify (email) {
  return email.split('@')[0].replace(/[._]/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
}

async function doStatus (job, status) {
  saving.value = true
  try {
    const result = await apiSetStatus(job.name, status)
    job.status = status
    const msgs = {
      'In Progress': 'En route ! 🚗',
      Completed: result?.unblocked?.length
        ? `Tâche terminée — ${result.unblocked.length} prochaine${result.unblocked.length > 1 ? 's' : ''} tâche${result.unblocked.length > 1 ? 's' : ''} débloquée${result.unblocked.length > 1 ? 's' : ''}`
        : 'Tâche terminée ✓',
    }
    Notify.create({ type: 'positive', message: msgs[status] || status })
    if (status === 'Completed') {
      sheetOpen.value = false
      // Reload to pick up unblocked siblings/children
      await loadTasks()
    }
  } catch (e) {
    Notify.create({ type: 'negative', message: 'Erreur: ' + e.message })
  } finally {
    saving.value = false
  }
}

function openGps (job) {
  const loc = job.service_location_name || ''
  window.open(`https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(loc)}`, '_blank')
}

function goScan (job) {
  sheetOpen.value = false
  router.push({ path: '/j/scan', query: {
    job: job.name, customer: job.customer, customer_name: job.customer_name,
    location: job.service_location, location_name: job.service_location_name,
  }})
}

function goDetail (job) {
  sheetOpen.value = false
  router.push({ path: '/j/job/' + job.name })
}

// ── SSE: real-time chain updates ───────────────────────────────────────────
// When another surface (dispatcher, another tech, webhook) completes a job,
// we receive `job-unblocked` events for any dependents that just opened up.
// Just refresh the list — cheaper than surgical state merging and avoids
// race conditions when multiple events arrive in quick succession.
let sseSource = null
let sseOpened = false        // 1re connexion vs RE-connexion (→ resync)
let sseReconnect = null
function connectSSE () {
  try {
    sseSource = new EventSource(`${HUB_URL}/sse?topics=dispatch`)
    sseSource.onopen = () => {
      // Resync sur RE-connexion (ex. après un redéploiement du hub) : le hub ne rejoue pas les événements
      // manqués pendant la coupure, donc on recharge la liste pour rattraper les statuts changés entre-temps.
      if (sseOpened) loadTasks()
      sseOpened = true
    }
    sseSource.addEventListener('job-unblocked', (e) => {
      try {
        const data = JSON.parse(e.data)
        // Only refresh if it's for us (or no tech specified — conservative refresh)
        if (!data.tech || data.tech === magicTechId.value) {
          loadTasks()
          Notify.create({
            type: 'info',
            message: `Nouvelle tâche débloquée : ${data.subject || data.job}`,
            icon: 'flag',
            timeout: 4000,
          })
        }
      } catch {}
    })
    sseSource.addEventListener('job-status', (e) => {
      // Picks up status changes made by dispatchers or other techs
      try {
        const data = JSON.parse(e.data)
        const local = jobs.value.find(j => j.name === data.job)
        if (local && local.status !== data.status) local.status = data.status
      } catch {}
    })
    sseSource.addEventListener('job-claimed', (e) => {
      // Another tech took a job → drop it from our claimable feed so we
      // don't try to grab something someone else already has.
      try {
        const data = JSON.parse(e.data)
        if (data.tech !== magicTechId.value) {
          groupJobs.value = groupJobs.value.filter(j => j.name !== data.job)
        }
      } catch {}
    })
    // Survit aux redéploiements du hub : si le flux meurt DÉFINITIVEMENT (502 pendant le restart → CLOSED,
    // plus de reconnexion native), on relance après 3 s — sinon le technicien perd le temps réel (nouvelles
    // tâches, changements de statut) jusqu'au prochain rechargement manuel. onopen resynchronisera.
    sseSource.onerror = () => {
      if (sseSource && sseSource.readyState === EventSource.CLOSED) {
        sseSource = null
        if (sseReconnect) clearTimeout(sseReconnect)
        sseReconnect = setTimeout(connectSSE, 3000)
      }
    }
  } catch {}
}
function disconnectSSE () {
  if (sseReconnect) { clearTimeout(sseReconnect); sseReconnect = null }
  if (sseSource) { sseSource.close(); sseSource = null }
}

onMounted(() => {
  window.addEventListener('online', () => { isOnline.value = true })
  window.addEventListener('offline', () => { isOnline.value = false })
  loadTasks()
  connectSSE()
})
onUnmounted(disconnectSSE)
</script>

<style lang="scss" scoped>
// Group subscription card — claimable jobs visually distinct from personal ones
.group-card {
  margin-top: 10px;
  padding: 12px 14px;
  background: var(--tg-bg-card, #fff);
  border: 1px dashed var(--tg-border, #d0d7de);
  border-radius: 12px;
  box-shadow: 0 1px 2px rgba(15, 23, 42, .04);
}
.group-card-row {
  display: flex; align-items: center; gap: 12px;
}
.empty-state-hint {
  margin-top: 16px;
  text-align: center;
}
.tg-chip.is-compact {
  padding: 2px 8px;
  font-size: 11px;
  border-radius: 999px;
  display: inline-flex; align-items: center; gap: 4px;
}
// `.spinning` + `@keyframes spin` already defined below for the hero refresh button.

// Hero row layouts (the rest is from tech.scss global tokens)
.hero-top {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 22px;
}
.hero-left {
  display: flex; align-items: center; gap: 12px;
}
.hero-name {
  font: 700 1.125rem/1.2 system-ui, -apple-system, sans-serif;
  color: #fff; letter-spacing: -0.005em;
}
.hero-refresh {
  width: 40px; height: 40px;
  border-radius: 50%;
  border: 0; background: rgba(255,255,255,0.12);
  color: #fff;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer;
  transition: background 160ms;
  &:active { background: rgba(255,255,255,0.22); }
  &:disabled { opacity: 0.6; }
  .spinning { animation: spin 0.9s linear infinite; }
}
@keyframes spin { to { transform: rotate(360deg); } }

.hero-progress {
  display: flex; align-items: center; gap: 18px;
}
.hero-progress-text { flex: 1; min-width: 0; }

.arc-numerator {
  font: 800 1.75rem/1 system-ui, -apple-system, sans-serif;
  color: #fff; letter-spacing: -0.02em;
}
.arc-denom {
  font: 600 0.75rem/1 system-ui, -apple-system, sans-serif;
  color: rgba(255,255,255,0.65); margin-top: 2px;
}

// Content padding (hero handles top; q-page bottom-reserve handles tab-island)
.tg-page--content {
  padding: 20px 16px 8px;
}

// Section header
.section-head {
  display: flex; align-items: center; gap: 8px;
  padding: 0 4px;
  margin-bottom: 10px;
  &--spaced { margin-top: 22px; }
  .section-head-chevron {
    margin-left: auto;
    font-size: 20px; color: var(--ink-400);
    transition: transform 200ms;
    &.is-open { transform: rotate(180deg); }
  }
}

// Peek sheet internals
.peek-title-row {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
}
.peek-close {
  width: 36px; height: 36px;
  border: 0; background: var(--surface-wash);
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer; color: var(--ink-700);
  &:active { background: var(--tg-100); }
}

.info-row {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 0;
  border-bottom: 1px solid rgba(15,23,42,0.05);
  .col { flex: 1; min-width: 0; }
  .info-icon { font-size: 20px; color: var(--ink-500); }
}
.info-row-pair {
  display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
  .info-row { border-bottom: 1px solid rgba(15,23,42,0.05); padding: 12px 0; }
}
.info-row-stack {
  padding: 12px 0;
  border-bottom: 1px solid rgba(15,23,42,0.05);
  .t-body-sm { margin-top: 4px; }
}
.peek-actions-row {
  display: grid; grid-template-columns: 1fr 1fr; gap: 10px;
}

// Empty state
.empty-state {
  text-align: center; padding: 60px 24px;
  .empty-icon {
    width: 72px; height: 72px;
    border-radius: 50%;
    background: var(--tg-50);
    display: inline-flex; align-items: center; justify-content: center;
    margin-bottom: 8px;
    .material-icons { font-size: 36px; color: var(--tg-500); }
  }
}

// Transitions
.fade-enter-active, .fade-leave-active { transition: opacity 200ms; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
.slide-down-enter-active, .slide-down-leave-active {
  transition: transform 240ms cubic-bezier(0.32, 0.72, 0, 1), opacity 200ms;
}
.slide-down-enter-from, .slide-down-leave-to { transform: translateY(-100%); opacity: 0; }
</style>
