diff --git a/src/App.vue b/src/App.vue index 3c0e59f..a6700ca 100644 --- a/src/App.vue +++ b/src/App.vue @@ -143,6 +143,7 @@ onMounted(async () => { await recurringStore.fetchEntries() recurringStore.checkRecurrences() setInterval(() => recurringStore.checkRecurrences(), 60000) + setInterval(() => onboardingStore.detectCompletions(), 5 * 60000) // Background calendar sync async function syncCalendars() { diff --git a/src/stores/onboarding.ts b/src/stores/onboarding.ts new file mode 100644 index 0000000..f281dcd --- /dev/null +++ b/src/stores/onboarding.ts @@ -0,0 +1,165 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { invoke } from '@tauri-apps/api/core' +import { useSettingsStore } from './settings' + +export type ChecklistItemKey = + | 'create_client' + | 'create_project' + | 'track_entry' + | 'review_entries' + | 'create_invoice' + | 'explore_reports' + +export interface ChecklistItem { + key: ChecklistItemKey + label: string + description: string + route: string + tourId: string + completed: boolean +} + +interface ChecklistState { + dismissed: boolean + items: Record +} + +const DEFAULT_STATE: ChecklistState = { + dismissed: false, + items: { + create_client: { completed: false }, + create_project: { completed: false }, + track_entry: { completed: false }, + review_entries: { completed: false }, + create_invoice: { completed: false }, + explore_reports: { completed: false }, + }, +} + +const ITEM_DEFINITIONS: Omit[] = [ + { key: 'create_client', label: 'Create your first client', description: 'Add a client to bill your work to', route: '/clients', tourId: 'clients' }, + { key: 'create_project', label: 'Create a project', description: 'Organize your work under a project', route: '/projects', tourId: 'projects' }, + { key: 'track_entry', label: 'Track your first time entry', description: 'Start the timer and log some work', route: '/timer', tourId: 'timer' }, + { key: 'review_entries', label: 'Review your entries', description: 'See your tracked time in list or timeline view', route: '/entries', tourId: 'entries' }, + { key: 'create_invoice', label: 'Create an invoice', description: 'Generate an invoice from tracked time', route: '/invoices', tourId: 'invoices' }, + { key: 'explore_reports', label: 'Explore reports', description: 'View hours, profitability, and expense reports', route: '/reports', tourId: 'reports' }, +] + +export const useOnboardingStore = defineStore('onboarding', () => { + const state = ref({ ...DEFAULT_STATE }) + const loaded = ref(false) + + const items = computed(() => + ITEM_DEFINITIONS.map(def => ({ + ...def, + completed: state.value.items[def.key]?.completed || false, + })) + ) + + const completedCount = computed(() => items.value.filter(i => i.completed).length) + const totalCount = computed(() => items.value.length) + const allComplete = computed(() => completedCount.value >= totalCount.value) + const isDismissed = computed(() => state.value.dismissed) + const isVisible = computed(() => loaded.value && !state.value.dismissed && !allComplete.value) + + async function load() { + const settingsStore = useSettingsStore() + const raw = settingsStore.settings.onboarding_checklist + if (raw) { + try { + const parsed = JSON.parse(raw) as ChecklistState + state.value = { ...DEFAULT_STATE, ...parsed, items: { ...DEFAULT_STATE.items, ...parsed.items } } + } catch { + state.value = { ...DEFAULT_STATE } + } + } + + // Auto-detect data-based completions + await detectCompletions() + loaded.value = true + } + + async function detectCompletions() { + try { + const clients = await invoke('get_clients') + if (clients.length > 0) markComplete('create_client') + } catch { /* individual failure - continue */ } + + try { + const projects = await invoke('get_projects') + if (projects.length > 0) markComplete('create_project') + } catch { /* individual failure - continue */ } + + try { + const entries = await invoke('get_time_entries', { startDate: null, endDate: null }) + if (entries.length > 0) markComplete('track_entry') + } catch { /* individual failure - continue */ } + + try { + const invoices = await invoke('get_invoices') + if (invoices.length > 0) markComplete('create_invoice') + } catch { /* individual failure - continue */ } + } + + function markComplete(key: ChecklistItemKey) { + if (!state.value.items[key].completed) { + state.value.items[key].completed = true + persist() + } + } + + function dismiss() { + state.value.dismissed = true + persist() + } + + function restore() { + state.value.dismissed = false + persist() + } + + function reset() { + state.value = { + dismissed: false, + items: { + create_client: { completed: false }, + create_project: { completed: false }, + track_entry: { completed: false }, + review_entries: { completed: false }, + create_invoice: { completed: false }, + explore_reports: { completed: false }, + }, + } + persist() + } + + async function persist() { + const settingsStore = useSettingsStore() + await settingsStore.updateSetting('onboarding_checklist', JSON.stringify(state.value)) + } + + // Called from router guard when user visits specific pages + function onRouteVisit(path: string) { + if (path === '/entries') markComplete('review_entries') + if (path === '/reports') markComplete('explore_reports') + } + + return { + state, + loaded, + items, + completedCount, + totalCount, + allComplete, + isDismissed, + isVisible, + load, + detectCompletions, + markComplete, + dismiss, + restore, + reset, + onRouteVisit, + } +})