e1601e7862
Spec C §3.6/§6.2. Бэкенд: GET /api/billing/balance-status (frozen + capacity + required + дефицит ₽/leads), Pest 6. Фронт: BalanceFrozenBanner (в AppLayout, глобально), BalanceCapacityIndicator (в BillingView под балансом), ProjectLimitOverloadDialog (409-перехват в NewProjectDialog: save-blocked/set-zero), tenantStore + api getBalanceStatus. Vitest +18. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
69 lines
2.7 KiB
Vue
69 lines
2.7 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* Постоянная подсказка под балансом (Billing v2 Spec C §3.6, Task 1.10).
|
||
*
|
||
* Чистый presentational-компонент: показывает, на сколько дней хватит ёмкости
|
||
* баланса (в лидах) при текущем дневном заказе всех eligible-проектов.
|
||
* Зелёный — хватает на 3+ дня; жёлтый — меньше 3 дней; красный — не хватает.
|
||
*/
|
||
import { computed } from 'vue';
|
||
|
||
const props = defineProps<{
|
||
/** Баланс в рублях (строка scale 2, например "1000.00"). */
|
||
balanceRub: string;
|
||
/** Сколько лидов покрывает баланс по текущему тарифу. */
|
||
capacityLeads: number;
|
||
/** Суммарный дневной заказ всех активных проектов (лидов/день). */
|
||
requiredLeadsPerDay: number;
|
||
}>();
|
||
|
||
const daysLeft = computed(() =>
|
||
props.requiredLeadsPerDay > 0 ? props.capacityLeads / props.requiredLeadsPerDay : Infinity,
|
||
);
|
||
|
||
const statusClass = computed(() => {
|
||
if (props.requiredLeadsPerDay > 0 && props.capacityLeads < props.requiredLeadsPerDay) {
|
||
return 'capacity-insufficient';
|
||
}
|
||
if (daysLeft.value < 3) return 'capacity-warning';
|
||
return 'capacity-ok';
|
||
});
|
||
|
||
const daysLabel = computed(() => (Number.isFinite(daysLeft.value) ? daysLeft.value.toFixed(1) : '∞'));
|
||
</script>
|
||
|
||
<template>
|
||
<div class="balance-capacity text-body-2" :class="statusClass" data-testid="balance-capacity-indicator">
|
||
<div>Баланс: {{ balanceRub }}₽ = до {{ capacityLeads }} лидов по тарифу</div>
|
||
<div>Проекты заказывают: {{ requiredLeadsPerDay }} лидов в день</div>
|
||
<div v-if="statusClass === 'capacity-insufficient'" class="capacity-note">
|
||
⚠️ Не хватает — пополните счёт
|
||
</div>
|
||
<div v-else-if="statusClass === 'capacity-warning'" class="capacity-note">
|
||
Хватит на ~{{ daysLabel }} дн. — скоро потребуется пополнение
|
||
</div>
|
||
<div v-else class="capacity-note">✅ Хватит на ~{{ daysLabel }} дн.</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.balance-capacity {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
line-height: 1.4;
|
||
}
|
||
.capacity-note {
|
||
font-weight: 600;
|
||
}
|
||
.capacity-ok .capacity-note {
|
||
color: rgb(var(--v-theme-success));
|
||
}
|
||
.capacity-warning .capacity-note {
|
||
color: rgb(var(--v-theme-warning));
|
||
}
|
||
.capacity-insufficient .capacity-note {
|
||
color: rgb(var(--v-theme-error));
|
||
}
|
||
</style>
|