ef0f7c803f
Task 0.7: SalesLayout (сайдбар ОТДЕЛ ПРОДАЖ, 2 секции nav, boss-only для head), SalesLoginView (реальные email+пароль, не демо-кнопки), сторы salesAuth/salesPeriod, api/sales.ts, PeriodPicker (этот/прошлый/позапрошлый/произвольный), HelpHint (?). Роуты /sales/* с гардом (токен→login, boss-only→/sales). Заглушка SalesStubView для экранов будущих фаз. Vitest SalesLogin 5/5, весь фронт 1005 без регрессий, type-check/lint чисто. Вёрстка по демо v8_sales.html. Один эскейп на сессию. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
265 lines
8.8 KiB
Vue
265 lines
8.8 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* Layout портала отдела продаж — sidebar #012019 с брендом «Лидерра / ОТДЕЛ ПРОДАЖ»,
|
||
* двумя группами навигации (Менеджер / Начальник), topbar с PeriodPicker.
|
||
*
|
||
* Источник дизайна: liderra_v8_handoff/concepts/v8_sales.html #screen-portal.
|
||
* Структурная модель: AdminLayout.vue.
|
||
*
|
||
* Секция «Начальник» отображается только при salesAuth.isHead === true.
|
||
*/
|
||
import { computed } from 'vue';
|
||
import { RouterView, useRoute, useRouter } from 'vue-router';
|
||
import { useSalesAuthStore } from '../stores/salesAuth';
|
||
import PeriodPicker from '../components/sales/PeriodPicker.vue';
|
||
|
||
interface NavItem {
|
||
title: string;
|
||
icon: string;
|
||
to: string;
|
||
}
|
||
|
||
const managerNav: NavItem[] = [
|
||
{ title: 'Сводка', icon: 'mdi-view-dashboard-outline', to: '/sales' },
|
||
{ title: 'Мои клиенты', icon: 'mdi-account-group-outline', to: '/sales/clients' },
|
||
{ title: 'Привязать клиента', icon: 'mdi-account-search-outline', to: '/sales/attach' },
|
||
{ title: 'Мой доход', icon: 'mdi-currency-rub', to: '/sales/income' },
|
||
];
|
||
|
||
const bossNav: NavItem[] = [
|
||
{ title: 'Сводка отдела', icon: 'mdi-chart-line', to: '/sales/boss' },
|
||
{ title: 'Результативность', icon: 'mdi-account-check-outline', to: '/sales/performance' },
|
||
{ title: 'Тарифы менеджеров', icon: 'mdi-tag-arrow-right', to: '/sales/tariffs' },
|
||
{ title: 'Счета', icon: 'mdi-file-document-outline', to: '/sales/invoices' },
|
||
{ title: 'Заявки на привязку', icon: 'mdi-file-check-outline', to: '/sales/requests' },
|
||
{ title: 'Выплаты', icon: 'mdi-credit-card-outline', to: '/sales/payouts' },
|
||
{ title: 'Менеджеры', icon: 'mdi-account-multiple-outline', to: '/sales/managers' },
|
||
];
|
||
|
||
const route = useRoute();
|
||
const router = useRouter();
|
||
const salesAuth = useSalesAuthStore();
|
||
|
||
const roleName = computed(() => (salesAuth.isHead ? 'Начальник' : 'Менеджер'));
|
||
|
||
const userInitials = computed(() => {
|
||
const u = salesAuth.user;
|
||
if (!u) return 'МП';
|
||
const parts = u.name.split(' ').filter(Boolean);
|
||
if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();
|
||
return u.name.slice(0, 2).toUpperCase();
|
||
});
|
||
|
||
const currentPageTitle = computed(() => {
|
||
const all = [...managerNav, ...bossNav];
|
||
return all.find((i) => route.path === i.to || route.path.startsWith(i.to + '/'))?.title ?? 'Продажи';
|
||
});
|
||
|
||
function isActive(to: string): boolean {
|
||
if (to === '/sales') return route.path === '/sales';
|
||
return route.path.startsWith(to);
|
||
}
|
||
|
||
async function handleLogout() {
|
||
await salesAuth.logout();
|
||
await router.push('/sales/login');
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<v-app>
|
||
<v-navigation-drawer color="#012019" theme="dark" :width="240" class="sales-drawer">
|
||
<!-- Brand block -->
|
||
<div class="brand-block">
|
||
<span class="brand-mark" aria-hidden="true">
|
||
<svg viewBox="0 0 48 48" width="22" height="22">
|
||
<path
|
||
d="M16 14 L16 34 L32 34"
|
||
stroke="#012019"
|
||
stroke-width="4.5"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
fill="none"
|
||
/>
|
||
<circle cx="32" cy="34" r="3.5" fill="#0F6E56" />
|
||
</svg>
|
||
</span>
|
||
<span class="brand-text">Лидерра<span class="brand-dot">.</span></span>
|
||
</div>
|
||
<div class="brand-sub">ОТДЕЛ ПРОДАЖ</div>
|
||
|
||
<v-list nav density="comfortable" class="app-nav" role="navigation" aria-label="Навигация отдела продаж">
|
||
<!-- МЕНЕДЖЕР group -->
|
||
<div class="nav-eyebrow">Менеджер</div>
|
||
<v-list-item
|
||
v-for="item in managerNav"
|
||
:key="item.to"
|
||
:to="item.to"
|
||
:prepend-icon="item.icon"
|
||
:active="isActive(item.to)"
|
||
rounded="lg"
|
||
class="nav-item"
|
||
:exact="item.to === '/sales'"
|
||
>
|
||
<v-list-item-title>{{ item.title }}</v-list-item-title>
|
||
</v-list-item>
|
||
|
||
<!-- НАЧАЛЬНИК group — only for head role -->
|
||
<template v-if="salesAuth.isHead">
|
||
<div class="nav-eyebrow nav-eyebrow--boss">Начальник</div>
|
||
<v-list-item
|
||
v-for="item in bossNav"
|
||
:key="item.to"
|
||
:to="item.to"
|
||
:prepend-icon="item.icon"
|
||
:active="isActive(item.to)"
|
||
rounded="lg"
|
||
class="nav-item"
|
||
>
|
||
<v-list-item-title>{{ item.title }}</v-list-item-title>
|
||
</v-list-item>
|
||
</template>
|
||
</v-list>
|
||
</v-navigation-drawer>
|
||
|
||
<v-app-bar :elevation="0" color="surface" :height="56" class="sales-topbar">
|
||
<div class="crumb">
|
||
<span class="text-medium-emphasis">Продажи</span>
|
||
<v-icon size="14" class="mx-1">mdi-chevron-right</v-icon>
|
||
<strong>{{ currentPageTitle }}</strong>
|
||
</div>
|
||
<v-spacer />
|
||
|
||
<!-- Period picker -->
|
||
<PeriodPicker class="mr-3" />
|
||
|
||
<!-- Role chip -->
|
||
<v-chip
|
||
:color="salesAuth.isHead ? '#7B4D00' : '#084635'"
|
||
:style="{
|
||
background: salesAuth.isHead ? '#FFF4DD' : '#E1EEEA',
|
||
color: salesAuth.isHead ? '#7B4D00' : '#084635',
|
||
}"
|
||
size="small"
|
||
class="role-chip mr-2"
|
||
label
|
||
>
|
||
{{ roleName.toUpperCase() }}
|
||
</v-chip>
|
||
|
||
<!-- User menu -->
|
||
<v-menu offset="8">
|
||
<template #activator="{ props }">
|
||
<v-btn
|
||
v-bind="props"
|
||
variant="outlined"
|
||
size="small"
|
||
class="user-chip mr-2"
|
||
aria-label="Меню пользователя"
|
||
>
|
||
<v-avatar size="24" color="#0F6E56" class="mr-2">
|
||
<span class="text-caption" style="color: #fff; font-size: 10px">{{ userInitials }}</span>
|
||
</v-avatar>
|
||
<span class="text-body-2">{{ salesAuth.user?.name ?? '' }}</span>
|
||
</v-btn>
|
||
</template>
|
||
<v-list density="compact" min-width="200">
|
||
<v-list-item v-if="salesAuth.user" :title="salesAuth.user.email" disabled />
|
||
<v-divider v-if="salesAuth.user" />
|
||
<v-list-item prepend-icon="mdi-logout" title="Выйти" @click="handleLogout" />
|
||
</v-list>
|
||
</v-menu>
|
||
</v-app-bar>
|
||
|
||
<v-main class="sales-main">
|
||
<RouterView />
|
||
</v-main>
|
||
</v-app>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.sales-drawer {
|
||
border-right: 1px solid rgba(255, 255, 255, 0.06);
|
||
}
|
||
|
||
.brand-block {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 18px 20px 4px;
|
||
}
|
||
|
||
.brand-mark {
|
||
width: 24px;
|
||
height: 24px;
|
||
border-radius: 5px;
|
||
background: #fff;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.brand-text {
|
||
font-weight: 600;
|
||
font-size: 16px;
|
||
color: #fff;
|
||
letter-spacing: -0.01em;
|
||
}
|
||
|
||
.brand-dot {
|
||
color: #32c8a9;
|
||
}
|
||
|
||
.brand-sub {
|
||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
||
font-size: 10px;
|
||
letter-spacing: 0.08em;
|
||
color: #32c8a9;
|
||
padding: 0 20px 14px;
|
||
text-transform: uppercase;
|
||
font-weight: 600;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||
}
|
||
|
||
.nav-eyebrow {
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
letter-spacing: 0.01em;
|
||
color: rgba(255, 255, 255, 0.38);
|
||
padding: 14px 16px 6px;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.nav-eyebrow--boss {
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.sales-topbar {
|
||
border-bottom: 1px solid #d9d5cd !important;
|
||
}
|
||
|
||
.crumb {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 14px;
|
||
margin-left: 8px;
|
||
}
|
||
|
||
.role-chip {
|
||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
||
font-size: 10px !important;
|
||
letter-spacing: 0.04em;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.user-chip {
|
||
text-transform: none;
|
||
border-color: #d9d5cd !important;
|
||
}
|
||
|
||
.sales-main {
|
||
background: #f6f3ec;
|
||
}
|
||
</style>
|