<template>
  <!-- Quick actions -->
  <div class="issue-actions q-mb-sm">
    <q-btn v-if="doc.status !== 'Closed' && doc.status !== 'Resolved'" dense unelevated no-caps
      icon="check_circle" label="Fermer le ticket" color="green-7" size="sm"
      @click="closeTicket" :loading="closingTicket" />
    <q-btn v-else dense unelevated no-caps
      icon="refresh" label="Réouvrir" color="orange-7" size="sm"
      @click="reopenTicket" :loading="closingTicket" />
    <FlowQuickButton flat dense size="sm" icon="account_tree" label="Flow"
      tooltip="Lancer / modifier un flow pour ce ticket"
      category="incident" applies-to="Issue" trigger-event="on_issue_opened" />
  </div>

  <div class="issue-field-grid">
    <div class="if-row">
      <span class="if-label">Statut</span>
      <InlineField :value="doc.status" field="status" doctype="Issue" :docname="docName"
        type="select" :options="['Open', 'Replied', 'On Hold', 'Resolved', 'Closed']"
        @saved="v => doc.status = v.value" />
    </div>
    <div class="if-row">
      <span class="if-label">Priorite</span>
      <InlineField :value="doc.priority" field="priority" doctype="Issue" :docname="docName"
        type="select" :options="['Low', 'Medium', 'High', 'Urgent']"
        @saved="v => doc.priority = v.value" />
    </div>
    <div class="if-row">
      <span class="if-label">Ouvert le</span>
      <span class="if-value">{{ formatDate(doc.opening_date) }}</span>
    </div>
    <div class="if-row" v-if="doc.resolution_time">
      <span class="if-label">Resolu le</span>
      <span class="if-value">{{ doc.resolution_time }}</span>
    </div>
    <div class="if-row">
      <span class="if-label">Type</span>
      <InlineField :value="doc.issue_type" field="issue_type" doctype="Issue" :docname="docName"
        type="select" :options="['Support', 'Installation', 'Demenagement', 'Facturation', 'Reseau', 'Autre']"
        placeholder="Type" @saved="v => doc.issue_type = v.value" />
    </div>
    <div class="if-row" v-if="doc.service_location">
      <span class="if-label">Adresse</span>
      <span class="if-value">{{ doc.service_location }}</span>
    </div>
    <div class="if-row" v-if="doc.raised_by">
      <span class="if-label">Soumis par</span>
      <span class="if-value">{{ doc.raised_by }}</span>
    </div>
    <div class="if-row">
      <span class="if-label">Assigné à</span>
      <q-select v-model="assignees" dense borderless multiple use-chips emit-value map-options
        :options="userOptions" option-value="value" option-label="label"
        input-class="editable-input" style="min-width:140px;flex:1"
        :loading="loadingUsers"
        @update:model-value="saveAssignees">
        <template #selected-item="scope">
          <q-chip dense removable size="sm" color="indigo-1" text-color="indigo-8"
            @remove="scope.removeAtIndex(scope.index)">
            {{ scope.opt.label || scope.opt }}
          </q-chip>
        </template>
      </q-select>
    </div>
  </div>

  <!-- Tags -->
  <div class="q-mt-sm">
    <div class="info-block-title" style="display:flex;align-items:center">
      Tags
      <q-btn flat dense round size="xs" icon="settings" color="grey-5" class="q-ml-xs"
        @click="showTagManager = !showTagManager">
        <q-tooltip>Gérer les tags</q-tooltip>
      </q-btn>
    </div>
    <div class="row items-center q-gutter-xs q-mb-xs">
      <q-chip v-for="tag in currentTags" :key="tag" dense removable size="sm"
        :style="{ background: getTagColor(tag), color: '#fff' }"
        @remove="removeTag(tag)">
        {{ tag }}
      </q-chip>
    </div>
    <q-select dense outlined use-input input-debounce="100" emit-value map-options
      :options="filteredTagOptions" option-value="value" option-label="label"
      :model-value="null" :loading="loadingTags"
      placeholder="Ajouter un tag..."
      style="max-width:250px" input-class="text-caption"
      @filter="filterTags" @update:model-value="addTag"
      new-value-mode="add-unique" @new-value="addNewTag">
      <template #option="scope">
        <q-item v-bind="scope.itemProps" dense>
          <q-item-section side>
            <div style="width:10px;height:10px;border-radius:50%" :style="{ background: scope.opt.color || '#6b7280' }" />
          </q-item-section>
          <q-item-section>
            <q-item-label>{{ scope.opt.label }}</q-item-label>
            <q-item-label caption v-if="scope.opt.category">{{ scope.opt.category }}</q-item-label>
          </q-item-section>
        </q-item>
      </template>
      <template #no-option>
        <q-item dense>
          <q-item-section class="text-caption text-grey-6">Tapez pour créer un tag</q-item-section>
        </q-item>
      </template>
    </q-select>

    <!-- Tag Manager -->
    <div v-if="showTagManager" class="tag-manager q-mt-sm">
      <div class="text-caption text-weight-bold text-grey-7 q-mb-xs">Gérer les tags ({{ allDispatchTags.length }})</div>
      <div v-for="t in allDispatchTags" :key="t.name" class="tag-mgr-row">
        <div class="tag-mgr-dot" :style="{ background: t.color || '#6b7280' }"
          @click="cycleTagColor(t)">
          <q-tooltip>Changer la couleur</q-tooltip>
        </div>
        <span v-if="editingTagName !== t.name" class="tag-mgr-label" @dblclick="startEditTag(t)">{{ t.label }}</span>
        <input v-else v-model="editingTagValue" class="tag-mgr-edit"
          @keyup.enter="saveTagRename(t)" @keyup.escape="editingTagName = null" @blur="saveTagRename(t)" />
        <span class="tag-mgr-cat text-caption text-grey-5">{{ t.category }}</span>
        <q-btn flat dense round size="xs" icon="delete" color="red-4"
          @click="deleteDispatchTag(t)" class="tag-mgr-del">
          <q-tooltip>Supprimer ce tag</q-tooltip>
        </q-btn>
      </div>
      <div v-if="!allDispatchTags.length" class="text-caption text-grey-5">Aucun tag</div>
    </div>
  </div>

  <!-- ═══ DISPATCH / PROJECT TASKS ═══ -->
  <div class="q-mt-md dispatch-section">
    <div class="row items-center q-mb-xs">
      <div class="info-block-title" style="margin-bottom:0">
        <q-icon name="account_tree" size="16px" class="q-mr-xs" />
        Tâches ({{ dispatchJobs.length }})
      </div>
      <q-space />
      <q-btn-group unelevated dense>
        <q-btn dense size="sm" icon="playlist_add" label="Projet"
          color="green-7" no-caps @click="showProjectWizard = true">
          <q-tooltip>Créer un projet (modèle multi-étapes)</q-tooltip>
        </q-btn>
        <q-btn dense size="sm" icon="add" label="Tâche"
          color="indigo-6" no-caps @click="showCreateDialog = true">
          <q-tooltip>Ajouter une tâche simple</q-tooltip>
        </q-btn>
      </q-btn-group>
    </div>

    <!-- Nested task tree -->
    <div v-if="dispatchJobs.length" class="dispatch-jobs-list">
      <!-- Root jobs (no parent) -->
      <template v-for="job in rootJobs" :key="job.name">
        <TaskNode :job="job" :all-jobs="dispatchJobs" :depth="0"
          @fire-webhook="fireWebhook" @deleted="onJobDeleted" @updated="onJobUpdated" />
      </template>

      <!-- Orphan jobs (parent_job references something outside this ticket) -->
      <template v-for="job in orphanJobs" :key="job.name">
        <TaskNode :job="job" :all-jobs="dispatchJobs" :depth="0"
          @fire-webhook="fireWebhook" @deleted="onJobDeleted" @updated="onJobUpdated" />
      </template>
    </div>

    <div v-else class="text-caption text-grey-5 q-pa-sm">
      Aucune tâche liée à ce ticket.
    </div>
  </div>

  <div class="q-mt-sm">
    <div class="info-block-title">Sujet</div>
    <InlineField :value="doc.subject" field="subject" doctype="Issue" :docname="docName"
      placeholder="Sujet du ticket" @saved="v => doc.subject = v.value" />
  </div>
  <div v-if="doc.issue_split_from" class="q-mt-md">
    <div class="info-block-title">Ticket parent</div>
    <div class="text-body2 erp-link" style="cursor:pointer" @click="$emit('navigate', 'Issue', doc.issue_split_from)">
      {{ doc.issue_split_from }}
    </div>
  </div>
  <div class="q-mt-md">
    <div class="info-block-title">Description</div>
    <InlineField :value="doc.description || ''" field="description" doctype="Issue" :docname="docName"
      type="textarea" placeholder="Ajouter une description..."
      @saved="v => doc.description = v.value" />
  </div>
  <div v-if="doc.resolution_details" class="q-mt-md">
    <div class="info-block-title">Resolution</div>
    <div class="modal-desc" v-html="doc.resolution_details"></div>
  </div>
  <div v-if="comms.length" class="q-mt-md">
    <div class="info-block-title">Echanges ({{ comms.length }})</div>
    <div v-for="comm in comms" :key="comm.name" class="comm-row">
      <div class="row items-start">
        <div class="col">
          <div class="text-caption text-weight-bold">{{ comm.sender || comm.owner }}</div>
          <div class="modal-desc q-mt-xs" v-html="comm.content"></div>
        </div>
        <div class="col-auto text-caption text-grey-6 text-right" style="min-width:90px">
          {{ formatDate(comm.creation) }}
        </div>
      </div>
    </div>
  </div>
  <div v-if="files.length" class="q-mt-md">
    <div class="info-block-title">Pieces jointes ({{ files.length }})</div>
    <div v-for="f in files" :key="f.name" class="q-py-xs">
      <a :href="erpFileUrl(f.file_url)" target="_blank" class="erp-link">
        <q-icon name="attach_file" size="14px" /> {{ f.file_name }}
      </a>
    </div>
  </div>
  <div v-if="comments.length" class="q-mt-md">
    <div class="info-block-title">Fil de discussion ({{ comments.length }})</div>
    <div v-for="c in comments" :key="c.name" class="thread-msg">
      <div class="thread-header">
        <q-icon name="person" size="14px" color="grey-6" class="q-mr-xs" />
        <span class="text-weight-bold">{{ c.comment_by || 'Systeme' }}</span>
        <span class="text-grey-5 q-ml-auto">{{ formatDateTime(c.creation) }}</span>
      </div>
      <div class="thread-body" v-html="c.content"></div>
    </div>
  </div>
  <div v-if="!doc.description && !doc.resolution_details && !comms.length && !comments.length" class="text-center text-grey-5 q-pa-lg">
    Aucun contenu pour ce ticket
  </div>

  <!-- Delete -->
  <div class="q-mt-lg" style="border-top:1px solid #fee2e2;padding-top:12px">
    <q-btn flat dense no-caps size="sm" icon="delete" label="Supprimer ce ticket"
      color="red-6" @click="confirmDelete">
      <q-tooltip>Suppression définitive du ticket</q-tooltip>
    </q-btn>
  </div>

  <div class="q-mt-md reply-box">
    <div class="info-block-title">Repondre</div>
    <q-input v-model="replyContent" dense outlined type="textarea" autogrow
      placeholder="Ecrire une reponse..."
      :input-style="{ fontSize: '0.85rem', minHeight: '50px' }"
      @keydown.ctrl.enter="sendReply" @keydown.meta.enter="sendReply" />
    <div class="row justify-end q-mt-xs">
      <q-btn unelevated dense size="sm" label="Envoyer" color="indigo-6" icon="send"
        :disable="!replyContent?.trim()" :loading="sendingReply" @click="sendReply" />
    </div>
  </div>

  <!-- Create single Dispatch Job dialog -->
  <UnifiedCreateModal
    v-model="showCreateDialog"
    mode="work-order"
    :context="dispatchContext"
    :existing-jobs="dispatchJobs"
    @created="onJobCreated"
  />

  <!-- Project Wizard (multi-step template) -->
  <ProjectWizard
    v-model="showProjectWizard"
    :issue="doc"
    :existing-jobs="dispatchJobs"
    @created="onJobCreated"
  />
</template>

<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { Notify, Dialog } from 'quasar'
import { formatDate, formatDateTime, erpFileUrl } from 'src/composables/useFormatters'
import { createDoc, updateDoc, deleteDoc, listDocs } from 'src/api/erp'
import { authFetch } from 'src/api/auth'
import { useAuthStore } from 'src/stores/auth'
import { BASE_URL } from 'src/config/erpnext'
import { fetchTags, updateTag, renameTag, deleteTag as deleteTagApi } from 'src/api/dispatch'
import InlineField from 'src/components/shared/InlineField.vue'
import UnifiedCreateModal from 'src/components/shared/UnifiedCreateModal.vue'
import FlowQuickButton from 'src/components/flow-editor/FlowQuickButton.vue'
import ProjectWizard from 'src/components/shared/ProjectWizard.vue'
import TaskNode from 'src/components/shared/TaskNode.vue'

const props = defineProps({
  doc: { type: Object, required: true },
  docName: String,
  title: String,
  comments: { type: Array, default: () => [] },
  comms: { type: Array, default: () => [] },
  files: { type: Array, default: () => [] },
  dispatchJobs: { type: Array, default: () => [] },
})
const emit = defineEmits(['navigate', 'reply-sent', 'dispatch-created', 'dispatch-deleted', 'dispatch-updated', 'deleted'])

const replyContent = ref('')
const sendingReply = ref(false)
const showCreateDialog = ref(false)
const showProjectWizard = ref(false)

const dispatchContext = computed(() => ({
  subject: props.doc?.subject || '',
  customer: props.doc?.customer || '',
  source_issue: props.doc?.name || '',
  service_location: props.doc?.service_location || '',
  issue_type: props.doc?.issue_type || '',
  priority: props.doc?.priority || 'medium',
}))

// ── Assignees ──

const userOptions = ref([])
const loadingUsers = ref(false)
const assignees = ref([])

function parseAssign (raw) {
  if (!raw) return []
  try { return JSON.parse(raw).filter(Boolean) } catch { return [] }
}

watch(() => props.doc?._assign, (val) => {
  assignees.value = parseAssign(val)
}, { immediate: true })

async function loadUsers () {
  if (userOptions.value.length) return
  loadingUsers.value = true
  try {
    const users = await listDocs('User', {
      filters: { enabled: 1, user_type: 'System User' },
      fields: ['name', 'full_name'],
      limit: 100,
      orderBy: 'full_name asc',
    })
    userOptions.value = users.map(u => ({ label: u.full_name || u.name, value: u.name }))
  } catch { userOptions.value = [] }
  loadingUsers.value = false
}

async function saveAssignees (newList) {
  const old = parseAssign(props.doc._assign)
  const added = newList.filter(u => !old.includes(u))
  const removed = old.filter(u => !newList.includes(u))
  try {
    for (const user of added) {
      await authFetch(BASE_URL + '/api/method/frappe.desk.form.assign_to.add', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ doctype: 'Issue', name: props.docName, assign_to: [user] }),
      })
    }
    for (const user of removed) {
      await authFetch(BASE_URL + '/api/method/frappe.desk.form.assign_to.remove', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ doctype: 'Issue', name: props.docName, assign_to: user }),
      })
    }
    props.doc._assign = JSON.stringify(newList)
    Notify.create({ type: 'positive', message: 'Assignation mise à jour', timeout: 1500 })
  } catch (e) {
    Notify.create({ type: 'negative', message: 'Erreur assignation: ' + e.message, timeout: 3000 })
    assignees.value = old
  }
}

onMounted(() => { loadUsers(); loadDispatchTags() })

// ── Tags (Dispatch Tags shared list) ──

const allDispatchTags = ref([])
const loadingTags = ref(false)
const filteredTagOptions = ref([])

function parseTags (raw) {
  if (!raw) return []
  return raw.split(',').map(t => t.trim()).filter(Boolean)
}

const currentTags = computed(() => parseTags(props.doc?._user_tags))

function getTagColor (label) {
  const t = allDispatchTags.value.find(x => x.label === label || x.name === label)
  return t?.color || '#6b7280'
}

function filterTags (val, update) {
  update(() => {
    const current = parseTags(props.doc?._user_tags)
    const all = allDispatchTags.value
      .map(t => ({ label: t.label, value: t.label, color: t.color, category: t.category || '' }))
      .filter(o => !current.includes(o.value))
    if (!val) { filteredTagOptions.value = all; return }
    const q = val.toLowerCase()
    filteredTagOptions.value = all.filter(o => o.label.toLowerCase().includes(q))
  })
}

async function addTag (tag) {
  if (!tag) return
  const label = typeof tag === 'string' ? tag : tag.value || tag
  if (currentTags.value.includes(label)) return
  try {
    await authFetch(BASE_URL + '/api/method/frappe.desk.doctype.tag.tag.add_tag', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ tag: label, dt: 'Issue', dn: props.docName }),
    })
    const cur = parseTags(props.doc._user_tags)
    cur.push(label)
    props.doc._user_tags = cur.join(',')
    Notify.create({ type: 'positive', message: `Tag "${label}" ajouté`, timeout: 1500 })
  } catch (e) {
    Notify.create({ type: 'negative', message: 'Erreur: ' + e.message, timeout: 3000 })
  }
}

function addNewTag (val, done) {
  if (val.length > 1) {
    done(val, 'add-unique')
    addTag(val)
  }
}

async function removeTag (tag) {
  try {
    await authFetch(BASE_URL + '/api/method/frappe.desk.doctype.tag.tag.remove_tag', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ tag, dt: 'Issue', dn: props.docName }),
    })
    const cur = parseTags(props.doc._user_tags).filter(t => t !== tag)
    props.doc._user_tags = cur.join(',')
    Notify.create({ type: 'positive', message: `Tag "${tag}" retiré`, timeout: 1500 })
  } catch (e) {
    Notify.create({ type: 'negative', message: 'Erreur: ' + e.message, timeout: 3000 })
  }
}

async function loadDispatchTags () {
  loadingTags.value = true
  try {
    allDispatchTags.value = await fetchTags()
  } catch { allDispatchTags.value = [] }
  loadingTags.value = false
}

// ── Tag Manager ──

const showTagManager = ref(false)
const editingTagName = ref(null)
const editingTagValue = ref('')

const TAG_COLORS = [
  '#6366f1', '#3b82f6', '#06b6d4', '#10b981', '#14b8a6',
  '#f59e0b', '#f97316', '#f43f5e', '#d946ef', '#8b5cf6',
  '#78716c', '#64748b',
]

function startEditTag (t) {
  editingTagName.value = t.name
  editingTagValue.value = t.label
}

async function saveTagRename (t) {
  const newLabel = editingTagValue.value.trim()
  editingTagName.value = null
  if (!newLabel || newLabel === t.label) return
  try {
    await renameTag(t.name, newLabel)
    t.label = newLabel
    t.name = newLabel
    Notify.create({ type: 'positive', message: `Tag renommé → "${newLabel}"`, timeout: 1500 })
  } catch (e) {
    Notify.create({ type: 'negative', message: 'Erreur: ' + e.message, timeout: 3000 })
  }
}

async function cycleTagColor (t) {
  const idx = TAG_COLORS.indexOf(t.color)
  const next = TAG_COLORS[(idx + 1) % TAG_COLORS.length]
  try {
    await updateTag(t.name, { color: next })
    t.color = next
  } catch (e) {
    Notify.create({ type: 'negative', message: 'Erreur: ' + e.message, timeout: 3000 })
  }
}

async function deleteDispatchTag (t) {
  Dialog.create({
    title: 'Supprimer le tag ?',
    message: `Le tag "${t.label}" sera retiré de tous les éléments.`,
    cancel: { flat: true, label: 'Annuler' },
    ok: { color: 'red', label: 'Supprimer', unelevated: true },
  }).onOk(async () => {
    try {
      await deleteTagApi(t.name)
      allDispatchTags.value = allDispatchTags.value.filter(x => x.name !== t.name)
      Notify.create({ type: 'positive', message: `Tag "${t.label}" supprimé`, timeout: 1500 })
    } catch (e) {
      Notify.create({ type: 'negative', message: 'Erreur: ' + e.message, timeout: 3000 })
    }
  })
}

// ── Delete ──

function confirmDelete () {
  Dialog.create({
    title: 'Supprimer ce ticket ?',
    message: `Le ticket ${props.docName} sera supprimé définitivement.`,
    cancel: { flat: true, label: 'Annuler' },
    ok: { color: 'red', label: 'Supprimer', unelevated: true },
    persistent: true,
  }).onOk(async () => {
    try {
      await deleteDoc('Issue', props.docName)
      Notify.create({ type: 'positive', message: 'Ticket supprimé', timeout: 2000 })
      emit('deleted', props.docName)
    } catch (e) {
      Notify.create({ type: 'negative', message: 'Erreur: ' + e.message, timeout: 3000 })
    }
  })
}

// ── Nested task tree ──

const rootJobs = computed(() => {
  // Jobs with no parent_job, or whose parent_job is not in this ticket's jobs
  const jobNames = new Set(props.dispatchJobs.map(j => j.name))
  return props.dispatchJobs.filter(j => !j.parent_job || !jobNames.has(j.parent_job))
    .sort((a, b) => (a.step_order || 0) - (b.step_order || 0))
})

const orphanJobs = computed(() => {
  // Jobs whose parent is in the list but weren't caught as roots or children
  // This handles edge cases
  const rootAndChildNames = new Set()
  function collectNames (jobs) {
    for (const j of jobs) {
      rootAndChildNames.add(j.name)
    }
  }
  collectNames(rootJobs.value)
  // Recursively collect all children
  function collectChildren (parentName) {
    const children = props.dispatchJobs.filter(j => j.parent_job === parentName)
    for (const c of children) {
      rootAndChildNames.add(c.name)
      collectChildren(c.name)
    }
  }
  for (const r of rootJobs.value) collectChildren(r.name)
  return props.dispatchJobs.filter(j => !rootAndChildNames.has(j.name))
})

// ── n8n webhook firing ──

async function fireWebhook (url, job) {
  if (!url) return
  try {
    await authFetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        event: url.includes('close') ? 'job_closed' : 'job_opened',
        job_name: job.name,
        job_subject: job.subject,
        job_status: job.status,
        job_type: job.job_type,
        ticket: props.doc?.name,
        customer: props.doc?.customer,
        timestamp: new Date().toISOString(),
      }),
    })
  } catch (err) {
    console.warn('[n8n webhook] fire failed:', err)
  }
}

function onJobCreated (job) {
  emit('dispatch-created', job)
}

function onJobDeleted (jobName) {
  emit('dispatch-deleted', jobName)
}

function onJobUpdated (jobName, data) {
  emit('dispatch-updated', jobName, data)
}

// ── Close / Reopen ──

const closingTicket = ref(false)

async function closeTicket () {
  closingTicket.value = true
  try {
    await updateDoc('Issue', props.docName, { status: 'Closed' })
    props.doc.status = 'Closed'
    Notify.create({ type: 'positive', message: 'Ticket fermé' })
  } catch (e) {
    Notify.create({ type: 'negative', message: `Erreur: ${e.message}` })
  } finally { closingTicket.value = false }
}

async function reopenTicket () {
  closingTicket.value = true
  try {
    await updateDoc('Issue', props.docName, { status: 'Open' })
    props.doc.status = 'Open'
    Notify.create({ type: 'positive', message: 'Ticket réouvert' })
  } catch (e) {
    Notify.create({ type: 'negative', message: `Erreur: ${e.message}` })
  } finally { closingTicket.value = false }
}

// ── Reply ──

async function sendReply () {
  if (!replyContent.value?.trim() || sendingReply.value) return
  sendingReply.value = true
  try {
    const authStore = useAuthStore()
    await createDoc('Communication', {
      communication_type: 'Communication',
      communication_medium: 'Other',
      sent_or_received: 'Sent',
      subject: props.title || props.docName,
      content: replyContent.value.trim(),
      sender: authStore.user || '',
      sender_full_name: authStore.user || '',
      reference_doctype: 'Issue',
      reference_name: props.docName,
    })
    replyContent.value = ''
    emit('reply-sent', props.docName)
    Notify.create({ type: 'positive', message: 'Reponse envoyee', timeout: 2000 })
  } catch {
    Notify.create({ type: 'negative', message: 'Erreur: reponse non envoyee', timeout: 3000 })
  } finally {
    sendingReply.value = false
  }
}
</script>

<style scoped>
.issue-actions {
  display: flex;
  gap: 8px;
}
.issue-field-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0 16px;
}
.if-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 0;
  font-size: 0.875rem;
  border-bottom: 1px solid #f1f5f9;
  min-height: 32px;
}
.if-label {
  font-size: 0.75rem;
  font-weight: 600;
  color: #6b7280;
  width: 80px;
  flex-shrink: 0;
}
.if-value {
  font-size: 0.875rem;
  color: #1f2937;
}
.dispatch-section {
  background: #f8fafc;
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  padding: 10px 12px;
}
.dispatch-jobs-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.tag-manager {
  background: #f8fafc;
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  padding: 8px 10px;
  max-height: 240px;
  overflow-y: auto;
}
.tag-mgr-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 3px 0;
  border-bottom: 1px solid #f1f5f9;
}
.tag-mgr-row:last-child { border-bottom: none; }
.tag-mgr-dot {
  width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; cursor: pointer;
  transition: transform 0.1s;
}
.tag-mgr-dot:hover { transform: scale(1.3); }
.tag-mgr-label {
  flex: 1; font-size: 0.8rem; cursor: pointer; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tag-mgr-label:hover { text-decoration: underline dotted; }
.tag-mgr-edit {
  flex: 1; font-size: 0.8rem; border: 1px solid #cbd5e1; border-radius: 4px;
  padding: 2px 6px; outline: none; min-width: 0;
}
.tag-mgr-edit:focus { border-color: #6366f1; }
.tag-mgr-cat { flex-shrink: 0; font-size: 0.65rem; }
.tag-mgr-del { opacity: 0.3; transition: opacity 0.15s; }
.tag-mgr-row:hover .tag-mgr-del { opacity: 1; }
</style>
