Adopt logistics visual system across webapp
This commit is contained in:
88
app/layouts/manager.vue
Normal file
88
app/layouts/manager.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
const route = useRoute()
|
||||
const localePath = useLocalePath()
|
||||
const { signOut } = useAuth()
|
||||
|
||||
const userData = useState<{
|
||||
activeTeam?: { name?: string | null }
|
||||
firstName?: string | null
|
||||
} | null>('me', () => null)
|
||||
|
||||
const navItems = [
|
||||
{ label: 'Orders', path: '/manager/orders', icon: 'lucide:package' },
|
||||
{ label: 'Quotations', path: '/manager/quotations', icon: 'lucide:file-text' },
|
||||
{ label: 'Tariffs', path: '/manager/tariffs', icon: 'lucide:waypoints' },
|
||||
]
|
||||
|
||||
function isActive(path: string) {
|
||||
const localized = localePath(path)
|
||||
return route.path === localized || route.path.startsWith(`${localized}/`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="manager-logistics-shell text-[#2f2418]">
|
||||
<div class="sticky top-0 z-50 px-3 pt-3 md:px-4">
|
||||
<div class="mx-auto max-w-[1440px]">
|
||||
<header class="rounded-[30px] border border-[#e1d7c7] bg-[#efe6d8]/95 px-4 py-4 shadow-[0_18px_40px_rgba(47,36,24,0.08)] backdrop-blur">
|
||||
<div class="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<NuxtLink
|
||||
:to="localePath('/')"
|
||||
class="flex h-12 min-w-[88px] items-center justify-center rounded-full bg-[#2f2418] px-5 text-sm font-black uppercase tracking-[0.2em] text-white"
|
||||
>
|
||||
Optovia
|
||||
</NuxtLink>
|
||||
<div>
|
||||
<p class="text-[11px] font-bold uppercase tracking-[0.18em] text-[#8a7761]">Logistics manager</p>
|
||||
<h1 class="text-xl font-black leading-tight text-[#2f2418] md:text-2xl">Control tower</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="flex flex-wrap items-center gap-2">
|
||||
<NuxtLink
|
||||
v-for="item in navItems"
|
||||
:key="item.path"
|
||||
:to="localePath(item.path)"
|
||||
class="inline-flex items-center gap-2 rounded-full px-4 py-2 text-sm font-bold transition"
|
||||
:class="isActive(item.path) ? 'bg-[#2f2418] text-white shadow-[0_10px_24px_rgba(47,36,24,0.16)]' : 'bg-white text-[#5f4b33] hover:bg-[#f8f3ec]'"
|
||||
>
|
||||
<Icon :name="item.icon" size="16" />
|
||||
<span>{{ item.label }}</span>
|
||||
</NuxtLink>
|
||||
</nav>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="rounded-full bg-white px-4 py-2 text-sm font-semibold text-[#5f4b33]">
|
||||
{{ userData?.activeTeam?.name || userData?.firstName || 'Active workspace' }}
|
||||
</div>
|
||||
<NuxtLink
|
||||
:to="localePath('/clientarea/orders')"
|
||||
class="inline-flex items-center gap-2 rounded-full bg-white px-4 py-2 text-sm font-bold text-[#5f4b33] transition hover:bg-[#f8f3ec]"
|
||||
>
|
||||
<Icon name="lucide:arrow-left" size="16" />
|
||||
<span>Client area</span>
|
||||
</NuxtLink>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 rounded-full bg-[#2f2418] px-4 py-2 text-sm font-bold text-white transition hover:bg-[#493824]"
|
||||
@click="signOut()"
|
||||
>
|
||||
<Icon name="lucide:log-out" size="16" />
|
||||
<span>Sign out</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="px-3 pb-5 pt-3 md:px-4 md:pb-6">
|
||||
<div class="mx-auto max-w-[1440px]">
|
||||
<section class="rounded-[34px] bg-[#f3eee6] px-4 py-4 shadow-[0_24px_72px_rgba(3,8,20,0.18)] md:px-5 md:py-5 lg:px-6 lg:py-6">
|
||||
<slot />
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="min-h-screen flex flex-col bg-base-300">
|
||||
<div class="min-h-screen flex flex-col manager-logistics-shell">
|
||||
<AiChatSidebar
|
||||
:open="isChatOpen"
|
||||
:width="chatWidth"
|
||||
@@ -121,7 +121,7 @@ const {
|
||||
} = useHeroScroll()
|
||||
|
||||
// Theme state
|
||||
const theme = useState<'cupcake' | 'night'>('theme', () => 'cupcake')
|
||||
const theme = useState<'silk' | 'night'>('theme', () => 'silk')
|
||||
|
||||
// User data state (shared across layouts)
|
||||
interface SelectedLocation {
|
||||
@@ -313,7 +313,7 @@ const onClickSignOut = () => {
|
||||
signOut(siteUrl)
|
||||
}
|
||||
|
||||
const applyTheme = (value: 'cupcake' | 'night') => {
|
||||
const applyTheme = (value: 'silk' | 'night') => {
|
||||
if (import.meta.client) {
|
||||
document.documentElement.setAttribute('data-theme', value)
|
||||
localStorage.setItem('theme', value)
|
||||
@@ -322,8 +322,8 @@ const applyTheme = (value: 'cupcake' | 'night') => {
|
||||
|
||||
onMounted(() => {
|
||||
const stored = import.meta.client ? localStorage.getItem('theme') : null
|
||||
if (stored === 'night' || stored === 'cupcake') {
|
||||
theme.value = stored as 'cupcake' | 'night'
|
||||
if (stored === 'night' || stored === 'silk') {
|
||||
theme.value = stored as 'silk' | 'night'
|
||||
}
|
||||
applyTheme(theme.value)
|
||||
})
|
||||
@@ -331,7 +331,7 @@ onMounted(() => {
|
||||
watch(theme, (value) => applyTheme(value))
|
||||
|
||||
const toggleTheme = () => {
|
||||
theme.value = theme.value === 'night' ? 'cupcake' : 'night'
|
||||
theme.value = theme.value === 'night' ? 'silk' : 'night'
|
||||
}
|
||||
|
||||
// Search handler for Quote mode - triggers search via shared state
|
||||
|
||||
Reference in New Issue
Block a user