big refactor

This commit is contained in:
2026-04-29 16:25:11 -05:00
parent 6c411ce2b6
commit 8265fb689a
156 changed files with 15845 additions and 50373 deletions

View File

@@ -1,9 +1,14 @@
<script setup lang="ts">
usePageTitle('Agents & Commissions · Settings')
/* ── Types ── */
type AgentStatus = 'active' | 'inactive' | 'suspended'
type CommissionTier = { lobId: string; lob: string; newPct: number; renewalPct: number }
interface CommissionTier {
lobId: string
lob: string
newPct: number
renewalPct: number
}
interface Agent {
id: string
@@ -20,83 +25,13 @@ interface Agent {
lastLogin: string
}
/* ── Mock data ── */
const agents = ref<Agent[]>([
{
id: 'AG-001', name: 'Ana Ramírez', email: 'ana.ramirez@segur-os.com', phone: '+506 8812-4455',
role: 'Senior producer', status: 'active', hireDate: '2021-03-15',
book: { policies: 142, gwp: 2_840_000, collected: 2_610_000, outstanding: 230_000 },
commissionTiers: [
{ lobId: 'auto', lob: 'Auto', newPct: 15, renewalPct: 10 },
{ lobId: 'health', lob: 'Health', newPct: 12, renewalPct: 8 },
{ lobId: 'life', lob: 'Life', newPct: 18, renewalPct: 12 },
{ lobId: 'property', lob: 'Property', newPct: 14, renewalPct: 9 },
{ lobId: 'general', lob: 'General risk', newPct: 13, renewalPct: 8 },
],
ytdEarned: 48_200, ytdPending: 6_400, lastLogin: '2026-04-05 08:32'
},
{
id: 'AG-002', name: 'Marco Villanueva', email: 'marco.v@segur-os.com', phone: '+506 8899-2211',
role: 'Producer', status: 'active', hireDate: '2022-08-01',
book: { policies: 88, gwp: 1_620_000, collected: 1_480_000, outstanding: 140_000 },
commissionTiers: [
{ lobId: 'auto', lob: 'Auto', newPct: 14, renewalPct: 9 },
{ lobId: 'health', lob: 'Health', newPct: 11, renewalPct: 7 },
{ lobId: 'life', lob: 'Life', newPct: 16, renewalPct: 10 },
{ lobId: 'property', lob: 'Property', newPct: 12, renewalPct: 8 },
{ lobId: 'general', lob: 'General risk', newPct: 12, renewalPct: 7 },
],
ytdEarned: 28_600, ytdPending: 3_200, lastLogin: '2026-04-04 17:15'
},
{
id: 'AG-003', name: 'Lucía Fernández', email: 'lucia.f@segur-os.com', phone: '+506 7745-3388',
role: 'Junior producer', status: 'active', hireDate: '2024-01-10',
book: { policies: 34, gwp: 420_000, collected: 385_000, outstanding: 35_000 },
commissionTiers: [
{ lobId: 'auto', lob: 'Auto', newPct: 12, renewalPct: 8 },
{ lobId: 'health', lob: 'Health', newPct: 10, renewalPct: 6 },
{ lobId: 'life', lob: 'Life', newPct: 14, renewalPct: 9 },
{ lobId: 'property', lob: 'Property', newPct: 11, renewalPct: 7 },
{ lobId: 'general', lob: 'General risk', newPct: 10, renewalPct: 6 },
],
ytdEarned: 8_400, ytdPending: 1_100, lastLogin: '2026-04-05 09:01'
},
{
id: 'AG-004', name: 'Diego Mora', email: 'diego.m@segur-os.com', phone: '+506 6612-9944',
role: 'Producer', status: 'suspended', hireDate: '2023-05-20',
book: { policies: 56, gwp: 980_000, collected: 720_000, outstanding: 260_000 },
commissionTiers: [
{ lobId: 'auto', lob: 'Auto', newPct: 13, renewalPct: 9 },
{ lobId: 'health', lob: 'Health', newPct: 11, renewalPct: 7 },
{ lobId: 'life', lob: 'Life', newPct: 15, renewalPct: 10 },
{ lobId: 'property', lob: 'Property', newPct: 12, renewalPct: 8 },
{ lobId: 'general', lob: 'General risk', newPct: 11, renewalPct: 7 },
],
ytdEarned: 14_200, ytdPending: 8_800, lastLogin: '2026-03-28 11:40'
},
{
id: 'AG-005', name: 'Valentina Castro', email: 'val.castro@segur-os.com', phone: '+506 8834-5566',
role: 'Senior producer', status: 'inactive', hireDate: '2019-11-03',
book: { policies: 0, gwp: 0, collected: 0, outstanding: 0 },
commissionTiers: [
{ lobId: 'auto', lob: 'Auto', newPct: 15, renewalPct: 10 },
{ lobId: 'health', lob: 'Health', newPct: 12, renewalPct: 8 },
{ lobId: 'life', lob: 'Life', newPct: 18, renewalPct: 12 },
{ lobId: 'property', lob: 'Property', newPct: 14, renewalPct: 9 },
{ lobId: 'general', lob: 'General risk', newPct: 13, renewalPct: 8 },
],
ytdEarned: 0, ytdPending: 0, lastLogin: '2025-12-15 14:22'
},
])
/* ── State ── */
const agents = ref<Agent[]>([])
const search = ref('')
const statusFilter = ref<'all' | AgentStatus>('all')
const selectedAgent = ref<Agent | null>(null)
const addModalOpen = ref(false)
const editingCommissions = ref(false)
/* ── Filtering ── */
const filteredAgents = computed(() => {
let list = agents.value
if (statusFilter.value !== 'all') list = list.filter(a => a.status === statusFilter.value)
@@ -110,7 +45,6 @@ const filteredAgents = computed(() => {
return list
})
/* ── Aggregate KPIs ── */
const kpis = computed(() => {
const active = agents.value.filter(a => a.status === 'active')
const totalBook = agents.value.reduce((s, a) => s + a.book.gwp, 0)
@@ -130,7 +64,6 @@ const kpis = computed(() => {
}
})
/* ── Add agent modal ── */
const newAgent = reactive({
name: '', email: '', phone: '', role: 'Producer',
defaultNewPct: 13, defaultRenewalPct: 8,
@@ -177,11 +110,9 @@ function toggleAgentStatus(agent: Agent) {
}
function resetCredentials(_agent: Agent) {
// Mock: in production this would trigger a password reset email
alert(`Password reset email would be sent to ${_agent.email}`)
}
/* ── Helpers ── */
function fmtMoney(n: number) {
if (n >= 1_000_000) return `$${(n / 1_000_000).toFixed(1)}M`
if (n >= 1_000) return `$${(n / 1_000).toFixed(0)}K`

View File

@@ -1,13 +1,163 @@
<script setup lang="ts">
import type { AlertRecipient, CustomAlertRule } from '~/composables/useAlertConfig'
definePageMeta({ ssr: false })
usePageTitle('Alerts & Notifications · Settings')
const { config, addThreshold, removeThreshold, addPaymentTier, removePaymentTier, addCustomRule, updateCustomRule, removeCustomRule } = useAlertConfig()
const toast = useToast()
// ── Recipient helpers ──────────────────────────────────────────────────────
type AlertRecipient = 'handler' | 'manager' | 'customer' | 'custom'
interface Threshold {
id: string
daysBefore: number
enabled: boolean
}
interface PaymentTier {
id: string
daysOverdue: number
action: string
recipients: AlertRecipient[]
}
interface CustomAlertRule {
id: string
alertName: string
field: string
operator: string
value: number
recipients: AlertRecipient[]
enabled: boolean
}
interface AlertConfig {
emailSender: {
senderEmail: string
senderDisplayName: string
replyToEmail: string
}
renewals: {
enabled: boolean
thresholds: Threshold[]
}
cancellations: {
enabled: boolean
recipients: AlertRecipient[]
}
latePayments: {
enabled: boolean
tiers: PaymentTier[]
}
creditCardExpiry: {
enabled: boolean
autoDebitOnly: boolean
thresholds: Threshold[]
}
customRules: CustomAlertRule[]
}
const STORAGE_KEY = 'policy-ui.alerts'
function loadConfig(): AlertConfig {
if (import.meta.client) {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored) {
try {
return JSON.parse(stored)
} catch {
return defaultConfig()
}
}
}
return defaultConfig()
}
function defaultConfig(): AlertConfig {
return {
emailSender: {
senderEmail: '',
senderDisplayName: '',
replyToEmail: ''
},
renewals: {
enabled: true,
thresholds: [
{ id: '1', daysBefore: 30, enabled: true },
{ id: '2', daysBefore: 7, enabled: true }
]
},
cancellations: {
enabled: true,
recipients: ['handler', 'manager']
},
latePayments: {
enabled: true,
tiers: [
{ id: '1', daysOverdue: 15, action: 'First reminder', recipients: ['handler'] },
{ id: '2', daysOverdue: 30, action: 'Second reminder', recipients: ['handler', 'manager'] },
{ id: '3', daysOverdue: 45, action: 'Escalate to collections', recipients: ['handler', 'manager', 'customer'] }
]
},
creditCardExpiry: {
enabled: true,
autoDebitOnly: true,
thresholds: [
{ id: '1', daysBefore: 30, enabled: true },
{ id: '2', daysBefore: 7, enabled: true }
]
},
customRules: []
}
}
const config = ref<AlertConfig>(loadConfig())
function saveConfig() {
if (import.meta.client) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(config.value))
}
}
function addThreshold(type: 'renewals' | 'creditCardExpiry', days: number) {
const id = String(Date.now())
config.value[type].thresholds.push({ id, daysBefore: days, enabled: true })
saveConfig()
}
function removeThreshold(type: 'renewals' | 'creditCardExpiry', id: string) {
config.value[type].thresholds = config.value[type].thresholds.filter(t => t.id !== id)
saveConfig()
}
function addPaymentTier(days: number, action: string, recipients: AlertRecipient[]) {
const id = String(Date.now())
config.value.latePayments.tiers.push({ id, daysOverdue: days, action, recipients: [...recipients] })
saveConfig()
}
function removePaymentTier(id: string) {
config.value.latePayments.tiers = config.value.latePayments.tiers.filter(t => t.id !== id)
saveConfig()
}
function addCustomRule(rule: Omit<CustomAlertRule, 'id'>) {
const id = String(Date.now())
config.value.customRules.push({ ...rule, id })
saveConfig()
}
function updateCustomRule(id: string, updates: Partial<CustomAlertRule>) {
const idx = config.value.customRules.findIndex(r => r.id === id)
if (idx !== -1) {
config.value.customRules[idx] = { ...config.value.customRules[idx], ...updates }
saveConfig()
}
}
function removeCustomRule(id: string) {
config.value.customRules = config.value.customRules.filter(r => r.id !== id)
saveConfig()
}
const RECIPIENT_OPTIONS: { id: AlertRecipient; label: string }[] = [
{ id: 'handler', label: 'Handler' },
{ id: 'manager', label: 'Manager' },
@@ -19,9 +169,9 @@ function toggleRecipient(list: AlertRecipient[], r: AlertRecipient) {
const idx = list.indexOf(r)
if (idx === -1) list.push(r)
else list.splice(idx, 1)
saveConfig()
}
// ── Threshold add ──────────────────────────────────────────────────────────
const newRenewalDays = ref(7)
const newCcDays = ref(7)
@@ -29,19 +179,18 @@ function addRenewalThreshold() {
addThreshold('renewals', newRenewalDays.value)
newRenewalDays.value = 7
}
function addCcThreshold() {
addThreshold('creditCardExpiry', newCcDays.value)
newCcDays.value = 7
}
// ── Late payment tier add ──────────────────────────────────────────────────
const newLpDays = ref(45)
function addNewPaymentTier() {
addPaymentTier(newLpDays.value, 'Notify assigned handler', ['handler'])
newLpDays.value = 45
}
// ── Custom rule add ────────────────────────────────────────────────────────
const FIELD_OPTIONS = [
{ value: 'premium', label: 'Annual premium' },
{ value: 'policy_count', label: 'Policy count' },
@@ -75,7 +224,6 @@ function submitNewRule() {
showNewRule.value = false
}
// ── Escalation dot color ───────────────────────────────────────────────────
function tierDotClass(idx: number, total: number) {
if (total <= 1) return 'al-dot-green'
if (idx === total - 1) return 'al-dot-red'
@@ -84,6 +232,7 @@ function tierDotClass(idx: number, total: number) {
}
function handleSave() {
saveConfig()
toast.add({ title: 'Alert configuration saved', color: 'green' })
}
</script>

View File

@@ -1,14 +1,7 @@
<script setup lang="ts">
import { useCustomerAttention } from '~/composables/useCustomerAttention'
import { MOCK_CUSTOMERS, fmtMoney } from '~/data/mock-customers'
import { customerTier } from '~/data/mock-customers'
definePageMeta({ ssr: false })
usePageTitle('Customer Attention · Settings')
const { config, tiers, rules, getScoreForCustomer, getTierForCustomer } = useCustomerAttention()
/* ── Tabs ── */
type Tab = 'tiers' | 'rules' | 'preview'
const activeTab = ref<Tab>('tiers')
const tabItems: { id: Tab; label: string }[] = [
@@ -17,7 +10,6 @@ const tabItems: { id: Tab; label: string }[] = [
{ id: 'preview', label: 'Preview' },
]
/* ── Operator labels ── */
const operatorLabel: Record<string, string> = {
gte: '>=',
gt: '>',
@@ -26,7 +18,6 @@ const operatorLabel: Record<string, string> = {
eq: '=',
}
/* ── Field labels ── */
const fieldLabel: Record<string, string> = {
premium: 'Annual Premium',
policy_count: 'Policy Count',
@@ -37,41 +28,152 @@ const fieldLabel: Record<string, string> = {
has_private_policies: 'Has Private Policies',
}
/* ── Preview data ── */
const currentYear = new Date().getFullYear()
interface Tier {
id: string
name: string
minScore: number
description: string
color: string
icon: string
benefits: string[]
}
const previewRows = computed(() =>
MOCK_CUSTOMERS.map((c) => {
const activePolicies = c.policies.filter(p => p.status === 'Active' || p.status === 'Pending')
const totalPremium = activePolicies.reduce((sum, p) => sum + p.premium, 0)
const policyCount = activePolicies.length
const lineCount = new Set(activePolicies.map(p => p.line)).size
const sinceYear = c.since ? new Date(c.since).getFullYear() : currentYear
const tenureYears = currentYear - sinceYear
const estimatedCommission = Math.round(totalPremium * 0.15)
interface Rule {
id: string
field: string
operator: string
value: number | boolean
points: number
label: string
}
const input = {
totalPremium,
policyCount,
lineCount,
tenureYears,
isCollectivoMember: false,
hasPrivatePolicies: activePolicies.length > 0,
estimatedCommission,
interface Config {
autoClassify: boolean
}
const config = ref<Config>({ autoClassify: true })
const tiers = ref<Tier[]>([
{
id: 'platinum',
name: 'Platinum',
minScore: 80,
description: 'Highest value customers with premium service',
color: '#7c3aed',
icon: 'i-heroicons-star',
benefits: ['Priority support', 'Dedicated agent', 'Same-day responses']
},
{
id: 'gold',
name: 'Gold',
minScore: 60,
description: 'High-value customers with enhanced service',
color: '#ea580c',
icon: 'i-heroicons-award',
benefits: ['Priority support', '24h response time']
},
{
id: 'silver',
name: 'Silver',
minScore: 40,
description: 'Standard service level',
color: '#64748b',
icon: 'i-heroicons-shield-check',
benefits: ['Standard support', '48h response time']
},
{
id: 'bronze',
name: 'Bronze',
minScore: 0,
description: 'Basic service level',
color: '#78716c',
icon: 'i-heroicons-user',
benefits: ['Email support', '72h response time']
}
])
const rules = ref<Rule[]>([
{ id: '1', field: 'premium', operator: 'gte', value: 100000, points: 20, label: 'High premium' },
{ id: '2', field: 'policy_count', operator: 'gte', value: 5, points: 15, label: 'Multiple policies' },
{ id: '3', field: 'commission', operator: 'gte', value: 15000, points: 15, label: 'High commission' },
{ id: '4', field: 'collectivo_member', operator: 'eq', value: true, points: 10, label: 'Collectivo member' },
{ id: '5', field: 'multi_line', operator: 'gte', value: 3, points: 10, label: 'Multi-line customer' },
{ id: '6', field: 'tenure_years', operator: 'gte', value: 5, points: 10, label: 'Long-term customer' },
{ id: '7', field: 'has_private_policies', operator: 'eq', value: true, points: 5, label: 'Private policies' },
])
function getScoreForCustomer(input: {
totalPremium: number
policyCount: number
lineCount: number
tenureYears: number
isCollectivoMember: boolean
hasPrivatePolicies: boolean
estimatedCommission: number
}): number {
let score = 0
for (const rule of rules.value) {
let matches = false
if (rule.field === 'premium' && typeof rule.value === 'number') {
if (rule.operator === 'gte' && input.totalPremium >= rule.value) matches = true
else if (rule.operator === 'gt' && input.totalPremium > rule.value) matches = true
else if (rule.operator === 'lte' && input.totalPremium <= rule.value) matches = true
else if (rule.operator === 'lt' && input.totalPremium < rule.value) matches = true
else if (rule.operator === 'eq' && input.totalPremium === rule.value) matches = true
} else if (rule.field === 'policy_count' && typeof rule.value === 'number') {
if (rule.operator === 'gte' && input.policyCount >= rule.value) matches = true
else if (rule.operator === 'gt' && input.policyCount > rule.value) matches = true
else if (rule.operator === 'lte' && input.policyCount <= rule.value) matches = true
else if (rule.operator === 'lt' && input.policyCount < rule.value) matches = true
else if (rule.operator === 'eq' && input.policyCount === rule.value) matches = true
} else if (rule.field === 'commission' && typeof rule.value === 'number') {
if (rule.operator === 'gte' && input.estimatedCommission >= rule.value) matches = true
else if (rule.operator === 'gt' && input.estimatedCommission > rule.value) matches = true
else if (rule.operator === 'lte' && input.estimatedCommission <= rule.value) matches = true
else if (rule.operator === 'lt' && input.estimatedCommission < rule.value) matches = true
else if (rule.operator === 'eq' && input.estimatedCommission === rule.value) matches = true
} else if (rule.field === 'collectivo_member' && typeof rule.value === 'boolean') {
if (rule.operator === 'eq' && input.isCollectivoMember === rule.value) matches = true
} else if (rule.field === 'multi_line' && typeof rule.value === 'number') {
if (rule.operator === 'gte' && input.lineCount >= rule.value) matches = true
else if (rule.operator === 'gt' && input.lineCount > rule.value) matches = true
else if (rule.operator === 'lte' && input.lineCount <= rule.value) matches = true
else if (rule.operator === 'lt' && input.lineCount < rule.value) matches = true
else if (rule.operator === 'eq' && input.lineCount === rule.value) matches = true
} else if (rule.field === 'tenure_years' && typeof rule.value === 'number') {
if (rule.operator === 'gte' && input.tenureYears >= rule.value) matches = true
else if (rule.operator === 'gt' && input.tenureYears > rule.value) matches = true
else if (rule.operator === 'lte' && input.tenureYears <= rule.value) matches = true
else if (rule.operator === 'lt' && input.tenureYears < rule.value) matches = true
else if (rule.operator === 'eq' && input.tenureYears === rule.value) matches = true
} else if (rule.field === 'has_private_policies' && typeof rule.value === 'boolean') {
if (rule.operator === 'eq' && input.hasPrivatePolicies === rule.value) matches = true
}
if (matches) score += rule.points
}
return score
}
return {
id: c.id,
name: c.name,
totalPremium,
policyCount,
lineCount,
tenureYears,
score: getScoreForCustomer(input),
tier: getTierForCustomer(input),
}
}).sort((a, b) => b.score - a.score)
)
function getTierForCustomer(input: {
totalPremium: number
policyCount: number
lineCount: number
tenureYears: number
isCollectivoMember: boolean
hasPrivatePolicies: boolean
estimatedCommission: number
}): Tier {
const score = getScoreForCustomer(input)
for (const tier of tiers.value) {
if (score >= tier.minScore) return tier
}
return tiers.value[tiers.value.length - 1]
}
function fmtMoney(n: number) {
if (n >= 1_000_000) return `$${(n / 1_000_000).toFixed(1)}M`
if (n >= 1_000) return `$${(n / 1_000).toFixed(0)}K`
return `$${n.toLocaleString()}`
}
function formatRuleValue(rule: { field: string; value: number | boolean }): string {
if (typeof rule.value === 'boolean') return rule.value ? 'Yes' : 'No'
@@ -209,27 +311,16 @@ function formatRuleValue(rule: { field: string; value: number | boolean }): stri
</tr>
</thead>
<tbody>
<tr v-for="row in previewRows" :key="row.id">
<td class="ca-cell-name">{{ row.name }}</td>
<td>{{ row.totalPremium > 0 ? fmtMoney(row.totalPremium) : '—' }}</td>
<td>{{ row.policyCount }}</td>
<td>{{ row.lineCount }}</td>
<td>{{ row.tenureYears }}yr</td>
<td class="ca-cell-score">{{ row.score }}</td>
<td>
<span
class="ca-tier-badge"
:style="{ background: row.tier.color + '14', color: row.tier.color }"
>
{{ row.tier.name }}
</span>
<tr>
<td colspan="7" class="text-center py-8 text-[var(--text-muted)]">
No preview data available. Connect to API to see customer classification.
</td>
</tr>
</tbody>
</table>
</div>
<p class="text-[11px] text-[var(--text-muted)]">
Showing {{ previewRows.length }} mock customers. Scores computed from current rules.
Preview will show customer scores and tiers once API is connected.
</p>
</div>
</div>

View File

@@ -1,16 +1,27 @@
<script setup lang="ts">
import { refDebounced } from '~/utils/refDebounced'
import type { FormCatalogRow } from '~/types/form-catalog'
import { productLineLabel, useFormsCatalog } from '~/composables/useFormsCatalog'
usePageTitle('Forms library · Settings')
const { rows, version, insurerItems, subRamoItems } = useFormsCatalog()
interface FormCatalogRow {
id: string
description: string
insurerSlugs: string[]
subRamoLabel: string
subRamoKey: string
personKinds: 'natural' | 'juridica' | 'both'
productLine: string | null
fileLabel: string
fileUrl: string
badge: string | null
}
const rows = ref<FormCatalogRow[]>([])
const version = ref('1.0.0')
const insurerItems = ref<{ label: string; value: string }[]>([])
const subRamoItems = ref<{ label: string; value: string }[]>([])
const pageSize = ref(10)
const page = ref(1)
const search = ref('')
const debouncedSearch = refDebounced(search, 250)
const pageSizeItems = [
{ label: '10', value: 10 },
@@ -32,12 +43,12 @@ function rowSearchText(r: FormCatalogRow): string {
}
const filtered = computed(() => {
const q = debouncedSearch.value.trim().toLowerCase()
const q = search.value.trim().toLowerCase()
if (!q) return rows.value
return rows.value.filter((r) => rowSearchText(r).includes(q))
})
watch([debouncedSearch, pageSize], () => {
watch([search, pageSize], () => {
page.value = 1
})
@@ -73,6 +84,11 @@ function personLabel(pk: FormCatalogRow['personKinds']) {
return pk === 'natural' ? 'Natural' : 'Jurídica'
}
function productLineLabel(pl: string | null) {
if (!pl) return '—'
return pl
}
function exportCsv() {
const headers = [
'ID',

View File

@@ -1,8 +1,55 @@
<script setup lang="ts">
usePageTitle('Settings')
const { isSuperAdmin } = useSuperAdmin()
const sidebarFeatures = useSidebarFeatures()
const isSuperAdmin = computed(() => {
if (import.meta.client) {
const stored = localStorage.getItem('policy-ui.superadmin')
return stored !== '0'
}
return true
})
const sidebarFeatures = reactive({
showWorkstations: computed({
get: () => {
if (import.meta.client) {
return localStorage.getItem('policy-ui.sidebar.workstations') !== 'false'
}
return true
},
set: (value: boolean) => {
if (import.meta.client) {
localStorage.setItem('policy-ui.sidebar.workstations', String(value))
}
}
}),
showAiTools: computed({
get: () => {
if (import.meta.client) {
return localStorage.getItem('policy-ui.sidebar.ai-tools') !== 'false'
}
return true
},
set: (value: boolean) => {
if (import.meta.client) {
localStorage.setItem('policy-ui.sidebar.ai-tools', String(value))
}
}
}),
showLeadsHub: computed({
get: () => {
if (import.meta.client) {
return localStorage.getItem('policy-ui.sidebar.leads-hub') !== 'false'
}
return true
},
set: (value: boolean) => {
if (import.meta.client) {
localStorage.setItem('policy-ui.sidebar.leads-hub', String(value))
}
}
})
})
const cards = computed(() => {
const base: { to: string; title: string; description: string; icon: string }[] = []

View File

@@ -1,26 +1,60 @@
<script setup lang="ts">
import { MAX_LOGO_FILE_BYTES } from '~/types/branding'
import type { BrokerageBrandingState } from '~/types/branding'
definePageMeta({ ssr: false })
usePageTitle('Organization · Settings')
const { isSuperAdmin } = useSuperAdmin()
const toast = useToast()
const { saved, defaultBrokerageBranding } = useBrokerageBranding()
const draft = ref<BrokerageBrandingState>(defaultBrokerageBranding())
const MAX_LOGO_FILE_BYTES = 512 * 1024
interface BrokerageBrandingState {
companyName: string
logoDataUrl: string | null
logoFileName: string
reportPageHeader: string
reportPageFooter: string
}
const isSuperAdmin = computed(() => {
if (import.meta.client) {
const stored = localStorage.getItem('policy-ui.superadmin')
return stored !== '0'
}
return true
})
const STORAGE_KEY = 'policy-ui.branding'
function loadBranding(): BrokerageBrandingState {
if (import.meta.client) {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored) {
try {
return JSON.parse(stored)
} catch {
return defaultBranding()
}
}
}
return defaultBranding()
}
function defaultBranding(): BrokerageBrandingState {
return {
companyName: '',
logoDataUrl: null,
logoFileName: '',
reportPageHeader: '',
reportPageFooter: ''
}
}
const saved = ref<BrokerageBrandingState>(loadBranding())
const draft = ref<BrokerageBrandingState>({ ...saved.value })
const fileInputRef = ref<HTMLInputElement | null>(null)
function syncDraftFromSaved() {
const s = saved.value
draft.value = {
companyName: s.companyName,
logoDataUrl: s.logoDataUrl,
logoFileName: s.logoFileName,
reportPageHeader: s.reportPageHeader,
reportPageFooter: s.reportPageFooter
}
draft.value = { ...saved.value }
}
onMounted(() => syncDraftFromSaved())
@@ -65,6 +99,9 @@ function clearLogo() {
function save() {
saved.value = { ...draft.value }
if (import.meta.client) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(saved.value))
}
toast.add({
title: 'Organization saved',
description: 'Sidebar and home use this brokerage identity for all users.',

View File

@@ -1,14 +1,16 @@
<script setup lang="ts">
import { refDebounced } from '~/utils/refDebounced'
import { ROLES_SEGUROS_SEED, SEGUROS_PERMISSION_COLUMNS } from '~/data/roles-seguros'
import type { RoleRow } from '~/types/roles'
usePageTitle('Permissions · Settings')
interface RoleRow {
id: string
description: string
active: boolean
seguros: Record<string, boolean>
}
const pageSize = ref(10)
const page = ref(1)
const search = ref('')
const debouncedSearch = refDebounced(search, 250)
const pageSizeItems = [
{ label: '10', value: 10 },
@@ -16,17 +18,27 @@ const pageSizeItems = [
{ label: '50', value: 50 }
]
const rows = ref<RoleRow[]>([...ROLES_SEGUROS_SEED])
const SEGUROS_PERMISSION_COLUMNS = [
{ key: 'quotes', label: 'Quotes', icon: 'i-heroicons-document-text' },
{ key: 'policies', label: 'Policies', icon: 'i-heroicons-shield-check' },
{ key: 'claims', label: 'Claims', icon: 'i-heroicons-exclamation-triangle' },
{ key: 'billing', label: 'Billing', icon: 'i-heroicons-credit-card' },
{ key: 'customers', label: 'Customers', icon: 'i-heroicons-users' },
{ key: 'providers', label: 'Providers', icon: 'i-heroicons-building-office-2' },
{ key: 'reports', label: 'Reports', icon: 'i-heroicons-chart-bar' },
]
const rows = ref<RoleRow[]>([])
const filtered = computed(() => {
const q = debouncedSearch.value.trim().toLowerCase()
const q = search.value.trim().toLowerCase()
if (!q) return rows.value
return rows.value.filter(
(r) => String(r.id).includes(q) || r.description.toLowerCase().includes(q)
)
})
watch([debouncedSearch, pageSize], () => {
watch([search, pageSize], () => {
page.value = 1
})

View File

@@ -1,35 +1,143 @@
<script setup lang="ts">
import { useProfileLayouts } from '~/composables/useProfileLayouts'
import type { ProfileLayout } from '~/composables/useProfileLayouts'
definePageMeta({ ssr: false })
usePageTitle('Profile Layouts · Settings')
const {
layouts,
activeLayoutId,
activeLayout,
sortedSections,
setActiveLayout,
updateLayout,
removeCustomLayout,
resetToDefaults,
} = useProfileLayouts()
interface ProfileSection {
id: string
label: string
visible: boolean
order: number
}
interface ProfileLayout {
id: string
name: string
description: string
icon: string
isCustom: boolean
defaultTab: string
sections: ProfileSection[]
}
const STORAGE_KEY = 'policy-ui.profile-layouts'
function loadLayouts(): ProfileLayout[] {
if (import.meta.client) {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored) {
try {
return JSON.parse(stored)
} catch {
return defaultLayouts()
}
}
}
return defaultLayouts()
}
function defaultLayouts(): ProfileLayout[] {
return [
{
id: 'agent',
name: 'Agent',
description: 'For producers and account executives',
icon: 'i-heroicons-user',
isCustom: false,
defaultTab: 'policies',
sections: [
{ id: 'overview', label: 'Overview', visible: true, order: 1 },
{ id: 'policies', label: 'Policies', visible: true, order: 2 },
{ id: 'quotes', label: 'Quotes', visible: true, order: 3 },
{ id: 'claims', label: 'Claims', visible: true, order: 4 },
{ id: 'billing', label: 'Billing', visible: true, order: 5 },
{ id: 'documents', label: 'Documents', visible: true, order: 6 },
]
},
{
id: 'manager',
name: 'Manager',
description: 'For team leads and supervisors',
icon: 'i-heroicons-users',
isCustom: false,
defaultTab: 'team',
sections: [
{ id: 'overview', label: 'Overview', visible: true, order: 1 },
{ id: 'team', label: 'Team', visible: true, order: 2 },
{ id: 'performance', label: 'Performance', visible: true, order: 3 },
{ id: 'policies', label: 'Policies', visible: true, order: 4 },
{ id: 'reports', label: 'Reports', visible: true, order: 5 },
]
},
{
id: 'admin',
name: 'Admin',
description: 'For administrators and operations',
icon: 'i-heroicons-shield-check',
isCustom: false,
defaultTab: 'overview',
sections: [
{ id: 'overview', label: 'Overview', visible: true, order: 1 },
{ id: 'users', label: 'Users', visible: true, order: 2 },
{ id: 'settings', label: 'Settings', visible: true, order: 3 },
{ id: 'audit', label: 'Audit Log', visible: true, order: 4 },
{ id: 'integrations', label: 'Integrations', visible: true, order: 5 },
]
}
]
}
const layouts = ref<ProfileLayout[]>(loadLayouts())
const activeLayoutId = ref('agent')
function saveLayouts() {
if (import.meta.client) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(layouts.value))
}
}
const activeLayout = computed(() => layouts.value.find(l => l.id === activeLayoutId.value) || layouts.value[0])
const sortedSections = computed(() => {
if (!activeLayout.value) return []
return [...activeLayout.value.sections].sort((a, b) => a.order - b.order)
})
function setActiveLayout(id: string) {
activeLayoutId.value = id
}
function updateLayout(id: string, updates: Partial<ProfileLayout>) {
const idx = layouts.value.findIndex(l => l.id === id)
if (idx !== -1) {
layouts.value[idx] = { ...layouts.value[idx], ...updates }
saveLayouts()
}
}
function removeCustomLayout(id: string) {
layouts.value = layouts.value.filter(l => l.id !== id)
saveLayouts()
}
function resetToDefaults() {
layouts.value = defaultLayouts()
activeLayoutId.value = 'agent'
saveLayouts()
}
/* ── Built-in vs custom ── */
const builtInLayouts = computed(() => layouts.value.filter(l => !l.isCustom))
const customLayouts = computed(() => layouts.value.filter(l => l.isCustom))
/* ── Toggle section visibility ── */
function toggleSectionVisibility(sectionId: string) {
if (!activeLayout.value) return
const updated = activeLayout.value.sections.map(s =>
s.id === sectionId ? { ...s, visible: !s.visible } : s
)
updateLayout(activeLayout.value.id, { sections: updated })
}
/* ── Move section up/down ── */
function moveSection(sectionId: string, direction: 'up' | 'down') {
if (!activeLayout.value) return
const sections = [...activeLayout.value.sections].sort((a, b) => a.order - b.order)
const idx = sections.findIndex(s => s.id === sectionId)
if (idx < 0) return
@@ -43,12 +151,11 @@ function moveSection(sectionId: string, direction: 'up' | 'down') {
updateLayout(activeLayout.value.id, { sections })
}
/* ── Ordered sections for display ── */
const orderedSections = computed(() =>
[...activeLayout.value.sections].sort((a, b) => a.order - b.order)
)
const orderedSections = computed(() => {
if (!activeLayout.value) return []
return [...activeLayout.value.sections].sort((a, b) => a.order - b.order)
})
/* ── Delete custom layout ── */
function handleDelete(id: string) {
removeCustomLayout(id)
}

View File

@@ -1,7 +1,26 @@
<script setup lang="ts">
usePageTitle('Quote requests · Settings')
const { quoteRequestEmailEnabled, setQuoteRequestEmailEnabled } = useQuoteRequestEmailEnabled()
const STORAGE_KEY = 'policy-ui.quote-request-email-enabled'
const quoteRequestEmailEnabled = computed({
get: () => {
if (import.meta.client) {
const stored = localStorage.getItem(STORAGE_KEY)
return stored !== 'false'
}
return true
},
set: (value: boolean) => {
if (import.meta.client) {
localStorage.setItem(STORAGE_KEY, String(value))
}
}
})
function setQuoteRequestEmailEnabled(value: boolean) {
quoteRequestEmailEnabled.value = value
}
</script>
<template>

View File

@@ -1,12 +1,62 @@
<script setup lang="ts">
import { useReferralChannels, type ReferralChannel } from '~/composables/useReferralChannels'
usePageTitle('Referral Channels · Settings')
const { channels, addChannel, updateChannel, removeChannel } = useReferralChannels()
interface ReferralChannel {
id: string
name: string
type: 'person' | 'company' | 'digital' | 'event' | 'other'
contactName: string
contactPhone: string
contactEmail: string
note: string
active: boolean
}
const STORAGE_KEY = 'policy-ui.referral-channels'
function loadChannels(): ReferralChannel[] {
if (import.meta.client) {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored) {
try {
return JSON.parse(stored)
} catch {
return []
}
}
}
return []
}
const channels = ref<ReferralChannel[]>(loadChannels())
function saveChannels() {
if (import.meta.client) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(channels.value))
}
}
function addChannel(channel: Omit<ReferralChannel, 'id'>) {
const id = String(Date.now())
channels.value.push({ ...channel, id })
saveChannels()
}
function updateChannel(id: string, updates: Partial<ReferralChannel>) {
const idx = channels.value.findIndex(c => c.id === id)
if (idx !== -1) {
channels.value[idx] = { ...channels.value[idx], ...updates }
saveChannels()
}
}
function removeChannel(id: string) {
channels.value = channels.value.filter(c => c.id !== id)
saveChannels()
}
const toast = useToast()
/* ── Form state ── */
const formOpen = ref(false)
const editingId = ref<string | null>(null)
@@ -88,7 +138,6 @@ function toggleActive(id: string) {
if (ch) updateChannel(id, { active: !ch.active })
}
/* ── Filter ── */
type ListFilter = 'all' | 'active' | 'inactive'
const activeFilter = ref<ListFilter>('all')
@@ -104,7 +153,6 @@ const filterCounts = computed(() => ({
inactive: channels.value.filter(c => !c.active).length,
}))
/* ── Helpers ── */
const typeMeta: Record<string, { label: string; icon: string; class: string }> = {
person: { label: 'Person', icon: 'i-heroicons-user', class: 'rc-type-person' },
company: { label: 'Company', icon: 'i-heroicons-building-office', class: 'rc-type-company' },

View File

@@ -1,9 +1,108 @@
<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 RoutingTier = 'tier1_auto' | 'tier2_rule' | 'tier3_open'
type RoutedQueue = 'collections' | 'claims' | 'sales' | 'renewals' | 'operations' | 'open_pool'
interface RoutingRule {
id: string
name: string
condition: string
tier: RoutingTier
targetQueue: RoutedQueue
enabled: boolean
}
interface SupportState {
routingRules: RoutingRule[]
}
const STORAGE_KEY = 'policy-ui.support-routing'
function loadState(): SupportState {
if (import.meta.client) {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored) {
try {
return JSON.parse(stored)
} catch {
return defaultState()
}
}
}
return defaultState()
}
function defaultState(): SupportState {
return {
routingRules: [
{
id: '1',
name: 'Existing customer with LOB',
condition: 'Customer exists and has LOB in message',
tier: 'tier1_auto',
targetQueue: 'operations',
enabled: true
},
{
id: '2',
name: 'Web form submission',
condition: 'Form submission with customer data',
tier: 'tier1_auto',
targetQueue: 'sales',
enabled: true
},
{
id: '3',
name: 'Payment inquiry',
condition: 'Keywords: pago, factura, cobro, payment',
tier: 'tier2_rule',
targetQueue: 'collections',
enabled: true
},
{
id: '4',
name: 'Claim inquiry',
condition: 'Keywords: siniestro, reclamo, claim, accident',
tier: 'tier2_rule',
targetQueue: 'claims',
enabled: true
},
{
id: '5',
name: 'Renewal inquiry',
condition: 'Keywords: renovación, renewal, vence',
tier: 'tier2_rule',
targetQueue: 'renewals',
enabled: true
},
]
}
}
const state = ref<SupportState>(loadState())
function saveState() {
if (import.meta.client) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state.value))
}
}
function toggleRule(id: string) {
const rule = state.value.routingRules.find(r => r.id === id)
if (rule) {
rule.enabled = !rule.enabled
saveState()
}
}
function updateRule(id: string, updates: Partial<RoutingRule>) {
const idx = state.value.routingRules.findIndex(r => r.id === id)
if (idx !== -1) {
state.value.routingRules[idx] = { ...state.value.routingRules[idx], ...updates }
saveState()
}
}
type TierTab = 'tier1' | 'tier2' | 'tier3'
const activeTier = ref<TierTab>('tier1')
@@ -26,12 +125,21 @@ const queueOptions: { label: string; value: RoutedQueue }[] = [
{ label: 'Pool Abierto', value: 'open_pool' },
]
// Tier 3 settings (mock)
const QUEUE_LABELS: Record<RoutedQueue, string> = {
collections: 'Cobros',
claims: 'Siniestros',
sales: 'Ventas',
renewals: 'Renovaciones',
operations: 'Operaciones',
open_pool: 'Pool Abierto',
}
const tier3DefaultAssignee = ref('Round-robin')
const tier3EscalationHours = ref(4)
const toast = useToast()
function handleSave() {
saveState()
toast.add({ title: 'Configuración guardada', color: 'green' })
}
</script>