feat(projects): UX «запустится сам после пополнения» + бейдж «ждёт пополнения»

Окно нехватки баланса («Проекты») и «Конкурентное поле» теперь честно
говорят, что взведённый проект запустится сам после пополнения (вместо
«пополните, чтобы запустить»); добавлена кнопка «Пока не надо» (оставить
черновиком). Бейдж взведённого проекта — «ждёт пополнения / запустится
сам» вместо «не хватает баланса». Синхронно в «Проектах» и «Конкурентном
поле». Тесты: ProjectLimitOverloadDialog + ProjectCardBadge обновлены/
дополнены; 8 фронт-спеков 59/59.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-07-15 21:41:53 +03:00
parent 7e148474a5
commit 0a4d4f3224
6 changed files with 33 additions and 15 deletions
@@ -87,7 +87,7 @@
{{ syncStatusLabel }}
</v-chip>
</template>
Пополните баланс, чтобы запустить проект
Проект запустится сам, как только пополните баланс
</v-tooltip>
<v-chip v-else :color="syncStatusColor" size="x-small" variant="tonal">
<v-icon start size="x-small">{{ syncStatusIcon }}</v-icon>
@@ -132,7 +132,7 @@ const progressColor = computed(() => (progressPercent.value >= 90 ? 'success' :
// 3. обычный sync_status
const syncStatusLabel = computed(() => {
if (props.project.preflight_blocked_at) {
return 'Не запущен — не хватает баланса';
return 'Ждёт пополнения — запустится сам';
}
if (props.project.balance_blocked) {
return 'Приостановлен — не хватает баланса';
@@ -31,6 +31,7 @@ const emit = defineEmits<{
'update:modelValue': [value: boolean];
'save-blocked': [];
'set-zero': [];
'keep-as-draft': [];
}>();
// Косяк 06: «Пополнить» прямо из окна — закрываем и ведём в биллинг.
@@ -44,15 +45,14 @@ function goTopup(): void {
<template>
<v-dialog :model-value="modelValue" max-width="520" @update:model-value="$emit('update:modelValue', $event)">
<v-card v-if="payload" data-testid="overload-dialog" data-tour="overload-dialog">
<v-card-title>Проект не запущен</v-card-title>
<v-card-title>Проект взведён запустится сам</v-card-title>
<v-card-text>
<p>
Проект создан, но <strong>не запущен</strong> на балансе недостаточно средств для запуска.
Сейчас на балансе недостаточно средств, поэтому проект <strong>пока не запущен</strong>.
</p>
<p class="text-medium-emphasis mt-2">
Чтобы запустить пополните примерно на
<strong>{{ payload.topup_rub ?? payload.deficit_leads }} </strong>
или уменьшите объём.
Он <strong>запустится сам</strong>, как только пополните баланс примерно на
<strong>{{ payload.topup_rub ?? payload.deficit_leads }} </strong>.
</p>
<p class="text-caption text-medium-emphasis mt-1">
Текущий баланс: {{ payload.current_balance_rub }}
@@ -60,6 +60,9 @@ function goTopup(): void {
</p>
</v-card-text>
<v-card-actions class="flex-wrap justify-end ga-2">
<v-btn variant="text" data-test="keep-draft" data-testid="overload-keep-draft" @click="$emit('keep-as-draft')">
Пока не надо
</v-btn>
<v-btn variant="text" data-test="close" data-testid="overload-cancel" @click="$emit('update:modelValue', false)">
Понятно
</v-btn>
@@ -82,7 +82,7 @@ async function create(launch: boolean): Promise<void> {
} catch (e) {
const code = (e as any)?.response?.data?.error;
errorMsg.value = code === 'balance_insufficient'
? 'Недостаточно средств для запуска всех проектов. Можно создать без запуска и пополнить баланс позже.'
? 'Недостаточно средств для запуска всех проектов. Проекты создадутся и запустятся сами, как только пополните баланс.'
: 'Не удалось создать проекты. Попробуйте ещё раз.';
nav.go('create');
}
@@ -116,11 +116,12 @@ async function create(launch: boolean): Promise<void> {
<!-- Сводка запуска: часть проектов отложена из-за баланса -->
<div v-if="launchSummary && launchSummary.deferred > 0" class="ld-alert ld-alert--launch" data-testid="launch-summary">
Создано, запущено {{ launchSummary.launched }}, отложено {{ launchSummary.deferred }}
они <strong>запустятся сами</strong>,
<template v-if="launchSummary.balance">
пополните ~{{ launchSummary.balance.topup_rub }} или уменьшите объём.
как только пополните баланс на ~{{ launchSummary.balance.topup_rub }} .
</template>
<template v-else>
пополните баланс или уменьшите объём.
как только пополните баланс.
</template>
</div>
@@ -132,7 +132,7 @@ function sourceValue(s: FieldSourceDto) {
}
function projStatus(s: FieldSourceDto): { label: string; cls: string } {
if (!s.project) return { label: 'проект не создан', cls: 'ld-b-none' };
if (s.project.preflight_blocked_at) return { label: '⛔ не хватает баланса', cls: 'ld-b-stop' };
if (s.project.preflight_blocked_at) return { label: '⏳ ждёт пополнения', cls: 'ld-b-stop' };
if (!s.project.is_active) return { label: '⏸ на паузе', cls: 'ld-b-stop' };
return { label: '▶ активен', cls: 'ld-b-run' };
}
@@ -273,7 +273,7 @@ async function bulkWork(action: 'pause' | 'resume' | 'delete') {
const result = await store.bulkProjectsById(action, projectIds);
const deferred = result.skipped.filter((s) => s.reason === 'balance_insufficient').length;
if (action === 'resume' && deferred > 0) {
flash(`Возобновлено ${result.updated}, отложено ${deferred}не хватает баланса`);
flash(`Возобновлено ${result.updated}, отложено ${deferred}запустятся сами после пополнения`);
} else {
flash(action === 'resume' ? 'Возобновлено' : 'Приостановлено — уже заказанные лиды дойдут');
}
+3 -3
View File
@@ -17,13 +17,13 @@ const baseProject = {
};
describe('ProjectCard — бейдж баланса', () => {
it('проект с preflight_blocked_at показывает «Не запущен — не хватает баланса»', () => {
it('проект с preflight_blocked_at показывает «Ждёт пополнения — запустится сам»', () => {
const project = { ...baseProject, preflight_blocked_at: '2026-07-02T10:00:00Z', balance_blocked: true };
const w = mount(ProjectCard, {
props: { project, selected: false },
global: { plugins: [vuetify], stubs: { RouterLink: true } },
});
expect(w.text()).toContain('Не запущен — не хватает баланса');
expect(w.text()).toContain('Ждёт пополнения — запустится сам');
});
it('обычная пауза (preflight_blocked_at=null) показывает «Приостановлен» а не блок баланса', () => {
@@ -32,7 +32,7 @@ describe('ProjectCard — бейдж баланса', () => {
props: { project, selected: false },
global: { plugins: [vuetify], stubs: { RouterLink: true } },
});
expect(w.text()).not.toContain('Не запущен — не хватает баланса');
expect(w.text()).not.toContain('Ждёт пополнения — запустится сам');
// «На паузе» из v-else блока
expect(w.text()).toContain('На паузе');
});
@@ -109,4 +109,18 @@ describe('ProjectLimitOverloadDialog — Task 12 (запуск отложен)',
expect(text).not.toContain('пополни ');
expect(text).not.toContain('поставь ');
});
// Фича 2026-07-15: честный текст «запустится сам после пополнения» + «Пока не надо».
it('говорит, что проект запустится сам после пополнения', () => {
const w = mountDialog();
expect(w.text()).toContain('запустится сам');
});
it('содержит кнопку «Пока не надо» [data-test="keep-draft"] и эмитит keep-as-draft', async () => {
const w = mountDialog();
const btn = w.find('[data-test="keep-draft"]');
expect(btn.exists()).toBe(true);
await btn.trigger('click');
expect(w.emitted('keep-as-draft')).toBeTruthy();
});
});