import { defineStore } from 'pinia' import { ref } from 'vue' import { invoke } from '@tauri-apps/api/core' import { useToastStore } from './toast' import { useTimerStore } from './timer' import { handleInvokeError } from '../utils/errorHandler' export interface RecurringEntry { id?: number project_id: number task_id?: number description?: string duration: number recurrence_rule: string // "daily", "weekdays", "weekly:mon,wed,fri", "monthly:15" time_of_day: string // "09:00" mode: 'auto_create' | 'prompt' | 'prefill_timer' enabled?: number // 1 = enabled, 0 = disabled last_triggered?: string // ISO date string } export const useRecurringStore = defineStore('recurring', () => { const entries = ref([]) const pendingPrompt = ref(null) const snoozedUntil = ref>(new Map()) async function fetchEntries() { try { entries.value = await invoke('get_recurring_entries') } catch (e) { handleInvokeError(e, 'Failed to fetch recurring entries', () => fetchEntries()) } } async function createEntry(entry: RecurringEntry) { try { const id = await invoke('create_recurring_entry', { entry }) await fetchEntries() return id } catch (e) { handleInvokeError(e, 'Failed to create recurring entry') throw e } } async function updateEntry(entry: RecurringEntry) { try { await invoke('update_recurring_entry', { entry }) await fetchEntries() } catch (e) { handleInvokeError(e, 'Failed to update recurring entry') throw e } } async function deleteEntry(id: number) { try { await invoke('delete_recurring_entry', { id }) await fetchEntries() } catch (e) { handleInvokeError(e, 'Failed to delete recurring entry') throw e } } async function toggleEnabled(entry: RecurringEntry) { const updated = { ...entry, enabled: entry.enabled === 1 ? 0 : 1 } await updateEntry(updated) } // Core recurrence check engine async function checkRecurrences() { if (entries.value.length === 0) return const now = new Date() const todayStr = now.toISOString().split('T')[0] // YYYY-MM-DD const currentTime = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}` const dayOfWeek = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'][now.getDay()] const dayOfMonth = now.getDate() for (const entry of entries.value) { if (entry.enabled === 0) continue // Check if already triggered today if (entry.last_triggered) { const lastDate = entry.last_triggered.split('T')[0] if (lastDate === todayStr) continue } // Check if current time has passed the scheduled time if (currentTime < entry.time_of_day) continue // Check recurrence rule const rule = entry.recurrence_rule let shouldFire = false if (rule === 'daily') { shouldFire = true } else if (rule === 'weekdays') { shouldFire = !['sat', 'sun'].includes(dayOfWeek) } else if (rule.startsWith('weekly:')) { const days = rule.replace('weekly:', '').split(',') shouldFire = days.includes(dayOfWeek) } else if (rule.startsWith('monthly:')) { const targetDay = parseInt(rule.replace('monthly:', '')) shouldFire = dayOfMonth === targetDay } if (!shouldFire) continue // Check snooze: skip if snoozed and not yet expired if (entry.id && snoozedUntil.value.has(entry.id)) { if (Date.now() < snoozedUntil.value.get(entry.id)!) continue snoozedUntil.value.delete(entry.id) } // Fire the recurrence based on mode await fireRecurrence(entry) } } async function fireRecurrence(entry: RecurringEntry) { const toastStore = useToastStore() if (entry.mode === 'auto_create') { // Silently create a time entry const now = new Date() const startTime = new Date(now) const [h, m] = entry.time_of_day.split(':').map(Number) startTime.setHours(h, m, 0, 0) const endTime = new Date(startTime.getTime() + entry.duration * 1000) try { await invoke('create_time_entry', { entry: { project_id: entry.project_id, task_id: entry.task_id || null, description: entry.description || null, start_time: startTime.toISOString(), end_time: endTime.toISOString(), duration: entry.duration, billable: 1, } }) toastStore.success('Recurring entry created') } catch (e) { console.error('Failed to auto-create recurring entry:', e) } } else if (entry.mode === 'prompt') { pendingPrompt.value = entry } else if (entry.mode === 'prefill_timer') { // Pre-fill the timer with project/task/description const timerStore = useTimerStore() if (timerStore.isStopped) { timerStore.setProject(entry.project_id) if (entry.task_id) timerStore.setTask(entry.task_id) if (entry.description) timerStore.setDescription(entry.description) toastStore.info('Timer pre-filled from recurring entry') } } // Mark as triggered today (skip for prompt mode - user action handlers do it) if (entry.mode !== 'prompt' && entry.id) { await markTriggered(entry.id) } } async function markTriggered(id: number) { try { await invoke('update_recurring_last_triggered', { id, lastTriggered: new Date().toISOString(), }) const idx = entries.value.findIndex(e => e.id === id) if (idx !== -1) { entries.value[idx].last_triggered = new Date().toISOString() } } catch (e) { console.error('Failed to update last_triggered:', e) } } function snoozePrompt() { const entry = pendingPrompt.value if (entry?.id) { snoozedUntil.value.set(entry.id, Date.now() + 30 * 60 * 1000) } pendingPrompt.value = null } async function confirmPrompt() { if (!pendingPrompt.value) return const entry = pendingPrompt.value const now = new Date() const [h, m] = entry.time_of_day.split(':').map(Number) const startTime = new Date(now) startTime.setHours(h, m, 0, 0) const endTime = new Date(startTime.getTime() + entry.duration * 1000) try { await invoke('create_time_entry', { entry: { project_id: entry.project_id, task_id: entry.task_id || null, description: entry.description || null, start_time: startTime.toISOString(), end_time: endTime.toISOString(), duration: entry.duration, billable: 1, } }) const toastStore = useToastStore() toastStore.success('Recurring entry created') if (entry.id) await markTriggered(entry.id) } catch (e) { handleInvokeError(e, 'Failed to create recurring entry') } pendingPrompt.value = null } async function skipPrompt() { const entry = pendingPrompt.value if (entry?.id) { await markTriggered(entry.id) } pendingPrompt.value = null } return { entries, pendingPrompt, fetchEntries, createEntry, updateEntry, deleteEntry, toggleEnabled, checkRecurrences, snoozePrompt, confirmPrompt, skipPrompt } })