232 lines
8.5 KiB
Vue
232 lines
8.5 KiB
Vue
<script setup lang="ts">
|
|
import { TIER_LABELS, QUEUE_LABELS, type RoutingTier, type RoutedQueue, type RoutingRule } from '~/data/mock-support'
|
|
|
|
usePageTitle('Support Routing')
|
|
|
|
const { state, toggleRule, updateRule } = useSupportTickets()
|
|
|
|
type TierTab = 'tier1' | 'tier2' | 'tier3'
|
|
const activeTier = ref<TierTab>('tier1')
|
|
|
|
const tierTabs: { id: TierTab; label: string; tier: RoutingTier; description: string }[] = [
|
|
{ id: 'tier1', label: 'Tier 1 — Auto', tier: 'tier1_auto', description: 'Ruteo automático para clientes conocidos, formularios web con LOB, y auto-cumplimiento de documentos.' },
|
|
{ id: 'tier2', label: 'Tier 2 — Reglas', tier: 'tier2_rule', description: 'Clasificación por palabras clave del mensaje. El LLM detecta la intención y dirige a la cola correcta.' },
|
|
{ id: 'tier3', label: 'Tier 3 — Pool', tier: 'tier3_open', description: 'Mensajes ambiguos que no coinciden con ninguna regla van al pool abierto para triaje manual.' },
|
|
]
|
|
|
|
const tier1Rules = computed(() => state.value.routingRules.filter(r => r.tier === 'tier1_auto'))
|
|
const tier2Rules = computed(() => state.value.routingRules.filter(r => r.tier === 'tier2_rule'))
|
|
|
|
const queueOptions: { label: string; value: RoutedQueue }[] = [
|
|
{ label: 'Cobros', value: 'collections' },
|
|
{ label: 'Siniestros', value: 'claims' },
|
|
{ label: 'Ventas', value: 'sales' },
|
|
{ label: 'Renovaciones', value: 'renewals' },
|
|
{ label: 'Operaciones', value: 'operations' },
|
|
{ label: 'Pool Abierto', value: 'open_pool' },
|
|
]
|
|
|
|
// Tier 3 settings (mock)
|
|
const tier3DefaultAssignee = ref('Round-robin')
|
|
const tier3EscalationHours = ref(4)
|
|
|
|
const toast = useToast()
|
|
function handleSave() {
|
|
toast.add({ title: 'Configuración guardada', color: 'green' })
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="srt-page">
|
|
<!-- Back + Header -->
|
|
<NuxtLink to="/settings" class="srt-back-link">
|
|
<UIcon name="i-heroicons-arrow-left" class="w-3.5 h-3.5" />
|
|
Volver a Configuración
|
|
</NuxtLink>
|
|
|
|
<div>
|
|
<h1 class="mt-1 text-2xl font-semibold tracking-tight text-[var(--text-primary)]">Support Routing</h1>
|
|
<p class="mt-2 text-[14px] leading-relaxed text-[var(--text-muted)]">
|
|
Configure 3-tier routing rules for incoming support requests — auto-routing, keyword classification, and open pool settings.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Tier tabs -->
|
|
<div class="srt-tier-tabs">
|
|
<button
|
|
v-for="tab in tierTabs"
|
|
:key="tab.id"
|
|
type="button"
|
|
class="srt-tier-tab"
|
|
:class="activeTier === tab.id ? 'srt-tier-on' : 'srt-tier-off'"
|
|
@click="activeTier = tab.id"
|
|
>
|
|
{{ tab.label }}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Tier description -->
|
|
<p class="text-[13px] text-[var(--text-muted)] -mt-2">
|
|
{{ tierTabs.find(t => t.id === activeTier)?.description }}
|
|
</p>
|
|
|
|
<!-- ═══════════ TIER 1 ═══════════ -->
|
|
<div v-if="activeTier === 'tier1'" class="srt-rules">
|
|
<div v-for="rule in tier1Rules" :key="rule.id" class="srt-rule-card">
|
|
<div class="srt-rule-header">
|
|
<div class="flex-1 min-w-0">
|
|
<p class="srt-rule-name">{{ rule.name }}</p>
|
|
<p class="srt-rule-condition">{{ rule.condition }}</p>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<span class="srt-queue-chip">{{ QUEUE_LABELS[rule.targetQueue] }}</span>
|
|
<USwitch :model-value="rule.enabled" @update:model-value="toggleRule(rule.id)" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════ TIER 2 ═══════════ -->
|
|
<div v-if="activeTier === 'tier2'" class="srt-rules">
|
|
<div v-for="rule in tier2Rules" :key="rule.id" class="srt-rule-card">
|
|
<div class="srt-rule-header">
|
|
<div class="flex-1 min-w-0">
|
|
<p class="srt-rule-name">{{ rule.name }}</p>
|
|
<p class="srt-rule-condition">{{ rule.condition }}</p>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<select
|
|
class="srt-queue-select"
|
|
:value="rule.targetQueue"
|
|
@change="updateRule(rule.id, { targetQueue: ($event.target as HTMLSelectElement).value as RoutedQueue })"
|
|
>
|
|
<option v-for="q in queueOptions" :key="q.value" :value="q.value">{{ q.label }}</option>
|
|
</select>
|
|
<USwitch :model-value="rule.enabled" @update:model-value="toggleRule(rule.id)" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════ TIER 3 ═══════════ -->
|
|
<div v-if="activeTier === 'tier3'" class="srt-rules">
|
|
<div class="srt-rule-card">
|
|
<div class="srt-rule-header">
|
|
<div class="flex-1">
|
|
<p class="srt-rule-name">Asignación por defecto</p>
|
|
<p class="srt-rule-condition">Tickets sin coincidencia de regla se asignan a:</p>
|
|
</div>
|
|
<select v-model="tier3DefaultAssignee" class="srt-queue-select">
|
|
<option>Round-robin</option>
|
|
<option>Ana R.</option>
|
|
<option>Marco V.</option>
|
|
<option>Carlos Villalba</option>
|
|
<option>María Fernanda Ortiz</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="srt-rule-card">
|
|
<div class="srt-rule-header">
|
|
<div class="flex-1">
|
|
<p class="srt-rule-name">Umbral de escalamiento</p>
|
|
<p class="srt-rule-condition">Si un ticket no asignado permanece en el pool más de este tiempo, se escala automáticamente.</p>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<input
|
|
v-model.number="tier3EscalationHours"
|
|
type="number"
|
|
min="1"
|
|
max="48"
|
|
class="srt-hour-input"
|
|
/>
|
|
<span class="text-[12px] text-[var(--text-muted)]">horas</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Save button -->
|
|
<div class="flex justify-end">
|
|
<button type="button" class="srt-save-btn" @click="handleSave">
|
|
Guardar configuración
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.srt-page {
|
|
max-width: 56rem;
|
|
margin: 0 auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
padding-bottom: 3rem;
|
|
}
|
|
|
|
.srt-back-link {
|
|
display: inline-flex; align-items: center; gap: 4px;
|
|
font-size: 12px; font-weight: 600; color: #01696f;
|
|
text-decoration: none;
|
|
}
|
|
.srt-back-link:hover { text-decoration: underline; }
|
|
|
|
/* ── Tier tabs ── */
|
|
.srt-tier-tabs {
|
|
display: inline-flex; gap: 2px; padding: 3px;
|
|
border-radius: 10px; background: rgba(0,0,0,0.04);
|
|
width: fit-content;
|
|
}
|
|
.srt-tier-tab {
|
|
padding: 8px 18px; border-radius: 8px;
|
|
font-size: 13px; font-weight: 500; border: none;
|
|
cursor: pointer; transition: all 150ms ease; white-space: nowrap;
|
|
}
|
|
.srt-tier-on { background: #fff; color: var(--text-primary); box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
|
|
.srt-tier-off { background: transparent; color: var(--text-muted); }
|
|
.srt-tier-off:hover { color: var(--text-primary); }
|
|
|
|
/* ── Rules ── */
|
|
.srt-rules { display: flex; flex-direction: column; gap: 10px; }
|
|
.srt-rule-card {
|
|
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);
|
|
}
|
|
.srt-rule-header {
|
|
display: flex; align-items: center; gap: 16px;
|
|
}
|
|
.srt-rule-name { font-size: 14px; font-weight: 600; color: var(--text-primary); }
|
|
.srt-rule-condition { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
|
|
|
|
.srt-queue-chip {
|
|
display: inline-flex; padding: 2px 8px; border-radius: 8px;
|
|
font-size: 11px; font-weight: 600;
|
|
background: rgba(1,105,111,0.06); color: #01696f;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.srt-queue-select {
|
|
padding: 5px 10px; border-radius: 8px; font-size: 12px; font-weight: 500;
|
|
border: 1px solid rgba(0,0,0,0.08); background: #fff; color: var(--text-primary);
|
|
cursor: pointer; min-width: 120px;
|
|
}
|
|
.srt-queue-select:focus { outline: none; border-color: #01696f; }
|
|
|
|
.srt-hour-input {
|
|
width: 60px; padding: 5px 8px; border-radius: 8px;
|
|
font-size: 13px; font-weight: 600; text-align: center;
|
|
border: 1px solid rgba(0,0,0,0.08); background: #fff; color: var(--text-primary);
|
|
}
|
|
.srt-hour-input:focus { outline: none; border-color: #01696f; }
|
|
|
|
.srt-save-btn {
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
padding: 8px 20px; border-radius: 8px;
|
|
background: #01696f; color: #fff;
|
|
font-size: 13px; font-weight: 500; border: none;
|
|
cursor: pointer; transition: all 150ms ease;
|
|
}
|
|
.srt-save-btn:hover { background: #015458; }
|
|
</style>
|