3cedf28f33
Дашборд: вопросик у KPI Получено/Конверсия/Активные, у воронки и у прогноза хватит на N дней; pp на п.п. Мастер проекта: подпись лимита заявок в день + вопросик у выбора источника Сайт/Звонок/СМС. Биллинг: вопросик у Цены за лид 7 ступеней. Тест FunnelChart 8/8, type-check чистый, затронутые спеки 58/60. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
159 lines
4.9 KiB
Vue
159 lines
4.9 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* DashboardBalance — карта баланса с runway-bar (segments по дням).
|
||
* Класс `.runway-fill` сохранён — Vitest тест считает их количество.
|
||
*
|
||
* Sprint 4 Phase B/3 — split DashboardView (audit O-refactor-04 закрытие).
|
||
*/
|
||
import { leadsWord, daysWord } from '../../utils/plural';
|
||
|
||
export interface Balance {
|
||
amount: string;
|
||
// B1-2: null = нет активных проектов (прогноз нечего считать) → «нет активных проектов».
|
||
runwayDays: number | null;
|
||
runwayMax: number;
|
||
runwayLeads: number;
|
||
}
|
||
|
||
defineProps<{
|
||
balance: Balance;
|
||
}>();
|
||
</script>
|
||
|
||
<template>
|
||
<v-col cols="12" sm="6" md="3">
|
||
<v-card variant="flat" color="secondary" class="balance-card pa-4">
|
||
<div class="balance-row1">
|
||
<span class="balance-label">Баланс</span>
|
||
<v-tooltip
|
||
text="«Хватит на N дней» — прогноз от дневного заказа ваших проектов. Если активных проектов нет, прогноз не считается."
|
||
location="top"
|
||
max-width="280"
|
||
>
|
||
<template #activator="{ props: tip }">
|
||
<v-icon
|
||
v-bind="tip"
|
||
size="14"
|
||
class="balance-hint ml-1"
|
||
icon="mdi-help-circle-outline"
|
||
aria-label="Что значит хватит на N дней"
|
||
tabindex="0"
|
||
/>
|
||
</template>
|
||
</v-tooltip>
|
||
<v-chip size="x-small" color="primary" variant="elevated" class="ml-2">LIVE</v-chip>
|
||
</div>
|
||
<div class="balance-amount">
|
||
<span class="num">{{ balance.amount }}</span>
|
||
<span class="ru"> ₽</span>
|
||
</div>
|
||
<div class="runway mt-3">
|
||
<div
|
||
class="runway-bar"
|
||
role="img"
|
||
:aria-label="
|
||
balance.runwayDays === null
|
||
? 'Прогноз недоступен — нет активных проектов'
|
||
: `Хватит на ${balance.runwayDays} ${daysWord(balance.runwayDays)} из ${balance.runwayMax}`
|
||
"
|
||
>
|
||
<span
|
||
v-for="i in balance.runwayMax"
|
||
:key="i"
|
||
class="runway-fill"
|
||
:class="{ filled: balance.runwayDays !== null && i <= balance.runwayDays }"
|
||
/>
|
||
</div>
|
||
<div class="runway-foot text-caption">
|
||
<span v-if="balance.runwayDays === null"
|
||
>≈ {{ balance.runwayLeads }} {{ leadsWord(balance.runwayLeads) }} · нет активных проектов</span
|
||
>
|
||
<span v-else
|
||
>≈ {{ balance.runwayLeads }} {{ leadsWord(balance.runwayLeads) }} · хватит на
|
||
<strong>{{ balance.runwayDays }} {{ daysWord(balance.runwayDays) }}</strong></span
|
||
>
|
||
<a href="/billing" class="ml-2 runway-action">пополнить →</a>
|
||
</div>
|
||
</div>
|
||
</v-card>
|
||
</v-col>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.balance-card {
|
||
color: #fff;
|
||
background: #012019 !important;
|
||
height: 100%;
|
||
}
|
||
|
||
.balance-row1 {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.balance-label {
|
||
font-size: 12px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.06em;
|
||
color: #7a8c87;
|
||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
||
}
|
||
.balance-hint {
|
||
color: #7a8c87;
|
||
cursor: help;
|
||
}
|
||
.balance-hint:hover {
|
||
color: #fff;
|
||
}
|
||
|
||
.balance-amount {
|
||
margin-top: 8px;
|
||
font-size: 32px;
|
||
font-weight: 600;
|
||
line-height: 1.1;
|
||
}
|
||
.balance-amount .num {
|
||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
||
font-feature-settings: 'tnum';
|
||
color: #fff;
|
||
letter-spacing: -0.01em;
|
||
}
|
||
.balance-amount .ru {
|
||
color: #7a8c87;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.runway-bar {
|
||
display: flex;
|
||
gap: 4px;
|
||
}
|
||
|
||
.runway-fill {
|
||
flex: 1;
|
||
height: 6px;
|
||
border-radius: 3px;
|
||
background: rgba(255, 255, 255, 0.08);
|
||
}
|
||
.runway-fill.filled {
|
||
background: #32c8a9;
|
||
}
|
||
|
||
.runway-foot {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-top: 6px;
|
||
color: #7a8c87;
|
||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
||
}
|
||
.runway-foot strong {
|
||
color: #fff;
|
||
font-weight: 500;
|
||
}
|
||
.runway-action {
|
||
color: #32c8a9;
|
||
text-decoration: none;
|
||
}
|
||
.runway-action:hover {
|
||
color: #fff;
|
||
}
|
||
</style>
|