feat(projects-ui): replace archive with delete, drop archived filter
- Remove archived_at from Project interface; rename store.archive → store.del
- BulkActionsBar: archive button → delete (testid, icon, confirm text)
- ProjectCard: archive menu item → delete (emit + icon)
- ProjectDetailsDrawer: confirm text + store.del call
- ProjectsView: @delete binding, remove 'Архивные' status filter entry
- vuetify.ts: add mdi-delete → Trash2 mapping
- All specs/stories updated: archived_at removed, archive → del renamed
- New test: del() calls DELETE /api/projects/{id}
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -29,11 +29,11 @@
|
||||
|
||||
<v-btn
|
||||
color="error"
|
||||
prepend-icon="mdi-archive"
|
||||
data-testid="bulk-archive"
|
||||
@click="confirmAndRun('archive')"
|
||||
prepend-icon="mdi-delete"
|
||||
data-testid="bulk-delete"
|
||||
@click="confirmAndRun('delete')"
|
||||
>
|
||||
Архивировать
|
||||
Удалить
|
||||
</v-btn>
|
||||
|
||||
<v-spacer />
|
||||
@@ -92,11 +92,10 @@ const skipToastText = ref('');
|
||||
const messages: Record<string, string> = {
|
||||
pause: 'Приостановить выбранные проекты?',
|
||||
resume: 'Возобновить выбранные проекты?',
|
||||
archive:
|
||||
'Архивировать выбранные проекты?\nДействие необратимо в Plan 5 (восстановление потребует ручного запроса).',
|
||||
delete: 'Удалить выбранные проекты? Действие необратимо. Проекты со сделками будут пропущены.',
|
||||
};
|
||||
|
||||
async function confirmAndRun(action: 'pause' | 'resume' | 'archive') {
|
||||
async function confirmAndRun(action: 'pause' | 'resume' | 'delete') {
|
||||
if (!window.confirm(messages[action])) return;
|
||||
await runBulk({ action });
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ const base = {
|
||||
daily_limit_target: 50,
|
||||
delivered_today: 32,
|
||||
is_active: true,
|
||||
archived_at: null,
|
||||
sync_status: 'ok' as const,
|
||||
};
|
||||
|
||||
|
||||
@@ -48,9 +48,9 @@
|
||||
<template #prepend><v-icon>mdi-refresh</v-icon></template>
|
||||
<v-list-item-title>Синхронизировать</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="$emit('archive', project)">
|
||||
<template #prepend><v-icon>mdi-archive</v-icon></template>
|
||||
<v-list-item-title>Архивировать</v-list-item-title>
|
||||
<v-list-item @click="$emit('delete', project)">
|
||||
<template #prepend><v-icon>mdi-delete</v-icon></template>
|
||||
<v-list-item-title>Удалить</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
@@ -97,7 +97,7 @@ defineEmits<{
|
||||
edit: [project: Project];
|
||||
'toggle-active': [project: Project];
|
||||
'sync-now': [project: Project];
|
||||
archive: [project: Project];
|
||||
delete: [project: Project];
|
||||
}>();
|
||||
|
||||
const typeLabel = computed(() => ({ site: 'Сайт', call: 'Звонок', sms: 'СМС' })[props.project.signal_type]);
|
||||
|
||||
@@ -63,10 +63,10 @@ async function onPause(): Promise<void> {
|
||||
async function onDelete(): Promise<void> {
|
||||
if (!props.project) return;
|
||||
const ok = window.confirm(
|
||||
'Архивировать проект? Действие необратимо в Plan 5 (восстановление потребует ручного запроса).',
|
||||
'Удалить проект? Действие необратимо. Если по проекту есть сделки — удаление будет заблокировано.',
|
||||
);
|
||||
if (!ok) return;
|
||||
await store.archive(props.project.id);
|
||||
await store.del(props.project.id);
|
||||
emit('close');
|
||||
}
|
||||
|
||||
|
||||
@@ -168,6 +168,7 @@ const lucideMap: Record<string, Component> = {
|
||||
'mdi-content-save-outline': Save,
|
||||
'mdi-credit-card-outline': CreditCard,
|
||||
'mdi-currency-rub': RussianRuble,
|
||||
'mdi-delete': Trash2,
|
||||
'mdi-delete-outline': Trash2,
|
||||
'mdi-dots-vertical': MoreVertical,
|
||||
'mdi-download': Download,
|
||||
|
||||
@@ -13,7 +13,6 @@ export interface Project {
|
||||
delivered_today: number;
|
||||
delivered_in_month?: number;
|
||||
is_active: boolean;
|
||||
archived_at: string | null;
|
||||
region_mask?: number;
|
||||
region_mode?: string;
|
||||
regions?: number[]; // Plan 6 — subject codes 1..89; пустой массив = вся РФ
|
||||
@@ -65,7 +64,7 @@ export const useProjectsStore = defineStore('projects', () => {
|
||||
return data.data;
|
||||
}
|
||||
|
||||
async function archive(id: number) {
|
||||
async function del(id: number) {
|
||||
await axios.delete(`/api/projects/${id}`);
|
||||
await fetch();
|
||||
}
|
||||
@@ -94,7 +93,7 @@ export const useProjectsStore = defineStore('projects', () => {
|
||||
selectedIds.value.clear();
|
||||
}
|
||||
|
||||
async function bulkAction(action: 'pause' | 'resume' | 'archive') {
|
||||
async function bulkAction(action: 'pause' | 'resume' | 'delete') {
|
||||
const ids = Array.from(selectedIds.value);
|
||||
if (!ids.length) return;
|
||||
await axios.post('/api/projects/bulk', { action, ids });
|
||||
@@ -103,7 +102,7 @@ export const useProjectsStore = defineStore('projects', () => {
|
||||
}
|
||||
|
||||
interface BulkPayload {
|
||||
action: 'pause' | 'resume' | 'archive' | 'update_regions' | 'update_days' | 'update_limit';
|
||||
action: 'pause' | 'resume' | 'delete' | 'update_regions' | 'update_days' | 'update_limit';
|
||||
add?: number;
|
||||
remove?: number;
|
||||
// Plan 6.5 — update_regions оперирует кодами субъектов (1..89), не bitmask ФО.
|
||||
@@ -200,7 +199,7 @@ export const useProjectsStore = defineStore('projects', () => {
|
||||
fetch,
|
||||
create,
|
||||
update,
|
||||
archive,
|
||||
del,
|
||||
syncNow,
|
||||
toggleActive,
|
||||
toggleSelect,
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
@edit="openEdit"
|
||||
@toggle-active="store.toggleActive"
|
||||
@sync-now="(p: Project) => store.syncNow(p.id)"
|
||||
@archive="(p: Project) => store.archive(p.id)"
|
||||
@delete="(p: Project) => store.del(p.id)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -123,7 +123,6 @@ const typeFilters = [
|
||||
const statusFilters = [
|
||||
{ title: 'Активные', value: 'active' },
|
||||
{ title: 'На паузе', value: 'paused' },
|
||||
{ title: 'Архивные', value: 'archived' },
|
||||
];
|
||||
|
||||
let searchTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
@@ -25,7 +25,6 @@ const sampleProject = {
|
||||
daily_limit_target: 50,
|
||||
delivered_today: 12,
|
||||
is_active: true,
|
||||
archived_at: null,
|
||||
sync_status: 'ok' as const,
|
||||
region_mask: 0,
|
||||
region_mode: 'include' as const,
|
||||
|
||||
@@ -169,7 +169,7 @@ describe('BulkActionsBar — extended', () => {
|
||||
expect((wrapper.vm as unknown as { regionsOpen: boolean }).regionsOpen).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps existing pause/resume/archive buttons', async () => {
|
||||
it('keeps existing pause/resume/delete buttons', async () => {
|
||||
setActivePinia(createPinia());
|
||||
vi.mocked(axios.post).mockResolvedValue({ data: { updated: 1, skipped: [], warnings: [] } });
|
||||
vi.mocked(axios.get).mockResolvedValue({ data: { data: [], meta: { total: 0 } } });
|
||||
@@ -184,6 +184,6 @@ describe('BulkActionsBar — extended', () => {
|
||||
});
|
||||
expect(wrapper.find('[data-testid="bulk-pause"]').exists()).toBe(true);
|
||||
expect(wrapper.find('[data-testid="bulk-resume"]').exists()).toBe(true);
|
||||
expect(wrapper.find('[data-testid="bulk-archive"]').exists()).toBe(true);
|
||||
expect(wrapper.find('[data-testid="bulk-delete"]').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,7 +23,6 @@ const sampleProject = {
|
||||
daily_limit_target: 10,
|
||||
delivered_today: 0,
|
||||
is_active: true,
|
||||
archived_at: null,
|
||||
sync_status: 'ok' as const,
|
||||
region_mask: 0,
|
||||
region_mode: 'include',
|
||||
|
||||
@@ -12,7 +12,6 @@ const baseProject = {
|
||||
daily_limit_target: 50,
|
||||
delivered_today: 32,
|
||||
is_active: true,
|
||||
archived_at: null,
|
||||
sync_status: 'ok' as const,
|
||||
};
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ const sample = {
|
||||
is_active: true,
|
||||
daily_limit_target: 50,
|
||||
delivered_today: 12,
|
||||
archived_at: null,
|
||||
sync_status: 'ok' as const,
|
||||
};
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ const sampleProject: Project = {
|
||||
daily_limit_target: 30,
|
||||
delivered_today: 0,
|
||||
is_active: true,
|
||||
archived_at: null,
|
||||
region_mask: 0,
|
||||
region_mode: 'include',
|
||||
regions: [],
|
||||
@@ -152,10 +151,10 @@ describe('ProjectDetailsDrawer', () => {
|
||||
expect(wrapper.get('[data-testid="pdd-pause"]').text()).toContain('Приостановить');
|
||||
});
|
||||
|
||||
it('Delete: confirm=true → archive + close emit', async () => {
|
||||
it('Delete: confirm=true → del + close emit', async () => {
|
||||
const wrapper = mount(ProjectDetailsDrawer, { props: { project: sampleProject } });
|
||||
const store = useProjectsStore();
|
||||
const spy = vi.spyOn(store, 'archive').mockResolvedValueOnce(undefined);
|
||||
const spy = vi.spyOn(store, 'del').mockResolvedValueOnce(undefined);
|
||||
vi.stubGlobal('confirm', () => true);
|
||||
|
||||
await wrapper.get('[data-testid="pdd-delete"]').trigger('click');
|
||||
@@ -166,10 +165,10 @@ describe('ProjectDetailsDrawer', () => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('Delete: confirm=false → no archive, no close', async () => {
|
||||
it('Delete: confirm=false → no del, no close', async () => {
|
||||
const wrapper = mount(ProjectDetailsDrawer, { props: { project: sampleProject } });
|
||||
const store = useProjectsStore();
|
||||
const spy = vi.spyOn(store, 'archive').mockResolvedValueOnce(undefined);
|
||||
const spy = vi.spyOn(store, 'del').mockResolvedValueOnce(undefined);
|
||||
vi.stubGlobal('confirm', () => false);
|
||||
|
||||
await wrapper.get('[data-testid="pdd-delete"]').trigger('click');
|
||||
|
||||
@@ -52,7 +52,6 @@ describe('ProjectsView', () => {
|
||||
daily_limit_target: 10,
|
||||
delivered_today: 0,
|
||||
is_active: true,
|
||||
archived_at: null,
|
||||
sync_status: 'ok',
|
||||
},
|
||||
],
|
||||
@@ -82,7 +81,6 @@ describe('ProjectsView', () => {
|
||||
daily_limit_target: 10,
|
||||
delivered_today: 0,
|
||||
is_active: true,
|
||||
archived_at: null,
|
||||
sync_status: 'ok',
|
||||
},
|
||||
{
|
||||
@@ -93,7 +91,6 @@ describe('ProjectsView', () => {
|
||||
daily_limit_target: 10,
|
||||
delivered_today: 0,
|
||||
is_active: true,
|
||||
archived_at: null,
|
||||
sync_status: 'ok',
|
||||
},
|
||||
],
|
||||
@@ -127,7 +124,6 @@ describe('ProjectsView × ProjectDetailsDrawer integration', () => {
|
||||
daily_limit_target: 10,
|
||||
delivered_today: 0,
|
||||
is_active: true,
|
||||
archived_at: null,
|
||||
sync_status: 'ok' as const,
|
||||
};
|
||||
const projectB = {
|
||||
@@ -138,7 +134,6 @@ describe('ProjectsView × ProjectDetailsDrawer integration', () => {
|
||||
daily_limit_target: 10,
|
||||
delivered_today: 0,
|
||||
is_active: true,
|
||||
archived_at: null,
|
||||
sync_status: 'ok' as const,
|
||||
};
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ const mountView = async () => {
|
||||
daily_limit_target: 100,
|
||||
delivered_today: 0,
|
||||
is_active: true,
|
||||
archived_at: null,
|
||||
region_mask: 1,
|
||||
region_mode: 'include',
|
||||
delivery_days_mask: 31,
|
||||
@@ -34,7 +33,6 @@ const mountView = async () => {
|
||||
daily_limit_target: 100,
|
||||
delivered_today: 0,
|
||||
is_active: true,
|
||||
archived_at: null,
|
||||
region_mask: 1,
|
||||
region_mode: 'include',
|
||||
delivery_days_mask: 31,
|
||||
|
||||
@@ -66,6 +66,14 @@ describe('projectsStore (no polling)', () => {
|
||||
expect(store.selectedIds.has(1)).toBe(false);
|
||||
});
|
||||
|
||||
it('del() calls DELETE /api/projects/{id}', async () => {
|
||||
const store = useProjectsStore();
|
||||
vi.spyOn(axios, 'delete').mockResolvedValue({ data: null });
|
||||
vi.spyOn(axios, 'get').mockResolvedValue({ data: { data: [], meta: { total: 0 } } });
|
||||
await store.del(7);
|
||||
expect(axios.delete).toHaveBeenCalledWith('/api/projects/7');
|
||||
});
|
||||
|
||||
it('bulkAction sends array of ids and clears selection', async () => {
|
||||
(axios.post as unknown as ReturnType<typeof vi.fn>).mockResolvedValue({ data: { updated: 2 } });
|
||||
(axios.get as unknown as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
@@ -104,7 +112,6 @@ describe('projectsStore (polling)', () => {
|
||||
daily_limit_target: 10,
|
||||
delivered_today: 0,
|
||||
is_active: true,
|
||||
archived_at: null,
|
||||
region_mask: 0,
|
||||
region_mode: 'include',
|
||||
delivery_days_mask: 127,
|
||||
@@ -133,7 +140,6 @@ describe('projectsStore (polling)', () => {
|
||||
daily_limit_target: 10,
|
||||
delivered_today: 0,
|
||||
is_active: true,
|
||||
archived_at: null,
|
||||
region_mask: 0,
|
||||
region_mode: 'include',
|
||||
delivery_days_mask: 127,
|
||||
|
||||
Reference in New Issue
Block a user