import { defineStore } from 'pinia'; import { ref, reactive, watch } from 'vue'; import axios from 'axios'; export interface Project { id: number; name: string; signal_type: 'site' | 'call' | 'sms'; signal_identifier?: string | null; sms_senders?: string[] | null; sms_keyword?: string | null; daily_limit_target: number; delivered_today: number; delivered_in_month?: number; is_active: boolean; archived_at: string | null; region_mask?: number; region_mode?: string; delivery_days_mask?: number; sync_status: 'ok' | 'pending' | 'failed'; last_synced_at?: string | null; } export const useProjectsStore = defineStore('projects', () => { const items = ref([]); const total = ref(0); const filters = reactive({ signal_type: '', status: '', search: '', page: 1, per_page: 20 }); const selectedIds = ref>(new Set()); const pendingIds = ref>(new Set()); const loading = ref(false); const selectAllByFilter = ref(false); // Closure state for polling — kept outside returned store surface. let pollTimeout: ReturnType | null = null; let currentDelay = 5000; const DELAY_OK = 5000; const DELAY_MAX = 30000; async function fetch() { loading.value = true; try { const params: Record = { page: filters.page, per_page: filters.per_page }; if (filters.signal_type) params.signal_type = filters.signal_type; if (filters.status) params.status = filters.status; if (filters.search) params.search = filters.search; const { data } = await axios.get('/api/projects', { params }); items.value = data.data; total.value = data.meta.total; } finally { loading.value = false; } } async function create(payload: Partial) { const { data } = await axios.post('/api/projects', payload); pendingIds.value.add(data.data.id); await fetch(); return data.data; } async function update(id: number, payload: Partial) { const { data } = await axios.patch(`/api/projects/${id}`, payload); await fetch(); return data.data; } async function archive(id: number) { await axios.delete(`/api/projects/${id}`); await fetch(); } async function syncNow(id: number) { await axios.post(`/api/projects/${id}/sync`); pendingIds.value.add(id); await fetch(); } async function toggleActive(project: Project) { await axios.patch(`/api/projects/${project.id}/toggle-active`, { is_active: !project.is_active }); await fetch(); } function toggleSelect(id: number) { selectAllByFilter.value = false; // user opted into manual mode if (selectedIds.value.has(id)) { selectedIds.value.delete(id); } else { selectedIds.value.add(id); } } function clearSelection() { selectedIds.value.clear(); } async function bulkAction(action: 'pause' | 'resume' | 'archive') { const ids = Array.from(selectedIds.value); if (!ids.length) return; await axios.post('/api/projects/bulk', { action, ids }); clearSelection(); await fetch(); } interface BulkPayload { action: 'pause' | 'resume' | 'archive' | 'update_regions' | 'update_days' | 'update_limit'; add?: number; remove?: number; delta?: number; replace?: number; } interface BulkResponse { updated: number; skipped: Array<{ id: number; reason: string }>; warnings: string[]; } async function bulkUpdate(payload: BulkPayload): Promise { const body: Record = { ...payload }; if (selectAllByFilter.value) { const f: Record = {}; if (filters.signal_type) f.signal_type = filters.signal_type; if (filters.status) f.status = filters.status; if (filters.search) f.search = filters.search; body.scope = { filter: f }; } else { body.ids = Array.from(selectedIds.value); } const { data } = await axios.post('/api/projects/bulk', body); clearSelection(); selectAllByFilter.value = false; await fetch(); return data; } // Watch filter changes — clear selection on switch watch( () => [filters.signal_type, filters.status, filters.search] as const, () => { clearSelection(); selectAllByFilter.value = false; }, ); function scheduleNext() { pollTimeout = setTimeout(async () => { pollTimeout = null; if (pendingIds.value.size === 0) { currentDelay = DELAY_OK; return; } try { const ids = Array.from(pendingIds.value).join(','); const { data } = await axios.get<{ data: Project[] }>('/api/projects', { params: { ids } }); for (const project of data.data) { const idx = items.value.findIndex((i) => i.id === project.id); if (idx !== -1) items.value[idx] = project; if (project.sync_status === 'ok' || project.sync_status === 'failed') { pendingIds.value.delete(project.id); } } currentDelay = DELAY_OK; } catch { // Exponential backoff to avoid hammering on transient errors. currentDelay = Math.min(currentDelay * 2, DELAY_MAX); } if (pendingIds.value.size > 0) { scheduleNext(); } }, currentDelay); } function startPolling() { if (pollTimeout !== null) return; scheduleNext(); } function stopPolling() { if (pollTimeout !== null) { clearTimeout(pollTimeout); pollTimeout = null; } currentDelay = DELAY_OK; } return { items, total, filters, selectedIds, pendingIds, loading, selectAllByFilter, fetch, create, update, archive, syncNow, toggleActive, toggleSelect, clearSelection, bulkAction, bulkUpdate, startPolling, stopPolling, }; });