30ef61dff8
3 view'а с >300 строк разделены на shell + sub-components: AdminTenantsView 377→155 (+ TenantsStatsHeader 82 / TenantsFilters 93 / TenantsTable 116). AdminTenantDetailView 436→109 (+ TenantDetailHeader 158 / TenantDetailTabs 176 + adminTenantDetailFormatters 43 composable). AppLayout 466→78 (+ AppSidebar 155 / AppTopbar 269; R0.6 hard-стоп снят явным запросом заказчика 10.05.2026). State (filterStatuses, tenantsState, activeTab, tenant, drawerOpen) остаётся в parent view'ах ради `defineExpose`-контракта Vitest тестов. Sub-components читают Pinia stores напрямую (auth + notifications + reminders) — без prop-drilling. AppTopbar 269 строк <300 — acceptance threshold выдержан (можно дальше split на NotificationsDropdown + UserMenu в отдельном flow, не критично). Регрессия: ESLint 0 + vue-tsc 0 + Vitest 416/416 + build OK 1.17 сек. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
79 lines
2.7 KiB
Vue
79 lines
2.7 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 + reminders 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 { useRemindersStore } from '../stores/reminders';
|
|
import { usePolling } from '../composables/usePolling';
|
|
import AppSidebar from '../components/layout/AppSidebar.vue';
|
|
import AppTopbar from '../components/layout/AppTopbar.vue';
|
|
|
|
const auth = useAuthStore();
|
|
const notifications = useNotificationsStore();
|
|
const reminders = useRemindersStore();
|
|
const route = useRoute();
|
|
|
|
const drawerOpen = ref(true);
|
|
|
|
// Тот же навигационный pool что в AppSidebar — для crumb-resolution в topbar
|
|
// (sidebar и topbar — независимые, но navGroups совпадают по контракту).
|
|
const navItems = computed(() => [
|
|
{ title: 'Дашборд', to: '/dashboard' },
|
|
{ title: 'Сделки', to: '/deals' },
|
|
{ title: 'Канбан', to: '/kanban' },
|
|
{ title: 'Напоминания', to: '/reminders' },
|
|
{ title: 'Биллинг', to: '/billing' },
|
|
{ title: 'Отчёты', to: '/reports' },
|
|
{ title: 'Менеджеры', to: '/managers' },
|
|
{ title: 'Настройки', to: '/settings' },
|
|
]);
|
|
|
|
const currentPageTitle = computed(() => {
|
|
return navItems.value.find((i) => i.to === route.path)?.title ?? 'Страница';
|
|
});
|
|
|
|
async function loadNotifications(): Promise<void> {
|
|
if (!auth.user) return;
|
|
await notifications.load(10);
|
|
}
|
|
|
|
async function loadReminderCounts(): Promise<void> {
|
|
if (!auth.user) return;
|
|
await reminders.refreshCounts();
|
|
}
|
|
|
|
onMounted(() => {
|
|
void loadNotifications();
|
|
void loadReminderCounts();
|
|
});
|
|
usePolling(loadNotifications, { intervalMs: 30_000, enabled: true });
|
|
usePolling(loadReminderCounts, { intervalMs: 60_000, enabled: true });
|
|
</script>
|
|
|
|
<template>
|
|
<v-app>
|
|
<AppSidebar v-model:drawer-open="drawerOpen" />
|
|
<AppTopbar :page-title="currentPageTitle" @toggle-drawer="drawerOpen = !drawerOpen" />
|
|
|
|
<v-main class="app-main">
|
|
<RouterView />
|
|
</v-main>
|
|
</v-app>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.app-main {
|
|
background: #f6f3ec;
|
|
}
|
|
</style>
|