356 lines
11 KiB
Vue
356 lines
11 KiB
Vue
<script setup lang="ts">
|
|
import { computed, onMounted, type Component } from 'vue'
|
|
import { Layout, Menu, Button, Space, Typography, Select } from 'ant-design-vue'
|
|
import {
|
|
HomeOutlined,
|
|
InfoCircleOutlined,
|
|
RobotOutlined,
|
|
DashboardOutlined,
|
|
LoginOutlined,
|
|
UserAddOutlined,
|
|
BuildOutlined,
|
|
PayCircleOutlined,
|
|
} from '@ant-design/icons-vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { useUserStore } from './stores/userStore'
|
|
|
|
const router = useRouter()
|
|
const route = useRoute()
|
|
const userStore = useUserStore()
|
|
|
|
type NavItem = {
|
|
key: string
|
|
label: string
|
|
icon: Component
|
|
path?: string
|
|
manager?: boolean
|
|
children?: NavItem[]
|
|
}
|
|
|
|
const navItems: NavItem[] = [
|
|
{ key: '/', label: 'Home', icon: HomeOutlined, path: '/' },
|
|
{ key: '/about', label: 'About', icon: InfoCircleOutlined, path: '/about' },
|
|
{ key: '/pricing', label: 'Pricing', icon: PayCircleOutlined, path: '/pricing' },
|
|
{ key: '/agents', label: 'Agents', icon: RobotOutlined, path: '/agents', manager: true },
|
|
{ key: '/organization', label: 'Organizations', icon: BuildOutlined, path: '/organization' },
|
|
{ key: '/progress', label: 'Progress', icon: DashboardOutlined, path: '/progress' },
|
|
]
|
|
|
|
const visibleNavItems = computed<NavItem[]>(() =>
|
|
navItems.filter((item) => (item.manager ? userStore.user?.is_manager : true)),
|
|
)
|
|
|
|
const organizationCount = computed(() => userStore.userJoinedOrganizations?.length || 0)
|
|
const singleOrganization = computed(() =>
|
|
organizationCount.value === 1 ? userStore.userJoinedOrganizations[0] : null,
|
|
)
|
|
|
|
const selectedKeys = computed(() => {
|
|
for (const item of visibleNavItems.value) {
|
|
if (item.key === '/' && route.path === '/') return [item.key]
|
|
if (route.path.startsWith(item.key)) return [item.key]
|
|
if (item.children) {
|
|
const childMatch = item.children.find((c) => route.path.startsWith(c.key))
|
|
if (childMatch) return [item.key]
|
|
}
|
|
}
|
|
return []
|
|
})
|
|
|
|
type SimpleMenuInfo = { key: string | number | Array<string | number> }
|
|
|
|
const onSelect = (info: SimpleMenuInfo) => {
|
|
const key = String(info.key)
|
|
let found: NavItem | undefined
|
|
for (const item of visibleNavItems.value) {
|
|
if (item.key === key) {
|
|
found = item
|
|
break
|
|
}
|
|
if (item.children) {
|
|
const child = item.children.find((c) => c.key === key)
|
|
if (child) {
|
|
found = child
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if (found && found.path && route.path !== found.path) {
|
|
const selectedOrgUuid = userStore.userSelectedOrganization?.uuid
|
|
if (found.path === '/organization' && selectedOrgUuid) {
|
|
router.push(`/organization/${selectedOrgUuid}`)
|
|
} else {
|
|
router.push(found.path)
|
|
}
|
|
}
|
|
}
|
|
|
|
const handleLogout = async () => {
|
|
await userStore.logout()
|
|
router.push('/')
|
|
}
|
|
|
|
const handleOrganizationChange = (value: string) => {
|
|
const organization = userStore.userJoinedOrganizations.find((item) => item.uuid === value) || null
|
|
userStore.setSelectedOrganization(organization)
|
|
if (organization) {
|
|
router.push(`/organization/${organization.uuid}`)
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
userStore.fetchSession()
|
|
})
|
|
|
|
const user = userStore
|
|
</script>
|
|
|
|
<template>
|
|
<Layout class="shell">
|
|
<Layout.Header class="shell-header">
|
|
<div class="brand" @click="route.path !== '/' && router.push('/')">Dynavera</div>
|
|
<div style="margin-right: 1rem" v-if="user.isAuthenticated"></div>
|
|
<Menu
|
|
mode="horizontal"
|
|
theme="light"
|
|
:selectedKeys="selectedKeys"
|
|
class="shell-menu"
|
|
@select="onSelect"
|
|
>
|
|
<template v-for="item in visibleNavItems" :key="item.key">
|
|
<Menu.SubMenu v-if="item.children" :key="`${item.key}-submenu`">
|
|
<template #title>
|
|
<span
|
|
@click.stop="
|
|
item.path && route.path !== item.path && router.push(item.path)
|
|
"
|
|
>
|
|
<Space size="small">
|
|
<component :is="item.icon" />
|
|
<span>{{ item.label }}</span>
|
|
</Space>
|
|
</span>
|
|
</template>
|
|
<Menu.Item
|
|
v-for="child in item.children"
|
|
:key="child.key"
|
|
@click="
|
|
child.path && route.path !== child.path && router.push(child.path)
|
|
"
|
|
>
|
|
<Space size="small">
|
|
<component :is="child.icon" />
|
|
<span>{{ child.label }}</span>
|
|
</Space>
|
|
</Menu.Item>
|
|
</Menu.SubMenu>
|
|
<Menu.Item
|
|
v-else
|
|
:key="`${item.key}-item`"
|
|
@click="item.path && route.path !== item.path && router.push(item.path)"
|
|
>
|
|
<Space size="small">
|
|
<component :is="item.icon" />
|
|
<span>{{ item.label }}</span>
|
|
</Space>
|
|
</Menu.Item>
|
|
</template>
|
|
</Menu>
|
|
<Space>
|
|
<template v-if="user.isAuthenticated">
|
|
<Select
|
|
v-if="
|
|
user.userJoinedOrganizations && user.userJoinedOrganizations.length > 1
|
|
"
|
|
:value="user.userSelectedOrganization?.uuid ?? undefined"
|
|
@change="(val) => handleOrganizationChange(String(val))"
|
|
style="min-width: 220px; margin-right: 0.5rem"
|
|
placeholder="Select organization"
|
|
>
|
|
<Select.Option
|
|
v-for="o in user.userJoinedOrganizations"
|
|
:key="o.uuid"
|
|
:value="o.uuid"
|
|
>
|
|
{{ o.name }}
|
|
</Select.Option>
|
|
</Select>
|
|
|
|
<Typography.Text
|
|
v-else-if="singleOrganization"
|
|
class="org-chip"
|
|
strong
|
|
>
|
|
{{ singleOrganization.name }}
|
|
</Typography.Text>
|
|
|
|
<Typography.Text class="user-chip" strong>
|
|
{{ user.displayName || 'Account' }}
|
|
</Typography.Text>
|
|
<Button ghost :loading="user.loading" @click="handleLogout">Logout</Button>
|
|
</template>
|
|
<template v-else>
|
|
<Button ghost @click="router.push('/login')">
|
|
<LoginOutlined />
|
|
Login
|
|
</Button>
|
|
<Button type="primary" @click="router.push('/register')">
|
|
<UserAddOutlined />
|
|
Register
|
|
</Button>
|
|
</template>
|
|
</Space>
|
|
</Layout.Header>
|
|
|
|
<Layout class="shell-body">
|
|
<Layout.Content class="shell-content">
|
|
<router-view />
|
|
</Layout.Content>
|
|
<Layout.Footer class="shell-footer">
|
|
<Typography.Text type="secondary">
|
|
<strong>Project Disclaimer:</strong>
|
|
This is a proof-of-concept demo project for educational purposes. All
|
|
testimonials, statistics, and company names are fictional placeholders.
|
|
</Typography.Text>
|
|
</Layout.Footer>
|
|
</Layout>
|
|
</Layout>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.shell {
|
|
min-height: 100vh;
|
|
background: #f5f7fb;
|
|
}
|
|
.shell-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
padding: 0 1.25rem;
|
|
background: #ffffff;
|
|
border-bottom: 1px solid #dbe3ec;
|
|
}
|
|
.brand {
|
|
color: #1f2937;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
font-size: 1.05rem;
|
|
}
|
|
.shell-menu {
|
|
flex: 1;
|
|
background: transparent;
|
|
border-bottom: none;
|
|
}
|
|
.shell-body {
|
|
background: #f5f7fb;
|
|
min-height: calc(100vh - 64px);
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.shell-content {
|
|
padding: 24px;
|
|
flex: 1;
|
|
min-height: calc(100vh - 64px - 64px);
|
|
}
|
|
.shell-footer {
|
|
text-align: center;
|
|
background: #ffffff;
|
|
border-top: 1px solid #dbe3ec;
|
|
}
|
|
:deep(.ant-menu-dark) {
|
|
background: transparent;
|
|
}
|
|
:deep(.ant-menu-dark .ant-menu-item-selected) {
|
|
background: transparent !important;
|
|
}
|
|
:deep(.ant-typography),
|
|
:deep(.ant-typography p),
|
|
:deep(.ant-typography span),
|
|
:deep(.ant-list-item),
|
|
:deep(.ant-list-item-meta-title),
|
|
:deep(.ant-list-item-meta-description),
|
|
:deep(.ant-statistic-title),
|
|
:deep(.ant-statistic-content),
|
|
:deep(.ant-card-meta-title),
|
|
:deep(.ant-card-meta-description) {
|
|
color: #1f2937;
|
|
}
|
|
:deep(.ant-typography-secondary) {
|
|
color: #6b7280 !important;
|
|
}
|
|
:deep(.ant-form-item-label > label) {
|
|
color: #1f2937;
|
|
}
|
|
:deep(.ant-input),
|
|
:deep(.ant-select-selector),
|
|
:deep(.ant-select-selection-item),
|
|
:deep(.ant-picker-input input) {
|
|
background: #ffffff;
|
|
color: #1f2937;
|
|
border-color: #d0d8e2;
|
|
}
|
|
:deep(.ant-input::placeholder),
|
|
:deep(.ant-select-selection-placeholder),
|
|
:deep(.ant-picker-input input::placeholder) {
|
|
color: #6b7280;
|
|
}
|
|
:deep(.ant-card) {
|
|
background: #ffffff;
|
|
border-color: #dbe3ec;
|
|
}
|
|
:deep(.ant-btn:not(.ant-btn-primary)) {
|
|
color: #1f2937;
|
|
border-color: #d0d8e2;
|
|
background: #ffffff;
|
|
}
|
|
:deep(.ant-btn-primary) {
|
|
background: #2563eb;
|
|
border: none;
|
|
}
|
|
.user-chip {
|
|
color: #1f2937;
|
|
}
|
|
.org-chip {
|
|
color: #1f2937;
|
|
padding: 0 0.5rem;
|
|
}
|
|
:deep(.ant-typography-secondary) {
|
|
color: #6b7280 !important;
|
|
}
|
|
:deep(.ant-form-item-label > label) {
|
|
color: #1f2937;
|
|
}
|
|
:deep(.ant-input),
|
|
:deep(.ant-select-selector),
|
|
:deep(.ant-select-selection-item),
|
|
:deep(.ant-picker-input input) {
|
|
background: #ffffff;
|
|
color: #1f2937;
|
|
border-color: #d0d8e2;
|
|
}
|
|
:deep(.ant-input::placeholder),
|
|
:deep(.ant-select-selection-placeholder),
|
|
:deep(.ant-picker-input input::placeholder) {
|
|
color: #6b7280;
|
|
}
|
|
:deep(.ant-card) {
|
|
background: #ffffff;
|
|
border-color: #dbe3ec;
|
|
}
|
|
:deep(.ant-btn:not(.ant-btn-primary)) {
|
|
color: #1f2937;
|
|
border-color: #d0d8e2;
|
|
background: #ffffff;
|
|
}
|
|
:deep(.ant-btn-primary) {
|
|
background: #2563eb;
|
|
border: none;
|
|
}
|
|
.user-chip {
|
|
color: #1f2937;
|
|
}
|
|
.org-chip {
|
|
color: #1f2937;
|
|
padding: 0 0.5rem;
|
|
}
|
|
</style>
|