feat(projects): информационный баннер о сроке изменений до 18:00 МСК
Закрывается крестиком, закрытие запоминается в localStorage. Чисто фронтенд (информация, без блокировок, без бэкенда). +3 Vitest. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,31 @@
|
||||
<v-btn color="primary" prepend-icon="mdi-plus" @click="openCreate">+ Создать проект</v-btn>
|
||||
</div>
|
||||
|
||||
<v-alert
|
||||
v-if="showCutoffBanner"
|
||||
data-testid="cutoff-banner"
|
||||
type="info"
|
||||
variant="tonal"
|
||||
border="start"
|
||||
class="mb-4"
|
||||
>
|
||||
<div class="d-flex justify-space-between align-start gap-2">
|
||||
<span>
|
||||
Важно: изменения по проектам (добавление, удаление, лимиты, рабочие дни, регионы)
|
||||
вносите <strong>до 18:00 МСК</strong>. Изменения после 18:00 применяются при следующей
|
||||
синхронизации — на следующий день.
|
||||
</span>
|
||||
<v-btn
|
||||
data-testid="cutoff-banner-close"
|
||||
icon="mdi-close"
|
||||
size="x-small"
|
||||
variant="text"
|
||||
aria-label="Скрыть уведомление"
|
||||
@click="dismissCutoffBanner"
|
||||
/>
|
||||
</div>
|
||||
</v-alert>
|
||||
|
||||
<div class="d-flex gap-3 mb-4">
|
||||
<v-select
|
||||
v-model="store.filters.signal_type"
|
||||
@@ -101,6 +126,15 @@ const createOpen = ref(false);
|
||||
const editOpen = ref(false);
|
||||
const editing = ref<Project | null>(null);
|
||||
|
||||
// Информационный баннер о сроке внесения изменений (синхронизация с поставщиком в 18:00 МСК).
|
||||
// Закрытие запоминается, чтобы не показывать повторно.
|
||||
const CUTOFF_BANNER_KEY = 'projects.cutoffBannerDismissed';
|
||||
const showCutoffBanner = ref(localStorage.getItem(CUTOFF_BANNER_KEY) !== '1');
|
||||
function dismissCutoffBanner(): void {
|
||||
showCutoffBanner.value = false;
|
||||
localStorage.setItem(CUTOFF_BANNER_KEY, '1');
|
||||
}
|
||||
|
||||
const singleSelectedProject = computed<Project | null>(() => {
|
||||
if (store.selectedIds.size !== 1) return null;
|
||||
const [id] = store.selectedIds;
|
||||
|
||||
@@ -239,3 +239,36 @@ describe('ProjectsView × ProjectDetailsDrawer integration', () => {
|
||||
expect(wrapper.find('.projects-view').classes()).not.toContain('has-drawer');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ProjectsView 18:00 cutoff banner', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
(axios.get as unknown as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
data: { data: [], meta: { total: 0, current_page: 1, per_page: 20 } },
|
||||
});
|
||||
});
|
||||
|
||||
it('shows the cutoff banner with the 18:00 deadline by default', async () => {
|
||||
const wrapper = factory();
|
||||
await flushPromises();
|
||||
const banner = wrapper.find('[data-testid="cutoff-banner"]');
|
||||
expect(banner.exists()).toBe(true);
|
||||
expect(banner.text()).toContain('18:00');
|
||||
});
|
||||
|
||||
it('hides the banner after the close button and remembers it in localStorage', async () => {
|
||||
const wrapper = factory();
|
||||
await flushPromises();
|
||||
await wrapper.find('[data-testid="cutoff-banner-close"]').trigger('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.find('[data-testid="cutoff-banner"]').exists()).toBe(false);
|
||||
expect(localStorage.getItem('projects.cutoffBannerDismissed')).toBe('1');
|
||||
});
|
||||
|
||||
it('stays hidden on next mount when previously dismissed', async () => {
|
||||
localStorage.setItem('projects.cutoffBannerDismissed', '1');
|
||||
const wrapper = factory();
|
||||
await flushPromises();
|
||||
expect(wrapper.find('[data-testid="cutoff-banner"]').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user