Files
portal/app/resources/js/layouts/AppLayout.vue
T
Дмитрий 813b58447e feat/onboarding: приветственный тур при первом входе Фаза 3
Лёгкий тур без сторонних зависимостей: подсвечивает пункты меню по порядку
пополнить→создать проект→где заявки→помощь. Показывается один раз localStorage,
можно пропустить. Якоря data-tour на AppSidebar, монтируется в AppLayout.
Тест WelcomeTour 4/4. NB: пиксельное позиционирование проверить визуально на выкате.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 12:36:15 +03:00

104 lines
4.1 KiB
Vue

<script setup lang="ts">
/**
* Default-layout для авторизованных страниц (Dashboard, Сделки, Канбан и т.п.).
*
* Sprint 4 Phase B/1 (audit O-refactor-04 хвост, R0.6 hard-стоп снят явным
* запросом заказчика 10.05.2026): структурно разделён на components/layout/
* {AppSidebar, AppTopbar}.vue. Лoad-cycle (notifications polling)
* + drawer state остаются здесь.
*
* Источник дизайна: liderra_v8_handoff/concepts/v8_dashboard.html.
*/
import { computed, onMounted, ref } from 'vue';
import { RouterView, useRoute } from 'vue-router';
import { useAuthStore } from '../stores/auth';
import { useNotificationsStore } from '../stores/notifications';
import { useTenantStore } from '../stores/tenantStore';
import { usePolling } from '../composables/usePolling';
import { POLLING_INTERVAL_MS, POLLING_REMINDERS_INTERVAL_MS } from '../constants/polling';
import AppSidebar from '../components/layout/AppSidebar.vue';
import AppTopbar from '../components/layout/AppTopbar.vue';
import DevIndexBadge from '../components/DevIndexBadge.vue';
import CommandPalette from '../components/layout/CommandPalette.vue';
import JivoWidget from '../components/support/JivoWidget.vue';
import BalanceFrozenBanner from '../components/billing/BalanceFrozenBanner.vue';
import ImpersonationSessionBanner from '../components/admin/ImpersonationSessionBanner.vue';
import WelcomeTour from '../components/layout/WelcomeTour.vue';
const auth = useAuthStore();
const notifications = useNotificationsStore();
const tenant = useTenantStore();
const route = useRoute();
const drawerOpen = ref(true);
// Тот же навигационный pool что в AppSidebar — для crumb-resolution в topbar
// (sidebar и topbar — независимые, но navGroups совпадают по контракту).
const navItems = computed(() => [
{ title: 'Проекты', to: '/projects' },
{ title: 'Сделки', to: '/deals' },
{ title: 'Канбан', to: '/kanban' },
{ title: 'Дашборд', to: '/dashboard' },
{ title: 'Биллинг', to: '/billing' },
{ title: 'Отчёты', to: '/reports' },
{ title: 'Настройки', to: '/settings' },
]);
const currentPageTitle = computed(() => {
// Сначала короткий title из sidebar-nav (Дашборд/Сделки/…), затем — route.meta.title
// для страниц вне sidebar (напр. Импорт данных), и только потом fallback.
return (
navItems.value.find((i) => i.to === route.path)?.title ?? (route.meta.title as string | undefined) ?? 'Страница'
);
});
async function loadNotifications(): Promise<void> {
if (!auth.user) return;
await notifications.load(10);
}
async function loadBalanceStatus(): Promise<void> {
if (!auth.user) return;
await tenant.load();
}
onMounted(() => {
void loadNotifications();
void loadBalanceStatus();
});
usePolling(loadNotifications, { intervalMs: POLLING_INTERVAL_MS, enabled: true });
usePolling(loadBalanceStatus, { intervalMs: POLLING_REMINDERS_INTERVAL_MS, enabled: true });
</script>
<template>
<v-app>
<ImpersonationSessionBanner />
<AppSidebar v-model:drawer-open="drawerOpen" />
<AppTopbar :page-title="currentPageTitle" @toggle-drawer="drawerOpen = !drawerOpen" />
<v-main class="app-main">
<BalanceFrozenBanner
:frozen="tenant.frozen"
:deficit-rub="tenant.deficitRub"
:deficit-leads="tenant.deficitLeads"
/>
<RouterView v-slot="{ Component, route: r }">
<Transition :name="(r.meta.transition as string) ?? 'ld-route-fadeup'" mode="out-in">
<component :is="Component" :key="r.fullPath" />
</Transition>
</RouterView>
</v-main>
<DevIndexBadge :index="route.meta.devIndex" :label="route.meta.devLabel" />
<CommandPalette />
<JivoWidget />
<WelcomeTour />
</v-app>
</template>
<style scoped>
.app-main {
background: #f6f3ec;
padding-left: 232px;
}
</style>