753 lines
32 KiB
Vue
753 lines
32 KiB
Vue
<script setup lang="ts">
|
|
import {
|
|
slaColor, fmtDate, fmtTime, fmtDateTime,
|
|
CHANNEL_LABELS, CHANNEL_ICONS, STATUS_LABELS,
|
|
PRIORITY_LABELS, INTENT_LABELS, TIER_LABELS, QUEUE_LABELS,
|
|
type SupportTicketDetail, type TicketMessage, type MessageType,
|
|
type TicketStatus, type SupportChannel,
|
|
} from '~/data/mock-support'
|
|
|
|
definePageMeta({ ssr: false })
|
|
usePageTitle('Ticket Detail')
|
|
|
|
const route = useRoute()
|
|
const ticketId = route.params.id as string
|
|
const { getDetail, updateStatus, assignTicket, addMessage } = useSupportTickets()
|
|
|
|
const ticket = ref<SupportTicketDetail | null>(getDetail(ticketId) ?? null)
|
|
|
|
// ── Tabs ──
|
|
type TabId = 'thread' | 'overview' | 'documents'
|
|
const activeTab = ref<TabId>('thread')
|
|
const tabs: { id: TabId; label: string; icon: string }[] = [
|
|
{ id: 'thread', label: 'Conversación', icon: 'i-heroicons-chat-bubble-left-right' },
|
|
{ id: 'overview', label: 'Resumen', icon: 'i-heroicons-squares-2x2' },
|
|
{ id: 'documents', label: 'Documentos', icon: 'i-heroicons-folder-open' },
|
|
]
|
|
|
|
// ── Helpers ──
|
|
const statusClass = (s: TicketStatus) => {
|
|
const map: Record<string, string> = {
|
|
open: 'spd-st-open', in_progress: 'spd-st-progress',
|
|
pending_customer: 'spd-st-pending', resolved: 'spd-st-resolved',
|
|
}
|
|
return map[s] ?? ''
|
|
}
|
|
|
|
const priorityClass = (p: string) => {
|
|
const map: Record<string, string> = {
|
|
urgent: 'spd-pri-urgent', high: 'spd-pri-high',
|
|
medium: 'spd-pri-medium', low: 'spd-pri-low',
|
|
}
|
|
return map[p] ?? ''
|
|
}
|
|
|
|
const tierClass = (t: string) => {
|
|
const map: Record<string, string> = {
|
|
tier1_auto: 'spd-tier-1', tier2_rule: 'spd-tier-2', tier3_open: 'spd-tier-3',
|
|
}
|
|
return map[t] ?? ''
|
|
}
|
|
|
|
const deliveryIcon = (s?: string) => {
|
|
if (s === 'read') return '✓✓'
|
|
if (s === 'delivered') return '✓✓'
|
|
if (s === 'sent') return '✓'
|
|
return ''
|
|
}
|
|
|
|
const deliveryClass = (s?: string) => s === 'read' ? 'spd-delivery-read' : 'spd-delivery-default'
|
|
|
|
// ── Composer ──
|
|
const composerChannel = ref<'whatsapp' | 'email' | 'internal_note'>(
|
|
ticket.value?.channel === 'whatsapp' ? 'whatsapp'
|
|
: ticket.value?.channel === 'email' ? 'email'
|
|
: 'internal_note'
|
|
)
|
|
const composerText = ref('')
|
|
|
|
function sendMessage() {
|
|
if (!composerText.value.trim() || !ticket.value) return
|
|
const typeMap: Record<string, MessageType> = {
|
|
whatsapp: 'whatsapp', email: 'email', internal_note: 'internal_note',
|
|
}
|
|
addMessage(ticketId, {
|
|
type: typeMap[composerChannel.value],
|
|
direction: composerChannel.value === 'internal_note' ? 'internal' : 'outbound',
|
|
from: ticket.value.assignedTo ?? 'Agente',
|
|
to: composerChannel.value === 'internal_note' ? null : ticket.value.customerName,
|
|
subject: composerChannel.value === 'email' ? `Re: ${ticket.value.subject}` : null,
|
|
body: composerText.value.trim(),
|
|
aiDigest: null,
|
|
deliveryStatus: composerChannel.value === 'whatsapp' ? 'sent' : undefined,
|
|
})
|
|
composerText.value = ''
|
|
// refresh detail
|
|
ticket.value = getDetail(ticketId) ?? null
|
|
}
|
|
|
|
// ── AI recap ──
|
|
const aiRecapOpen = ref(true)
|
|
|
|
// ── Actions ──
|
|
const toast = useToast()
|
|
function handleResolve() {
|
|
updateStatus(ticketId, 'resolved')
|
|
ticket.value = getDetail(ticketId) ?? null
|
|
toast.add({ title: 'Ticket resuelto', color: 'green' })
|
|
}
|
|
|
|
function handleAssign() {
|
|
const agent = prompt('Asignar a:')
|
|
if (agent) {
|
|
assignTicket(ticketId, agent)
|
|
ticket.value = getDetail(ticketId) ?? null
|
|
toast.add({ title: `Asignado a ${agent}`, color: 'green' })
|
|
}
|
|
}
|
|
|
|
function handleEscalate() {
|
|
toast.add({ title: 'Escalado', description: 'Ticket movido al pool abierto para triaje.', color: 'neutral' })
|
|
}
|
|
|
|
// ── Sorted messages (oldest first) ──
|
|
const sortedMessages = computed(() => {
|
|
if (!ticket.value) return []
|
|
return [...ticket.value.messages].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="spd-root mx-auto max-w-5xl pb-12">
|
|
<!-- Not found -->
|
|
<template v-if="!ticket">
|
|
<div class="spd-not-found">
|
|
<UIcon name="i-heroicons-exclamation-triangle" class="w-8 h-8" />
|
|
<h2>Ticket no encontrado</h2>
|
|
<p>No existe un ticket con ID <strong>{{ ticketId }}</strong>.</p>
|
|
<NuxtLink to="/support" class="spd-back-link">Volver a Soporte</NuxtLink>
|
|
</div>
|
|
</template>
|
|
|
|
<template v-else>
|
|
<!-- Back link -->
|
|
<NuxtLink to="/support" class="spd-back-link">
|
|
<UIcon name="i-heroicons-arrow-left" class="w-3.5 h-3.5" />
|
|
Volver a Soporte
|
|
</NuxtLink>
|
|
|
|
<!-- ═══════════ HEADER ═══════════ -->
|
|
<div class="spd-detail-header">
|
|
<div class="spd-header-left">
|
|
<div class="spd-title-row">
|
|
<h1 class="spd-title">{{ ticket.id }}</h1>
|
|
<span :class="['spd-status-pill', statusClass(ticket.status)]">{{ STATUS_LABELS[ticket.status] }}</span>
|
|
<span :class="['spd-priority-badge', priorityClass(ticket.priority)]">{{ PRIORITY_LABELS[ticket.priority] }}</span>
|
|
<span :class="['spd-tier-badge', tierClass(ticket.routingTier)]">{{ TIER_LABELS[ticket.routingTier] }}</span>
|
|
<UIcon :name="CHANNEL_ICONS[ticket.channel]" class="w-5 h-5" :style="ticket.channel === 'whatsapp' ? 'color: #25D366;' : 'color: #8a8a86;'" :title="CHANNEL_LABELS[ticket.channel]" />
|
|
</div>
|
|
<div class="spd-header-meta">
|
|
<span>Cliente: <strong>{{ ticket.customerName }}</strong></span>
|
|
<template v-if="ticket.policyNumber">
|
|
<span class="spd-meta-sep">·</span>
|
|
<span>Póliza: <NuxtLink :to="`/policies/${ticket.policyId}`" class="spd-meta-link">{{ ticket.policyNumber }}</NuxtLink></span>
|
|
</template>
|
|
<span class="spd-meta-sep">·</span>
|
|
<span>Creado: {{ fmtDate(ticket.createdAt) }}</span>
|
|
<span class="spd-meta-sep">·</span>
|
|
<span>{{ ticket.daysOpen }} días abierto</span>
|
|
<span class="spd-meta-sep">·</span>
|
|
<span>Asignado: {{ ticket.assignedTo ?? 'Sin asignar' }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="spd-header-actions">
|
|
<button type="button" class="spd-action-btn" @click="handleAssign">
|
|
<UIcon name="i-heroicons-user-plus" class="w-4 h-4" />
|
|
Asignar
|
|
</button>
|
|
<button type="button" class="spd-action-btn" @click="handleEscalate">
|
|
<UIcon name="i-heroicons-arrow-up-circle" class="w-4 h-4" />
|
|
Escalar
|
|
</button>
|
|
<button v-if="ticket.status !== 'resolved'" type="button" class="spd-action-btn-primary" @click="handleResolve">
|
|
<UIcon name="i-heroicons-check-circle" class="w-4 h-4" />
|
|
Resolver
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════ POLICY CONTEXT PANEL ═══════════ -->
|
|
<div v-if="ticket.linkedPolicies.length" class="spd-context-panel">
|
|
<p class="spd-context-title">
|
|
<UIcon name="i-heroicons-briefcase" class="w-4 h-4" />
|
|
Contexto del Cliente
|
|
</p>
|
|
<div class="spd-context-policies">
|
|
<div v-for="pol in ticket.linkedPolicies" :key="pol.id" class="spd-context-policy-row">
|
|
<span class="spd-context-pol-number">{{ pol.number }}</span>
|
|
<span class="spd-context-pol-chip">{{ pol.lob }}</span>
|
|
<span class="text-[12px] text-[var(--text-muted)]">{{ pol.carrier }}</span>
|
|
<span class="text-[12px]" :class="pol.status === 'Vigente' ? 'text-emerald-600' : 'text-[var(--text-muted)]'">{{ pol.status }}</span>
|
|
<span class="text-[11px] text-[var(--text-muted)]">Ren: {{ pol.renewal }}</span>
|
|
</div>
|
|
</div>
|
|
<div v-if="ticket.linkedClaims.length" class="mt-2">
|
|
<p class="text-[11px] font-semibold uppercase text-[var(--text-muted)]">Siniestros vinculados</p>
|
|
<div v-for="cl in ticket.linkedClaims" :key="cl.id" class="spd-context-claim">
|
|
<span class="text-[12px] font-mono font-semibold text-[#01696f]">{{ cl.id }}</span>
|
|
<span class="text-[12px] text-[var(--text-muted)]">{{ cl.type }} — {{ cl.status }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════ QUICK STATS ═══════════ -->
|
|
<div class="spd-quick-strip">
|
|
<div class="spd-quick-item">
|
|
<span class="spd-quick-label">SLA</span>
|
|
<span class="spd-quick-val" :style="`color: ${ticket.slaPercent >= 100 ? '#c13838' : ticket.slaPercent >= 70 ? '#c27b1a' : '#059669'}`">{{ ticket.slaPercent }}%</span>
|
|
</div>
|
|
<div class="spd-quick-item">
|
|
<span class="spd-quick-label">Mensajes</span>
|
|
<span class="spd-quick-val">{{ ticket.messageCount }}</span>
|
|
</div>
|
|
<div class="spd-quick-item">
|
|
<span class="spd-quick-label">Días abierto</span>
|
|
<span class="spd-quick-val">{{ ticket.daysOpen }}</span>
|
|
</div>
|
|
<div class="spd-quick-item">
|
|
<span class="spd-quick-label">Cola</span>
|
|
<span class="spd-quick-val" style="font-size: 13px;">{{ QUEUE_LABELS[ticket.routedQueue] }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════ AI SECTION ═══════════ -->
|
|
<div class="spd-ai-panel">
|
|
<div class="spd-ai-header" @click="aiRecapOpen = !aiRecapOpen" style="cursor: pointer;">
|
|
<div class="flex items-center gap-2">
|
|
<span class="spd-ai-badge">IA</span>
|
|
<span class="text-[13px] font-semibold text-[var(--text-primary)]">Análisis automático</span>
|
|
</div>
|
|
<UIcon :name="aiRecapOpen ? 'i-heroicons-chevron-up' : 'i-heroicons-chevron-down'" class="w-4 h-4 text-[var(--text-muted)]" />
|
|
</div>
|
|
<div v-if="aiRecapOpen" class="spd-ai-body">
|
|
<div class="spd-ai-row">
|
|
<span class="spd-ai-label">Resumen</span>
|
|
<p class="text-[13px] text-[var(--text-primary)]">{{ ticket.aiSummary }}</p>
|
|
</div>
|
|
<div class="spd-ai-row">
|
|
<span class="spd-ai-label">Intención detectada</span>
|
|
<span class="text-[13px] font-medium text-[var(--text-primary)]">{{ INTENT_LABELS[ticket.aiSuggestedIntent] }}</span>
|
|
<span class="spd-ai-confidence">{{ Math.round(ticket.aiConfidence * 100) }}% confianza</span>
|
|
</div>
|
|
<div v-if="ticket.routingTrace.length" class="spd-ai-row">
|
|
<span class="spd-ai-label">Traza de ruteo</span>
|
|
<div class="spd-routing-trace">
|
|
<div v-for="(step, i) in ticket.routingTrace" :key="i" class="spd-trace-step">
|
|
<span class="spd-trace-dot" />
|
|
<div>
|
|
<p class="text-[12px] font-medium text-[var(--text-primary)]">{{ step.step }}</p>
|
|
<p class="text-[11px] text-[var(--text-muted)]">{{ step.result }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════ TABS ═══════════ -->
|
|
<div class="spd-main-tabs">
|
|
<button
|
|
v-for="tab in tabs"
|
|
:key="tab.id"
|
|
type="button"
|
|
class="spd-tab"
|
|
:class="activeTab === tab.id ? 'spd-tab-on' : 'spd-tab-off'"
|
|
@click="activeTab = tab.id"
|
|
>
|
|
<UIcon :name="tab.icon" class="w-4 h-4" />
|
|
{{ tab.label }}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- ═══════════ THREAD TAB ═══════════ -->
|
|
<div v-if="activeTab === 'thread'" class="spd-thread">
|
|
<div v-for="msg in sortedMessages" :key="msg.id" class="spd-msg" :class="[
|
|
msg.type === 'system' ? 'spd-msg-system' : '',
|
|
msg.type === 'internal_note' ? 'spd-msg-note' : '',
|
|
msg.direction === 'inbound' && msg.type !== 'system' && msg.type !== 'internal_note' ? 'spd-msg-inbound' : '',
|
|
msg.direction === 'outbound' ? 'spd-msg-outbound' : '',
|
|
]">
|
|
<!-- System message -->
|
|
<template v-if="msg.type === 'system'">
|
|
<div class="spd-msg-system-inner">
|
|
<UIcon name="i-heroicons-cog-6-tooth" class="w-3.5 h-3.5" />
|
|
<span>{{ msg.body }}</span>
|
|
<span class="spd-msg-time">{{ fmtDateTime(msg.timestamp) }}</span>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Internal note -->
|
|
<template v-else-if="msg.type === 'internal_note'">
|
|
<div class="spd-msg-note-inner">
|
|
<div class="spd-msg-note-header">
|
|
<UIcon name="i-heroicons-lock-closed" class="w-3.5 h-3.5" />
|
|
<span class="font-semibold">{{ msg.from }}</span>
|
|
<span class="spd-msg-time">{{ fmtDateTime(msg.timestamp) }}</span>
|
|
</div>
|
|
<p class="spd-msg-note-body">{{ msg.body }}</p>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Email message -->
|
|
<template v-else-if="msg.type === 'email'">
|
|
<div class="spd-msg-email" :class="msg.direction === 'outbound' ? 'spd-email-outbound' : 'spd-email-inbound'">
|
|
<div class="spd-email-header">
|
|
<UIcon name="i-heroicons-envelope" class="w-3.5 h-3.5" />
|
|
<span class="font-semibold text-[12px]">{{ msg.from }}</span>
|
|
<span v-if="msg.to" class="text-[11px] text-[var(--text-muted)]">→ {{ msg.to }}</span>
|
|
<span class="spd-msg-time ml-auto">{{ fmtDateTime(msg.timestamp) }}</span>
|
|
</div>
|
|
<p v-if="msg.subject" class="spd-email-subject">{{ msg.subject }}</p>
|
|
<p class="spd-email-body">{{ msg.body }}</p>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- WhatsApp / Phone note -->
|
|
<template v-else>
|
|
<div class="spd-msg-bubble" :class="msg.direction === 'outbound' ? 'spd-bubble-outbound' : 'spd-bubble-inbound'">
|
|
<p class="spd-bubble-from">{{ msg.from }}</p>
|
|
<p class="spd-bubble-body">{{ msg.body }}</p>
|
|
<div class="spd-bubble-footer">
|
|
<span class="spd-bubble-time">{{ fmtTime(msg.timestamp) }}</span>
|
|
<span v-if="msg.direction === 'outbound' && msg.deliveryStatus" :class="['spd-delivery', deliveryClass(msg.deliveryStatus)]">
|
|
{{ deliveryIcon(msg.deliveryStatus) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- ═══════════ REPLY COMPOSER ═══════════ -->
|
|
<div class="spd-composer" :class="composerChannel === 'whatsapp' ? 'spd-composer-wa' : composerChannel === 'internal_note' ? 'spd-composer-note' : ''">
|
|
<div class="spd-composer-channel-row">
|
|
<button
|
|
v-for="ch in ([
|
|
{ id: 'whatsapp', label: 'WhatsApp', icon: 'i-heroicons-chat-bubble-left-ellipsis' },
|
|
{ id: 'email', label: 'Email', icon: 'i-heroicons-envelope' },
|
|
{ id: 'internal_note', label: 'Nota interna', icon: 'i-heroicons-lock-closed' },
|
|
] as { id: typeof composerChannel; label: string; icon: string }[])"
|
|
:key="ch.id"
|
|
type="button"
|
|
class="spd-composer-ch-btn"
|
|
:class="composerChannel === ch.id ? 'spd-ch-active' : 'spd-ch-inactive'"
|
|
@click="composerChannel = ch.id"
|
|
>
|
|
<UIcon :name="ch.icon" class="w-3.5 h-3.5" />
|
|
{{ ch.label }}
|
|
</button>
|
|
</div>
|
|
<div class="spd-composer-input-row">
|
|
<textarea
|
|
v-model="composerText"
|
|
class="spd-composer-textarea"
|
|
:placeholder="composerChannel === 'internal_note' ? 'Escribir nota interna...' : `Responder vía ${composerChannel === 'whatsapp' ? 'WhatsApp' : 'email'}...`"
|
|
rows="2"
|
|
@keydown.meta.enter="sendMessage"
|
|
@keydown.ctrl.enter="sendMessage"
|
|
/>
|
|
<button type="button" class="spd-composer-send" :disabled="!composerText.trim()" @click="sendMessage">
|
|
<UIcon name="i-heroicons-paper-airplane" class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════ OVERVIEW TAB ═══════════ -->
|
|
<div v-if="activeTab === 'overview'" class="spd-overview">
|
|
<div class="spd-overview-section">
|
|
<h3 class="spd-section-title">Traza de Ruteo</h3>
|
|
<div class="spd-routing-trace">
|
|
<div v-for="(step, i) in ticket.routingTrace" :key="i" class="spd-trace-step">
|
|
<span class="spd-trace-dot" />
|
|
<div>
|
|
<p class="text-[12px] font-medium text-[var(--text-primary)]">{{ step.step }}</p>
|
|
<p class="text-[11px] text-[var(--text-muted)]">{{ step.result }} — {{ fmtDateTime(step.timestamp) }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="spd-overview-section">
|
|
<h3 class="spd-section-title">Información del Cliente</h3>
|
|
<div class="spd-info-grid">
|
|
<div class="spd-info-item">
|
|
<span class="spd-info-label">Nombre</span>
|
|
<span class="spd-info-value">{{ ticket.customerName }}</span>
|
|
</div>
|
|
<div class="spd-info-item">
|
|
<span class="spd-info-label">Email</span>
|
|
<span class="spd-info-value">{{ ticket.customerEmail ?? '—' }}</span>
|
|
</div>
|
|
<div class="spd-info-item">
|
|
<span class="spd-info-label">Teléfono</span>
|
|
<span class="spd-info-value">{{ ticket.customerPhone ?? '—' }}</span>
|
|
</div>
|
|
<div class="spd-info-item">
|
|
<span class="spd-info-label">Canal de entrada</span>
|
|
<span class="spd-info-value">{{ CHANNEL_LABELS[ticket.channel] }}</span>
|
|
</div>
|
|
<div class="spd-info-item">
|
|
<span class="spd-info-label">Intención</span>
|
|
<span class="spd-info-value">{{ INTENT_LABELS[ticket.intentCategory] }}</span>
|
|
</div>
|
|
<div class="spd-info-item">
|
|
<span class="spd-info-label">Cola</span>
|
|
<span class="spd-info-value">{{ QUEUE_LABELS[ticket.routedQueue] }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════ DOCUMENTS TAB ═══════════ -->
|
|
<div v-if="activeTab === 'documents'" class="spd-documents">
|
|
<div class="spd-docs-placeholder">
|
|
<UIcon name="i-heroicons-folder-open" class="w-8 h-8 text-[var(--text-muted)]" />
|
|
<p class="mt-2 text-sm font-medium text-[var(--text-primary)]">Documentos adjuntos</p>
|
|
<p class="mt-1 text-[12px] text-[var(--text-muted)]">Los archivos compartidos en la conversación aparecerán aquí. Funcionalidad de carga próximamente.</p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* ══════════ BASE ══════════ */
|
|
.spd-root { display: flex; flex-direction: column; gap: 20px; }
|
|
|
|
.spd-not-found {
|
|
text-align: center; padding: 64px 24px;
|
|
display: flex; flex-direction: column; align-items: center; gap: 8px;
|
|
color: var(--text-muted);
|
|
}
|
|
.spd-not-found h2 { font-size: 18px; font-weight: 600; color: var(--text-primary); }
|
|
|
|
.spd-back-link {
|
|
display: inline-flex; align-items: center; gap: 4px;
|
|
font-size: 12px; font-weight: 600; color: #01696f;
|
|
text-decoration: none;
|
|
}
|
|
.spd-back-link:hover { text-decoration: underline; }
|
|
|
|
/* ══════════ HEADER ══════════ */
|
|
.spd-detail-header {
|
|
display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; flex-wrap: wrap;
|
|
}
|
|
.spd-header-left { flex: 1; min-width: 0; }
|
|
.spd-title-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
.spd-title { font-size: 24px; font-weight: 700; color: var(--text-primary); font-family: 'SF Mono', 'Fira Code', monospace; }
|
|
.spd-header-meta {
|
|
margin-top: 6px; display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
|
|
font-size: 13px; color: var(--text-muted);
|
|
}
|
|
.spd-meta-sep { color: rgba(0,0,0,0.15); }
|
|
.spd-meta-link { color: #01696f; text-decoration: none; font-weight: 500; }
|
|
.spd-meta-link:hover { text-decoration: underline; }
|
|
|
|
.spd-header-actions { display: flex; gap: 8px; flex-shrink: 0; }
|
|
.spd-action-btn {
|
|
display: inline-flex; align-items: center; gap: 5px;
|
|
padding: 7px 12px; border-radius: 8px;
|
|
font-size: 12px; font-weight: 600;
|
|
background: #fff; color: var(--text-primary);
|
|
border: 1px solid rgba(0,0,0,0.1);
|
|
cursor: pointer; transition: all 150ms ease;
|
|
}
|
|
.spd-action-btn:hover { border-color: rgba(0,0,0,0.2); background: rgba(0,0,0,0.02); }
|
|
.spd-action-btn-primary {
|
|
display: inline-flex; align-items: center; gap: 5px;
|
|
padding: 7px 14px; border-radius: 8px;
|
|
font-size: 12px; font-weight: 600;
|
|
background: #01696f; color: #fff; border: none;
|
|
cursor: pointer; transition: all 150ms ease;
|
|
}
|
|
.spd-action-btn-primary:hover { background: #015458; }
|
|
|
|
/* ── Status pill / Priority / Tier badges ── */
|
|
.spd-status-pill {
|
|
display: inline-flex; padding: 2px 8px; border-radius: 9999px;
|
|
font-size: 11px; font-weight: 600; white-space: nowrap;
|
|
}
|
|
.spd-st-open { background: rgba(193,56,56,0.08); color: #c13838; }
|
|
.spd-st-progress { background: rgba(194,123,26,0.08); color: #c27b1a; }
|
|
.spd-st-pending { background: rgba(147,51,234,0.08); color: #9333ea; }
|
|
.spd-st-resolved { background: rgba(0,0,0,0.04); color: #8a8a86; }
|
|
|
|
.spd-priority-badge {
|
|
display: inline-flex; padding: 1px 7px; border-radius: 9999px;
|
|
font-size: 10px; font-weight: 600; white-space: nowrap;
|
|
}
|
|
.spd-pri-urgent { background: rgba(193,56,56,0.12); color: #c13838; }
|
|
.spd-pri-high { background: rgba(194,123,26,0.08); color: #c27b1a; }
|
|
.spd-pri-medium { background: rgba(0,0,0,0.05); color: #6b6b68; }
|
|
.spd-pri-low { background: rgba(0,0,0,0.03); color: #8a8a86; }
|
|
|
|
.spd-tier-badge {
|
|
display: inline-flex; padding: 2px 7px; border-radius: 8px;
|
|
font-size: 10px; font-weight: 600; white-space: nowrap;
|
|
}
|
|
.spd-tier-1 { background: rgba(16,185,129,0.08); color: #059669; }
|
|
.spd-tier-2 { background: rgba(59,130,246,0.08); color: #2563eb; }
|
|
.spd-tier-3 { background: rgba(194,123,26,0.08); color: #c27b1a; }
|
|
|
|
/* ══════════ CONTEXT PANEL ══════════ */
|
|
.spd-context-panel {
|
|
padding: 16px; border-radius: 12px;
|
|
border: 1px solid rgba(0,0,0,0.06); background: #fff;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.03);
|
|
}
|
|
.spd-context-title {
|
|
display: flex; align-items: center; gap: 6px;
|
|
font-size: 12px; font-weight: 700; text-transform: uppercase;
|
|
letter-spacing: 0.04em; color: #8a8a86; margin-bottom: 10px;
|
|
}
|
|
.spd-context-policies { display: flex; flex-direction: column; gap: 6px; }
|
|
.spd-context-policy-row {
|
|
display: flex; align-items: center; gap: 12px;
|
|
padding: 6px 10px; border-radius: 8px; background: rgba(0,0,0,0.015);
|
|
}
|
|
.spd-context-pol-number {
|
|
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
font-size: 12px; font-weight: 600; color: #01696f;
|
|
}
|
|
.spd-context-pol-chip {
|
|
display: inline-flex; padding: 1px 6px; border-radius: 6px;
|
|
font-size: 10px; font-weight: 600;
|
|
background: rgba(1,105,111,0.06); color: #01696f;
|
|
}
|
|
.spd-context-claim {
|
|
display: flex; align-items: center; gap: 10px;
|
|
padding: 4px 10px; margin-top: 4px;
|
|
}
|
|
|
|
/* ══════════ QUICK STATS ══════════ */
|
|
.spd-quick-strip {
|
|
display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px;
|
|
border-radius: 12px; border: 1px solid rgba(0,0,0,0.06);
|
|
background: rgba(0,0,0,0.06); overflow: hidden;
|
|
}
|
|
.spd-quick-item { padding: 12px 16px; background: #fff; }
|
|
.spd-quick-item:first-child { border-radius: 12px 0 0 12px; }
|
|
.spd-quick-item:last-child { border-radius: 0 12px 12px 0; }
|
|
.spd-quick-label {
|
|
font-size: 10px; font-weight: 600; text-transform: uppercase;
|
|
letter-spacing: 0.04em; color: #8a8a86;
|
|
}
|
|
.spd-quick-val {
|
|
margin-top: 2px; font-size: 18px; font-weight: 600;
|
|
color: var(--text-primary); font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
/* ══════════ AI PANEL ══════════ */
|
|
.spd-ai-panel {
|
|
border-radius: 12px;
|
|
background: rgba(1,105,111,0.02);
|
|
border: 1px solid rgba(1,105,111,0.1);
|
|
border-left: 3px solid #01696f;
|
|
overflow: hidden;
|
|
}
|
|
.spd-ai-header {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
padding: 12px 16px;
|
|
}
|
|
.spd-ai-badge {
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
width: 24px; height: 24px; border-radius: 6px;
|
|
background: rgba(1,105,111,0.1); color: #01696f;
|
|
font-size: 10px; font-weight: 800;
|
|
}
|
|
.spd-ai-body { padding: 0 16px 16px; display: flex; flex-direction: column; gap: 12px; }
|
|
.spd-ai-row { display: flex; flex-direction: column; gap: 4px; }
|
|
.spd-ai-label {
|
|
font-size: 10px; font-weight: 600; text-transform: uppercase;
|
|
letter-spacing: 0.04em; color: #8a8a86;
|
|
}
|
|
.spd-ai-confidence {
|
|
font-size: 11px; font-weight: 600; color: #059669;
|
|
background: rgba(5,150,105,0.08); padding: 1px 6px; border-radius: 6px;
|
|
display: inline-flex; width: fit-content;
|
|
}
|
|
|
|
/* ── Routing trace ── */
|
|
.spd-routing-trace { display: flex; flex-direction: column; gap: 8px; padding-left: 4px; }
|
|
.spd-trace-step { display: flex; align-items: flex-start; gap: 10px; }
|
|
.spd-trace-dot {
|
|
width: 8px; height: 8px; border-radius: 50%; margin-top: 4px;
|
|
background: #01696f; flex-shrink: 0;
|
|
}
|
|
|
|
/* ══════════ TABS ══════════ */
|
|
.spd-main-tabs {
|
|
display: flex; gap: 4px; border-bottom: 1px solid rgba(0,0,0,0.06);
|
|
padding-bottom: 0;
|
|
}
|
|
.spd-tab {
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
padding: 10px 16px; font-size: 13px; font-weight: 500;
|
|
border: none; cursor: pointer;
|
|
background: transparent; border-bottom: 2px solid transparent;
|
|
transition: all 150ms ease; color: var(--text-muted);
|
|
}
|
|
.spd-tab-on { color: #01696f; border-bottom-color: #01696f; }
|
|
.spd-tab-off:hover { color: var(--text-primary); }
|
|
|
|
/* ══════════ THREAD ══════════ */
|
|
.spd-thread {
|
|
display: flex; flex-direction: column; gap: 12px;
|
|
}
|
|
.spd-msg { display: flex; }
|
|
|
|
/* System messages */
|
|
.spd-msg-system { justify-content: center; }
|
|
.spd-msg-system-inner {
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
padding: 4px 12px; border-radius: 8px;
|
|
background: rgba(0,0,0,0.03);
|
|
font-size: 11px; color: var(--text-muted);
|
|
}
|
|
|
|
/* Internal notes */
|
|
.spd-msg-note { justify-content: center; }
|
|
.spd-msg-note-inner {
|
|
max-width: 80%; padding: 10px 14px; border-radius: 10px;
|
|
background: rgba(250, 204, 21, 0.08);
|
|
border: 1px solid rgba(250, 204, 21, 0.2);
|
|
}
|
|
.spd-msg-note-header {
|
|
display: flex; align-items: center; gap: 6px;
|
|
font-size: 12px; color: #92400e;
|
|
}
|
|
.spd-msg-note-body { margin-top: 4px; font-size: 13px; color: var(--text-primary); }
|
|
|
|
/* Inbound messages — left aligned */
|
|
.spd-msg-inbound { justify-content: flex-start; }
|
|
|
|
/* Outbound messages — right aligned */
|
|
.spd-msg-outbound { justify-content: flex-end; }
|
|
|
|
/* Message bubbles (WhatsApp/phone) */
|
|
.spd-msg-bubble {
|
|
max-width: 65%; padding: 8px 12px; border-radius: 12px;
|
|
position: relative;
|
|
}
|
|
.spd-bubble-inbound {
|
|
background: rgba(37, 211, 102, 0.08);
|
|
border: 1px solid rgba(37, 211, 102, 0.15);
|
|
border-bottom-left-radius: 4px;
|
|
}
|
|
.spd-bubble-outbound {
|
|
background: rgba(1, 105, 111, 0.06);
|
|
border: 1px solid rgba(1, 105, 111, 0.12);
|
|
border-bottom-right-radius: 4px;
|
|
}
|
|
.spd-bubble-from { font-size: 11px; font-weight: 700; color: var(--text-muted); margin-bottom: 2px; }
|
|
.spd-bubble-body { font-size: 13px; color: var(--text-primary); line-height: 1.5; white-space: pre-wrap; }
|
|
.spd-bubble-footer { display: flex; align-items: center; justify-content: flex-end; gap: 4px; margin-top: 4px; }
|
|
.spd-bubble-time { font-size: 10px; color: var(--text-muted); }
|
|
.spd-delivery { font-size: 12px; }
|
|
.spd-delivery-default { color: var(--text-muted); }
|
|
.spd-delivery-read { color: #2563eb; }
|
|
|
|
/* Email messages */
|
|
.spd-msg-email {
|
|
max-width: 75%; padding: 12px 14px; border-radius: 12px;
|
|
border: 1px solid rgba(0,0,0,0.06); background: #fff;
|
|
box-shadow: 0 1px 2px rgba(0,0,0,0.04);
|
|
}
|
|
.spd-email-inbound { border-bottom-left-radius: 4px; }
|
|
.spd-email-outbound { border-bottom-right-radius: 4px; }
|
|
.spd-email-header { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
|
.spd-email-subject { font-size: 12px; font-weight: 600; color: var(--text-primary); margin-top: 6px; }
|
|
.spd-email-body { font-size: 13px; color: var(--text-primary); margin-top: 6px; line-height: 1.5; white-space: pre-wrap; }
|
|
|
|
.spd-msg-time { font-size: 10px; color: var(--text-muted); }
|
|
|
|
/* ══════════ COMPOSER ══════════ */
|
|
.spd-composer {
|
|
border-radius: 12px; border: 1px solid rgba(0,0,0,0.08);
|
|
background: #fff; overflow: hidden;
|
|
position: sticky; bottom: 0;
|
|
}
|
|
.spd-composer-wa { border-color: rgba(37,211,102,0.3); }
|
|
.spd-composer-note { border-color: rgba(250,204,21,0.3); background: rgba(250,204,21,0.02); }
|
|
|
|
.spd-composer-channel-row {
|
|
display: flex; gap: 2px; padding: 8px 12px;
|
|
border-bottom: 1px solid rgba(0,0,0,0.04);
|
|
}
|
|
.spd-composer-ch-btn {
|
|
display: inline-flex; align-items: center; gap: 4px;
|
|
padding: 4px 10px; border-radius: 6px;
|
|
font-size: 11px; font-weight: 600; border: none; cursor: pointer;
|
|
transition: all 150ms ease;
|
|
}
|
|
.spd-ch-active { background: #01696f; color: #fff; }
|
|
.spd-composer-wa .spd-ch-active { background: #25D366; color: #fff; }
|
|
.spd-composer-note .spd-ch-active { background: #d97706; color: #fff; }
|
|
.spd-ch-inactive { background: transparent; color: var(--text-muted); }
|
|
.spd-ch-inactive:hover { color: var(--text-primary); }
|
|
|
|
.spd-composer-input-row { display: flex; align-items: flex-end; gap: 8px; padding: 8px 12px; }
|
|
.spd-composer-textarea {
|
|
flex: 1; resize: none; border: none; outline: none;
|
|
font-size: 13px; color: var(--text-primary);
|
|
background: transparent; font-family: inherit;
|
|
line-height: 1.5;
|
|
}
|
|
.spd-composer-textarea::placeholder { color: var(--text-muted); }
|
|
.spd-composer-send {
|
|
width: 36px; height: 36px; border-radius: 50%;
|
|
background: #01696f; color: #fff; border: none;
|
|
display: flex; align-items: center; justify-content: center;
|
|
cursor: pointer; transition: all 150ms ease; flex-shrink: 0;
|
|
}
|
|
.spd-composer-wa .spd-composer-send { background: #25D366; }
|
|
.spd-composer-note .spd-composer-send { background: #d97706; }
|
|
.spd-composer-send:hover { opacity: 0.85; }
|
|
.spd-composer-send:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
|
|
/* ══════════ OVERVIEW TAB ══════════ */
|
|
.spd-overview { display: flex; flex-direction: column; gap: 24px; }
|
|
.spd-overview-section { }
|
|
.spd-section-title {
|
|
font-size: 14px; font-weight: 600; color: var(--text-primary);
|
|
margin-bottom: 12px;
|
|
}
|
|
.spd-info-grid {
|
|
display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px;
|
|
}
|
|
.spd-info-item {
|
|
padding: 10px 14px; border-radius: 8px;
|
|
background: rgba(0,0,0,0.015); border: 1px solid rgba(0,0,0,0.04);
|
|
}
|
|
.spd-info-label {
|
|
font-size: 10px; font-weight: 600; text-transform: uppercase;
|
|
letter-spacing: 0.04em; color: #8a8a86;
|
|
}
|
|
.spd-info-value { font-size: 13px; font-weight: 500; color: var(--text-primary); margin-top: 2px; }
|
|
|
|
/* ══════════ DOCUMENTS TAB ══════════ */
|
|
.spd-documents { }
|
|
.spd-docs-placeholder {
|
|
text-align: center; padding: 48px 24px;
|
|
border-radius: 12px; border: 1px dashed rgba(0,0,0,0.1);
|
|
background: rgba(0,0,0,0.01);
|
|
display: flex; flex-direction: column; align-items: center;
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.spd-quick-strip { grid-template-columns: repeat(2, 1fr); }
|
|
.spd-info-grid { grid-template-columns: 1fr; }
|
|
.spd-msg-bubble, .spd-msg-email { max-width: 85%; }
|
|
}
|
|
</style>
|