feat(deals): DealsBulkBar — status-change only (drop export/delete/trash)

This commit is contained in:
Дмитрий
2026-05-17 15:32:00 +03:00
parent 583e1986ff
commit b1ea9208c2
2 changed files with 80 additions and 79 deletions
@@ -1,20 +1,13 @@
<script setup lang="ts">
/**
* Sticky-bar bulk-actions для выбранных сделок (Sprint 3 Phase C).
*
* Показывается когда selectedCount > 0. В trash-mode — только кнопка
* «Восстановить»; в обычном режиме — Сменить статус (menu со списком),
* Экспорт, Удалить.
*
* Контракт: stateless presentation — родитель держит `selected`, `statusMenuOpen`,
* `leadStatuses`, передаёт через props и слушает emit'ы.
* Sticky-bar массовой смены статуса для выбранных сделок (редизайн 2026-05-17).
* Только смена статуса — корзина/экспорт убраны (экспорт — панель по датам).
*/
import type { MockDeal } from '../../composables/mockDeals';
import type { LeadStatus } from '../../composables/leadStatuses';
defineProps<{
selectedCount: number;
trashMode: boolean;
statusMenuOpen: boolean;
leadStatuses: LeadStatus[];
}>();
@@ -22,9 +15,6 @@ defineProps<{
defineEmits<{
'update:statusMenuOpen': [value: boolean];
'apply-status': [slug: MockDeal['statusSlug']];
'apply-export': [];
'request-delete': [];
'apply-restore-trash': [];
'clear-selected': [];
}>();
</script>
@@ -39,73 +29,38 @@ defineEmits<{
data-testid="bulk-bar"
>
<div class="bulk-bar-inner">
<span class="bulk-count">
Выбрано <span class="num">{{ selectedCount }}</span>
</span>
<span class="bulk-count">Выбрано <span class="num">{{ selectedCount }}</span></span>
<v-spacer />
<!-- В trash-mode только Восстановить; в обычном режиме полный набор. -->
<v-btn
v-if="trashMode"
variant="tonal"
color="success"
size="small"
prepend-icon="mdi-restore"
data-testid="bulk-restore-trash-btn"
@click="$emit('apply-restore-trash')"
<v-menu
:model-value="statusMenuOpen"
:close-on-content-click="false"
@update:model-value="(v: boolean) => $emit('update:statusMenuOpen', v)"
>
Восстановить
</v-btn>
<template v-if="!trashMode">
<v-menu
:model-value="statusMenuOpen"
:close-on-content-click="false"
@update:model-value="(v: boolean) => $emit('update:statusMenuOpen', v)"
>
<template #activator="{ props: menuProps }">
<v-btn
v-bind="menuProps"
variant="tonal"
size="small"
prepend-icon="mdi-tag-arrow-right"
data-testid="bulk-status-btn"
>
Сменить статус
</v-btn>
</template>
<v-list density="compact" max-height="320" min-width="240">
<v-list-item
v-for="s in leadStatuses"
:key="s.slug"
:data-testid="`bulk-status-item-${s.slug}`"
@click="$emit('apply-status', s.slug)"
>
<template #prepend>
<span class="status-dot" :style="{ background: s.colorHex }" />
</template>
<v-list-item-title>{{ s.nameRu }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-btn
variant="tonal"
size="small"
prepend-icon="mdi-download"
data-testid="bulk-export-btn"
@click="$emit('apply-export')"
>
Экспорт
</v-btn>
<v-btn
variant="tonal"
color="error"
size="small"
prepend-icon="mdi-trash-can-outline"
data-testid="bulk-delete-btn"
@click="$emit('request-delete')"
>
Удалить
</v-btn>
</template>
<template #activator="{ props: menuProps }">
<v-btn
v-bind="menuProps"
variant="tonal"
size="small"
prepend-icon="mdi-tag-arrow-right"
data-testid="bulk-status-btn"
>
Сменить статус
</v-btn>
</template>
<v-list density="compact" max-height="320" min-width="240">
<v-list-item
v-for="s in leadStatuses"
:key="s.slug"
:data-testid="`bulk-status-item-${s.slug}`"
@click="$emit('apply-status', s.slug)"
>
<template #prepend>
<span class="status-dot" :style="{ background: s.colorHex }" />
</template>
<v-list-item-title>{{ s.nameRu }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-btn
icon="mdi-close"
variant="text"
@@ -123,7 +78,6 @@ defineEmits<{
font-feature-settings: 'tnum';
font-weight: 500;
}
.status-dot {
display: inline-block;
width: 6px;
@@ -131,7 +85,6 @@ defineEmits<{
border-radius: 50%;
margin-right: 6px;
}
.bulk-bar {
position: sticky;
top: 0;
+48
View File
@@ -0,0 +1,48 @@
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
import DealsBulkBar from '../../resources/js/components/deals/DealsBulkBar.vue';
const vuetify = createVuetify();
const leadStatuses = [
{ slug: 'new', nameRu: 'Новые', isSystem: true, sortOrder: 1, colorHex: '#3B82F6' },
{ slug: 'viewed', nameRu: 'Просмотрено', isSystem: true, sortOrder: 2, colorHex: '#8B5CF6' },
];
describe('DealsBulkBar', () => {
it('скрыт при selectedCount=0', () => {
const w = mount(DealsBulkBar, {
props: { selectedCount: 0, statusMenuOpen: false, leadStatuses },
global: { plugins: [vuetify] },
});
expect(w.find('[data-testid="bulk-bar"]').exists()).toBe(false);
});
it('виден при selectedCount>0 и показывает количество', () => {
const w = mount(DealsBulkBar, {
props: { selectedCount: 3, statusMenuOpen: false, leadStatuses },
global: { plugins: [vuetify] },
});
const bar = w.find('[data-testid="bulk-bar"]');
expect(bar.exists()).toBe(true);
expect(bar.text()).toContain('3');
});
it('НЕ содержит кнопок Экспорт/Удалить', () => {
const w = mount(DealsBulkBar, {
props: { selectedCount: 2, statusMenuOpen: false, leadStatuses },
global: { plugins: [vuetify] },
});
expect(w.find('[data-testid="bulk-export-btn"]').exists()).toBe(false);
expect(w.find('[data-testid="bulk-delete-btn"]').exists()).toBe(false);
});
it('✕ эмитит clear-selected', async () => {
const w = mount(DealsBulkBar, {
props: { selectedCount: 2, statusMenuOpen: false, leadStatuses },
global: { plugins: [vuetify] },
});
await w.find('[data-testid="bulk-clear-btn"]').trigger('click');
expect(w.emitted('clear-selected')).toBeTruthy();
});
});