Files
portal/app/resources/js/layouts/SalesLayout.vue
T
Дмитрий 869590a1b2 feat(sales): маршруты + пункты меню воронки (менеджер/начальник)
Этап 1 Task 14. /sales/prospects (менеджер), /sales/prospects/board
(начальник, salesBossOnly). Пункты «Потенциальные клиенты» и «Воронка отдела».

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 16:46:54 +03:00

278 lines
9.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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-clock-outline', to: '/sales/prospects' },
{ 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-filter-variant', to: '/sales/prospects/board' },
{ 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>
<!-- Поиск клиентов (Python sales-finder, отдельная служба под /finder/) -->
<v-list-item
href="/finder/"
target="_blank"
rel="noopener"
prepend-icon="mdi-map-search-outline"
rounded="lg"
class="nav-item"
>
<v-list-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>