WIP jordan
This commit is contained in:
231
app/pages/settings/support-routing.vue
Normal file
231
app/pages/settings/support-routing.vue
Normal file
@@ -0,0 +1,231 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user