bddd2a8d7d
Без имени дашборд звал коллега — звучит как к чужому. Теперь Доброе утро! без обращения; имя клиент задаёт в Настройки Профиль. U6 автопополнение уже имел тултип-пояснение. Тест DashboardPageHead 6/6. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
113 lines
4.4 KiB
Vue
113 lines
4.4 KiB
Vue
<script setup lang="ts">
|
|
/**
|
|
* DashboardPageHead — приветствие + meta-строка (новые лиды / средняя стоимость) +
|
|
* range-toggle (Сегодня / 7 дней / 30 дней / Период…). Использует Vue 3.5
|
|
* `defineModel` для двусторонней привязки range.
|
|
*
|
|
* Sprint 4 Phase B/3 — split DashboardView (audit O-refactor-04 закрытие).
|
|
* F5 (17.06.2026): meta-строка раньше была захардкоженной «рыбой» (+3 / 11 / 38 /
|
|
* 2 248 ₽). Теперь числа приходят пропами из реального /api/dashboard/summary:
|
|
* сегодня/вчера — из activity.points, средняя стоимость — avg_lead_cost_rub.
|
|
*/
|
|
import { computed } from 'vue';
|
|
import { useAuthStore } from '../../stores/auth';
|
|
|
|
const range = defineModel<'today' | '7d' | '30d' | 'custom'>({ required: true });
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
/** Получено лидов сегодня. */
|
|
leadsToday?: number;
|
|
/** Получено лидов вчера. */
|
|
leadsYesterday?: number;
|
|
/** Средняя стоимость лида (₽) за окно; null — если в окне нет rub-списаний. */
|
|
avgLeadCostRub?: number | null;
|
|
}>(),
|
|
{ leadsToday: 0, leadsYesterday: 0, avgLeadCostRub: null },
|
|
);
|
|
|
|
const auth = useAuthStore();
|
|
|
|
/** Имя залогиненного пользователя (было захардкожено «Иван»). */
|
|
const firstName = computed(() => auth.user?.first_name?.trim() || '');
|
|
// U2 (UX-аудит 25.06): без имени НЕ зовём «коллега» (звучит как к чужому) —
|
|
// приветствуем нейтрально «Доброе утро!». Имя клиент задаёт в Настройки→Профиль.
|
|
const hasName = computed(() => firstName.value !== '');
|
|
|
|
/** Приветствие по времени суток (МСК машины пользователя). */
|
|
const greeting = computed(() => {
|
|
const h = new Date().getHours();
|
|
if (h < 6) return 'Доброй ночи';
|
|
if (h < 12) return 'Доброе утро';
|
|
if (h < 18) return 'Добрый день';
|
|
return 'Добрый вечер';
|
|
});
|
|
|
|
/** Средняя стоимость с разделителями тысяч и ₽; «—» если данных нет. */
|
|
const avgCostLabel = computed(() => {
|
|
const v = props.avgLeadCostRub;
|
|
if (v === null || v === undefined) return '—';
|
|
const int = Math.round(v)
|
|
.toString()
|
|
.replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
|
|
return `${int}\u00A0₽`;
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<header class="page-head">
|
|
<div>
|
|
<h1 class="text-h4 mb-2 page-greet">
|
|
{{ greeting }}<template v-if="hasName">, <em class="text-primary">{{ firstName }}</em></template
|
|
><template v-else>!</template>
|
|
</h1>
|
|
<div class="page-meta text-body-2 text-medium-emphasis">
|
|
<span>сегодня <span class="num">{{ leadsToday }}</span> · вчера <span class="num">{{ leadsYesterday }}</span></span>
|
|
<span class="sep">·</span>
|
|
<span>средняя стоимость <span class="num">{{ avgCostLabel }}</span></span>
|
|
</div>
|
|
</div>
|
|
<v-btn-toggle v-model="range" mandatory color="primary" density="comfortable" variant="outlined">
|
|
<v-btn value="today" size="small">Сегодня</v-btn>
|
|
<v-btn value="7d" size="small">7 дней</v-btn>
|
|
<v-btn value="30d" size="small">30 дней</v-btn>
|
|
<v-btn value="custom" size="small">Период…</v-btn>
|
|
</v-btn-toggle>
|
|
</header>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.page-head {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
flex-wrap: wrap;
|
|
gap: 16px;
|
|
}
|
|
|
|
.page-greet {
|
|
font-variation-settings: 'opsz' 28;
|
|
letter-spacing: -0.018em;
|
|
}
|
|
.page-greet em {
|
|
font-style: normal;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.page-meta {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
align-items: center;
|
|
}
|
|
.page-meta .sep {
|
|
/* WCAG2AA 4.5:1 fix (was #92907b → 2.92:1 on ivory; #6b6356 → 5.33:1). */
|
|
color: #6b6356;
|
|
}
|
|
.num {
|
|
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
|
font-feature-settings: 'tnum';
|
|
font-weight: 500;
|
|
}
|
|
</style>
|