351 lines
11 KiB
Vue
351 lines
11 KiB
Vue
<script setup lang="ts">
|
|
import { ref, onMounted } from 'vue'
|
|
import {
|
|
Sidebar,
|
|
SidebarContent,
|
|
SidebarFooter,
|
|
SidebarGroup,
|
|
SidebarGroupContent,
|
|
SidebarGroupLabel,
|
|
SidebarHeader,
|
|
SidebarMenu,
|
|
SidebarMenuButton,
|
|
SidebarMenuItem,
|
|
SidebarMenuSub,
|
|
SidebarMenuSubButton,
|
|
SidebarMenuSubItem,
|
|
SidebarRail,
|
|
} from '@/components/ui/sidebar'
|
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
|
|
import { LayoutGrid, Boxes, Settings, Home, ChevronRight, Database, Layers, LogOut, Users, Globe, Building, Phone } from 'lucide-vue-next'
|
|
import { useSoftphone } from '~/composables/useSoftphone'
|
|
|
|
const { logout } = useAuth()
|
|
const { api } = useApi()
|
|
const softphone = useSoftphone()
|
|
|
|
const handleLogout = async () => {
|
|
await logout()
|
|
}
|
|
|
|
// Check if user is central admin (by checking if we're on a central subdomain)
|
|
// Use ref instead of computed to avoid hydration mismatch
|
|
const isCentralAdmin = ref(false)
|
|
|
|
// Fetch objects and group by app
|
|
const apps = ref<any[]>([])
|
|
const topLevelObjects = ref<any[]>([])
|
|
const loading = ref(true)
|
|
|
|
onMounted(async () => {
|
|
// Set isCentralAdmin first
|
|
if (process.client) {
|
|
const hostname = window.location.hostname
|
|
const parts = hostname.split('.')
|
|
const subdomain = parts.length >= 2 ? parts[0] : null
|
|
const centralSubdomains = ['central', 'admin']
|
|
isCentralAdmin.value = subdomain ? centralSubdomains.includes(subdomain) : false
|
|
}
|
|
|
|
// Don't fetch tenant objects if we're on a central subdomain
|
|
if (isCentralAdmin.value) {
|
|
loading.value = false
|
|
return
|
|
}
|
|
|
|
try {
|
|
const response = await api.get('/setup/objects')
|
|
const allObjects = response.data || response || []
|
|
|
|
// Group objects by app
|
|
const appMap = new Map<string, any>()
|
|
const noAppObjects: any[] = []
|
|
|
|
allObjects.forEach((obj: any) => {
|
|
const appId = obj.app_id || obj.appId
|
|
if (appId) {
|
|
if (!appMap.has(appId)) {
|
|
appMap.set(appId, {
|
|
id: appId,
|
|
name: obj.app?.name || obj.app?.label || 'Unknown App',
|
|
label: obj.app?.label || obj.app?.name || 'Unknown App',
|
|
objects: []
|
|
})
|
|
}
|
|
appMap.get(appId)!.objects.push(obj)
|
|
} else {
|
|
noAppObjects.push(obj)
|
|
}
|
|
})
|
|
|
|
apps.value = Array.from(appMap.values())
|
|
topLevelObjects.value = noAppObjects
|
|
} catch (e) {
|
|
console.error('Failed to load objects:', e)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
})
|
|
|
|
const staticMenuItems = [
|
|
{
|
|
title: 'Home',
|
|
url: '/',
|
|
icon: Home,
|
|
},
|
|
{
|
|
title: 'Setup',
|
|
icon: Settings,
|
|
items: [
|
|
{
|
|
title: 'Apps',
|
|
url: '/setup/apps',
|
|
icon: LayoutGrid,
|
|
},
|
|
{
|
|
title: 'Objects',
|
|
url: '/setup/objects',
|
|
icon: Boxes,
|
|
},
|
|
{
|
|
title: 'Users',
|
|
url: '/setup/users',
|
|
icon: Users,
|
|
},
|
|
{
|
|
title: 'Roles',
|
|
url: '/setup/roles',
|
|
icon: Layers,
|
|
},
|
|
],
|
|
},
|
|
]
|
|
|
|
const centralAdminMenuItems: Array<{
|
|
title: string
|
|
icon: any
|
|
url?: string
|
|
items?: Array<{
|
|
title: string
|
|
url: string
|
|
icon: any
|
|
}>
|
|
}> = [
|
|
{
|
|
title: 'Central Admin',
|
|
icon: Settings,
|
|
items: [
|
|
{
|
|
title: 'Tenants',
|
|
url: '/central/tenants',
|
|
icon: Building,
|
|
},
|
|
{
|
|
title: 'Domains',
|
|
url: '/central/domains',
|
|
icon: Globe,
|
|
},
|
|
{
|
|
title: 'Admin Users',
|
|
url: '/central/users',
|
|
icon: Users,
|
|
},
|
|
],
|
|
},
|
|
]
|
|
</script>
|
|
|
|
<template>
|
|
<Sidebar collapsible="icon">
|
|
<SidebarHeader>
|
|
<SidebarMenu>
|
|
<SidebarMenuItem>
|
|
<SidebarMenuButton size="lg" as-child>
|
|
<NuxtLink to="/">
|
|
<div
|
|
class="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground"
|
|
>
|
|
<LayoutGrid class="size-4" />
|
|
</div>
|
|
<div class="flex flex-col gap-0.5 leading-none">
|
|
<span class="font-semibold">Neo Platform</span>
|
|
<span class="text-xs">Multi-tenant CRM</span>
|
|
</div>
|
|
</NuxtLink>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
</SidebarMenu>
|
|
</SidebarHeader>
|
|
<SidebarContent>
|
|
<!-- Static Menu Items -->
|
|
<SidebarGroup>
|
|
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
|
|
<SidebarGroupContent>
|
|
<SidebarMenu>
|
|
<template v-for="item in staticMenuItems" :key="item.title">
|
|
<!-- Simple menu item -->
|
|
<SidebarMenuItem v-if="!item.items">
|
|
<SidebarMenuButton as-child>
|
|
<NuxtLink :to="item.url">
|
|
<component :is="item.icon" />
|
|
<span>{{ item.title }}</span>
|
|
</NuxtLink>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
|
|
<!-- Collapsible menu item with submenu -->
|
|
<Collapsible v-else as-child :default-open="false" class="group/collapsible">
|
|
<SidebarMenuItem>
|
|
<CollapsibleTrigger as-child>
|
|
<SidebarMenuButton :tooltip="item.title">
|
|
<component :is="item.icon" />
|
|
<span>{{ item.title }}</span>
|
|
<ChevronRight
|
|
class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
|
|
/>
|
|
</SidebarMenuButton>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent>
|
|
<SidebarMenuSub>
|
|
<SidebarMenuSubItem v-for="subItem in item.items" :key="subItem.title">
|
|
<SidebarMenuSubButton as-child>
|
|
<NuxtLink :to="subItem.url">
|
|
<component :is="subItem.icon" />
|
|
<span>{{ subItem.title }}</span>
|
|
</NuxtLink>
|
|
</SidebarMenuSubButton>
|
|
</SidebarMenuSubItem>
|
|
</SidebarMenuSub>
|
|
</CollapsibleContent>
|
|
</SidebarMenuItem>
|
|
</Collapsible>
|
|
</template>
|
|
</SidebarMenu>
|
|
</SidebarGroupContent>
|
|
</SidebarGroup>
|
|
|
|
<!-- Central Admin Menu Items (only visible to central admins) -->
|
|
<SidebarGroup v-if="isCentralAdmin">
|
|
<SidebarGroupLabel>Central Administration</SidebarGroupLabel>
|
|
<SidebarGroupContent>
|
|
<SidebarMenu>
|
|
<template v-for="item in centralAdminMenuItems" :key="item.title">
|
|
<!-- Simple menu item -->
|
|
<SidebarMenuItem v-if="!item.items">
|
|
<SidebarMenuButton as-child>
|
|
<NuxtLink :to="item.url">
|
|
<component :is="item.icon" />
|
|
<span>{{ item.title }}</span>
|
|
</NuxtLink>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
|
|
<!-- Collapsible menu item with submenu -->
|
|
<Collapsible v-else-if="item.items" as-child :default-open="true" class="group/collapsible">
|
|
<SidebarMenuItem>
|
|
<CollapsibleTrigger as-child>
|
|
<SidebarMenuButton :tooltip="item.title">
|
|
<component :is="item.icon" />
|
|
<span>{{ item.title }}</span>
|
|
<ChevronRight
|
|
class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
|
|
/>
|
|
</SidebarMenuButton>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent>
|
|
<SidebarMenuSub>
|
|
<SidebarMenuSubItem v-for="subItem in item.items" :key="subItem.title">
|
|
<SidebarMenuSubButton as-child>
|
|
<NuxtLink :to="subItem.url">
|
|
<component v-if="subItem.icon" :is="subItem.icon" />
|
|
<span>{{ subItem.title }}</span>
|
|
</NuxtLink>
|
|
</SidebarMenuSubButton>
|
|
</SidebarMenuSubItem>
|
|
</SidebarMenuSub>
|
|
</CollapsibleContent>
|
|
</SidebarMenuItem>
|
|
</Collapsible>
|
|
</template>
|
|
</SidebarMenu>
|
|
</SidebarGroupContent>
|
|
</SidebarGroup>
|
|
|
|
<!-- Top-level Objects (no app) -->
|
|
<SidebarGroup v-if="!loading && topLevelObjects.length > 0">
|
|
<SidebarGroupLabel>Objects</SidebarGroupLabel>
|
|
<SidebarGroupContent>
|
|
<SidebarMenu>
|
|
<SidebarMenuItem v-for="obj in topLevelObjects" :key="obj.id">
|
|
<SidebarMenuButton as-child>
|
|
<NuxtLink :to="`/${obj.apiName.toLowerCase()}`">
|
|
<Database class="h-4 w-4" />
|
|
<span>{{ obj.label || obj.apiName }}</span>
|
|
</NuxtLink>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
</SidebarMenu>
|
|
</SidebarGroupContent>
|
|
</SidebarGroup>
|
|
|
|
<!-- App-grouped Objects -->
|
|
<SidebarGroup v-if="!loading && apps.length > 0">
|
|
<SidebarGroupLabel>Apps</SidebarGroupLabel>
|
|
<SidebarGroupContent>
|
|
<SidebarMenu>
|
|
<Collapsible
|
|
v-for="app in apps"
|
|
:key="app.id"
|
|
as-child
|
|
:default-open="true"
|
|
class="group/collapsible"
|
|
>
|
|
<SidebarMenuItem>
|
|
<CollapsibleTrigger as-child>
|
|
<SidebarMenuButton :tooltip="app.label">
|
|
<LayoutGrid class="h-4 w-4" />
|
|
<span>{{ app.label }}</span>
|
|
<ChevronRight
|
|
class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
|
|
/>
|
|
</SidebarMenuButton>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent>
|
|
<SidebarMenuSub>
|
|
<SidebarMenuSubItem v-for="obj in app.objects" :key="obj.id">
|
|
<SidebarMenuSubButton as-child>
|
|
<NuxtLink :to="`/${obj.apiName.toLowerCase()}`">
|
|
<Database class="h-4 w-4" />
|
|
<span>{{ obj.label || obj.apiName }}</span>
|
|
</NuxtLink>
|
|
</SidebarMenuSubButton>
|
|
</SidebarMenuSubItem>
|
|
</SidebarMenuSub>
|
|
</CollapsibleContent>
|
|
</SidebarMenuItem>
|
|
</Collapsible>
|
|
</SidebarMenu>
|
|
</SidebarGroupContent>
|
|
</SidebarGroup>
|
|
</SidebarContent>
|
|
<SidebarFooter>
|
|
<SidebarMenu>
|
|
<SidebarMenuItem v-if="!isCentralAdmin">
|
|
<SidebarMenuButton @click="softphone.open" class="cursor-pointer hover:bg-accent">
|
|
<Phone class="h-4 w-4" />
|
|
<span>Softphone</span>
|
|
<span v-if="softphone.hasIncomingCall.value" class="ml-auto h-2 w-2 rounded-full bg-red-500 animate-pulse"></span>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
<SidebarMenuItem>
|
|
<SidebarMenuButton @click="handleLogout" class="cursor-pointer hover:bg-accent">
|
|
<LogOut class="h-4 w-4" />
|
|
<span>Logout</span>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
</SidebarMenu>
|
|
</SidebarFooter>
|
|
<SidebarRail />
|
|
</Sidebar>
|
|
</template>
|