import { createSignal } from "solid-js"; import { createStore } from "solid-js/store"; import { pb } from "@/lib/pocketbase"; import { STORAGE_KEY_PREFIX, TASGRID_DATA_MODE } from "@/lib/app-config"; // Helper to get the current user's personalized favorite tag export const getFavoriteTag = () => { const userId = pb.authStore.model?.id; return userId ? `__${userId}_favorite__` : "__favorite__"; }; export const [now, setNow] = createSignal(Date.now()); // Context: 'mine' is a temporary bootstrap alias for the current user's personal context bucket. export type TaskContext = "mine" | { userId: string, name: string } | { bucketId: string, name: string, isPersonal?: boolean }; export const [activeTaskId, setActiveTaskId] = createSignal(null); export const [activeNoteId, setActiveNoteId] = createSignal(null); export const [currentTaskContext, setCurrentTaskContext] = createSignal("mine"); export type UrgencyLevel = number; export interface TaskShareRef { contextId: string; key: string; kind: "user" | "bucket" | "note"; policy?: "collaborative" | "handoff"; } export interface NoteRef { noteId: string; key: string; } export interface Task { id: string; title: string; startDate: string; dueDate: string; priority: number; status: number; completed: boolean; tags: string[]; labelTags: string[]; shareRefs: TaskShareRef[]; noteRefs: NoteRef[]; ownerId: string | null; createdBy: string | null; updatedBy?: string | null; content?: string; deletedAt?: number | null; created: string; updated: string; recurrence?: { type: "daily" | "weekly" | "monthly"; days?: number[]; dayOfMonth?: number; lastUncompleted?: string; }; size?: number; sharedWith?: Array<{ userId: string; access: "view" | "edit" }>; attachments?: string[]; } export interface TaskTemplate { id: string; name: string; title: string; priority: number; urgency: number; tags: string[]; content: string; } export interface TagDefinition { id: string; name: string; value: number; color?: string; theme?: "light" | "dark"; isUser?: boolean; isBucket?: boolean; } export interface Note { id: string; title: string; key: string; content?: string; tags: string[]; isPrivate: boolean; user: string; tasks: string[]; created: string; updated: string; deletedAt?: number | null; } export interface FilterTag { name: string; excluded: boolean; } export interface Filter { query: string; tags: FilterTag[]; priorityMin: number; priorityMax: number; urgencyMin: number; urgencyMax: number; editedToday: boolean; ownedByMe: boolean; starred: boolean; jobStatus: FilterTag[]; jobDivision: FilterTag[]; } export interface FilterTemplate { id: string; name: string; filter: Filter; } export interface ShareContext { id: string; key: string; displayName: string; kind: "user" | "bucket" | "note"; policy: "collaborative" | "handoff"; targetUserId?: string | null; supervisorUserIds?: string[]; color?: string; description?: string; deletedAt?: number | null; deletedBy?: string | null; } export interface ShareRule { id: string; ownerId: string; targetUserId: string; type: "tag" | "all"; tagName?: string; share_mode?: "ADD_USER" | "SEND_TASK"; } export interface SupervisedContextAccess { ownerUserId: string; ownerName: string; contextId: string; contextName: string; } export interface Bucket { id: string; name: string; color: string; description?: string; policy?: "collaborative" | "handoff"; key?: string; deletedAt?: number | null; } export interface TaskStore { tasks: Task[]; isInitializing: boolean; quickloadFadeTaskIds: string[]; dismissedTaskUpdateIndicators: Record; collapsedUntilUpdatedTasks: Record; isNotesLoading: boolean; hasLoadedNotesMetadata: boolean; loadingNoteContentIds: string[]; pWeight: number; uWeight: number; matrixScaleDays: number; prefId?: string; tagDefinitions: TagDefinition[]; templates: TaskTemplate[]; filter: Filter; shareRules: ShareRule[]; contexts: ShareContext[]; supervisedContexts: SupervisedContextAccess[]; filterTemplates: FilterTemplate[]; buckets: Bucket[]; subscribedBuckets: string[]; personalContextSupervisorIds: string[]; notes: Note[]; isNotepadMode: boolean; quickloadTasks: string[]; noteFilter: Filter; lastCompletedTaskCleanupDate: string; } export interface ScopedTaskgridPrefs { pWeight?: number; uWeight?: number; matrixScaleDays?: number; tagDefinitions?: TagDefinition[]; filter?: Filter; filterTemplates?: FilterTemplate[]; noteFilterTemplates?: FilterTemplate[]; quickloadTasks?: string[]; noteFilter?: Filter; subscribedBuckets?: string[]; supervisorUserIds?: string[]; dismissedTaskUpdateIndicators?: Record; collapsedUntilUpdatedTasks?: Record; migratedStructuredTagsV2?: boolean; migratedPersonalContextRefsV1?: boolean; migratedContextOwnershipV2?: boolean; lastCompletedTaskCleanupDate?: string; } export const createDefaultFilter = (): Filter => ({ query: "", tags: [], priorityMin: 1, priorityMax: 10, urgencyMin: 1, urgencyMax: 10, editedToday: false, ownedByMe: false, starred: false, jobStatus: [], jobDivision: [] }); export const [store, setStore] = createStore({ tasks: [], isInitializing: false, quickloadFadeTaskIds: [], dismissedTaskUpdateIndicators: {}, collapsedUntilUpdatedTasks: {}, isNotesLoading: false, hasLoadedNotesMetadata: false, loadingNoteContentIds: [], pWeight: 1.0, uWeight: 1.0, matrixScaleDays: 30, tagDefinitions: [], templates: [], filter: createDefaultFilter(), shareRules: [], contexts: [], supervisedContexts: [], filterTemplates: [], buckets: [], subscribedBuckets: [], personalContextSupervisorIds: [], notes: [], isNotepadMode: false, quickloadTasks: [], lastCompletedTaskCleanupDate: "", noteFilter: createDefaultFilter() }); const PREF_ENVIRONMENTS_KEY = "environments"; export const getStorageKey = () => { const userId = pb.authStore.model?.id; return userId ? `${STORAGE_KEY_PREFIX}_data_${userId}` : null; }; export const getPersistedStoreSnapshot = () => { const { isInitializing, quickloadFadeTaskIds, isNotesLoading, hasLoadedNotesMetadata, loadingNoteContentIds, ...persisted } = store; return persisted; }; export const readScopedPrefs = (rawPrefs: any): ScopedTaskgridPrefs => { const scoped = rawPrefs?.[PREF_ENVIRONMENTS_KEY]?.[TASGRID_DATA_MODE]; return (scoped || rawPrefs || {}) as ScopedTaskgridPrefs; }; export const writeScopedPrefs = (rawPrefs: any, scopedPrefs: ScopedTaskgridPrefs) => ({ ...(rawPrefs || {}), [PREF_ENVIRONMENTS_KEY]: { ...(rawPrefs?.[PREF_ENVIRONMENTS_KEY] || {}), [TASGRID_DATA_MODE]: scopedPrefs } }); export const applyScopedPrefsToStore = (prefs: ScopedTaskgridPrefs) => { const currentUserId = pb.authStore.model?.id; const personalContext = currentUserId ? store.contexts.find(context => !context.deletedAt && context.kind === "user" && context.targetUserId === currentUserId ) || null : null; setStore({ pWeight: prefs.pWeight || 1.0, uWeight: prefs.uWeight || 1.0, matrixScaleDays: prefs.matrixScaleDays || 30, prefId: currentUserId, subscribedBuckets: prefs.subscribedBuckets || [], personalContextSupervisorIds: personalContext?.supervisorUserIds || prefs.supervisorUserIds || [], tagDefinitions: (prefs.tagDefinitions || []).filter((def: TagDefinition) => !def.name.startsWith("@")), filterTemplates: prefs.filterTemplates || [], quickloadTasks: prefs.quickloadTasks || [], dismissedTaskUpdateIndicators: prefs.dismissedTaskUpdateIndicators || {}, collapsedUntilUpdatedTasks: prefs.collapsedUntilUpdatedTasks || {}, noteFilter: store.noteFilter, filter: store.filter }); }; export const unwrapRelationId = (value: unknown): string | null => { if (typeof value === "string") { return value || null; } if (Array.isArray(value)) { const first = value[0]; return typeof first === "string" ? first : null; } return null; }; export const unwrapRelationIds = (value: unknown): string[] => { if (Array.isArray(value)) { return value .map(item => { if (typeof item === "string") return item; if (item && typeof item === "object" && "id" in item && typeof (item as any).id === "string") { return (item as any).id; } return null; }) .filter((item): item is string => !!item); } if (typeof value === "string") { const trimmed = value.trim(); if (!trimmed) return []; try { const parsed = JSON.parse(trimmed); return unwrapRelationIds(parsed); } catch { return trimmed.includes(",") ? trimmed.split(",").map(item => item.trim()).filter(Boolean) : [trimmed]; } } if (value && typeof value === "object" && "id" in value && typeof (value as any).id === "string") { return [(value as any).id]; } return []; };