95bba384a1
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
148 lines
4.9 KiB
Vue
148 lines
4.9 KiB
Vue
<template>
|
|
<div class="projects-view">
|
|
<div class="d-flex justify-space-between align-center mb-4">
|
|
<h1 class="text-h4">Проекты</h1>
|
|
<v-btn color="primary" prepend-icon="mdi-plus" @click="openCreate">+ Создать проект</v-btn>
|
|
</div>
|
|
|
|
<div class="d-flex gap-3 mb-4">
|
|
<v-select
|
|
v-model="store.filters.signal_type"
|
|
:items="typeFilters"
|
|
label="Тип"
|
|
clearable
|
|
density="comfortable"
|
|
style="max-width: 180px"
|
|
hide-details
|
|
@update:model-value="store.fetch()"
|
|
/>
|
|
<v-select
|
|
v-model="store.filters.status"
|
|
:items="statusFilters"
|
|
label="Статус"
|
|
clearable
|
|
density="comfortable"
|
|
style="max-width: 180px"
|
|
hide-details
|
|
@update:model-value="store.fetch()"
|
|
/>
|
|
<v-text-field
|
|
v-model="store.filters.search"
|
|
label="Поиск"
|
|
prepend-inner-icon="mdi-magnify"
|
|
density="comfortable"
|
|
hide-details
|
|
style="max-width: 240px"
|
|
@input="onSearchDebounced"
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
v-if="!store.loading && store.items.length > 0"
|
|
class="projects-toolbar d-flex align-center gap-3 mb-3"
|
|
>
|
|
<v-checkbox
|
|
data-testid="select-all-toggle"
|
|
:model-value="store.selectAllByFilter"
|
|
:indeterminate="!store.selectAllByFilter && store.selectedIds.size > 0"
|
|
hide-details
|
|
density="compact"
|
|
@change="onToggleSelectAll"
|
|
/>
|
|
<span class="text-body-2">Выбрано: {{ store.selectedIds.size }} из {{ store.total }} (по текущим фильтрам)</span>
|
|
</div>
|
|
|
|
<div v-if="store.loading" class="text-center py-8">
|
|
<v-progress-circular indeterminate color="primary" />
|
|
</div>
|
|
<div v-else-if="store.items.length === 0" class="text-center py-12 text-medium-emphasis">
|
|
Нет проектов. Создайте первый — кнопка справа сверху.
|
|
</div>
|
|
<div v-else class="projects-grid">
|
|
<ProjectCard
|
|
v-for="project in store.items"
|
|
:key="project.id"
|
|
:project="project"
|
|
:selected="store.selectedIds.has(project.id)"
|
|
@toggle-select="store.toggleSelect"
|
|
@edit="openEdit"
|
|
@toggle-active="store.toggleActive"
|
|
@sync-now="(p: Project) => store.syncNow(p.id)"
|
|
@archive="(p: Project) => store.archive(p.id)"
|
|
/>
|
|
</div>
|
|
|
|
<BulkActionsBar v-if="store.selectedIds.size > 0" />
|
|
<NewProjectDialog v-model="createOpen" mode="create" @saved="store.fetch()" />
|
|
<EditProjectDialog v-model="editOpen" :project="editing" @saved="store.fetch()" />
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
|
import { useProjectsStore, type Project } from '../stores/projectsStore';
|
|
import ProjectCard from '../components/projects/ProjectCard.vue';
|
|
import BulkActionsBar from '../components/projects/BulkActionsBar.vue';
|
|
import NewProjectDialog from './projects/NewProjectDialog.vue';
|
|
import EditProjectDialog from './projects/EditProjectDialog.vue';
|
|
|
|
const store = useProjectsStore();
|
|
const createOpen = ref(false);
|
|
const editOpen = ref(false);
|
|
const editing = ref<Project | null>(null);
|
|
|
|
const typeFilters = [
|
|
{ title: 'Сайт', value: 'site' },
|
|
{ title: 'Звонок', value: 'call' },
|
|
{ title: 'СМС', value: 'sms' },
|
|
];
|
|
|
|
const statusFilters = [
|
|
{ title: 'Активные', value: 'active' },
|
|
{ title: 'На паузе', value: 'paused' },
|
|
{ title: 'Архивные', value: 'archived' },
|
|
];
|
|
|
|
let searchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
function onSearchDebounced() {
|
|
if (searchTimer) clearTimeout(searchTimer);
|
|
searchTimer = setTimeout(() => store.fetch(), 300);
|
|
}
|
|
|
|
function openCreate() {
|
|
createOpen.value = true;
|
|
}
|
|
function openEdit(project: Project) {
|
|
editing.value = project;
|
|
editOpen.value = true;
|
|
}
|
|
|
|
function onToggleSelectAll(value: boolean | null) {
|
|
if (value) {
|
|
store.selectAllByFilter = true;
|
|
store.items.forEach((p: Project) => store.selectedIds.add(p.id));
|
|
} else {
|
|
store.selectAllByFilter = false;
|
|
store.clearSelection();
|
|
}
|
|
}
|
|
|
|
watch(
|
|
() => store.pendingIds.size,
|
|
(size) => {
|
|
if (size > 0) store.startPolling();
|
|
},
|
|
);
|
|
|
|
onMounted(store.fetch);
|
|
onUnmounted(() => store.stopPolling());
|
|
</script>
|
|
|
|
<style scoped>
|
|
.projects-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
gap: 16px;
|
|
}
|
|
</style>
|