<script setup>
import { date, string } from 'alga-js'
import { DateTimePicker } from 'tedir-calendar'
import { SelectBox } from 'tedir-select'
import { toast } from 'bulma-toast'
import { linkifyHtml } from '~/utils/linkify'

const route = useRoute()
const router = useRouter()
const tabs = useTabs()
const windowFrom = useWindowFrom()
const config = useRuntimeConfig()

const props = defineProps({
  modelValue: Object,
  tab: String,
})

const emit = defineEmits(['update:modelValue', 'save', 'saved'])

const organizations = useCookie('logship_organizations')
const organization = useCookie('logship_organization')
const organizationId = useCookie('logship_organization_id')
const user = useCookie('logship_user')
const userId = useCookie('logship_user_id')

const clientName = ref('')
onMounted(() => {
  const getClient = localStorage.getItem('logship_clients')
  if(getClient) {
    const clientObj = JSON.parse(getClient)
    clientName.value = clientObj[0].name
  }
  if(route.params?.id) {
    loadChatHistory()
  }
})

const loading = ref({})
const getOptions = ref({})
const setOptions = async (url, key) => {
  loading.value[key] = true
  const res = await $fetch('/api/'+url, {
    headers: useRequestHeaders(['cookie'])
  })
  if(res) {
    getOptions.value[key] = res

    if(key === 'users' && res?.records?.[0]) {
      form.value.userId = String(res.records[0].id || '')
      form.value.user = res.records[0].Name || ''
    }
  }
  loading.value[key] = false
}

const form = ref({
  id: props.modelValue.id || '',
  uid: props.modelValue?.uid || '',
  client: props.modelValue.client || '',
  clientId: props.modelValue.clientId || '',
  organization: props.modelValue.organization || organization.value?.Name || '',
  organizationId: props.modelValue.organizationId || organizationId.value || '',
  isActive: props.modelValue.isActive ?? true,
  documentNo: props.modelValue.documentNo || '',
  requestAmt: props.modelValue.requestAmt || 0,
  summary: props.modelValue.summary || '',
  isEscalated: props.modelValue.isEscalated ?? false,
  isEMailWhenDue: props.modelValue.isEMailWhenDue ?? false,
  dateLastAction: props.modelValue.dateLastAction || date.now('date'),
  processed: props.modelValue.processed ?? false,
  isSelfService: props.modelValue.isSelfService ?? false,
  isInvoiced: props.modelValue.isInvoiced ?? false,
  isComplete: props.modelValue.isComplete ?? false,
  isMerchantAction: props.modelValue.isMerchantAction ?? false,
  qtySpent: props.modelValue.qtySpent || 0,
  qtyInvoiced: props.modelValue.qtyInvoiced || 0,
  priority: props.modelValue.priority || '',
  priorityId: props.modelValue.priorityId || '',
  dueType: props.modelValue.dueType || '',
  dueTypeId: props.modelValue.dueTypeId || '',
  salesRep: props.modelValue.salesRep || user.value?.Name || '',
  salesRepId: props.modelValue.salesRepId || user.value?.id || userId.value || '',
  partner: props.modelValue.partner || '',
  partnerId: props.modelValue.partnerId || '',
  user: props.modelValue.user || '',
  userId: props.modelValue.userId || '',
  nextAction: props.modelValue.nextAction || '',
  nextActionId: props.modelValue.nextActionId || '',
  requestType: props.modelValue.requestType || '',
  requestTypeId: props.modelValue.requestTypeId || '',
  group: props.modelValue.group || '',
  groupId: props.modelValue.groupId || '',
  category: props.modelValue.category || '',
  categoryId: props.modelValue.categoryId || '',
  status: props.modelValue.status || '',
  statusId: props.modelValue.statusId || '',
  resolution: props.modelValue.resolution || '',
  resolutionId: props.modelValue.resolutionId || '',
  priorityUser: props.modelValue.priorityUser || '',
  priorityUserId: props.modelValue.priorityUserId || '',
  confidentialType: props.modelValue.confidentialType || '',
  confidentialTypeId: props.modelValue.confidentialTypeId || '',
  confidentialTypeEntry: props.modelValue.confidentialTypeEntry || '',
  confidentialTypeEntryId: props.modelValue.confidentialTypeEntryId || '',
  role: props.modelValue.role || '',
  roleId: props.modelValue.roleId || '',
  dateNextAction: props.modelValue.dateNextAction || date.now(''),
  standardResponse: props.modelValue.standardResponse || '',
  standardResponseId: props.modelValue.standardResponseId || '',
  mailText: props.modelValue.mailText || '',
  mailTextId: props.modelValue.mailTextId || '',
  result: props.modelValue.result || '',
  lastResult: props.modelValue.lastResult || '',
  taskStatusId: props.modelValue.taskStatusId || '',
  qtyPlan: props.modelValue.qtyPlan || '',
  startTime: props.modelValue.startTime || date.now(''),
  startDate: props.modelValue.startDate || date.now(''),
  endTime: props.modelValue.endTime || date.now(''),
  endDate: props.modelValue.endDate || date.now(''),
  processing: props.modelValue.processing ?? false,
  asset: props.modelValue.asset || '',
  assetId: props.modelValue.assetId || '',
  project: props.modelValue.project || '',
  projectId: props.modelValue.projectId || '',
  order: props.modelValue.order || '',
  orderId: props.modelValue.orderId || '',
  invoice: props.modelValue.invoice || '',
  invoiceId: props.modelValue.invoiceId || '',
  payment: props.modelValue.payment || '',
  paymentId: props.modelValue.paymentId || '',
  inOut: props.modelValue.inOut || '',
  inOutId: props.modelValue.inOutId || '',
  product: props.modelValue.product || '',
  productId: props.modelValue.productId || '',
  productSpent: props.modelValue.productSpent || '',
  productSpentId: props.modelValue.productSpentId || '',
  rma: props.modelValue.rma || '',
  rmaId: props.modelValue.rmaId || '',
  campaign: props.modelValue.campaign || '',
  campaignId: props.modelValue.campaignId || '',
  alert: {
    status: props.modelValue.alert.status || '',
    message: props.modelValue.alert.message || '',
    processing: props.modelValue.alert.processing ?? false,
  }
})

const populateByPartner = (val, opt) => {
  form.value.partner = opt.Name || ''
  form.value.partnerId = opt.id || ''
  setOptions('partners/partners/'+opt.id+'/users', 'users')
}

// Flag to avoid triggering auto-save while the form is being hydrated from the parent
const isHydrating = ref(true)

// Snapshot of the last-known-good form state. autoSubmit compares against this
// so the PUT only fires when the user actually changed a field — picker/hydration
// echoes and mouseleave-on-scroll that don't change any value no longer fire PUTs.
// The snapshot is built from the exact normalized PUT body so picker/date re-
// serialization that preserves the underlying value compares equal.
const toIso = (v) => {
  try { return new Date(v).toJSON().replace('.000Z', 'Z') } catch { return v }
}
// Parse a user-entered amount into a plain JS Number. Accepts "82,50", "82.50",
// "1.234,56", "1,234.56", numbers — always returns a dot-decimal Number.
const parseAmount = (v) => {
  if (typeof v === 'number') return isNaN(v) ? 0 : v
  if (v === null || v === undefined || v === '') return 0
  let s = String(v).trim().replace(/\s/g, '')
  const hasComma = s.indexOf(',') !== -1
  const hasDot = s.indexOf('.') !== -1
  if (hasComma && !hasDot) {
    s = s.replace(',', '.')
  } else if (hasComma && hasDot) {
    // The rightmost separator is the decimal one
    if (s.lastIndexOf(',') > s.lastIndexOf('.')) {
      s = s.replace(/\./g, '').replace(',', '.')
    } else {
      s = s.replace(/,/g, '')
    }
  }
  const n = Number(s)
  return isNaN(n) ? 0 : n
}
const buildPutBody = (f) => ({
  id: f.id,
  organizationId: f.organizationId,
  isActive: f.isActive,
  documentNo: f.documentNo,
  requestAmt: parseAmount(f.requestAmt),
  summary: f.summary,
  isEscalated: f.isEscalated,
  isEMailWhenDue: f.isEMailWhenDue,
  dateLastAction: toIso(f.dateLastAction),
  processed: f.processed,
  isSelfService: f.isSelfService,
  isInvoiced: f.isInvoiced,
  isComplete: f.isComplete,
  isMerchantAction: f.isMerchantAction,
  qtySpent: f.qtySpent,
  qtyInvoiced: f.qtyInvoiced,
  priorityId: f.priorityId,
  dueTypeId: f.dueTypeId,
  salesRepId: f.salesRepId,
  partnerId: f.partnerId,
  userId: f.userId,
  nextActionId: f.nextActionId,
  requestTypeId: f.requestTypeId,
  groupId: f.groupId,
  categoryId: f.categoryId,
  statusId: f.statusId,
  resolutionId: f.resolutionId,
  priorityUserId: f.priorityUserId,
  confidentialTypeId: f.confidentialTypeId,
  confidentialTypeEntryId: f.confidentialTypeEntryId,
  roleId: f.roleId,
  dateNextAction: toIso(f.dateNextAction),
  standardResponseId: f.standardResponseId,
  mailTextId: f.mailTextId,
  result: f.result,
  lastResult: f.lastResult,
  taskStatusId: f.taskStatusId,
  activityId: f.activityId,
  qtyPlan: f.qtyPlan,
  startTime: toIso(f.startTime),
  startDate: toIso(f.startDate),
  endTime: toIso(f.endTime),
  endDate: toIso(f.endDate),
  assetId: f.assetId,
  projectId: f.projectId,
  orderId: f.orderId,
  invoiceId: f.invoiceId,
  paymentId: f.paymentId,
  inOutId: f.inOutId,
  product: f.product,
  productId: f.productId,
  productSpent: f.productSpent,
  productSpentId: f.productSpentId,
  rma: f.rma,
  rmaId: f.rmaId,
  campaign: f.campaign,
  campaignId: f.campaignId,
})
const formSnapshot = ref('')
const snapshotOf = (f) => {
  if (!f) return ''
  try { return JSON.stringify(buildPutBody(f)) } catch { return '' }
}
// Tracks the last known-persisted value of isMerchantAction so we can detect
// transitions (false → true) and fire the merchant email notification once.
const lastSavedIsMerchantAction = ref(false)

watch(() => props.modelValue, () => {
  isHydrating.value = true
  form.value = props.modelValue
  formSnapshot.value = snapshotOf(props.modelValue)
  lastSavedIsMerchantAction.value = !!props.modelValue?.isMerchantAction

  if(form.value.partnerId !== '' && windowFrom.value === 'partners') {
    populateByPartner('', {id: form.value.partnerId, Name: form.value.partner})
  }

  // Reload chat only if we haven't yet loaded for this request id (guard inside handles the rest)
  if(route.params?.id && chatLoadedFor.value !== String(route.params.id)) {
    loadChatHistory()
  }
  nextTick(() => { isHydrating.value = false })
})

onMounted(() => {
  // Initial snapshot from the ref() initializer above — watch() doesn't fire on
  // mount when props.modelValue is already populated.
  formSnapshot.value = snapshotOf(form.value)
  lastSavedIsMerchantAction.value = !!form.value?.isMerchantAction
  // Allow auto-saves only after initial hydration is complete
  nextTick(() => { isHydrating.value = false })
})

const debounce = (func, timeout = 300) => {
  let timer;
  return (...args) => {
    clearTimeout(timer)
    timer = setTimeout(() => { func.apply(this, args); }, timeout)
  }
}

const autoSaving = ref(false)
const autoSubmit = async () => {
  if (isHydrating.value) return
  if (!form.value.id) return
  // Required fields guard — matches the parent save button enablement logic
  if (!form.value.summary || !form.value.requestTypeId || !form.value.priorityId ||
      !form.value.dueTypeId || !form.value.confidentialTypeId ||
      !form.value.confidentialTypeEntryId || !form.value.salesRepId) return
  if (autoSaving.value) return
  // Skip if nothing actually changed since the last snapshot — prevents phantom
  // saves triggered by picker/hydration/mouseleave events that didn't alter any value.
  const currentSnapshot = snapshotOf(form.value)
  if (currentSnapshot === formSnapshot.value) return
  autoSaving.value = true

  try {
    const res = await $fetch('/api/requests/requests/update', {
      method: 'PUT',
      headers: useRequestHeaders(['cookie']),
      body: buildPutBody(form.value)
    })
    if (Number(res?.status) === 200) {
      // Refresh snapshot so the next autoSubmit only fires on a NEW user change
      formSnapshot.value = snapshotOf(form.value)
      // Detect a fresh transition of isMerchantAction false→true and notify the
      // merchant by email. Only fires on the first save that flipped it on.
      if (form.value.isMerchantAction && !lastSavedIsMerchantAction.value) {
        $fetch(`/api/requests/requests/${form.value.id}/action-notification`, {
          method: 'POST',
          headers: useRequestHeaders(['cookie']),
          body: { action: 'merchant-action' }
        }).catch(() => { /* non-fatal */ })
      }
      lastSavedIsMerchantAction.value = !!form.value.isMerchantAction
      emit('saved', form.value)
      toast({ message: 'Gespeichert', type: 'is-success', duration: 1200 })
      await loadChatHistory(true)
    } else if (res?.message) {
      toast({ message: res.message, type: 'is-danger', duration: 2500 })
    }
  } catch (err) {
    console.error('Auto-save failed:', err)
  } finally {
    autoSaving.value = false
  }
}

const save = debounce(async () => {
  emit('update:modelValue', form.value)
  emit('save', form.value)
  await autoSubmit()
}, 500)

// Chat system
const chatMessages = ref([])
const newMessage = ref('')
const sendingMessage = ref(false)
const chatContainerRef = ref(null)
const chatLoading = ref(false)
const chatLoadedFor = ref('')

// Parse iDempiere / Strapi timestamps robustly.
// Handles: "2026-04-10T18:34:58Z", "2026-04-10T18:34:58.123Z",
// "2026-04-10T18:34:58+02:00", "2026-04-10 18:34:58" (no T, no Z),
// numeric epoch and Date objects.
const parseDate = (v) => {
  if (!v && v !== 0) return null
  if (v instanceof Date) return isNaN(v.getTime()) ? null : v
  if (typeof v === 'number') {
    const d = new Date(v)
    return isNaN(d.getTime()) ? null : d
  }
  const s = String(v).trim()
  if (!s) return null
  // Try ISO as-is
  let d = new Date(s)
  if (!isNaN(d.getTime())) return d
  // Try with T inserted
  const iso = s.replace(' ', 'T')
  d = new Date(iso)
  if (!isNaN(d.getTime())) return d
  // Try adding Z (assume UTC)
  d = new Date(iso + 'Z')
  if (!isNaN(d.getTime())) return d
  return null
}

// iDempiere returns Created/Updated as German LOCAL wall-clock time but tags it
// as UTC ("…T18:34:58Z"). Parsing that with new Date() re-applies the local
// offset, so message timestamps show +2h and their inflated getTime() sorts
// them after the true-UTC Strapi attachment dates (a new attachment then lands
// on top). Strip the bogus tz marker so the wall-clock is read as local time —
// this aligns display with the (correct) attachment times AND fixes ordering.
// Strapi attachment dates are genuine UTC and must NOT be passed through this.
const stripIdempiereTz = (v) => {
  if (!v) return v
  return String(v).trim().replace(' ', 'T').replace(/(\.\d+)?(Z|[+-]\d{2}:?\d{2})$/i, '')
}

// German datetime: "HH:MM dd.mm.YYYY"
const formatGermanDateTime = (d) => {
  if (!d || isNaN(d.getTime())) return ''
  const hh = String(d.getHours()).padStart(2, '0')
  const mm = String(d.getMinutes()).padStart(2, '0')
  const DD = String(d.getDate()).padStart(2, '0')
  const MM = String(d.getMonth() + 1).padStart(2, '0')
  return `${hh}:${mm} ${DD}.${MM}.${d.getFullYear()}`
}

// Replace any ISO timestamps embedded in free text (e.g. "[START:2026-04-13T08:56:39Z]")
// with German "HH:MM dd.mm.YYYY" so summary/history content reads naturally.
const formatIsoDatesInText = (text) => {
  if (!text) return ''
  return String(text).replace(
    /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?/g,
    (match) => {
      const d = new Date(match)
      return isNaN(d.getTime()) ? match : formatGermanDateTime(d)
    }
  )
}

const formatChatDate = (dateStr) => {
  const d = parseDate(dateStr)
  if (!d) return dateStr ? String(dateStr) : ''
  const now = new Date()
  const isToday = d.toDateString() === now.toDateString()
  const yesterday = new Date(now)
  yesterday.setDate(yesterday.getDate() - 1)
  const isYesterday = d.toDateString() === yesterday.toDateString()

  const time = d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })
  const datePart = `${String(d.getDate()).padStart(2, '0')}.${String(d.getMonth() + 1).padStart(2, '0')}.${d.getFullYear()}`

  if (isToday) return `${time} Heute`
  if (isYesterday) return `${time} Gestern`
  return `${time} ${datePart}`
}

const formatDateGroup = (dateStr) => {
  const d = parseDate(dateStr)
  if (!d) return 'Ohne Datum'
  const now = new Date()
  if (d.toDateString() === now.toDateString()) return 'Heute'
  const yesterday = new Date(now)
  yesterday.setDate(yesterday.getDate() - 1)
  if (d.toDateString() === yesterday.toDateString()) return 'Gestern'
  return d.toLocaleDateString('de-DE', { day: '2-digit', month: 'long', year: 'numeric' })
}

// Friendly labels for common R_Request columns in the changelog
const columnLabels = {
  Summary: 'Zusammenfassung',
  Result: 'Ergebnis',
  LastResult: 'Letztes Ergebnis',
  RequestAmt: 'Betrag',
  QtySpent: 'Menge verbraucht',
  QtyInvoiced: 'Menge abgerechnet',
  QtyPlan: 'Menge geplant',
  Priority: 'Prioritat',
  PriorityUser: 'User Prioritat',
  DueType: 'Falligkeit',
  IsActive: 'Aktiv',
  IsEscalated: 'Eskaliert',
  IsInvoiced: 'Abgerechnet',
  IsSelfService: 'Self Service',
  IsEMailWhenDue: 'E-Mail bei Fallig',
  Processed: 'Verarbeitet',
  ConfidentialType: 'Vertraulichkeit',
  ConfidentialTypeEntry: 'Eintrag Vertraulichkeit',
  R_RequestType_ID: 'Typ',
  R_Category_ID: 'Kategorie',
  R_Group_ID: 'Gruppe',
  R_Status_ID: 'Status',
  R_Resolution_ID: 'Losung',
  R_StandardResponse_ID: 'Standardantwort',
  R_MailText_ID: 'Mail Vorlage',
  C_BPartner_ID: 'Geschaftspartner',
  AD_User_ID: 'Benutzer/Kontakt',
  SalesRep_ID: 'Vertreter',
  AD_Role_ID: 'Rolle',
  AD_Org_ID: 'Organisation',
  C_Project_ID: 'Projekt',
  C_Order_ID: 'Auftrag',
  C_Invoice_ID: 'Rechnung',
  C_Payment_ID: 'Zahlung',
  M_InOut_ID: 'Lieferschein',
  M_Product_ID: 'Produkt',
  M_ProductSpent_ID: 'Produkt verbraucht',
  M_RMA_ID: 'RMA',
  A_Asset_ID: 'Anlage',
  C_Activity_ID: 'Aktivitat',
  C_Campaign_ID: 'Kampagne',
  TaskStatus: 'Task Status',
  DateLastAction: 'Letzte Aktion',
  StartDate: 'Startdatum',
  EndDate: 'Enddatum',
  StartTime: 'Startzeit',
  EndTime: 'Endzeit',
  DocumentNo: 'Dok-Nr.'
}

const resolveColumnLabel = (item) => {
  const raw = item?.AD_Column_ID?.identifier || item?.ColumnName || item?.columnName || ''
  if (!raw) return 'Feld'
  return columnLabels[raw] || raw.replace(/_ID$/, '').replace(/_/g, ' ')
}

const formatChangeValue = (val) => {
  if (val === null || val === undefined || val === '') return '∅'
  const str = String(val).trim()
  if (!str) return '∅'
  if (str === 'true' || str === 'Y') return 'Ja'
  if (str === 'false' || str === 'N') return 'Nein'
  // ISO date -> "HH:MM dd.mm.YYYY"
  if (/^\d{4}-\d{2}-\d{2}T/.test(str)) {
    const d = new Date(str)
    if (!isNaN(d.getTime())) return formatGermanDateTime(d)
  }
  return str.length > 120 ? str.slice(0, 120) + '…' : str
}

const loadChatHistory = async (force = false) => {
  const requestId = route.params?.id
  if (!requestId) return
  // Skip duplicate concurrent loads for the same request unless forced
  if (chatLoading.value) return
  if (!force && chatLoadedFor.value === String(requestId) && chatMessages.value.length > 0) return
  chatLoading.value = true
  try {
    const allMessages = []

    // Load request updates (r_requestupdate) — only keep ones with Summary / Result text
    try {
      const updRes = await $fetch(`/api/requests/requests/${route.params.id}/request-update`, {
        headers: useRequestHeaders(['cookie'])
      })
      if (updRes?.records) {
        for (const item of updRes.records) {
          const text = (item.Result || item.Summary || '').toString().trim()
          if (!text) continue
          // Admin view: prefer the author's org name (tells us at a glance
          // which merchant the message came from) over the raw user identifier.
          allMessages.push({
            id: 'upd-' + item.id,
            text,
            author: item.AD_Org_ID?.identifier || item.CreatedBy?.identifier || item.UpdatedBy?.identifier || 'System',
            createdById: item.CreatedBy?.id || item.UpdatedBy?.id || null,
            date: stripIdempiereTz(item.Created || item.Updated || ''),
            source: 'update',
            qtySpent: item.QtySpent || 0,
          })
        }
      }
    } catch (e) { console.warn('Could not load request updates:', e) }

    // Load request history (r_requestaction) — first entry shows full Summary as "Aktion";
    // subsequent entries are "Ergänzung" with field-level diffs in "Alt → Neu" form.
    try {
      const histRes = await $fetch(`/api/requests/requests/${route.params.id}/request-history`, {
        headers: useRequestHeaders(['cookie'])
      })
      if (histRes?.records) {
        const sorted = [...histRes.records].sort((a, b) =>
          new Date(a.Created || a.Updated || 0).getTime() - new Date(b.Created || b.Updated || 0).getTime()
        )
        // Relational / scalar fields to compare between consecutive snapshots
        const fieldLabels = {
          R_RequestType_ID: 'Typ',
          R_Category_ID: 'Kategorie',
          R_Group_ID: 'Gruppe',
          R_Status_ID: 'Status',
          R_Resolution_ID: 'Losung',
          Priority: 'Prioritat',
          DueType: 'Falligkeit',
          SalesRep_ID: 'Vertreter',
          C_BPartner_ID: 'Partner',
          AD_User_ID: 'Benutzer',
          AD_Role_ID: 'Rolle',
          ConfidentialType: 'Vertraulichkeit',
          TaskStatus: 'Task Status',
          IsActive: 'Aktiv',
          IsEscalated: 'Eskaliert',
          IsInvoiced: 'Abgerechnet',
        }
        const extractVal = (v) => {
          if (v === null || v === undefined) return ''
          if (typeof v === 'object') return v.identifier || v.id || ''
          return v
        }
        let prevItem = null
        for (const item of sorted) {
          const fullSummary = (item.Summary || '').toString()
          const prevSummary = prevItem ? (prevItem.Summary || '').toString() : ''

          // First entry: show full Summary as a simple "Aktion".
          // Skip when an r_requestupdate message with the same text already exists —
          // iDempiere seeds both tables with the initial Summary at ticket creation,
          // which otherwise renders the first message twice.
          if (!prevItem) {
            const text = formatIsoDatesInText(fullSummary).trim()
            const duplicateOfUpdate = text && allMessages.some(
              m => m.source === 'update' && (m.text || '').trim() === text
            )
            if (text && !duplicateOfUpdate) {
              allMessages.push({
                id: 'hist-' + item.id,
                text,
                author: item.AD_Org_ID?.identifier || item.CreatedBy?.identifier || item.UpdatedBy?.identifier || 'System',
                createdById: item.CreatedBy?.id || item.UpdatedBy?.id || null,
                date: stripIdempiereTz(item.Created || item.Updated || ''),
                source: 'history',
                isDiff: false,
                qtySpent: 0,
              })
            }
            prevItem = item
            continue
          }

          // Subsequent entries: build field-level diff
          const changes = []

          // Summary diff
          if (fullSummary !== prevSummary) {
            if (prevSummary && fullSummary.startsWith(prevSummary)) {
              const added = fullSummary.slice(prevSummary.length).replace(/^\n+/, '').trim()
              if (added) {
                changes.push({
                  label: 'Zusammenfassung',
                  oldV: '…',
                  newV: formatIsoDatesInText(added),
                })
              }
            } else if (fullSummary && prevSummary && prevSummary.startsWith(fullSummary)) {
              const removed = prevSummary.slice(fullSummary.length).replace(/^\n+/, '').trim()
              if (removed) {
                changes.push({
                  label: 'Zusammenfassung',
                  oldV: formatIsoDatesInText(removed),
                  newV: '…',
                })
              }
            } else {
              changes.push({
                label: 'Zusammenfassung',
                oldV: formatIsoDatesInText(prevSummary) || '∅',
                newV: formatIsoDatesInText(fullSummary) || '∅',
              })
            }
          }

          // Other field diffs
          for (const [field, label] of Object.entries(fieldLabels)) {
            const oldRaw = extractVal(prevItem[field])
            const newRaw = extractVal(item[field])
            if (String(oldRaw) === String(newRaw)) continue
            changes.push({
              label,
              oldV: formatChangeValue(oldRaw),
              newV: formatChangeValue(newRaw),
            })
          }

          if (changes.length) {
            allMessages.push({
              id: 'hist-' + item.id,
              text: '',
              changes,
              author: item.AD_Org_ID?.identifier || item.CreatedBy?.identifier || item.UpdatedBy?.identifier || 'System',
              createdById: item.CreatedBy?.id || item.UpdatedBy?.id || null,
              date: stripIdempiereTz(item.Created || item.Updated || ''),
              source: 'history',
              isDiff: true,
              qtySpent: 0,
            })
          }

          prevItem = item
        }
      }
    } catch (e) { console.warn('Could not load request history:', e) }

    // Load attachments (Strapi) — chronological, with preview
    try {
      const attMeta = await $fetch(`/api/attachments/R_Request/${route.params.id}`, {
        headers: useRequestHeaders(['cookie'])
      })
      if (attMeta?.Strapi_Attachment_documentId) {
        const files = await $fetch(`/api/attachments/R_Request/${attMeta.Strapi_Attachment_documentId}/files`, {
          headers: useRequestHeaders(['cookie'])
        })
        if (Array.isArray(files)) {
          for (const f of files) {
            if (!f?.hash) continue
            const urlPath = (f?.formats?.large?.url || f?.url || '').replace('/uploads/', '')
            const fullUrl = config.public?.strapi + '/files-api/' + (urlPath || (f.hash + (f.ext || '')))
            allMessages.push({
              id: 'att-' + f.id,
              text: '',
              attachment: {
                name: f.name || f.hash,
                ext: (f.ext || '').toLowerCase(),
                mime: f.mime || '',
                size: f.size || 0,
                width: f.width || 0,
                height: f.height || 0,
                url: fullUrl,
                thumb: f?.formats?.thumbnail?.url
                  ? config.public?.strapi + '/files-api/' + f.formats.thumbnail.url.replace('/uploads/', '')
                  : fullUrl,
              },
              author: 'Anhang',
              date: f.createdAt || f.updatedAt || '',
              source: 'attachment',
              qtySpent: 0,
            })
          }
        }
      }
    } catch (e) { console.warn('Could not load attachments:', e) }

    // Load changelog (ad_changelog) — OldValue -> NewValue per column change
    try {
      const logRes = await $fetch(`/api/changelog/${route.params.id}`, {
        headers: useRequestHeaders(['cookie'])
      })
      const records = Array.isArray(logRes) ? logRes : (logRes?.records || [])
      // Group by timestamp + user so simultaneous changes show as a single "change set"
      const groups = new Map()
      for (const item of records) {
        const ts = stripIdempiereTz(item.Updated || item.Created || '')
        const by = item.UpdatedBy?.identifier || item.CreatedBy?.identifier || 'System'
        const byId = item.UpdatedBy?.id || item.CreatedBy?.id || null
        const key = ts + '|' + by
        if (!groups.has(key)) groups.set(key, { date: ts, author: by, createdById: byId, items: [] })
        groups.get(key).items.push(item)
      }
      for (const [key, grp] of groups.entries()) {
        const lines = []
        for (const it of grp.items) {
          const raw = it?.AD_Column_ID?.identifier || it?.ColumnName || it?.columnName || ''
          // Only include known request-related columns — skip order/invoice/other table fields
          if (raw && !columnLabels[raw]) continue
          const label = resolveColumnLabel(it)
          const oldV = formatChangeValue(it.OldValue)
          const newV = formatChangeValue(it.NewValue)
          if (oldV === newV) continue
          lines.push({ label, oldV, newV })
        }
        if (!lines.length) continue
        allMessages.push({
          id: 'chg-' + key,
          text: '',
          changes: lines,
          author: grp.author,
          createdById: grp.createdById,
          date: grp.date,
          source: 'change',
          qtySpent: 0,
        })
      }
    } catch (e) { console.warn('Could not load changelog:', e) }

    const sorted = allMessages.sort((a, b) => {
      const ta = parseDate(a.date)?.getTime() ?? 0
      const tb = parseDate(b.date)?.getTime() ?? 0
      return ta - tb
    })

    // Pin the request's own Summary to the top so every string/line it holds
    // (e.g. [TASK], [START], [END], [DURATION], [VOICE], ...) is always visible,
    // even if the individual r_requestaction snapshots are missing lines.
    // Skip when an existing history/update message already renders the same text —
    // otherwise newly created tickets show the initial message twice.
    const mainSummary = (form.value?.summary || '').toString().trim()
    if (mainSummary) {
      const pinnedText = formatIsoDatesInText(mainSummary)
      const alreadyShown = sorted.some(m => (m.text || '').trim().includes(pinnedText))
      if (!alreadyShown) {
        sorted.unshift({
          id: 'req-summary-' + requestId,
          text: pinnedText,
          author: form.value?.organization || form.value?.salesRep || 'System',
          date: sorted[0]?.date || '',
          source: 'request',
          qtySpent: 0,
        })
      }
    }

    // Only overwrite with an empty result if we currently have nothing; otherwise
    // keep the previously populated data to avoid a transient wipe from a race.
    if (sorted.length > 0 || chatMessages.value.length === 0) {
      chatMessages.value = sorted
      chatLoadedFor.value = String(requestId)
    }
    nextTick(() => scrollChatToBottom())

    // Resolve each message author's actual organization companyname:
    // r_requestupdate.AD_Org_ID reflects the TICKET's org (not the sender's),
    // so admin messages would otherwise appear under the merchant's org name.
    // We look up every unique createdById in parallel and rewrite `author`
    // with the user's own org companyname (e.g. "LogYou GmbH"), not the
    // lowercase identifier slug.
    resolveAuthorOrgs(sorted)
  } catch (err) {
    console.error('Error loading chat history:', err)
  } finally {
    chatLoading.value = false
  }
}

// Cache (user id -> org companyname) across calls.
const userOrgCache = new Map()
const resolveAuthorOrgs = async (messages) => {
  const ids = Array.from(new Set(messages.map(m => m.createdById).filter(Boolean)))
  const toFetch = ids.filter(id => !userOrgCache.has(id))
  if (toFetch.length === 0) {
    applyAuthorOrgs(messages)
    return
  }
  const results = await Promise.all(toFetch.map(id =>
    $fetch(`/api/admin/users/${id}/org-companyname`, {
      headers: useRequestHeaders(['cookie'])
    }).then(res => [id, res?.companyname || null])
      .catch(() => [id, null])
  ))
  for (const [id, orgName] of results) userOrgCache.set(id, orgName)
  applyAuthorOrgs(messages)
}
const applyAuthorOrgs = (messages) => {
  chatMessages.value = messages.map(m => {
    const orgName = m.createdById ? userOrgCache.get(m.createdById) : null
    return orgName ? { ...m, author: orgName } : m
  })
}

const scrollChatToBottom = () => {
  if (chatContainerRef.value) {
    chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight
  }
}

// Amount calculator — enter hours + minutes + note, calculate amount using the
// partner's hourly rate (fulfillment_service_hourly_rate) with a 33 EUR fallback.
// Persists the total amount on the request and appends a chat update entry.
const showAmountCalc = ref(false)
const amountCalc = ref({
  hours: 0,
  minutes: 0,
  note: '',
  hourlyRate: 33,
  rateLoading: false,
  saving: false
})

const calculatedAmount = computed(() => {
  const h = Number(amountCalc.value.hours) || 0
  const m = Number(amountCalc.value.minutes) || 0
  const totalHours = h + (m / 60)
  const rate = Number(amountCalc.value.hourlyRate) || 33
  return Math.round(totalHours * rate * 100) / 100
})

// Pull fulfillment_service_hourly_rate out of a c_bpartner record, trying
// every case variant iDempiere might return. Matches the mobile extractor.
const extractHourlyRate = (record) => {
  if (!record || typeof record !== 'object') return null
  const candidates = [
    record.fulfillment_service_hourly_rate,
    record.Fulfillment_Service_Hourly_Rate,
    record.FULFILLMENT_SERVICE_HOURLY_RATE
  ]
  if (candidates.every(v => v === undefined)) {
    for (const k of Object.keys(record)) {
      if (k.toLowerCase() === 'fulfillment_service_hourly_rate') {
        candidates.push(record[k]); break
      }
    }
  }
  for (const raw of candidates) {
    const n = Number(raw)
    if (Number.isFinite(n) && n > 0) return n
  }
  return null
}

// Read the hourly rate off a given bpartner id by fetching the full record
// (no $select, so case variations don't strand the field).
const readHourlyRateFor = async (bpartnerId) => {
  if (!bpartnerId) return null
  try {
    // NB: the /api/filters/[model]/[query] route already prepends $filter= —
    // pass only the bare expression (a literal "$filter=" here 400s in iDempiere).
    const res = await $fetch(
      `/api/filters/c_bpartner/${encodeURIComponent('C_BPartner_ID eq ' + bpartnerId)}`,
      { headers: useRequestHeaders(['cookie']) }
    )
    const record = res?.records?.[0] || res
    return extractHourlyRate(record)
  } catch (err) {
    console.warn('[Zeit abrechnen] Rate lookup failed for bpartner', bpartnerId, err)
    return null
  }
}

const fetchHourlyRate = async () => {
  amountCalc.value.rateLoading = true
  try {
    // 1. Request's own C_BPartner_ID
    const requestPartnerId = form.value.partnerId
    let rate = await readHourlyRateFor(requestPartnerId)
    let source = rate !== null ? `request bpartner ${requestPartnerId}` : null

    // 2. Fall back to the bpartner linked to the ticket's organization.
    //    The dedicated endpoint returns the full c_bpartner record so the
    //    rate is extracted with the same case-tolerant helper.
    if (rate === null) {
      const orgId = form.value.organizationId || organizationId.value
      if (orgId) {
        try {
          const orgBpRes = await $fetch(`/api/admin/organizations/${orgId}/bpartner`, {
            headers: useRequestHeaders(['cookie'])
          })
          if (orgBpRes?.id) {
            const orgRate = extractHourlyRate(orgBpRes)
            if (orgRate !== null) {
              rate = orgRate
              source = `org ${orgId} → bpartner ${orgBpRes.id}`
            }
          }
        } catch (err) {
          console.warn('[Zeit abrechnen] Could not resolve org bpartner:', err)
        }
      }
    }

    // 3. Hard fallback
    if (rate === null) {
      console.log('[Zeit abrechnen] No hourly rate found, using fallback 33')
      amountCalc.value.hourlyRate = 33
    } else {
      console.log('[Zeit abrechnen] Hourly rate from', source, '→', rate)
      amountCalc.value.hourlyRate = rate
    }
  } catch (err) {
    console.warn('Could not load hourly rate, using fallback 33:', err)
    amountCalc.value.hourlyRate = 33
  } finally {
    amountCalc.value.rateLoading = false
  }
}

const openAmountCalc = async () => {
  amountCalc.value = {
    hours: 0,
    minutes: 0,
    note: '',
    hourlyRate: 33,
    rateLoading: false,
    saving: false
  }
  showAmountCalc.value = true
  await fetchHourlyRate()
}

const closeAmountCalc = () => {
  if (amountCalc.value.saving) return
  showAmountCalc.value = false
}

const saveAmountCalc = async () => {
  const h = Number(amountCalc.value.hours) || 0
  const m = Number(amountCalc.value.minutes) || 0
  if (h <= 0 && m <= 0) {
    toast({ message: 'Bitte Stunden oder Minuten eingeben', type: 'is-warning', duration: 2000 })
    return
  }
  if (!route.params?.id) return
  amountCalc.value.saving = true

  const amount = calculatedAmount.value
  const durationMin = h * 60 + m
  const rate = Number(amountCalc.value.hourlyRate) || 33
  const note = (amountCalc.value.note || '').trim()
  const createdAtStr = formatGermanDateTime(new Date())

  const prevAmt = Math.round(parseAmount(form.value.requestAmt) * 100) / 100
  const prevQty = Number(form.value.qtySpent) || 0
  const newTotal = Math.round((prevAmt + amount) * 100) / 100

  const fmtEur = (v) => v.toFixed(2).replace('.', ',') + ' €'
  const rateStr = rate.toFixed(2).replace('.', ',') + ' €/h'
  const amountStr = fmtEur(amount)
  const timeStr = `${h}h ${m}min`

  const resultLines = [
    '[Zeitabrechnung]',
    `Zeit erfasst: ${timeStr}`,
    `Stundensatz: ${rateStr}`,
    `Berechneter Betrag: ${amountStr}`
  ]
  if (prevAmt > 0) {
    resultLines.push(`Vorheriger Betrag: ${fmtEur(prevAmt)} (hinzugerechnet)`)
    resultLines.push(`Neuer Gesamtbetrag: ${fmtEur(newTotal)}`)
  } else {
    resultLines.push(`Neuer Gesamtbetrag: ${fmtEur(newTotal)}`)
  }
  resultLines.push(`Erstellt: ${createdAtStr}`)
  if (note) {
    resultLines.push('Notiz:')
    resultLines.push(note)
  }
  const resultText = resultLines.join('\n')

  try {
    // 1. Accumulate amount + qtySpent on the request itself and persist
    form.value.requestAmt = newTotal
    form.value.qtySpent = prevQty + durationMin
    await autoSubmit()

    // 2. Append a chat update with the calculation details
    const res = await $fetch(`/api/requests/requests/${route.params.id}/request-update`, {
      method: 'POST',
      headers: useRequestHeaders(['cookie']),
      body: {
        organizationId: form.value.organizationId || organizationId.value,
        requestId: route.params.id,
        confidentialTypeEntryId: form.value.confidentialTypeEntryId || 'I',
        isActive: true,
        qtySpent: durationMin,
        qtyInvoiced: 0,
        result: resultText
      }
    })

    if (Number(res?.status) === 200 || res?.id) {
      const totalStr = fmtEur(newTotal)
      const msg = prevAmt > 0
        ? `+${amountStr} (Gesamt: ${totalStr})`
        : `Betrag berechnet: ${amountStr}`
      toast({ message: msg, type: 'is-success', duration: 2500 })
      showAmountCalc.value = false
      await loadChatHistory(true)
    } else {
      toast({ message: res?.message || 'Fehler beim Speichern', type: 'is-danger', duration: 2500 })
    }
  } catch (err) {
    console.error('saveAmountCalc error:', err)
    toast({ message: 'Fehler: ' + (err?.message || 'Unbekannt'), type: 'is-danger', duration: 3000 })
  } finally {
    amountCalc.value.saving = false
  }
}

// Core send routine. When assignToMerchant=true, additionally flips the
// isMerchantAction flag to true (firing the merchant email) in the same action.
const sendMessage = async ({ assignToMerchant = false } = {}) => {
  if (!newMessage.value.trim() || !route.params?.id) return
  sendingMessage.value = true

  try {
    const res = await $fetch(`/api/requests/requests/${route.params.id}/request-update`, {
      method: 'POST',
      headers: useRequestHeaders(['cookie']),
      body: {
        organizationId: form.value.organizationId || organizationId.value,
        requestId: route.params.id,
        confidentialTypeEntryId: form.value.confidentialTypeEntryId || 'I',
        isActive: true,
        qtySpent: 0,
        qtyInvoiced: 0,
        result: newMessage.value.trim()
      }
    })

    if (Number(res?.status) === 200 || res?.id) {
      newMessage.value = ''
      await loadChatHistory(true)

      if (assignToMerchant) {
        // Only trigger if not already assigned — avoids a redundant email
        if (!lastSavedIsMerchantAction.value) {
          try {
            await $fetch('/api/requests/requests/update', {
              method: 'PUT',
              headers: useRequestHeaders(['cookie']),
              body: { id: route.params.id, isMerchantAction: true }
            })
            form.value.isMerchantAction = true
            lastSavedIsMerchantAction.value = true
            formSnapshot.value = snapshotOf(form.value)
            $fetch(`/api/requests/requests/${route.params.id}/action-notification`, {
              method: 'POST',
              headers: useRequestHeaders(['cookie']),
              body: { action: 'merchant-action' }
            }).catch(() => {})
          } catch (e) {
            console.error('Assign to merchant failed:', e)
          }
        }
        toast({ message: 'Gesendet & dem Merchant zugewiesen', type: 'is-success', duration: 2000 })
      } else {
        toast({ message: 'Nachricht gesendet', type: 'is-success', duration: 1500 })
      }
    } else {
      toast({ message: res?.message || 'Fehler', type: 'is-danger', duration: 2000 })
    }
  } catch (err) {
    console.error('Send message error:', err)
    toast({ message: 'Fehler beim Senden', type: 'is-danger', duration: 2000 })
  } finally {
    sendingMessage.value = false
  }
}

const sendAndAssign = () => sendMessage({ assignToMerchant: true })

// Group messages by date for timeline separators
const groupedMessages = computed(() => {
  const groups = []
  let currentDate = ''
  for (const msg of chatMessages.value) {
    const dateGroup = formatDateGroup(msg.date)
    if (dateGroup !== currentDate) {
      currentDate = dateGroup
      groups.push({ type: 'date', label: dateGroup })
    }
    groups.push({ type: 'message', ...msg })
  }
  return groups
})

const currentUserName = computed(() => user.value?.Name || '')

// Viewer is merchant if their org matches the ticket's org
const viewerIsMerchant = computed(() => {
  return Number(organizationId.value) === Number(form.value.organizationId)
})

const isMerchantItem = (item) => {
  const isOwnMsg = Number(item.createdById) === Number(userId.value)
  return viewerIsMerchant.value ? isOwnMsg : !isOwnMsg
}

// Reference links (Partner / Order / Invoice / Product) — open in an internal
// tab while preserving the current ticket tab, following the tabs composable
// pattern used elsewhere in the app.
const refRoutes = {
  partner:  { path: (id) => `/partners/partners/${id}/edit`,   labelKey: 'partner',  iconLabel: 'Geschaftspartner' },
  order:    { path: (id) => `/sales/orders/${id}/edit`,        labelKey: 'order',    iconLabel: 'Auftrag' },
  invoice:  { path: (id) => `/sales/invoices/${id}/edit`,      labelKey: 'invoice',  iconLabel: 'Rechnung' },
  product:  { path: (id) => `/materials/products/${id}/edit`,  labelKey: 'product',  iconLabel: 'Produkt' },
  inOut:    { path: (id) => `/sales/shipments/${id}/edit`,     labelKey: 'inOut',    iconLabel: 'Lieferschein' },
}
const openRefTab = (kind) => {
  const cfg = refRoutes[kind]
  if (!cfg) return
  const id = form.value[kind + 'Id']
  if (!id) return
  const link = cfg.path(id)
  const labelText = form.value[cfg.labelKey] || cfg.iconLabel
  tabs.value = (tabs.value || []).map(t => ({ ...t, active: false }))
  const existing = tabs.value.findIndex(t => t.link === link)
  if (existing >= 0) tabs.value[existing].active = true
  else tabs.value.push({ link, text: labelText, active: true })
  try { localStorage.setItem('tabs', JSON.stringify(tabs.value)) } catch (e) {}
  router.push(link)
}
const clearRef = (kind) => {
  form.value[kind + 'Id'] = ''
  form.value[refRoutes[kind].labelKey] = ''
  refPreview.value[kind] = null
  save()
}

// Priority slider — iDempiere uses codes 1/3/5/7/9 (Urgent → Minor).
// Red (Urgent) is rendered on the right via `direction: rtl` on the slider
// wrap so the most critical visual cue sits where the eye ends.
const priorityStops = [
  { id: '1', label: 'Urgent',   colour: '#dc2626' },
  { id: '3', label: 'High',     colour: '#ea580c' },
  { id: '5', label: 'Medium',   colour: '#eab308' },
  { id: '7', label: 'Low',      colour: '#3b82f6' },
  { id: '9', label: 'Minor',    colour: '#6b7280' },
]
const priorityModel = computed({
  get: () => Number(form.value?.priorityId) || 5,
  set: (v) => { form.value.priorityId = String(v) }
})
const priorityInfo = computed(() =>
  priorityStops.find(s => s.id === String(priorityModel.value)) || priorityStops[2]
)
const onPriorityInput = (ev) => { priorityModel.value = Number(ev.target.value) }

// --- Shortcut search-&-pick modal ---
// The inline SelectBox dropdowns in the narrow shortcuts column were clipped
// by neighbouring cards. All picking happens through this single Teleported
// modal instead: click a shortcut -> modal opens with a search field + results.
const pickerConfig = {
  partner: {
    title: 'Geschaftspartner verknupfen', icon: 'ti-building-store',
    model: 'c_bpartner',
    filterField: 'Name',
    listUrl: 'models/c_bpartner?$top=30&$orderby=Name asc',
    display: (r) => r?.Name || r?.identifier || '',
    sub: (r) => r?.Value || r?.TaxID || ''
  },
  order: {
    title: 'Auftrag verknupfen', icon: 'ti-file-text',
    model: 'c_order',
    filterField: 'DocumentNo',
    listUrl: 'models/c_order?$top=30&$orderby=DateOrdered desc',
    display: (r) => r?.DocumentNo || r?.identifier || '',
    sub: (r) => r?.C_BPartner_ID?.identifier || ''
  },
  invoice: {
    title: 'Rechnung verknupfen', icon: 'ti-receipt',
    model: 'c_invoice',
    filterField: 'DocumentNo',
    listUrl: 'models/c_invoice?$top=30&$orderby=DateInvoiced desc',
    display: (r) => r?.DocumentNo || r?.identifier || '',
    sub: (r) => r?.C_BPartner_ID?.identifier || ''
  },
  product: {
    title: 'Produkt verknupfen', icon: 'ti-package',
    model: 'm_product',
    filterField: 'Name',
    listUrl: 'models/m_product?$top=30&$orderby=Name asc',
    display: (r) => r?.Name || r?.identifier || '',
    sub: (r) => r?.Value || r?.SKU || ''
  },
  inOut: {
    title: 'Lieferschein verknupfen', icon: 'ti-truck-delivery',
    model: 'm_inout',
    filterField: 'DocumentNo',
    listUrl: 'models/m_inout?$top=30&$orderby=MovementDate desc',
    display: (r) => r?.DocumentNo || r?.identifier || '',
    sub: (r) => r?.C_BPartner_ID?.identifier || ''
  }
}
const picker = ref({ open: false, kind: '', query: '', results: [], loading: false })
let pickerSearchTimer = null

const openPicker = (kind) => {
  picker.value = { open: true, kind, query: '', results: [], loading: true }
  runPickerSearch()
}
const closePicker = () => { picker.value.open = false }
const runPickerSearch = async () => {
  const kind = picker.value.kind
  const cfg = pickerConfig[kind]
  if (!cfg) return
  picker.value.loading = true
  try {
    const q = (picker.value.query || '').trim()
    const url = q
      ? `/api/filters/${cfg.model}/${string.urlEncode(`contains(${cfg.filterField},'${q.replace(/'/g, "''")}')`)}`
      : `/api/${cfg.listUrl}`
    const res = await $fetch(url, { headers: useRequestHeaders(['cookie']) })
    picker.value.results = Array.isArray(res?.records) ? res.records : (Array.isArray(res) ? res : [])
  } catch (e) {
    picker.value.results = []
  } finally {
    picker.value.loading = false
  }
}
const onPickerQueryInput = () => {
  if (pickerSearchTimer) clearTimeout(pickerSearchTimer)
  pickerSearchTimer = setTimeout(runPickerSearch, 250)
}
const pickResult = (record) => {
  const kind = picker.value.kind
  const cfg = pickerConfig[kind]
  if (!cfg || !record?.id) return
  form.value[kind + 'Id'] = String(record.id)
  form.value[refRoutes[kind].labelKey] = cfg.display(record)
  closePicker()
  save()
  // Kick off the preview fetch for the freshly-linked record so the card fills
  // in immediately instead of waiting for the next reactive re-entry.
  loadRefPreview(kind)
}

// --- Reference preview data (order / invoice / product) ---
// Fetched on demand once a shortcut is linked; shown as a compact summary
// inside the ref card (date, customer, total, status) — mirrors the my-tickets
// reference panel pattern.
const refPreview = ref({ order: null, invoice: null, product: null, inOut: null })
const refPreviewLoading = ref({ order: false, invoice: false, product: false, inOut: false })

// Order behind a directly-referenced shipment (Reference tab). The "Lieferschein"
// link points at one m_inout, and that shipment carries a C_Order_ID. It is
// resolved as a side-effect of loadRefPreview('inOut') below (single fetch), so
// it must be declared before loadRefPreview — the immediate inOutId watcher
// runs during setup and touches it.
const inOutOrder = ref(null)

const loadRefPreview = async (kind) => {
  const id = form.value[kind + 'Id']
  if (!id) {
    refPreview.value[kind] = null
    // A directly-referenced shipment also feeds the "order behind it" panel.
    if (kind === 'inOut') inOutOrder.value = null
    return
  }
  if (refPreview.value[kind]?.id && String(refPreview.value[kind].id) === String(id)) return
  refPreviewLoading.value[kind] = true
  try {
    let url = ''
    if (kind === 'order')    url = `/api/orders/${id}`
    if (kind === 'invoice')  url = `/api/invoices/${id}`
    if (kind === 'product')  url = `/api/materials/products/${id}`
    if (kind === 'inOut')    url = `/api/inouts/${id}`
    if (!url) return
    const res = await $fetch(url, { headers: useRequestHeaders(['cookie']) })
    refPreview.value[kind] = res || null
    // The shipment carries a C_Order_ID — resolve it here (single fetch) so the
    // Reference-tab "Auftrag zum Lieferschein" panel can offer open/adopt.
    if (kind === 'inOut') {
      const orderId = res?.C_Order_ID?.id || ''
      inOutOrder.value = orderId
        ? { id: orderId, documentNo: res?.C_Order_ID?.identifier || '' }
        : null
    }
  } catch (e) {
    refPreview.value[kind] = null
    if (kind === 'inOut') inOutOrder.value = null
  } finally {
    refPreviewLoading.value[kind] = false
  }
}

// Watch each shortcut id — load preview on change (covers both initial mount
// via retrieve() and later relinking via the picker modal).
watch(() => form.value?.orderId,   () => loadRefPreview('order'),   { immediate: true })
watch(() => form.value?.invoiceId, () => loadRefPreview('invoice'), { immediate: true })
watch(() => form.value?.productId, () => loadRefPreview('product'), { immediate: true })
watch(() => form.value?.inOutId,   () => loadRefPreview('inOut'),   { immediate: true })

// Preview helpers — shared formatting for order / invoice cards
const docStatusLabels = { DR: 'Entwurf', IP: 'In Bearbeitung', CO: 'Abgeschlossen', CL: 'Geschlossen', VO: 'Storniert', RE: 'Reaktiviert', AP: 'Genehmigt', NA: 'Nicht genehmigt', WP: 'In Bearbeitung', WC: 'Wartet' }
const docStatusPalette = {
  CO: { bg: '#d1fae5', color: '#065f46', border: '#6ee7b7' },
  CL: { bg: '#e5e7eb', color: '#374151', border: '#d1d5db' },
  DR: { bg: '#fef3c7', color: '#92400e', border: '#fcd34d' },
  IP: { bg: '#dbeafe', color: '#1e40af', border: '#93c5fd' },
  WP: { bg: '#dbeafe', color: '#1e40af', border: '#93c5fd' },
  VO: { bg: '#fee2e2', color: '#991b1b', border: '#fca5a5' },
  AP: { bg: '#d1fae5', color: '#065f46', border: '#6ee7b7' }
}
const docStatusStyle = (code) => {
  const p = docStatusPalette[code] || { bg: '#f3f4f6', color: '#4b5563', border: '#d1d5db' }
  return { background: p.bg, color: p.color, borderColor: p.border }
}
const formatRefMoney = (val, currency) => {
  const n = Number(val || 0)
  const cur = currency || 'EUR'
  try { return new Intl.NumberFormat('de-DE', { style: 'currency', currency: cur }).format(n) }
  catch { return n.toFixed(2) + ' ' + cur }
}
const formatRefDate = (v) => {
  if (!v) return ''
  try { return new Date(v).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) }
  catch { return String(v) }
}

// --- Lieferscheine (shipments) of the linked order ---
// Every m_inout whose C_Order_ID is the ticket's linked order. One order can
// produce several deliveries, so this is a list shown as its own reference card
// in the Basic tab. Loaded on demand once an order is linked.
const orderShipments = ref([])
const orderShipmentsLoading = ref(false)
const orderShipmentsFor = ref('')

const loadOrderShipments = async () => {
  const orderId = form.value?.orderId
  if (!orderId) { orderShipments.value = []; orderShipmentsFor.value = ''; return }
  // Already loaded for this exact order — skip the refetch.
  if (orderShipmentsFor.value === String(orderId)) return
  orderShipmentsLoading.value = true
  try {
    const res = await $fetch(`/api/orders/${orderId}/shipments`, {
      headers: useRequestHeaders(['cookie'])
    })
    orderShipments.value = Array.isArray(res?.records) ? res.records : []
    orderShipmentsFor.value = String(orderId)
  } catch (e) {
    orderShipments.value = []
  } finally {
    orderShipmentsLoading.value = false
  }
}
watch(() => form.value?.orderId, () => loadOrderShipments(), { immediate: true })

// Open a shipment (Lieferschein) in its own internal tab — mirrors openRefTab.
const openShipmentTab = (id, docNo) => {
  if (!id) return
  const link = `/sales/shipments/${id}/edit`
  const labelText = docNo || 'Lieferschein'
  tabs.value = (tabs.value || []).map(t => ({ ...t, active: false }))
  const existing = tabs.value.findIndex(t => t.link === link)
  if (existing >= 0) tabs.value[existing].active = true
  else tabs.value.push({ link, text: labelText, active: true })
  try { localStorage.setItem('tabs', JSON.stringify(tabs.value)) } catch (e) {}
  router.push(link)
}

// Open an order in its own internal tab — used from the order card AND from the
// order resolved off a directly-referenced shipment (below).
const openOrderTab = (id, docNo) => {
  if (!id) return
  const link = `/sales/orders/${id}/edit`
  const labelText = docNo || 'Auftrag'
  tabs.value = (tabs.value || []).map(t => ({ ...t, active: false }))
  const existing = tabs.value.findIndex(t => t.link === link)
  if (existing >= 0) tabs.value[existing].active = true
  else tabs.value.push({ link, text: labelText, active: true })
  try { localStorage.setItem('tabs', JSON.stringify(tabs.value)) } catch (e) {}
  router.push(link)
}

// Adopt the shipment's order as the ticket's order reference. Only offered when
// the ticket has no order linked, or a different one.
const adoptInOutOrder = () => {
  if (!inOutOrder.value?.id) return
  form.value.orderId = String(inOutOrder.value.id)
  form.value.order = inOutOrder.value.documentNo || ''
  loadRefPreview('order')
  save()
}

// Attachment viewer (image / video / audio / pdf)
const attachmentViewerOpen = ref(false)
const attachmentViewerData = ref({})

const isImageExt = (ext) => ['.jpg', '.jpeg', '.png', '.webp', '.gif', '.bmp', '.svg'].includes((ext || '').toLowerCase())
const isPdfExt = (ext) => (ext || '').toLowerCase() === '.pdf'
const isVideoExt = (ext) => ['.mp4', '.m4v', '.avi', '.mpeg', '.webm', '.mov'].includes((ext || '').toLowerCase())
const isAudioExt = (ext) => ['.mp3', '.wav', '.ogg', '.m4a', '.webm'].includes((ext || '').toLowerCase())

const mimeFor = (a) => {
  if (a.mime) return a.mime
  if (isImageExt(a.ext)) return 'image/' + a.ext.replace('.', '')
  if (isPdfExt(a.ext)) return 'application/pdf'
  if (isVideoExt(a.ext)) return 'video/' + a.ext.replace('.', '')
  if (isAudioExt(a.ext)) return 'audio/' + a.ext.replace('.', '')
  return 'application/octet-stream'
}

const openAttachmentViewer = (a) => {
  attachmentViewerData.value = {
    name: a.name,
    source: a.url,
    type: mimeFor(a),
    width: a.width || 1366,
    height: a.height || 768
  }
  attachmentViewerOpen.value = true
}

const formatFileSize = (bytes) => {
  if (!bytes) return ''
  const kb = bytes / 1024
  if (kb < 1024) return kb.toFixed(1) + ' KB'
  return (kb / 1024).toFixed(2) + ' MB'
}

// Drag-drop file upload into chat
const isDragging = ref(false)
const isUploading = ref(false)
let dragCounter = 0

const onDragEnter = (e) => {
  e.preventDefault()
  dragCounter++
  if (e.dataTransfer?.types?.includes('Files')) isDragging.value = true
}
const onDragOver = (e) => { e.preventDefault() }
const onDragLeave = (e) => {
  e.preventDefault()
  dragCounter--
  if (dragCounter <= 0) { isDragging.value = false; dragCounter = 0 }
}
const onDrop = async (e) => {
  e.preventDefault()
  isDragging.value = false
  dragCounter = 0
  const files = Array.from(e.dataTransfer?.files || [])
  if (!files.length || !route.params?.id) return
  await uploadFilesToStrapi(files)
}

const uploadFilesToStrapi = async (files) => {
  isUploading.value = true
  try {
    // 1. Get or create Strapi attachment record (same as AttachmentModal.strapiConnection)
    let strapiAttachmentId = ''
    let strapiAttachmentDocumentId = ''

    const attMeta = await $fetch(`/api/attachments/R_Request/${route.params.id}`, {
      headers: useRequestHeaders(['cookie'])
    })
    if (attMeta?.Strapi_Attachment_ID && attMeta?.Strapi_Attachment_documentId) {
      strapiAttachmentId = attMeta.Strapi_Attachment_ID
      strapiAttachmentDocumentId = attMeta.Strapi_Attachment_documentId
    } else {
      const res = await $fetch(`/api/attachments/R_Request/${route.params.id}`, {
        method: 'POST',
        headers: useRequestHeaders(['cookie']),
        body: {
          uid: form.value.uid,
          organizationId: form.value.organizationId || organizationId.value,
          data: 'TG9nc2hpcCBBdHRhY2htZW50IEV4YW1wbGU='
        }
      })
      if (res?.Strapi_Attachment_ID && res?.Strapi_Attachment_documentId) {
        strapiAttachmentId = res.Strapi_Attachment_ID
        strapiAttachmentDocumentId = res.Strapi_Attachment_documentId
      }
    }

    if (!strapiAttachmentId) {
      toast({ message: 'Could not create attachment record', type: 'is-danger', duration: 2000 })
      return
    }

    // 2. Upload each file to Strapi
    for (const file of files) {
      const formData = new FormData()
      formData.append('field', 'attachment')
      formData.append('ref', 'api::ad-attachment.ad-attachment')
      formData.append('refId', strapiAttachmentId)
      formData.append('files', file)
      formData.append('fileInfo', JSON.stringify({
        caption: String(route.params.id),
        alternativeText: String(form.value.uid),
      }))

      await fetch(config.public.strapi + '/media-api/upload', {
        method: 'POST',
        headers: { Authorization: 'Bearer ' + config.public.strapitoken },
        body: formData
      })
    }

    toast({ message: `${files.length} file${files.length > 1 ? 's' : ''} uploaded`, type: 'is-success', duration: 2000 })

    // 3. Reload chat to show new attachments
    await loadChatHistory(true)
  } catch (err) {
    console.error('Drag-drop upload failed:', err)
    toast({ message: 'Upload failed: ' + (err.message || 'Unknown error'), type: 'is-danger', duration: 3000 })
  } finally {
    isUploading.value = false
  }
}
</script>

<template>
  <div @mouseleave.self="save">
    <div class="rf-container">

      <template v-if="tab === 'basic'">
        <div class="rf-grid rf-grid-chat">
          <!-- Left: Full chat -->
          <div class="rf-chat-column">
            <div class="rf-card rf-card-chat rf-card-chat-full" @dragenter="onDragEnter" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop" :class="{ 'rf-drag-active': isDragging }">
              <div v-if="isDragging" class="rf-drop-overlay">
                <div class="rf-drop-overlay-content">
                  <i class="ti ti-cloud-upload" style="font-size: 48px;"></i>
                  <span>Dateien hier ablegen</span>
                </div>
              </div>
              <div v-if="isUploading" class="rf-drop-overlay rf-drop-uploading">
                <div class="rf-drop-overlay-content">
                  <i class="ti ti-loader-2 spin" style="font-size: 36px;"></i>
                  <span>Wird hochgeladen...</span>
                </div>
              </div>
              <div class="rf-card-title">
                <i class="ti ti-messages"></i>
                <span>Verlauf</span>
                <span class="chat-count" v-if="chatMessages.length">{{ chatMessages.length }}</span>
                <button type="button" class="rf-amount-calc-btn rf-amount-calc-btn-header" title="Zeit abrechnen (Stunden/Minuten)" @click.stop.prevent="openAmountCalc" :disabled="!form.id">
                  <i class="ti ti-plus"></i>
                  <i class="ti ti-calculator"></i>
                  <span>Zeit abrechnen</span>
                </button>
              </div>

              <div class="chat-container" ref="chatContainerRef">
                <div v-if="chatMessages.length === 0" class="chat-empty">
                  <i class="ti ti-message-dots"></i>
                  <span>Noch keine Nachrichten</span>
                </div>

                <template v-for="(item, idx) in groupedMessages" :key="idx">
                  <div v-if="item.type === 'date'" class="chat-date-sep">
                    <span>{{ item.label }}</span>
                  </div>

                  <div v-else class="chat-bubble" :class="{ 'is-merchant': isMerchantItem(item), 'is-support': !isMerchantItem(item), 'is-history': item.source === 'history', 'is-change': item.source === 'change', 'is-attachment': item.source === 'attachment', 'is-request': item.source === 'request' }">
                    <div class="chat-bubble-header">
                      <span class="chat-author">
                        <i class="ti ti-user-circle" v-if="item.source !== 'attachment'"></i>
                        <i class="ti ti-paperclip" v-else></i>
                        {{ item.author }}
                      </span>
                      <span class="chat-source-badge badge-request" v-if="item.source === 'request'">Anfrage</span>
                      <span class="chat-source-badge badge-history" v-else-if="item.source === 'history' && !item.isDiff">Aktion</span>
                      <span class="chat-source-badge badge-diff" v-else-if="item.source === 'history' && item.isDiff">Ergänzung</span>
                      <span class="chat-source-badge badge-change" v-else-if="item.source === 'change'">Änderung</span>
                      <span class="chat-source-badge badge-attachment" v-else-if="item.source === 'attachment'">Anhang</span>
                      <span class="chat-time">{{ formatChatDate(item.date) }}</span>
                    </div>
                    <div v-if="(item.source === 'change' || item.source === 'history') && item.changes?.length" class="chat-changes">
                      <div v-for="(c, cidx) in item.changes" :key="cidx" class="chat-change-row">
                        <span class="chat-change-label">{{ c.label }}</span>
                        <div class="chat-change-diff">
                          <span class="chat-change-old">{{ c.oldV }}</span>
                          <i class="ti ti-arrow-narrow-right chat-change-arrow"></i>
                          <span class="chat-change-new">{{ c.newV }}</span>
                        </div>
                      </div>
                    </div>
                    <div v-else-if="item.source === 'attachment' && item.attachment" class="chat-attachment">
                      <!-- Image preview -->
                      <div v-if="isImageExt(item.attachment.ext)" class="chat-att-preview" @click="openAttachmentViewer(item.attachment)">
                        <img :src="item.attachment.thumb || item.attachment.url" :alt="item.attachment.name" />
                      </div>
                      <!-- PDF preview -->
                      <div v-else-if="isPdfExt(item.attachment.ext)" class="chat-att-preview chat-att-pdf" @click="openAttachmentViewer(item.attachment)">
                        <embed :src="item.attachment.url + '#toolbar=0&navpanes=0'" type="application/pdf" />
                      </div>
                      <!-- Audio inline -->
                      <audio v-else-if="isAudioExt(item.attachment.ext)" controls class="chat-att-audio">
                        <source :src="item.attachment.url" :type="mimeFor(item.attachment)" />
                      </audio>
                      <!-- Video inline -->
                      <video v-else-if="isVideoExt(item.attachment.ext)" controls class="chat-att-video">
                        <source :src="item.attachment.url" :type="mimeFor(item.attachment)" />
                      </video>
                      <!-- Generic file icon -->
                      <div v-else class="chat-att-file">
                        <i class="ti ti-file"></i>
                      </div>
                      <div class="chat-att-meta">
                        <div class="chat-att-name">{{ item.attachment.name }}</div>
                        <div class="chat-att-size" v-if="item.attachment.size">{{ formatFileSize(item.attachment.size) }}</div>
                        <div class="chat-att-actions">
                          <button
                            v-if="isImageExt(item.attachment.ext) || isPdfExt(item.attachment.ext) || isVideoExt(item.attachment.ext)"
                            type="button" class="chat-att-btn" @click="openAttachmentViewer(item.attachment)"
                          >
                            <i class="ti ti-eye"></i>
                            <span>Ansehen</span>
                          </button>
                          <button
                            v-if="isAudioExt(item.attachment.ext)"
                            type="button" class="chat-att-btn" @click="openAttachmentViewer(item.attachment)"
                          >
                            <i class="ti ti-headphones"></i>
                            <span>Anhören</span>
                          </button>
                          <a :href="item.attachment.url" download :title="item.attachment.name" target="_blank" class="chat-att-btn">
                            <i class="ti ti-download"></i>
                            <span>Download</span>
                          </a>
                        </div>
                      </div>
                    </div>
                    <div v-else-if="item.text" class="chat-bubble-body" v-html="linkifyHtml(item.text)"></div>
                    <div v-if="item.qtySpent" class="chat-bubble-meta">
                      <i class="ti ti-clock"></i>
                      {{ item.qtySpent }} min
                    </div>
                  </div>
                </template>
              </div>

              <div v-if="form.id" class="chat-input-area">
                <textarea
                  v-model="newMessage"
                  class="chat-input"
                  placeholder="Nachricht eingeben..."
                  rows="3"
                  @keydown.ctrl.enter="sendMessage()"
                  @keydown.meta.enter="sendMessage()"
                ></textarea>
                <div class="chat-input-actions">
                  <span class="chat-hint">Strg+Enter zum Senden</span>
                  <div class="chat-send-group">
                    <button class="chat-send-btn" @click="sendMessage()" :disabled="!newMessage.trim() || sendingMessage">
                      <i :class="sendingMessage ? 'ti ti-loader-2 spin' : 'ti ti-send'"></i>
                      <span>{{ sendingMessage ? 'Sende...' : 'Senden' }}</span>
                    </button>
                    <button class="chat-send-btn chat-send-btn--assign" @click="sendAndAssign" :disabled="!newMessage.trim() || sendingMessage" title="Senden und Ticket an den Merchant zur Aktion zuweisen">
                      <i class="ti ti-user-exclamation"></i>
                      <span>Senden &amp; zuweisen</span>
                    </button>
                  </div>
                </div>
              </div>
            </div>
          </div>

          <!-- Right: All fields -->
          <div class="rf-fields-column">
            <!-- Priority slider (moved out of the top status bar so it
                 groups with the other per-ticket fields). Urgent = red on
                 the RIGHT via direction: rtl. -->
            <div class="rf-card rf-priority-card" :style="{ '--prio-colour': priorityInfo.colour }">
              <div class="rf-card-title"><i class="ti ti-flag"></i><span>Prioritat</span><span class="rf-priority-value" :style="{ color: priorityInfo.colour }">{{ priorityInfo.label }}</span></div>
              <div class="rf-priority-slider-wrap">
                <input
                  type="range"
                  min="1"
                  max="9"
                  step="2"
                  class="rf-priority-slider"
                  :value="priorityModel"
                  @input="onPriorityInput"
                  @change="save"
                />
                <div class="rf-priority-stops" aria-hidden="true">
                  <span
                    v-for="s in priorityStops"
                    :key="s.id"
                    class="rf-priority-stop"
                    :class="{ 'rf-priority-stop--active': String(s.id) === String(priorityModel) }"
                    :style="{ background: s.colour }"
                    :title="s.label"
                  ></span>
                </div>
              </div>
            </div>

            <!-- Key info -->
            <div class="rf-card">
              <div class="rf-card-title"><i class="ti ti-file-text"></i><span>Anfrage</span></div>
              <div class="rf-row rf-row-compact">
                <div class="rf-field">
                  <label class="rf-label">Dok-Nr.</label>
                  <input type="text" v-model="form.documentNo" class="rf-input" @keyup="save" />
                </div>
                <div class="rf-field">
                  <label class="rf-label">Betrag</label>
                  <input type="text" v-model="form.requestAmt" class="rf-input" @keyup="save" />
                </div>
              </div>
              <div class="rf-row rf-row-compact">
                <div class="rf-field">
                  <label class="rf-label">Typ</label>
                  <ClientOnly><SelectBox v-model="form.requestTypeId" :options="getOptions?.['request_types']?.records || [{id: form.requestTypeId, Name: form.requestType}]" prop="Name" dataprop="id" datatype="string" :size="5" @click="setOptions('models/r_requesttype', 'request_types')" :loading="loading?.['request_types'] ?? true" @change="save" /></ClientOnly>
                </div>
                <div class="rf-field">
                  <label class="rf-label">Kategorie</label>
                  <ClientOnly><SelectBox v-model="form.categoryId" :options="getOptions?.['request_categories']?.records || [{id: form.categoryId, Name: form.category}]" prop="Name" dataprop="id" datatype="string" :size="5" @click="setOptions('models/r_category', 'request_categories')" :loading="loading?.['request_categories'] ?? true" @change="save" /></ClientOnly>
                </div>
              </div>
              <!-- Prioritat is controlled by the colour-graded slider in the
                   status bar at the top; no duplicate select here. -->
              <div class="rf-row rf-row-compact">
                <div class="rf-field">
                  <label class="rf-label">Falligkeit</label>
                  <select v-model="form.dueTypeId" class="rf-select" @change="save">
                    <option value="5">Due</option><option value="3">Overdue</option><option value="7">Scheduled</option>
                  </select>
                </div>
              </div>
            </div>

            <!-- Organisation -->
            <div class="rf-card">
              <div class="rf-card-title"><i class="ti ti-building"></i><span>Organisation</span></div>
              <div class="rf-field">
                <label class="rf-label">Organisation</label>
                <input v-if="form.id !== ''" type="text" :value="form.organization" class="rf-input" readonly />
                <SelectBox v-else v-model="form.organizationId" :options="getOptions?.['orgs']?.records || [{id: form.organizationId, Name: form.organization}]" prop="Name" dataprop="id" datatype="string" :size="5" @click="setOptions('models/ad_org', 'orgs')" @change="(val, opt) => {form.organization = opt.Name; save();}" :loading="loading?.['orgs'] ?? true" />
              </div>
            </div>

          </div>

          <!-- Shortcuts column — stacks the four reference cards directly
               next to the chat so linked Partner / Order / Invoice / Product
               records are always visible without scrolling the fields column.
               Picking happens through the picker modal (Teleported below) to
               avoid the dropdown being clipped by neighbouring cards. -->
          <div class="rf-shortcuts-column">
            <div class="rf-ref-stack">
              <!-- Partner -->
              <div class="rf-ref-card">
                <div class="rf-ref-head">
                  <span class="rf-ref-icon rf-ref-icon--partner"><i class="ti ti-building-store"></i></span>
                  <div class="rf-ref-head-body">
                    <div class="rf-ref-eyebrow">Geschaftspartner</div>
                    <div class="rf-ref-title" v-if="form.partnerId">{{ form.partner || '—' }}</div>
                    <div class="rf-ref-title rf-ref-title--empty" v-else>Nicht verknupft</div>
                  </div>
                </div>
                <div class="rf-ref-body">
                  <button v-if="!form.partnerId" type="button" class="rf-ref-link-btn" @click="openPicker('partner')"><i class="ti ti-search"></i><span>Verknupfen</span></button>
                  <div v-else class="rf-ref-actions">
                    <button type="button" class="rf-ref-btn rf-ref-btn--primary" @click="openRefTab('partner')"><i class="ti ti-external-link"></i><span>Offnen</span></button>
                    <button type="button" class="rf-ref-btn rf-ref-btn--muted" @click="openPicker('partner')" title="Andere wahlen"><i class="ti ti-arrows-exchange"></i></button>
                    <button type="button" class="rf-ref-btn rf-ref-btn--muted" @click="clearRef('partner')" title="Verknupfung entfernen"><i class="ti ti-x"></i></button>
                  </div>
                </div>
              </div>

              <!-- Order -->
              <div class="rf-ref-card">
                <div class="rf-ref-head">
                  <span class="rf-ref-icon rf-ref-icon--order"><i class="ti ti-file-text"></i></span>
                  <div class="rf-ref-head-body">
                    <div class="rf-ref-eyebrow">Auftrag</div>
                    <div class="rf-ref-title" v-if="form.orderId">{{ form.order || '—' }}</div>
                    <div class="rf-ref-title rf-ref-title--empty" v-else>Nicht verknupft</div>
                  </div>
                </div>
                <div class="rf-ref-preview" v-if="form.orderId">
                  <div v-if="refPreviewLoading.order" class="rf-ref-preview-loading"><i class="ti ti-loader-2 spin"></i></div>
                  <template v-else-if="refPreview.order">
                    <div class="rf-ref-prev-row" v-if="refPreview.order.DateOrdered"><span class="rf-ref-prev-label">Datum</span><span class="rf-ref-prev-val">{{ formatRefDate(refPreview.order.DateOrdered) }}</span></div>
                    <div class="rf-ref-prev-row" v-if="refPreview.order.C_BPartner_ID"><span class="rf-ref-prev-label">Kunde</span><span class="rf-ref-prev-val">{{ refPreview.order.C_BPartner_ID.identifier }}</span></div>
                    <div class="rf-ref-prev-row"><span class="rf-ref-prev-label">Summe</span><span class="rf-ref-prev-val rf-ref-prev-val--strong">{{ formatRefMoney(refPreview.order.GrandTotal, refPreview.order.C_Currency_ID?.identifier) }}</span></div>
                    <div class="rf-ref-prev-row"><span class="rf-ref-prev-label">Status</span><span class="rf-ref-chip" :style="docStatusStyle(refPreview.order.DocStatus?.id || refPreview.order.DocStatus)">{{ docStatusLabels[refPreview.order.DocStatus?.id || refPreview.order.DocStatus] || refPreview.order.DocStatus?.identifier || refPreview.order.DocStatus || '-' }}</span></div>
                  </template>
                </div>
                <div class="rf-ref-body">
                  <button v-if="!form.orderId" type="button" class="rf-ref-link-btn" @click="openPicker('order')"><i class="ti ti-search"></i><span>Verknupfen</span></button>
                  <div v-else class="rf-ref-actions">
                    <button type="button" class="rf-ref-btn rf-ref-btn--primary" @click="openRefTab('order')"><i class="ti ti-external-link"></i><span>Offnen</span></button>
                    <button type="button" class="rf-ref-btn rf-ref-btn--muted" @click="openPicker('order')" title="Anderen wahlen"><i class="ti ti-arrows-exchange"></i></button>
                    <button type="button" class="rf-ref-btn rf-ref-btn--muted" @click="clearRef('order')" title="Verknupfung entfernen"><i class="ti ti-x"></i></button>
                  </div>
                </div>
              </div>

              <!-- Lieferschein — the shipment referenced directly on the ticket
                   (r_request.M_InOut_ID). Independent of whether an order is
                   linked, so it shows even when the ticket has no Auftrag. -->
              <div class="rf-ref-card">
                <div class="rf-ref-head">
                  <span class="rf-ref-icon rf-ref-icon--shipment"><i class="ti ti-truck-delivery"></i></span>
                  <div class="rf-ref-head-body">
                    <div class="rf-ref-eyebrow">Lieferschein</div>
                    <div class="rf-ref-title" v-if="form.inOutId">{{ form.inOut || '—' }}</div>
                    <div class="rf-ref-title rf-ref-title--empty" v-else>Nicht verknupft</div>
                  </div>
                </div>
                <div class="rf-ref-preview" v-if="form.inOutId">
                  <div v-if="refPreviewLoading.inOut" class="rf-ref-preview-loading"><i class="ti ti-loader-2 spin"></i></div>
                  <template v-else-if="refPreview.inOut">
                    <div class="rf-ref-prev-row" v-if="refPreview.inOut.MovementDate"><span class="rf-ref-prev-label">Datum</span><span class="rf-ref-prev-val">{{ formatRefDate(refPreview.inOut.MovementDate) }}</span></div>
                    <div class="rf-ref-prev-row" v-if="refPreview.inOut.C_BPartner_ID"><span class="rf-ref-prev-label">Kunde</span><span class="rf-ref-prev-val">{{ refPreview.inOut.C_BPartner_ID.identifier }}</span></div>
                    <div class="rf-ref-prev-row" v-if="refPreview.inOut.TrackingNo"><span class="rf-ref-prev-label">Tracking</span><span class="rf-ref-prev-val">{{ refPreview.inOut.TrackingNo }}</span></div>
                    <div class="rf-ref-prev-row"><span class="rf-ref-prev-label">Status</span><span class="rf-ref-chip" :style="docStatusStyle(refPreview.inOut.DocStatus?.id || refPreview.inOut.DocStatus)">{{ docStatusLabels[refPreview.inOut.DocStatus?.id || refPreview.inOut.DocStatus] || refPreview.inOut.DocStatus?.identifier || refPreview.inOut.DocStatus || '-' }}</span></div>
                  </template>
                </div>
                <div class="rf-ref-body">
                  <button v-if="!form.inOutId" type="button" class="rf-ref-link-btn" @click="openPicker('inOut')"><i class="ti ti-search"></i><span>Verknupfen</span></button>
                  <div v-else class="rf-ref-actions">
                    <button type="button" class="rf-ref-btn rf-ref-btn--primary" @click="openRefTab('inOut')"><i class="ti ti-external-link"></i><span>Offnen</span></button>
                    <button type="button" class="rf-ref-btn rf-ref-btn--muted" @click="openPicker('inOut')" title="Anderen wahlen"><i class="ti ti-arrows-exchange"></i></button>
                    <button type="button" class="rf-ref-btn rf-ref-btn--muted" @click="clearRef('inOut')" title="Verknupfung entfernen"><i class="ti ti-x"></i></button>
                  </div>
                </div>
              </div>

              <!-- Lieferscheine (shipments) of the linked order -->
              <div class="rf-ref-card" v-if="form.orderId">
                <div class="rf-ref-head">
                  <span class="rf-ref-icon rf-ref-icon--shipment"><i class="ti ti-truck-delivery"></i></span>
                  <div class="rf-ref-head-body">
                    <div class="rf-ref-eyebrow">Lieferscheine</div>
                    <div class="rf-ref-title" v-if="orderShipments.length">{{ orderShipments.length }} {{ orderShipments.length === 1 ? 'Lieferschein' : 'Lieferscheine' }}</div>
                    <div class="rf-ref-title rf-ref-title--empty" v-else-if="orderShipmentsLoading">Wird geladen…</div>
                    <div class="rf-ref-title rf-ref-title--empty" v-else>Keine Lieferscheine</div>
                  </div>
                </div>
                <div class="rf-ref-preview" v-if="orderShipments.length || orderShipmentsLoading">
                  <div v-if="orderShipmentsLoading" class="rf-ref-preview-loading"><i class="ti ti-loader-2 spin"></i></div>
                  <template v-else>
                    <button
                      v-for="ship in orderShipments"
                      :key="ship.id"
                      type="button"
                      class="rf-ship-row"
                      @click="openShipmentTab(ship.id, ship.DocumentNo)"
                      :title="'Lieferschein ' + (ship.DocumentNo || '') + ' offnen'"
                    >
                      <span class="rf-ship-icon ti ti-truck-delivery"></span>
                      <span class="rf-ship-info">
                        <span class="rf-ship-docno">{{ ship.DocumentNo || '—' }}</span>
                        <span class="rf-ship-meta">
                          <span v-if="ship.MovementDate">{{ formatRefDate(ship.MovementDate) }}</span>
                          <span v-if="ship.TrackingNo" class="rf-ship-track"><i class="ti ti-barcode"></i>{{ ship.TrackingNo }}</span>
                        </span>
                      </span>
                      <span class="rf-ship-chip" :style="docStatusStyle(ship.DocStatus?.id || ship.DocStatus)">{{ docStatusLabels[ship.DocStatus?.id || ship.DocStatus] || ship.DocStatus?.identifier || ship.DocStatus || '-' }}</span>
                      <span class="rf-ship-open ti ti-external-link"></span>
                    </button>
                  </template>
                </div>
              </div>

              <!-- Invoice -->
              <div class="rf-ref-card">
                <div class="rf-ref-head">
                  <span class="rf-ref-icon rf-ref-icon--invoice"><i class="ti ti-receipt"></i></span>
                  <div class="rf-ref-head-body">
                    <div class="rf-ref-eyebrow">Rechnung</div>
                    <div class="rf-ref-title" v-if="form.invoiceId">{{ form.invoice || '—' }}</div>
                    <div class="rf-ref-title rf-ref-title--empty" v-else>Nicht verknupft</div>
                  </div>
                </div>
                <div class="rf-ref-preview" v-if="form.invoiceId">
                  <div v-if="refPreviewLoading.invoice" class="rf-ref-preview-loading"><i class="ti ti-loader-2 spin"></i></div>
                  <template v-else-if="refPreview.invoice">
                    <div class="rf-ref-prev-row" v-if="refPreview.invoice.DateInvoiced"><span class="rf-ref-prev-label">Datum</span><span class="rf-ref-prev-val">{{ formatRefDate(refPreview.invoice.DateInvoiced) }}</span></div>
                    <div class="rf-ref-prev-row" v-if="refPreview.invoice.C_BPartner_ID"><span class="rf-ref-prev-label">Kunde</span><span class="rf-ref-prev-val">{{ refPreview.invoice.C_BPartner_ID.identifier }}</span></div>
                    <div class="rf-ref-prev-row"><span class="rf-ref-prev-label">Summe</span><span class="rf-ref-prev-val rf-ref-prev-val--strong">{{ formatRefMoney(refPreview.invoice.GrandTotal, refPreview.invoice.C_Currency_ID?.identifier) }}</span></div>
                    <div class="rf-ref-prev-row"><span class="rf-ref-prev-label">Status</span><span class="rf-ref-chip" :style="docStatusStyle(refPreview.invoice.DocStatus?.id || refPreview.invoice.DocStatus)">{{ docStatusLabels[refPreview.invoice.DocStatus?.id || refPreview.invoice.DocStatus] || refPreview.invoice.DocStatus?.identifier || refPreview.invoice.DocStatus || '-' }}</span></div>
                  </template>
                </div>
                <div class="rf-ref-body">
                  <button v-if="!form.invoiceId" type="button" class="rf-ref-link-btn" @click="openPicker('invoice')"><i class="ti ti-search"></i><span>Verknupfen</span></button>
                  <div v-else class="rf-ref-actions">
                    <button type="button" class="rf-ref-btn rf-ref-btn--primary" @click="openRefTab('invoice')"><i class="ti ti-external-link"></i><span>Offnen</span></button>
                    <button type="button" class="rf-ref-btn rf-ref-btn--muted" @click="openPicker('invoice')" title="Andere wahlen"><i class="ti ti-arrows-exchange"></i></button>
                    <button type="button" class="rf-ref-btn rf-ref-btn--muted" @click="clearRef('invoice')" title="Verknupfung entfernen"><i class="ti ti-x"></i></button>
                  </div>
                </div>
              </div>

              <!-- Product -->
              <div class="rf-ref-card">
                <div class="rf-ref-head">
                  <span class="rf-ref-icon rf-ref-icon--product"><i class="ti ti-package"></i></span>
                  <div class="rf-ref-head-body">
                    <div class="rf-ref-eyebrow">Produkt</div>
                    <div class="rf-ref-title" v-if="form.productId">{{ form.product || '—' }}</div>
                    <div class="rf-ref-title rf-ref-title--empty" v-else>Nicht verknupft</div>
                  </div>
                </div>
                <div class="rf-ref-preview" v-if="form.productId">
                  <div v-if="refPreviewLoading.product" class="rf-ref-preview-loading"><i class="ti ti-loader-2 spin"></i></div>
                  <template v-else-if="refPreview.product">
                    <div class="rf-ref-prev-row" v-if="refPreview.product.Value"><span class="rf-ref-prev-label">SKU</span><span class="rf-ref-prev-val">{{ refPreview.product.Value }}</span></div>
                    <div class="rf-ref-prev-row" v-if="refPreview.product.M_Product_Category_ID"><span class="rf-ref-prev-label">Kategorie</span><span class="rf-ref-prev-val">{{ refPreview.product.M_Product_Category_ID.identifier }}</span></div>
                    <div class="rf-ref-prev-row" v-if="refPreview.product.C_UOM_ID"><span class="rf-ref-prev-label">Einheit</span><span class="rf-ref-prev-val">{{ refPreview.product.C_UOM_ID.identifier }}</span></div>
                    <div class="rf-ref-prev-row"><span class="rf-ref-prev-label">Status</span><span class="rf-ref-chip" :style="refPreview.product.IsActive ? { background: '#d1fae5', color: '#065f46', borderColor: '#6ee7b7' } : { background: '#fee2e2', color: '#991b1b', borderColor: '#fca5a5' }">{{ refPreview.product.IsActive ? 'Aktiv' : 'Inaktiv' }}</span></div>
                  </template>
                </div>
                <div class="rf-ref-body">
                  <button v-if="!form.productId" type="button" class="rf-ref-link-btn" @click="openPicker('product')"><i class="ti ti-search"></i><span>Verknupfen</span></button>
                  <div v-else class="rf-ref-actions">
                    <button type="button" class="rf-ref-btn rf-ref-btn--primary" @click="openRefTab('product')"><i class="ti ti-external-link"></i><span>Offnen</span></button>
                    <button type="button" class="rf-ref-btn rf-ref-btn--muted" @click="openPicker('product')" title="Anderes wahlen"><i class="ti ti-arrows-exchange"></i></button>
                    <button type="button" class="rf-ref-btn rf-ref-btn--muted" @click="clearRef('product')" title="Verknupfung entfernen"><i class="ti ti-x"></i></button>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </template>

      <template v-else-if="tab === 'action'">
        <div class="rf-card">
          <div class="rf-card-title"><i class="ti ti-clock"></i><span>Zeiterfassung</span></div>
          <div class="rf-row">
            <div class="rf-field"><label class="rf-label">Startzeit</label><DateTimePicker v-model="form.startTime" @handler="save()" /></div>
            <div class="rf-field"><label class="rf-label">Endzeit</label><DateTimePicker v-model="form.endTime" @handler="save()" /></div>
            <div class="rf-field"><label class="rf-label">Startdatum</label><DateTimePicker v-model="form.startDate" @handler="save()" /></div>
            <div class="rf-field"><label class="rf-label">Enddatum</label><DateTimePicker v-model="form.endDate" @handler="save()" /></div>
          </div>
          <div class="rf-row">
            <div class="rf-field"><label class="rf-label">Menge verbraucht</label><input type="text" v-model="form.qtySpent" class="rf-input" @keyup="save" /></div>
            <div class="rf-field"><label class="rf-label">Menge geplant</label><input type="text" v-model="form.qtyPlan" class="rf-input" @keyup="save" /></div>
            <div class="rf-field">
              <label class="rf-label">Task Status</label>
              <select v-model="form.taskStatusId" class="rf-select" @change="save">
                <option value="0">0% Not Started</option><option value="2">20% Started</option><option value="4">40% Busy</option>
                <option value="6">60% Good Progress</option><option value="8">80% Nearly Done</option><option value="9">90% Finishing</option>
                <option value="A">95% Almost Done</option><option value="C">99% Cleaning up</option><option value="D">100% Complete</option>
              </select>
            </div>
            <div class="rf-field"><label class="rf-label">Nachste Aktion</label><DateTimePicker v-model="form.dateNextAction" @handler="save()" /></div>
          </div>
        </div>
        <div class="rf-card">
          <div class="rf-card-title"><i class="ti ti-settings"></i><span>Aktionen</span></div>
          <div class="rf-row">
            <div class="rf-field"><label class="rf-label">Eintrag Vertraulichkeit</label><select v-model="form.confidentialTypeEntryId" class="rf-select" @change="save"><option value="I">Internal</option><option value="C">Partner Confidential</option><option value="A">Public Information</option></select></div>
            <div class="rf-field"><label class="rf-label">Aktivitat</label><ClientOnly><SelectBox v-model="form.activityId" :options="getOptions?.['activities']?.records || [{id: form.activityId, Name: form.activity}]" prop="Name" dataprop="id" datatype="string" :size="5" @click="setOptions('models/c_activity', 'activities')" :loading="loading?.['activities'] ?? true" @change="save" /></ClientOnly></div>
            <div class="rf-field"><label class="rf-label">Standardantwort</label><ClientOnly><SelectBox v-model="form.standardResponseId" :options="getOptions?.['standard_responses']?.records || [{id: form.standardResponseId, Name: form.standardResponse}]" prop="Name" dataprop="id" datatype="string" :size="5" @click="setOptions('models/r_standardresponse', 'standard_responses')" :loading="loading?.['standard_responses'] ?? true" @change="save" /></ClientOnly></div>
            <div class="rf-field"><label class="rf-label">Mail Vorlage</label><ClientOnly><SelectBox v-model="form.mailTextId" :options="getOptions?.['mail_texts']?.records || [{id: form.mailTextId, Name: form.mailText}]" prop="Name" dataprop="id" datatype="string" :size="5" @click="setOptions('models/r_mailtext', 'mail_texts')" :loading="loading?.['mail_texts'] ?? true" @change="save" /></ClientOnly></div>
          </div>
          <div class="rf-row">
            <div class="rf-field"><label class="rf-label">Produkt verbraucht</label><ClientOnly><SelectBox v-model="form.productSpentId" :options="getOptions?.['product_spents']?.records || [{id: form.productSpentId, Name: form.productSpent}]" prop="Name" dataprop="id" datatype="string" :size="5" @click="setOptions('models/m_product', 'product_spents')" :loading="loading?.['product_spents'] ?? true" @change="save" /></ClientOnly></div>
          </div>
        </div>
        <div class="rf-card">
          <div class="rf-card-title"><i class="ti ti-clipboard-check"></i><span>Ergebnis</span></div>
          <textarea v-model="form.result" class="rf-textarea rf-textarea-sm" @keyup="save" placeholder="Ergebnis eingeben..."></textarea>
        </div>
      </template>

      <template v-else-if="tab === 'reference'">
        <div class="rf-card">
          <div class="rf-card-title"><i class="ti ti-users"></i><span>Partner & Kontakt</span></div>
          <div class="rf-row">
            <div class="rf-field"><label class="rf-label">Benutzer/Kontakt</label><ClientOnly><SelectBox v-model="form.userId" :options="getOptions?.['users']?.records || [{id: form.userId, Name: form.user}]" prop="Name" dataprop="id" datatype="string" :size="5" @click="setOptions('models/ad_user', 'users')" :loading="loading?.['users'] ?? true" @change="save" /></ClientOnly></div>
          </div>
        </div>
        <div class="rf-card">
          <div class="rf-card-title"><i class="ti ti-link"></i><span>Verknupfungen</span></div>
          <div class="rf-row">
            <div class="rf-field"><label class="rf-label">Projekt</label><ClientOnly><SelectBox v-model="form.projectId" :options="getOptions?.['projects']?.records || [{id: form.projectId, Name: form.project}]" prop="Name" dataprop="id" datatype="string" :size="5" @click="setOptions('models/c_project', 'projects')" :loading="loading?.['projects'] ?? true" @change="save" /></ClientOnly></div>
            <div class="rf-field"><label class="rf-label">Zahlung</label><ClientOnly><SelectBox v-model="form.paymentId" :options="getOptions?.['payments']?.records || [{id: form.paymentId, DocumentNo: form.payment}]" prop="DocumentNo" dataprop="id" datatype="string" :size="5" @click="setOptions('models/c_payment', 'payments')" :loading="loading?.['payments'] ?? true" @change="save" /></ClientOnly></div>
            <div class="rf-field"><label class="rf-label">Lieferschein</label><ClientOnly><SelectBox v-model="form.inOutId" :options="getOptions?.['in_outs']?.records || [{id: form.inOutId, DocumentNo: form.inOut}]" prop="DocumentNo" dataprop="id" datatype="string" :size="5" @click="setOptions('models/m_inout', 'in_outs')" :loading="loading?.['in_outs'] ?? true" @change="save" /></ClientOnly></div>
          </div>
          <!-- Auftrag zum gewählten Lieferschein (aus dessen C_Order_ID abgeleitet) -->
          <div class="rf-row" v-if="form.inOutId && (refPreviewLoading.inOut || inOutOrder)">
            <div class="rf-field rf-field--full">
              <div class="rf-inout-order">
                <span class="rf-inout-order-icon ti ti-file-text"></span>
                <div class="rf-inout-order-body">
                  <span class="rf-inout-order-eyebrow">Auftrag zum Lieferschein</span>
                  <span v-if="refPreviewLoading.inOut" class="rf-inout-order-loading"><i class="ti ti-loader-2 spin"></i> Wird ermittelt…</span>
                  <span v-else-if="inOutOrder" class="rf-inout-order-name">{{ inOutOrder.documentNo || ('#' + inOutOrder.id) }}</span>
                </div>
                <div class="rf-inout-order-actions" v-if="inOutOrder && !refPreviewLoading.inOut">
                  <button type="button" class="rf-ref-btn rf-ref-btn--primary" @click="openOrderTab(inOutOrder.id, inOutOrder.documentNo)"><i class="ti ti-external-link"></i><span>Auftrag offnen</span></button>
                  <button v-if="String(form.orderId) !== String(inOutOrder.id)" type="button" class="rf-ref-btn" @click="adoptInOutOrder" title="Als Auftrag des Tickets ubernehmen"><i class="ti ti-link"></i><span>Ubernehmen</span></button>
                </div>
              </div>
            </div>
          </div>
          <div class="rf-row">
            <div class="rf-field"><label class="rf-label">Anlage</label><ClientOnly><SelectBox v-model="form.assetId" :options="getOptions?.['assets']?.records || [{id: form.assetId, Name: form.asset}]" prop="Name" dataprop="id" datatype="string" :size="5" @click="setOptions('models/a_asset', 'assets')" :loading="loading?.['assets'] ?? true" @change="save" /></ClientOnly></div>
            <div class="rf-field"><label class="rf-label">RMA</label><ClientOnly><SelectBox v-model="form.rmaId" :options="getOptions?.['rma']?.records || [{id: form.rmaId, DocumentNo: form.rma}]" prop="DocumentNo" dataprop="id" datatype="string" :size="5" @click="setOptions('models/m_rma', 'rma')" :loading="loading?.['rma'] ?? true" @change="save" /></ClientOnly></div>
            <div class="rf-field"><label class="rf-label">Kampagne</label><ClientOnly><SelectBox v-model="form.campaignId" :options="getOptions?.['campaigns']?.records || [{id: form.campaignId, Name: form.campaign}]" prop="Name" dataprop="id" datatype="string" :size="5" @click="setOptions('models/c_campaign', 'campaigns')" :loading="loading?.['campaigns'] ?? true" @change="save" /></ClientOnly></div>
          </div>
        </div>
      </template>

      <template v-else-if="tab === 'history'">
        <div class="rf-card">
          <div class="rf-card-title"><i class="ti ti-history"></i><span>Verlauf</span></div>
          <div class="rf-row">
            <div class="rf-field"><label class="rf-label">Letzte Aktion</label><input type="text" :value="form.dateLastAction" class="rf-input" readonly /></div>
            <div class="rf-field"><label class="rf-label">Letztes Ergebnis</label><input type="text" :value="form.lastResult" class="rf-input" readonly /></div>
            <div class="rf-field"><label class="rf-label">Letzte Warnung</label><input type="text" class="rf-input" readonly /></div>
          </div>
        </div>
        <div class="rf-card">
          <div class="rf-card-title"><i class="ti ti-info-circle"></i><span>System</span></div>
          <div class="rf-row">
            <div class="rf-field"><label class="rf-label">Erstellt</label><input type="text" class="rf-input" readonly /></div>
            <div class="rf-field"><label class="rf-label">Erstellt von</label><input type="text" class="rf-input" readonly /></div>
            <div class="rf-field"><label class="rf-label">Aktualisiert</label><input type="text" class="rf-input" readonly /></div>
            <div class="rf-field"><label class="rf-label">Aktualisiert von</label><input type="text" class="rf-input" readonly /></div>
          </div>
        </div>
      </template>

    </div>

    <AttachmentViewer
      v-model="attachmentViewerOpen"
      :type="attachmentViewerData.type"
      :source="attachmentViewerData.source"
      :title="attachmentViewerData.name"
      :width="attachmentViewerData.width"
      :height="attachmentViewerData.height"
    />
  </div>

  <Teleport to="body">
    <div class="modal" :class="{'active': showAmountCalc}" :style="{display: showAmountCalc ? 'block' : 'none'}">
      <div class="modalDialog modalCenter" @click.self="closeAmountCalc">
        <div class="modalContent">
          <div class="modalHeader">
            <h3 class="modalTitle">Zeit abrechnen</h3>
            <div class="cursor-pointer" @click="closeAmountCalc">
              <span class="tf-icon ti ti-x ti-sm me-1"></span>
            </div>
          </div>
          <div class="modalBody">
            <div class="amt-calc-body">
              <div class="amt-calc-row">
                <div class="amt-calc-field">
                  <label class="rf-label">Stunden</label>
                  <input type="number" min="0" step="1" v-model.number="amountCalc.hours" class="rf-input" />
                </div>
                <div class="amt-calc-field">
                  <label class="rf-label">Minuten</label>
                  <input type="number" min="0" max="59" step="1" v-model.number="amountCalc.minutes" class="rf-input" />
                </div>
              </div>
              <div class="amt-calc-field">
                <label class="rf-label">Notiz</label>
                <textarea v-model="amountCalc.note" class="rf-textarea rf-textarea-sm" placeholder="Notiz zur Zeiterfassung..."></textarea>
              </div>
              <div class="amt-calc-summary">
                <div class="amt-calc-line">
                  <span>Stundensatz</span>
                  <strong v-if="!amountCalc.rateLoading">{{ Number(amountCalc.hourlyRate).toFixed(2).replace('.', ',') }} €/h</strong>
                  <span v-else class="amt-calc-loading"><i class="ti ti-loader-2 spin"></i> lade...</span>
                </div>
                <div class="amt-calc-line">
                  <span>Zeit</span>
                  <strong>{{ Number(amountCalc.hours) || 0 }}h {{ Number(amountCalc.minutes) || 0 }}min</strong>
                </div>
                <div class="amt-calc-line">
                  <span>Berechneter Betrag</span>
                  <strong>{{ calculatedAmount.toFixed(2).replace('.', ',') }} €</strong>
                </div>
                <div v-if="parseAmount(form.requestAmt) > 0" class="amt-calc-line">
                  <span>Vorheriger Betrag</span>
                  <strong>+ {{ parseAmount(form.requestAmt).toFixed(2).replace('.', ',') }} €</strong>
                </div>
                <div class="amt-calc-line amt-calc-total">
                  <span>Neuer Gesamtbetrag</span>
                  <strong>{{ (Math.round((parseAmount(form.requestAmt) + calculatedAmount) * 100) / 100).toFixed(2).replace('.', ',') }} €</strong>
                </div>
              </div>
            </div>
          </div>
          <div class="modalFooter">
            <div></div>
            <div class="d-flex gap-10px">
              <button type="button" class="button" @click="closeAmountCalc" :disabled="amountCalc.saving">Abbrechen</button>
              <button type="button" class="button is-primary" @click="saveAmountCalc" :disabled="amountCalc.saving || amountCalc.rateLoading">
                <i v-if="amountCalc.saving" class="ti ti-loader-2 spin"></i>
                <span>{{ amountCalc.saving ? 'Speichern...' : 'Speichern' }}</span>
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
  </Teleport>

  <!-- Shortcut picker modal: used by Partner / Order / Invoice / Product
       shortcuts so the dropdown isn't clipped by neighbouring cards. -->
  <Teleport to="body">
    <div class="modal rf-picker-modal" :class="{'active': picker.open}" :style="{display: picker.open ? 'block' : 'none'}" @click.self="closePicker">
      <div class="modalDialog modalCenter" @click.self="closePicker">
        <div class="modalContent rf-picker-content">
          <div class="modalHeader">
            <h3 class="modalTitle">
              <i v-if="pickerConfig[picker.kind]?.icon" class="ti" :class="pickerConfig[picker.kind].icon"></i>
              <span>{{ pickerConfig[picker.kind]?.title || 'Verknupfen' }}</span>
            </h3>
            <div class="cursor-pointer" @click="closePicker">
              <span class="tf-icon ti ti-x ti-sm me-1"></span>
            </div>
          </div>
          <div class="modalBody">
            <div class="rf-picker-search">
              <i class="ti ti-search rf-picker-search-icon"></i>
              <input
                type="text"
                class="rf-picker-search-input"
                placeholder="Suchen..."
                v-model="picker.query"
                @input="onPickerQueryInput"
                @keydown.enter.prevent="runPickerSearch"
              />
            </div>
            <div class="rf-picker-results">
              <div v-if="picker.loading" class="rf-picker-empty"><i class="ti ti-loader-2 spin"></i><span>Laden...</span></div>
              <div v-else-if="!picker.results.length" class="rf-picker-empty"><i class="ti ti-mood-empty"></i><span>Keine Treffer</span></div>
              <button
                v-for="r in picker.results"
                :key="r.id"
                type="button"
                class="rf-picker-result"
                @click="pickResult(r)"
              >
                <div class="rf-picker-result-main">{{ pickerConfig[picker.kind]?.display(r) }}</div>
                <div class="rf-picker-result-sub" v-if="pickerConfig[picker.kind]?.sub(r)">{{ pickerConfig[picker.kind].sub(r) }}</div>
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
  </Teleport>
</template>

<style scoped>
.rf-container { padding: 20px 24px; }

/* Amount calculator — use compound selector so these win over `.rf-card-title i` (same specificity) */
.rf-amount-calc-btn.rf-amount-calc-btn {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 5px 10px; border: 1px solid transparent;
  background: #4a90e2; color: #fff; border-radius: 5px;
  cursor: pointer; font-size: 12px; font-weight: 600; line-height: 1;
  text-transform: none; letter-spacing: 0.2px;
  transition: background 0.2s, color 0.2s;
}
.rf-amount-calc-btn.rf-amount-calc-btn i { font-size: 14px; color: #fff; line-height: 1; }
.rf-amount-calc-btn.rf-amount-calc-btn:hover:not(:disabled) { background: #1d4ed8; color: #fff; }
.rf-amount-calc-btn.rf-amount-calc-btn:hover:not(:disabled) i { color: #fff; }
.rf-amount-calc-btn.rf-amount-calc-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.rf-amount-calc-btn-header { margin-left: auto; }

.amt-calc-body { padding: 20px 24px; display: flex; flex-direction: column; gap: 14px; }
.amt-calc-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.amt-calc-field { display: flex; flex-direction: column; }
.amt-calc-field .rf-textarea { min-height: 110px; }
.amt-calc-summary {
  margin-top: 4px; padding: 12px 14px;
  background: var(--theme-bg-secondary, #f8f9fa);
  border: 1px solid var(--theme-border, #e4e3e6);
  border-radius: 8px;
}
.amt-calc-line { display: flex; justify-content: space-between; align-items: center; padding: 4px 0; font-size: 14px; color: var(--theme-text-primary); }
.amt-calc-total { font-size: 17px; font-weight: 700; border-top: 1px dashed var(--theme-border, #e4e3e6); margin-top: 4px; padding-top: 10px; }
.amt-calc-total strong { color: #2563eb; }
.amt-calc-loading { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; color: var(--theme-text-secondary, #888); }

.rf-grid-main { display: grid; grid-template-columns: 1fr 320px; gap: 20px; }
@media (max-width: 1024px) { .rf-grid-main { grid-template-columns: 1fr; } }

.rf-grid-chat { display: grid; grid-template-columns: minmax(0, 2.2fr) minmax(0, 0.75fr) minmax(0, 1fr); gap: 16px; }
@media (max-width: 1400px) { .rf-grid-chat { grid-template-columns: minmax(0, 2fr) minmax(0, 0.75fr) minmax(0, 1fr); gap: 14px; } }
@media (max-width: 1100px) { .rf-grid-chat { grid-template-columns: 1fr; gap: 14px; } }

.rf-chat-column { order: 1; display: flex; flex-direction: column; min-height: 0; min-width: 0; }
.rf-shortcuts-column { order: 2; display: flex; flex-direction: column; gap: 12px; min-width: 0; }
.rf-fields-column { order: 3; display: flex; flex-direction: column; gap: 16px; min-width: 0; }

.rf-card-chat-full { display: flex; flex-direction: column; height: 75vh; min-height: 600px; position: relative; }
.rf-card-chat-full .chat-container { flex: 1; max-height: none; overflow-y: auto; }

.rf-drag-active { outline: 2px dashed #3b82f6; outline-offset: -2px; }
.rf-drop-overlay {
  position: absolute; inset: 0; z-index: 20;
  background: rgba(59, 130, 246, 0.08); backdrop-filter: blur(2px);
  display: flex; align-items: center; justify-content: center;
  border-radius: 10px; pointer-events: none;
}
.rf-drop-uploading { background: rgba(255, 255, 255, 0.85); }
.rf-drop-overlay-content {
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  color: #3b82f6; font-size: 15px; font-weight: 600;
}
@keyframes spin { to { transform: rotate(360deg); } }
.spin { animation: spin 1s linear infinite; }

.rf-row-compact { grid-template-columns: 1fr 1fr; }

.rf-main { display: flex; flex-direction: column; gap: 20px; }
.rf-sidebar { display: flex; flex-direction: column; gap: 16px; }

.rf-card { background: var(--theme-bg-elevated, #ffffff); border: 1px solid var(--theme-border, #e4e3e6); border-radius: 10px; padding: 20px; }
.rf-card-header { padding: 16px 20px; }
.rf-card-chat { padding: 0; display: flex; flex-direction: column; overflow: hidden; }
.rf-card-chat .rf-card-title { padding: 16px 20px 12px; margin-bottom: 0; }

.rf-card-title { display: flex; align-items: center; gap: 8px; font-size: 15px; font-weight: 700; color: var(--theme-text-primary); margin-bottom: 16px; padding-bottom: 10px; border-bottom: 2px solid var(--theme-border, #e4e3e6); }
.rf-card-title i { font-size: 18px; color: #4a90e2; }

.rf-header-row { display: flex; gap: 16px; flex-wrap: wrap; }
.rf-header-row .rf-field { flex: 1; min-width: 120px; }

.rf-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 12px; }
.rf-row:last-child { margin-bottom: 0; }

.rf-field { margin-bottom: 12px; }
.rf-field:last-child { margin-bottom: 0; }

.rf-label { display: block; font-size: 12px; font-weight: 600; color: var(--theme-text-secondary, #888); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 5px; }

.rf-input, .rf-select { width: 100%; padding: 8px 12px; background: var(--theme-input-bg, #f8f9fa); border: 1px solid var(--theme-border, #e4e3e6); border-radius: 6px; font-size: 14px; color: var(--theme-text-primary); transition: border-color 0.2s; }
.rf-input:focus, .rf-select:focus { outline: none; border-color: #4a90e2; box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1); }
.rf-input[readonly] { background: var(--theme-bg-secondary, #f0f0f0); cursor: default; }
.rf-select { appearance: auto; cursor: pointer; }

.rf-textarea { width: 100%; min-height: 300px; padding: 14px 16px; background: var(--theme-input-bg, #f8f9fa); border: 1px solid var(--theme-border, #e4e3e6); border-radius: 8px; font-size: 14px; line-height: 1.6; color: var(--theme-text-primary); resize: vertical; font-family: inherit; }
.rf-textarea:focus { outline: none; border-color: #4a90e2; box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1); }
.rf-textarea-sm { min-height: 200px; }

.rf-checks { display: flex; flex-direction: row; flex-wrap: wrap; gap: 10px 18px; align-items: center; }
.rf-check { display: flex; align-items: center; gap: 10px; cursor: pointer; font-size: 14px; color: var(--theme-text-primary); padding: 6px 8px; border-radius: 6px; transition: background 0.15s; }
.rf-check:hover { background: var(--theme-bg-hover, rgba(0,0,0,0.03)); }
.rf-check input[type="checkbox"] { width: 18px; height: 18px; accent-color: #4a90e2; cursor: pointer; }
.rf-check--merchant-action { border: 1.5px dashed #fca5a5; }
.rf-check--merchant-action.rf-check--flagged { background: #fee2e2; border-color: #dc2626; border-style: solid; color: #991b1b; font-weight: 600; }
.rf-check--merchant-action.rf-check--flagged input[type="checkbox"] { accent-color: #dc2626; }

.rf-field-sm { min-width: 100px; }

/* Reference cards (Partner / Order / Invoice / Product) shown in the Basic tab
   right column. Compact preview when linked, inline picker when empty. */
.rf-ref-stack { display: flex; flex-direction: column; gap: 10px; }
.rf-ref-card { background: var(--theme-bg-elevated, #ffffff); border: 1px solid var(--theme-border, #e4e3e6); border-radius: 10px; overflow: hidden; transition: border-color 0.15s, box-shadow 0.15s; }
.rf-ref-card:hover { border-color: #c4b5fd; box-shadow: 0 2px 8px rgba(139, 92, 246, 0.08); }
.rf-ref-head { display: flex; align-items: center; gap: 10px; padding: 10px 14px; background: var(--theme-bg-secondary, #fafafa); border-bottom: 1px solid var(--theme-border, #f0f0f0); }
.rf-ref-icon { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 8px; font-size: 1.0625rem; flex-shrink: 0; }
.rf-ref-icon--partner { background: rgba(34, 197, 94, 0.12); color: #16a34a; }
.rf-ref-icon--order { background: rgba(59, 130, 246, 0.12); color: #2563eb; }
.rf-ref-icon--invoice { background: rgba(168, 85, 247, 0.12); color: #9333ea; }
.rf-ref-icon--product { background: rgba(245, 158, 11, 0.12); color: #d97706; }
.rf-ref-head-body { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.rf-ref-eyebrow { font-size: 0.6875rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: var(--theme-text-secondary, #9ca3af); }
.rf-ref-title { font-size: 0.875rem; font-weight: 600; color: var(--theme-text-primary, #1a1a2e); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rf-ref-title--empty { font-style: italic; color: var(--theme-text-secondary, #9ca3af); font-weight: 500; }
.rf-ref-body { padding: 10px 14px; }
.rf-ref-picker { }
.rf-ref-actions { display: flex; gap: 8px; }
.rf-ref-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 6px 12px; border-radius: 7px; border: 1.5px solid var(--theme-border, #e0e0e0); background: var(--theme-bg-elevated, #fff); color: var(--theme-text-primary, #333); font-size: 0.8125rem; font-weight: 600; cursor: pointer; transition: all 0.15s; }
.rf-ref-btn:hover { border-color: #8b5cf6; color: #8b5cf6; }
.rf-ref-btn--primary { flex: 1; background: #8b5cf6; border-color: #8b5cf6; color: #fff; }
.rf-ref-btn--primary:hover { background: #7c3aed; border-color: #7c3aed; color: #fff; }
.rf-ref-btn--muted { color: var(--theme-text-secondary, #6b7280); padding: 6px 10px; }
.rf-ref-btn--muted:hover { border-color: #ef4444; color: #ef4444; }
[data-mode="dark"] .rf-ref-head { background: var(--theme-bg-secondary, #1f2937); }

/* Priority card in the right column — colour-graded slider with dots.
   direction: rtl flips the track so Urgent (red) is on the right. */
.rf-priority-card { --prio-colour: #6b7280; border-left: 4px solid var(--prio-colour); transition: border-color 0.22s ease; }
.rf-priority-card .rf-card-title { padding: 0; border-bottom: none; margin-bottom: 12px; }
.rf-priority-value { margin-left: auto; font-size: 0.9375rem; font-weight: 800; letter-spacing: 0.01em; }
.rf-priority-slider-wrap { position: relative; padding: 4px 4px 0 4px; direction: rtl; }
.rf-priority-slider {
  -webkit-appearance: none; appearance: none;
  width: 100%; height: 8px;
  background: linear-gradient(90deg, #dc2626 0%, #ea580c 25%, #eab308 50%, #3b82f6 75%, #6b7280 100%);
  border-radius: 999px;
  outline: none; cursor: pointer; margin: 0; padding: 0;
}
.rf-priority-slider::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 22px; height: 22px;
  background: #fff; border: 3px solid var(--prio-colour);
  border-radius: 50%; cursor: pointer;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
  transition: transform 0.15s ease, border-color 0.22s ease;
}
.rf-priority-slider::-webkit-slider-thumb:hover { transform: scale(1.1); }
.rf-priority-slider::-moz-range-thumb {
  width: 22px; height: 22px;
  background: #fff; border: 3px solid var(--prio-colour);
  border-radius: 50%; cursor: pointer;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
}
.rf-priority-stops { display: flex; justify-content: space-between; margin-top: 6px; padding: 0 2px; }
.rf-priority-stop {
  display: inline-block; width: 10px; height: 10px;
  border-radius: 50%; opacity: 0.35;
  border: 1px solid rgba(0, 0, 0, 0.15);
  transition: opacity 0.15s ease, transform 0.15s ease;
}
.rf-priority-stop--active { opacity: 1; transform: scale(1.35); box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.8), 0 2px 4px rgba(0, 0, 0, 0.2); }

/* Empty-state "Verknupfen" button — opens the picker modal */
.rf-ref-link-btn { width: 100%; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 8px 12px; border: 1.5px dashed var(--theme-border, #d1d5db); border-radius: 8px; background: transparent; color: var(--theme-text-secondary, #6b7280); font-size: 0.8125rem; font-weight: 600; cursor: pointer; transition: all 0.15s; }
.rf-ref-link-btn:hover { border-color: #8b5cf6; color: #8b5cf6; background: rgba(139, 92, 246, 0.04); }

/* Compact inline preview (Order / Invoice / Product) shown between header
   and action buttons once the shortcut is linked. */
.rf-ref-preview { padding: 8px 14px; display: flex; flex-direction: column; gap: 4px; border-bottom: 1px solid var(--theme-border, #f0f0f0); font-size: 0.8125rem; }
.rf-ref-preview-loading { display: flex; align-items: center; justify-content: center; color: var(--theme-text-secondary, #9ca3af); font-size: 1rem; padding: 8px 0; }
.rf-ref-prev-row { display: flex; justify-content: space-between; align-items: center; gap: 8px; min-width: 0; }
.rf-ref-prev-label { color: var(--theme-text-secondary, #9ca3af); font-weight: 500; flex-shrink: 0; font-size: 0.75rem; }
.rf-ref-prev-val { color: var(--theme-text-primary, #1a1a2e); font-weight: 500; text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.rf-ref-prev-val--strong { font-weight: 700; }
.rf-ref-chip { padding: 1px 7px; border-radius: 5px; font-size: 0.6875rem; font-weight: 700; border: 1px solid; white-space: nowrap; }

/* Lieferscheine (shipments) reference card — one clickable row per delivery */
.rf-ref-icon--shipment { background: rgba(20, 184, 166, 0.12); color: #0d9488; }
.rf-ship-row { display: flex; align-items: center; gap: 8px; width: 100%; padding: 7px 8px; border: 1px solid var(--theme-border, #ededed); border-radius: 7px; background: var(--theme-bg-elevated, #fff); color: var(--theme-text-primary, #1a1a2e); cursor: pointer; transition: all 0.12s; text-align: left; }
.rf-ship-row:hover { border-color: #0d9488; background: rgba(13, 148, 136, 0.05); }
.rf-ship-icon { color: #0d9488; font-size: 1rem; flex-shrink: 0; }
.rf-ship-info { display: flex; flex-direction: column; min-width: 0; flex: 1; gap: 1px; }
.rf-ship-docno { font-weight: 600; font-size: 0.8125rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rf-ship-meta { display: flex; align-items: center; gap: 8px; font-size: 0.6875rem; color: var(--theme-text-secondary, #9ca3af); min-width: 0; }
.rf-ship-track { display: inline-flex; align-items: center; gap: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rf-ship-chip { padding: 1px 7px; border-radius: 5px; font-size: 0.625rem; font-weight: 700; border: 1px solid; white-space: nowrap; flex-shrink: 0; }
.rf-ship-open { color: var(--theme-text-secondary, #9ca3af); font-size: 0.875rem; flex-shrink: 0; }

/* Order resolved from the directly-referenced shipment (Reference tab) */
.rf-field--full { flex: 1 1 100%; }
.rf-inout-order { display: flex; align-items: center; gap: 12px; padding: 12px 14px; border: 1px solid var(--theme-border, #e4e3e6); border-radius: 10px; background: var(--theme-bg-secondary, #fafafa); }
.rf-inout-order-icon { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 8px; background: rgba(59, 130, 246, 0.12); color: #2563eb; font-size: 1.0625rem; flex-shrink: 0; }
.rf-inout-order-body { display: flex; flex-direction: column; min-width: 0; flex: 1; gap: 2px; }
.rf-inout-order-eyebrow { font-size: 0.6875rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: var(--theme-text-secondary, #9ca3af); }
.rf-inout-order-name { font-size: 0.9375rem; font-weight: 600; color: var(--theme-text-primary, #1a1a2e); }
.rf-inout-order-loading { font-size: 0.8125rem; color: var(--theme-text-secondary, #9ca3af); font-style: italic; }
.rf-inout-order-actions { display: flex; gap: 8px; flex-shrink: 0; }
[data-mode="dark"] .rf-inout-order { background: var(--theme-bg-secondary, #1f2937); }

.spin { animation: rf-spin 0.8s linear infinite; }
@keyframes rf-spin { to { transform: rotate(360deg); } }

/* Picker modal — styled to sit well above any neighbouring card so the
   dropdown-like results list is always visible. */
.rf-picker-modal .modalContent { max-width: 520px; width: 92%; }
.rf-picker-content { display: flex; flex-direction: column; max-height: 82vh; }
.rf-picker-search { position: relative; margin-bottom: 12px; }
.rf-picker-search-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--theme-text-secondary, #9ca3af); font-size: 1rem; pointer-events: none; }
.rf-picker-search-input { width: 100%; padding: 10px 12px 10px 36px; border: 1.5px solid var(--theme-border, #e0e0e0); border-radius: 8px; background: var(--theme-bg-elevated, #fff); color: var(--theme-text-primary, #1a1a2e); font-size: 0.9375rem; outline: none; transition: border-color 0.15s, box-shadow 0.15s; }
.rf-picker-search-input:focus { border-color: #8b5cf6; box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15); }
.rf-picker-results { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 4px; max-height: 54vh; }
.rf-picker-empty { display: flex; align-items: center; justify-content: center; gap: 8px; padding: 24px 12px; color: var(--theme-text-secondary, #9ca3af); font-size: 0.875rem; }
.rf-picker-result { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; width: 100%; padding: 10px 12px; border: 1px solid var(--theme-border, #e8e8e8); border-radius: 8px; background: var(--theme-bg-elevated, #fff); color: var(--theme-text-primary, #1a1a2e); cursor: pointer; transition: all 0.12s; text-align: left; }
.rf-picker-result:hover { border-color: #8b5cf6; background: rgba(139, 92, 246, 0.05); }
.rf-picker-result-main { font-weight: 600; font-size: 0.9375rem; color: var(--theme-text-primary, #1a1a2e); word-break: break-word; }
.rf-picker-result-sub { font-size: 0.75rem; color: var(--theme-text-secondary, #6b7280); }

/* Details / collapsible */
.rf-details { padding: 0; }
.rf-details-summary { cursor: pointer; list-style: none; margin-bottom: 0; padding: 16px 20px 16px; border-bottom: none; user-select: none; }
.rf-details-summary::-webkit-details-marker { display: none; }
.rf-details-summary .rf-chevron { margin-left: auto; transition: transform 0.2s; font-size: 16px; color: var(--theme-text-secondary, #888); }
.rf-details[open] .rf-details-summary .rf-chevron { transform: rotate(180deg); }
.rf-details[open] .rf-details-summary { border-bottom: 2px solid var(--theme-border, #e4e3e6); }
.rf-details-body { padding: 16px 20px 20px; }

/* Chat */
.chat-count { margin-left: auto; background: #4a90e2; color: #fff; font-size: 12px; font-weight: 700; padding: 2px 8px; border-radius: 10px; min-width: 24px; text-align: center; }

.chat-container { max-height: 500px; min-height: 200px; overflow-y: auto; padding: 16px 20px; display: flex; flex-direction: column; gap: 8px; }

.chat-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; padding: 40px 20px; color: var(--theme-text-secondary, #aaa); }
.chat-empty i { font-size: 36px; opacity: 0.4; }

/* Date separator */
.chat-date-sep { display: flex; align-items: center; gap: 12px; margin: 12px 0 4px; }
.chat-date-sep::before, .chat-date-sep::after { content: ''; flex: 1; height: 1px; background: var(--theme-border, #e0e0e0); }
.chat-date-sep span { font-size: 11px; font-weight: 700; color: var(--theme-text-secondary, #999); text-transform: uppercase; letter-spacing: 1px; white-space: nowrap; }

/* Chat bubble */
.chat-bubble { max-width: 85%; padding: 10px 14px; border-radius: 12px; background: var(--theme-bg-secondary, #f0f2f5); border: 1px solid var(--theme-border, #e4e3e6); position: relative; }
.chat-bubble.is-merchant { align-self: flex-end; background: #dbeafe; border-color: #93c5fd; border-bottom-right-radius: 4px; }
.chat-bubble.is-support { align-self: flex-start; border-bottom-left-radius: 4px; }
.chat-bubble.is-history { background: #fef9e7; border-color: #f0e0a0; border-left: 3px solid #eab308; }
.chat-bubble.is-change { background: #eef4ff; border-color: #cddbf5; border-left: 3px solid #4a90e2; max-width: 92%; }
.chat-bubble.is-request { background: #ecfeff; border-color: #a5f3fc; border-left: 3px solid #06b6d4; max-width: 95%; }

.chat-source-badge { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; padding: 1px 6px; border-radius: 4px; }
.chat-source-badge.badge-history { background: #fef3c7; color: #92400e; }
.chat-source-badge.badge-diff { background: #dcfce7; color: #166534; }
.chat-source-badge.badge-change { background: #dbeafe; color: #1e40af; }
.chat-source-badge.badge-request { background: #cffafe; color: #155e75; }

/* Change bubble content */
.chat-changes { display: flex; flex-direction: column; gap: 6px; margin-top: 4px; }
.chat-change-row { display: grid; grid-template-columns: minmax(110px, auto) 1fr; gap: 10px; align-items: center; padding: 6px 8px; background: rgba(255,255,255,0.6); border-radius: 6px; }
.chat-change-label { font-size: 12px; font-weight: 700; color: var(--theme-text-secondary, #556); text-transform: uppercase; letter-spacing: 0.3px; }
.chat-change-diff { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 13px; }
.chat-change-old { text-decoration: line-through; color: #b91c1c; background: #fee2e2; padding: 1px 6px; border-radius: 4px; word-break: break-word; }
.chat-change-new { color: #065f46; background: #d1fae5; padding: 1px 6px; border-radius: 4px; font-weight: 600; word-break: break-word; }
.chat-change-arrow { color: #4a90e2; font-size: 16px; flex-shrink: 0; }

/* Attachment bubble */
.chat-bubble.is-attachment { background: #f5f3ff; border-color: #ddd6fe; border-left: 3px solid #8b5cf6; max-width: 85%; }
.chat-source-badge.badge-attachment { background: #ede9fe; color: #5b21b6; }

.chat-attachment { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }
.chat-att-preview { cursor: pointer; border-radius: 8px; overflow: hidden; background: #fff; border: 1px solid var(--theme-border, #e4e3e6); display: flex; align-items: center; justify-content: center; max-height: 260px; transition: transform 0.15s; }
.chat-att-preview:hover { transform: scale(1.01); }
.chat-att-preview img { max-width: 100%; max-height: 260px; object-fit: contain; display: block; }
.chat-att-pdf { height: 260px; }
.chat-att-pdf embed { width: 100%; height: 100%; border: none; pointer-events: none; }

.chat-att-audio, .chat-att-video { width: 100%; border-radius: 8px; background: #fff; }
.chat-att-video { max-height: 260px; }

.chat-att-file { display: flex; align-items: center; justify-content: center; padding: 24px; background: #fff; border: 1px solid var(--theme-border, #e4e3e6); border-radius: 8px; }
.chat-att-file i { font-size: 32px; color: #8b5cf6; }

.chat-att-meta { display: flex; flex-direction: column; gap: 4px; }
.chat-att-name { font-size: 13px; font-weight: 600; color: var(--theme-text-primary); word-break: break-word; }
.chat-att-size { font-size: 11px; color: var(--theme-text-secondary, #888); }
.chat-att-actions { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 4px; }
.chat-att-btn { display: inline-flex; align-items: center; gap: 4px; padding: 4px 10px; font-size: 12px; font-weight: 600; color: #5b21b6; background: #ede9fe; border: 1px solid #ddd6fe; border-radius: 6px; cursor: pointer; text-decoration: none; transition: background 0.15s; }
.chat-att-btn:hover { background: #ddd6fe; }
.chat-att-btn i { font-size: 14px; }

.chat-bubble-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 4px; flex-wrap: wrap; }
.chat-bubble-header .chat-time { margin-left: auto; }
.chat-author { display: flex; align-items: center; gap: 4px; font-size: 12px; font-weight: 700; color: #4a90e2; }
.chat-author i { font-size: 14px; }
.chat-time { font-size: 11px; color: var(--theme-text-secondary, #999); white-space: nowrap; }

.chat-bubble-body { font-size: 14px; line-height: 1.55; color: var(--theme-text-primary); white-space: pre-wrap; word-break: break-word; }
/* linkified URLs inside message text (rendered via v-html → needs :deep) */
.chat-bubble-body :deep(a) { color: #2563eb; text-decoration: underline; word-break: break-all; }
.chat-bubble-body :deep(a:hover) { color: #1d4ed8; }

.chat-bubble-meta { display: flex; align-items: center; gap: 4px; margin-top: 6px; font-size: 11px; color: var(--theme-text-secondary, #999); }
.chat-bubble-meta i { font-size: 13px; }

/* Chat input */
.chat-input-area { padding: 12px 20px 16px; border-top: 1px solid var(--theme-border, #e4e3e6); background: var(--theme-bg-elevated, #fafafa); }

.chat-input { width: 100%; min-height: 80px; max-height: 200px; padding: 10px 14px; background: var(--theme-input-bg, #ffffff); border: 1px solid var(--theme-border, #e4e3e6); border-radius: 10px; font-size: 14px; line-height: 1.5; color: var(--theme-text-primary); resize: vertical; font-family: inherit; }
.chat-input:focus { outline: none; border-color: #4a90e2; box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1); }
.chat-input::placeholder { color: var(--theme-text-secondary, #aaa); }

.chat-input-actions { display: flex; align-items: center; justify-content: space-between; margin-top: 8px; }
.chat-hint { font-size: 11px; color: var(--theme-text-secondary, #aaa); }

.chat-send-group { display: flex; gap: 8px; align-items: center; }
.chat-send-btn { display: flex; align-items: center; gap: 6px; padding: 8px 18px; background: #4a90e2; border: none; border-radius: 8px; color: #ffffff; font-size: 14px; font-weight: 600; cursor: pointer; transition: background 0.15s; }
.chat-send-btn:hover:not(:disabled) { background: #3a7bc8; }
.chat-send-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.chat-send-btn i { font-size: 16px; }
.chat-send-btn--assign { background: #dc2626; }
.chat-send-btn--assign:hover:not(:disabled) { background: #b91c1c; }

.spin { animation: spin 1s linear infinite; }
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
</style>
