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:
@@ -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' ? 'Возобновлено' : 'Приостановлено — уже заказанные лиды дойдут');
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user