509 lines
19 KiB
Vue
509 lines
19 KiB
Vue
<script setup lang="ts">
|
|
const route = useRoute()
|
|
|
|
const STORAGE_KEY_BRANDING = 'policy-ui.branding'
|
|
const STORAGE_KEY_SUPERADMIN = 'policy-ui.superadmin'
|
|
const STORAGE_KEY_SIDEBAR_WORKSTATIONS = 'policy-ui.sidebar.workstations'
|
|
const STORAGE_KEY_SIDEBAR_AI_TOOLS = 'policy-ui.sidebar.ai-tools'
|
|
const STORAGE_KEY_SIDEBAR_LEADS_HUB = 'policy-ui.sidebar.leads-hub'
|
|
const STORAGE_KEY_SIDEBAR_COLLAPSED = 'policy-ui.sidebar.collapsed'
|
|
|
|
interface BrokerageBrandingState {
|
|
companyName: string
|
|
logoDataUrl: string | null
|
|
logoFileName: string
|
|
reportPageHeader: string
|
|
reportPageFooter: string
|
|
}
|
|
|
|
function loadBranding(): BrokerageBrandingState {
|
|
if (import.meta.client) {
|
|
const stored = localStorage.getItem(STORAGE_KEY_BRANDING)
|
|
if (stored) {
|
|
try {
|
|
return JSON.parse(stored)
|
|
} catch {
|
|
return defaultBranding()
|
|
}
|
|
}
|
|
}
|
|
return defaultBranding()
|
|
}
|
|
|
|
function defaultBranding(): BrokerageBrandingState {
|
|
return {
|
|
companyName: '',
|
|
logoDataUrl: null,
|
|
logoFileName: '',
|
|
reportPageHeader: '',
|
|
reportPageFooter: ''
|
|
}
|
|
}
|
|
|
|
const branding = ref<BrokerageBrandingState>(loadBranding())
|
|
const sidebarTitle = computed(() => branding.value.companyName || 'Segur-OS')
|
|
|
|
const isSuperAdmin = computed(() => {
|
|
if (import.meta.client) {
|
|
const stored = localStorage.getItem(STORAGE_KEY_SUPERADMIN)
|
|
return stored !== '0'
|
|
}
|
|
return true
|
|
})
|
|
|
|
const sidebarCollapsed = computed({
|
|
get: () => {
|
|
if (import.meta.client) {
|
|
return localStorage.getItem(STORAGE_KEY_SIDEBAR_COLLAPSED) === 'true'
|
|
}
|
|
return false
|
|
},
|
|
set: (value: boolean) => {
|
|
if (import.meta.client) {
|
|
localStorage.setItem(STORAGE_KEY_SIDEBAR_COLLAPSED, String(value))
|
|
}
|
|
}
|
|
})
|
|
|
|
function toggleSidebar() {
|
|
sidebarCollapsed.value = !sidebarCollapsed.value
|
|
}
|
|
|
|
const sidebarFeatures = reactive({
|
|
showWorkstations: computed({
|
|
get: () => {
|
|
if (import.meta.client) {
|
|
return localStorage.getItem(STORAGE_KEY_SIDEBAR_WORKSTATIONS) !== 'false'
|
|
}
|
|
return true
|
|
},
|
|
set: (value: boolean) => {
|
|
if (import.meta.client) {
|
|
localStorage.setItem(STORAGE_KEY_SIDEBAR_WORKSTATIONS, String(value))
|
|
}
|
|
}
|
|
}),
|
|
showAiTools: computed({
|
|
get: () => {
|
|
if (import.meta.client) {
|
|
return localStorage.getItem(STORAGE_KEY_SIDEBAR_AI_TOOLS) !== 'false'
|
|
}
|
|
return true
|
|
},
|
|
set: (value: boolean) => {
|
|
if (import.meta.client) {
|
|
localStorage.setItem(STORAGE_KEY_SIDEBAR_AI_TOOLS, String(value))
|
|
}
|
|
}
|
|
}),
|
|
showLeadsHub: computed({
|
|
get: () => {
|
|
if (import.meta.client) {
|
|
return localStorage.getItem(STORAGE_KEY_SIDEBAR_LEADS_HUB) !== 'false'
|
|
}
|
|
return true
|
|
},
|
|
set: (value: boolean) => {
|
|
if (import.meta.client) {
|
|
localStorage.setItem(STORAGE_KEY_SIDEBAR_LEADS_HUB, String(value))
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
const openGroups = ref({
|
|
quotes: false,
|
|
sales: false,
|
|
cartera: false,
|
|
customerService: false,
|
|
workstation: false,
|
|
aiTools: false,
|
|
backOffice: false
|
|
})
|
|
|
|
watch(() => route.path, (p) => {
|
|
if (p.startsWith('/quotes') && p !== '/quotes/new' && p !== '/quotes/compare') openGroups.value.quotes = true
|
|
if (p.startsWith('/onboarding') || (p.startsWith('/sales') && !p.startsWith('/sales/leads')) || p === '/quotes/new' || p === '/quotes/compare' || p.startsWith('/registration')) openGroups.value.sales = true
|
|
if (p.startsWith('/customers') || p.startsWith('/policies') || p.startsWith('/cartera')) openGroups.value.cartera = true
|
|
if (p.startsWith('/support') || p.startsWith('/claims') || p.startsWith('/collections') || p.startsWith('/renewals') || p.startsWith('/sales/leads')) openGroups.value.customerService = true
|
|
if (p.startsWith('/workstation')) openGroups.value.workstation = true
|
|
if (p.startsWith('/ai-tools')) openGroups.value.aiTools = true
|
|
if (p.startsWith('/back-office')) openGroups.value.backOffice = true
|
|
}, { immediate: true })
|
|
|
|
function toggleGroup(key: string) {
|
|
openGroups.value[key] = !openGroups.value[key]
|
|
}
|
|
|
|
function isActive(path: string, exact = false) {
|
|
const p = route.path
|
|
return exact ? p === path : p === path || p.startsWith(`${path}/`)
|
|
}
|
|
|
|
function linkClass(path: string, exact = false) {
|
|
const active = isActive(path, exact)
|
|
return [
|
|
'app-sidebar-link sidebar-parent-link flex w-full items-center',
|
|
active
|
|
? 'app-sidebar-link-active'
|
|
: 'sidebar-link-inactive'
|
|
]
|
|
}
|
|
|
|
function subLinkClass(path: string, exact = false) {
|
|
const active = isActive(path, exact)
|
|
return [
|
|
'app-sidebar-link sidebar-child-link flex w-full items-center',
|
|
active
|
|
? 'app-sidebar-child-active'
|
|
: 'sidebar-link-inactive'
|
|
]
|
|
}
|
|
|
|
const isSettingsRoute = computed(() => route.path.startsWith('/settings'))
|
|
|
|
function groupBtnClass(key: string) {
|
|
const hasActive =
|
|
(key === 'quotes' && ((route.path.startsWith('/quotes') && route.path !== '/quotes/new' && route.path !== '/quotes/compare'))) ||
|
|
(key === 'sales' && (route.path.startsWith('/onboarding') || (route.path.startsWith('/sales') && !route.path.startsWith('/sales/leads')) || route.path === '/quotes/new' || route.path === '/quotes/compare' || route.path.startsWith('/registration'))) ||
|
|
(key === 'cartera' &&
|
|
(route.path.startsWith('/customers') ||
|
|
route.path.startsWith('/policies') ||
|
|
route.path.startsWith('/cartera'))) ||
|
|
(key === 'customerService' &&
|
|
(route.path.startsWith('/support') ||
|
|
route.path.startsWith('/claims') ||
|
|
route.path.startsWith('/collections') ||
|
|
route.path.startsWith('/renewals') ||
|
|
route.path.startsWith('/sales/leads'))) ||
|
|
(key === 'workstation' && route.path.startsWith('/workstation')) ||
|
|
(key === 'aiTools' && route.path.startsWith('/ai-tools')) ||
|
|
(key === 'backOffice' && route.path.startsWith('/back-office'))
|
|
return [
|
|
'app-sidebar-link sidebar-parent-link flex w-full items-center text-left',
|
|
hasActive
|
|
? 'sidebar-parent-active'
|
|
: 'sidebar-link-inactive'
|
|
]
|
|
}
|
|
|
|
async function onTopRefresh() {
|
|
try {
|
|
await refreshNuxtData()
|
|
} catch {
|
|
if (import.meta.client) window.location.reload()
|
|
}
|
|
}
|
|
|
|
function onKeydown(e: KeyboardEvent) {
|
|
if ((e.metaKey || e.ctrlKey) && e.key === 'b') {
|
|
e.preventDefault()
|
|
toggleSidebar()
|
|
}
|
|
}
|
|
onMounted(() => {
|
|
if (import.meta.client) window.addEventListener('keydown', onKeydown)
|
|
})
|
|
onUnmounted(() => {
|
|
if (import.meta.client) window.removeEventListener('keydown', onKeydown)
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="flex h-screen flex-col overflow-hidden" style="background: var(--page-bg); color: var(--text-primary);">
|
|
<LayoutAppTopBar
|
|
:sidebar-collapsed="sidebarCollapsed"
|
|
:brand-title="sidebarTitle"
|
|
@toggle-sidebar="toggleSidebar"
|
|
@refresh="onTopRefresh"
|
|
/>
|
|
|
|
<div class="flex min-h-0 flex-1">
|
|
<!-- Sidebar -->
|
|
<aside
|
|
class="app-sidebar hidden min-h-0 flex-col md:flex md:shrink-0"
|
|
:class="
|
|
sidebarCollapsed
|
|
? 'md:w-0 md:min-w-0 md:max-w-0 md:overflow-hidden md:opacity-0'
|
|
: 'sidebar-open'
|
|
"
|
|
:aria-hidden="sidebarCollapsed ? 'true' : 'false'"
|
|
>
|
|
<!-- Navigation -->
|
|
<nav class="flex flex-1 flex-col overflow-y-auto overflow-x-hidden" style="padding: 8px 12px 4px;">
|
|
<!-- Dashboard — standalone (shrink-0 prevents compression when groups expand) -->
|
|
<NuxtLink to="/" :class="[...linkClass('/', true), 'shrink-0']">
|
|
<UIcon name="i-heroicons-squares-2x2" class="sidebar-icon shrink-0" />
|
|
<span>My Dashboard</span>
|
|
</NuxtLink>
|
|
<NuxtLink to="/calendar" :class="[...linkClass('/calendar'), 'shrink-0']">
|
|
<UIcon name="i-heroicons-calendar-days" class="sidebar-icon shrink-0" />
|
|
<span>Calendar</span>
|
|
</NuxtLink>
|
|
|
|
<!-- ── BUSINESS section ── -->
|
|
<p class="app-sidebar-section-label">Sales</p>
|
|
|
|
<div class="flex flex-col">
|
|
<button type="button" :class="groupBtnClass('quotes')" @click="toggleGroup('quotes')">
|
|
<UIcon name="i-heroicons-calculator" class="sidebar-icon shrink-0" />
|
|
<span class="flex-1 text-left">Quotes</span>
|
|
<UIcon
|
|
:name="openGroups.quotes ? 'i-heroicons-chevron-down' : 'i-heroicons-chevron-right'"
|
|
class="sidebar-chevron shrink-0"
|
|
/>
|
|
</button>
|
|
<div v-if="openGroups.quotes" class="sidebar-children">
|
|
<NuxtLink to="/quotes/new?tab=car" :class="subLinkClass('/quotes/new', true)">Auto</NuxtLink>
|
|
<NuxtLink to="/quotes/new?tab=life" :class="subLinkClass('/quotes/new')">Life</NuxtLink>
|
|
<NuxtLink to="/quotes/new?tab=fire_structure" :class="subLinkClass('/quotes/new')">Fire Structure</NuxtLink>
|
|
<NuxtLink to="/quotes/new?tab=fire_contents" :class="subLinkClass('/quotes/new')">Fire Contents</NuxtLink>
|
|
</div>
|
|
|
|
<button type="button" :class="groupBtnClass('sales')" @click="toggleGroup('sales')">
|
|
<UIcon name="i-heroicons-funnel" class="sidebar-icon shrink-0" />
|
|
<span class="flex-1 text-left">Sales</span>
|
|
<UIcon
|
|
:name="openGroups.sales ? 'i-heroicons-chevron-down' : 'i-heroicons-chevron-right'"
|
|
class="sidebar-chevron shrink-0"
|
|
/>
|
|
</button>
|
|
<div v-if="openGroups.sales" class="sidebar-children">
|
|
<NuxtLink to="/onboarding" :class="subLinkClass('/onboarding', true)">Sales Pipeline</NuxtLink>
|
|
<NuxtLink to="/sales/quick-lead" :class="subLinkClass('/sales/quick-lead')">Quick Lead</NuxtLink>
|
|
<NuxtLink to="/customers/new" :class="subLinkClass('/customers/new', true)">New Customer</NuxtLink>
|
|
<NuxtLink to="/quotes/new" :class="subLinkClass('/quotes/new', true)">New Quote</NuxtLink>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── OPERATIONS section ── -->
|
|
<p class="app-sidebar-section-label">Operations</p>
|
|
|
|
<div class="flex flex-col">
|
|
<button type="button" :class="groupBtnClass('cartera')" @click="toggleGroup('cartera')">
|
|
<UIcon name="i-heroicons-briefcase" class="sidebar-icon shrink-0" />
|
|
<span class="flex-1 text-left">Cartera</span>
|
|
<UIcon
|
|
:name="openGroups.cartera ? 'i-heroicons-chevron-down' : 'i-heroicons-chevron-right'"
|
|
class="sidebar-chevron shrink-0"
|
|
/>
|
|
</button>
|
|
<div v-if="openGroups.cartera" class="sidebar-children">
|
|
<NuxtLink to="/customers" :class="subLinkClass('/customers', true)">Customers</NuxtLink>
|
|
<NuxtLink to="/policies" :class="subLinkClass('/policies', true)">Policies</NuxtLink>
|
|
<NuxtLink to="/policies/groups" :class="subLinkClass('/policies/groups')">Collectivos</NuxtLink>
|
|
</div>
|
|
|
|
<button type="button" :class="groupBtnClass('customerService')" @click="toggleGroup('customerService')">
|
|
<UIcon name="i-heroicons-lifebuoy" class="sidebar-icon shrink-0" />
|
|
<span class="flex-1 text-left">Customer Service</span>
|
|
<UIcon
|
|
:name="openGroups.customerService ? 'i-heroicons-chevron-down' : 'i-heroicons-chevron-right'"
|
|
class="sidebar-chevron shrink-0"
|
|
/>
|
|
</button>
|
|
<div v-if="openGroups.customerService" class="sidebar-children">
|
|
<NuxtLink v-if="sidebarFeatures.showLeadsHub" to="/sales/leads" :class="subLinkClass('/sales/leads')">Incoming Leads</NuxtLink>
|
|
<NuxtLink to="/support" :class="subLinkClass('/support', true)">Incoming Support</NuxtLink>
|
|
<NuxtLink to="/claims" :class="subLinkClass('/claims')">Claims</NuxtLink>
|
|
<NuxtLink to="/collections" :class="subLinkClass('/collections')">Collections</NuxtLink>
|
|
<NuxtLink to="/renewals" :class="subLinkClass('/renewals')">Renewals</NuxtLink>
|
|
<NuxtLink to="/support/collectivos" :class="subLinkClass('/support/collectivos')">Collectivos</NuxtLink>
|
|
</div>
|
|
|
|
<NuxtLink to="/analysis" :class="linkClass('/analysis')">
|
|
<UIcon name="i-heroicons-chart-bar-square" class="sidebar-icon shrink-0" />
|
|
Reports & Analysis
|
|
</NuxtLink>
|
|
</div>
|
|
|
|
<!-- ── BACK OFFICE section ── -->
|
|
<p class="app-sidebar-section-label">Back Office</p>
|
|
|
|
<div class="flex flex-col">
|
|
<button type="button" :class="groupBtnClass('backOffice')" @click="toggleGroup('backOffice')">
|
|
<UIcon name="i-heroicons-inbox-stack" class="sidebar-icon shrink-0" />
|
|
<span class="flex-1 text-left">Back Office</span>
|
|
<UIcon
|
|
:name="openGroups.backOffice ? 'i-heroicons-chevron-down' : 'i-heroicons-chevron-right'"
|
|
class="sidebar-chevron shrink-0"
|
|
/>
|
|
</button>
|
|
<div v-if="openGroups.backOffice" class="sidebar-children">
|
|
<NuxtLink to="/back-office/workload" :class="subLinkClass('/back-office/workload', true)">Task List</NuxtLink>
|
|
<NuxtLink to="/back-office/workload/kanban" :class="subLinkClass('/back-office/workload/kanban')">Kanban Board</NuxtLink>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── WORKSTATION section ── -->
|
|
<template v-if="sidebarFeatures.showWorkstations || sidebarFeatures.showAiTools">
|
|
<p class="app-sidebar-section-label">Workstations</p>
|
|
|
|
<div class="flex flex-col">
|
|
<template v-if="sidebarFeatures.showWorkstations">
|
|
<button type="button" :class="groupBtnClass('workstation')" @click="toggleGroup('workstation')">
|
|
<UIcon name="i-heroicons-inbox-stack" class="sidebar-icon shrink-0" />
|
|
<span class="flex-1 text-left">Workstations</span>
|
|
<UIcon
|
|
:name="openGroups.workstation ? 'i-heroicons-chevron-down' : 'i-heroicons-chevron-right'"
|
|
class="sidebar-chevron shrink-0"
|
|
/>
|
|
</button>
|
|
<div v-if="openGroups.workstation" class="sidebar-children">
|
|
<NuxtLink to="/workstation/collectivos" :class="subLinkClass('/workstation/collectivos')">Collectivos</NuxtLink>
|
|
<NuxtLink to="/workstation/collections" :class="subLinkClass('/workstation/collections')">Collections</NuxtLink>
|
|
<NuxtLink to="/workstation/claims" :class="subLinkClass('/workstation/claims')">Claims</NuxtLink>
|
|
<NuxtLink to="/workstation/renewals" :class="subLinkClass('/workstation/renewals')">Renewals</NuxtLink>
|
|
<NuxtLink to="/workstation/customer-service" :class="subLinkClass('/workstation/customer-service')">Customer Service</NuxtLink>
|
|
<NuxtLink to="/workstation/facturacion" :class="subLinkClass('/workstation/facturacion')">Facturación y Comisiones</NuxtLink>
|
|
</div>
|
|
</template>
|
|
|
|
<template v-if="sidebarFeatures.showAiTools">
|
|
<button type="button" :class="groupBtnClass('aiTools')" @click="toggleGroup('aiTools')">
|
|
<UIcon name="i-heroicons-sparkles" class="sidebar-icon shrink-0" />
|
|
<span class="flex-1 text-left">AI Tools</span>
|
|
<UIcon
|
|
:name="openGroups.aiTools ? 'i-heroicons-chevron-down' : 'i-heroicons-chevron-right'"
|
|
class="sidebar-chevron shrink-0"
|
|
/>
|
|
</button>
|
|
<div v-if="openGroups.aiTools" class="sidebar-children">
|
|
<NuxtLink to="/ai-tools/sales-factory" :class="subLinkClass('/ai-tools/sales-factory')">Sales Factory</NuxtLink>
|
|
<NuxtLink to="/ai-tools/policy-comparator" :class="subLinkClass('/ai-tools/policy-comparator')">Policy Comparator</NuxtLink>
|
|
<NuxtLink to="/ai-tools/email-writer" :class="subLinkClass('/ai-tools/email-writer')">Email Writer</NuxtLink>
|
|
<NuxtLink to="/ai-tools/case-assistant" :class="subLinkClass('/ai-tools/case-assistant')">Case Assistant</NuxtLink>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- ── Footer zone ── -->
|
|
<div class="mt-auto" style="padding-top: 8px;">
|
|
<div class="sidebar-footer-divider" />
|
|
<NuxtLink to="/settings" :class="linkClass('/settings')" class="sidebar-footer-link">
|
|
<UIcon name="i-heroicons-cog-6-tooth" class="sidebar-icon shrink-0" style="width: 16px; height: 16px;" />
|
|
<span>Settings</span>
|
|
</NuxtLink>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Collapse sidebar — pinned outside scrollable nav -->
|
|
<div style="padding: 4px 12px 8px;">
|
|
<button
|
|
type="button"
|
|
class="app-sidebar-link sidebar-footer-link flex w-full items-center sidebar-link-inactive"
|
|
title="Hide sidebar (Ctrl+B)"
|
|
@click="toggleSidebar"
|
|
>
|
|
<UIcon name="i-heroicons-chevron-double-left" class="sidebar-icon shrink-0" style="width: 16px; height: 16px;" />
|
|
<span>Hide sidebar</span>
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Main content -->
|
|
<main
|
|
class="flex min-h-0 min-w-0 flex-1 flex-col"
|
|
:data-app-surface="isSettingsRoute ? 'settings' : undefined"
|
|
>
|
|
<div class="flex-1 overflow-y-auto flex flex-col" style="padding: 16px 24px 32px;">
|
|
<NuxtPage :key="route.fullPath" />
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* ── Sidebar shell ── */
|
|
.app-sidebar {
|
|
background: var(--sidebar-bg);
|
|
box-shadow: 1px 0 0 0 rgba(0, 0, 0, 0.04);
|
|
transition: width 200ms ease, opacity 200ms ease;
|
|
}
|
|
.sidebar-open {
|
|
width: 252px;
|
|
opacity: 1;
|
|
}
|
|
|
|
/* ── Parent link row: 40px height, 20px icon, 10px gap ── */
|
|
.sidebar-parent-link {
|
|
height: 40px;
|
|
min-height: 40px;
|
|
flex-shrink: 0;
|
|
padding: 0 8px;
|
|
gap: 10px;
|
|
font-size: 14px;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
/* ── Parent icon: 20px, outline variant at 0.5 opacity ── */
|
|
.sidebar-icon {
|
|
width: 20px;
|
|
height: 20px;
|
|
opacity: 0.5;
|
|
}
|
|
/* Active parent icon: slightly elevated but not full */
|
|
.app-sidebar-link-active .sidebar-icon {
|
|
opacity: 0.7;
|
|
}
|
|
|
|
/* ── Chevron: 14px ── */
|
|
.sidebar-chevron {
|
|
width: 14px;
|
|
height: 14px;
|
|
color: #c0c0bc;
|
|
transition: transform 150ms ease;
|
|
}
|
|
|
|
/* ── Inactive link: text-secondary, no color change on hover ── */
|
|
.sidebar-link-inactive {
|
|
color: #6b6b68;
|
|
}
|
|
|
|
/* ── Parent with active child: text-primary, font-medium ── */
|
|
.sidebar-parent-active {
|
|
font-weight: 500;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
/* ── Child items: text only, no icons, indented 36px ── */
|
|
.sidebar-children {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
padding-left: 38px;
|
|
padding-top: 4px;
|
|
padding-bottom: 4px;
|
|
}
|
|
|
|
/* ── Child link: 32px row, 13px font ── */
|
|
.sidebar-child-link {
|
|
height: 32px;
|
|
padding: 0 8px;
|
|
font-size: 13px;
|
|
color: #6b6b68;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
/* ── Footer divider ── */
|
|
.sidebar-footer-divider {
|
|
height: 1px;
|
|
background: rgba(0, 0, 0, 0.06);
|
|
margin: 0 16px 8px;
|
|
}
|
|
|
|
/* ── Footer links ── */
|
|
.sidebar-footer-link {
|
|
height: 36px;
|
|
padding: 0 8px;
|
|
gap: 10px;
|
|
font-size: 13px;
|
|
color: #6b6b68 !important;
|
|
}
|
|
.sidebar-footer-link .sidebar-icon {
|
|
opacity: 0.6;
|
|
}
|
|
</style>
|