WIP jordan

This commit is contained in:
Jordan Weingarten
2026-04-16 11:11:44 -05:00
parent ff2d7b18b5
commit 67482f6629
163 changed files with 50627 additions and 728 deletions

View 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>