/** * Contact Import Store */ import { computed, ref } from 'vue' import { defineStore } from 'pinia' import { entityService } from '../services/entityService' import type { CollectionIdentifier } from '../types/common' import type { EntityImportResponse } from '../types/entity' import type { ImportCounters, ImportFileAdd, ImportFileEntry, ImportFileOptions, ImportSession, ImportSessionStage } from '../types/import' const RECENT_RESULTS_CAP = 25 const emptyCounters = (): ImportCounters => ({ discovered: 0, processed: 0, created: 0, updated: 0, exists: 0, error: 0 }) export const useImportStore = defineStore('chronoImportStore', () => { // State const nextId = ref(0) const files = ref([]) const stage = ref('idle') const running = ref(false) const sessions = ref>({}) const order = ref([]) // Computed const totals = computed(() => order.value.reduce((total, id) => { const counters = sessions.value[id]?.counters if (counters) Object.keys(total).forEach(key => { total[key as keyof ImportCounters] += counters[key as keyof ImportCounters] }) return total }, emptyCounters())) // Actions function addFile(file: ImportFileAdd): number { const id = nextId.value++ files.value.push({ file: { id, ...file }, collectionId: null, options: { supersede: false } }) return id } function removeAllFiles() { files.value = [] } function reset() { stage.value = 'idle'; running.value = false; sessions.value = {}; order.value = []; } function entryFor(id: number) { return files.value.find(entry => entry.file.id === id) } /** Set the destination collection for a queued file. */ function setCollectionForFile(id: number, collectionId: CollectionIdentifier | null) { const entry = entryFor(id); if (entry) entry.collectionId = collectionId; } /** Merge option changes for a queued file without dropping untouched keys. */ function setOptionsForFile(id: number, options: Partial) { const entry = entryFor(id); if (entry) entry.options = { ...entry.options, ...options }; } async function startImport(): Promise { const entries = [...files.value] sessions.value = {} order.value = entries.map(entry => entry.file.id) for (const entry of entries) sessions.value[entry.file.id] = { fileId: entry.file.id, fileName: entry.file.name, targetIdentifier: entry.collectionId, status: 'pending', counters: emptyCounters(), recentResults: [], lastError: null, } running.value = true stage.value = 'importing' try { for (const entry of entries) { const session = sessions.value[entry.file.id]! if (!entry.collectionId) throw new Error(`No calendar selected for "${entry.file.name}"`) session.status = 'importing' try { await entityService.import( { target: entry.collectionId, data: entry.file.contents, options: entry.options }, (object: EntityImportResponse) => { session.counters.processed++ session.counters[object.disposition]++ session.recentResults = [object, ...session.recentResults].slice(0, RECENT_RESULTS_CAP) }, expected => { session.counters.discovered = expected }, ) session.status = session.counters.error ? 'error' : 'completed' } catch (error) { session.status = 'error' session.lastError = error instanceof Error ? error.message : String(error) throw error } } stage.value = 'completed' return totals.value } catch (error) { stage.value = 'error' throw error } finally { running.value = false } } return { files, stage, running, sessions, order, totals, addFile, removeAllFiles, reset, setCollectionForFile, setOptionsForFile, startImport } })