import { reconcile } from "solid-js/store"; import { toast } from "solid-sonner"; import { pb } from "@/lib/pocketbase"; import { CONTEXTS_COLLECTION, NOTES_COLLECTION, TASGRID_COLLECTION } from "@/lib/constants"; import { buildNoteTag, buildShareTag, normalizeContextKey, normalizeNoteKey, parseTags } from "@/lib/tags"; import { activeNoteId, currentTaskContext, type Bucket, type Note, type NoteRef, readScopedPrefs, type ShareContext, type ShareRule, setStore, store, type TagDefinition, type Task, type TaskContext, type TaskShareRef, unwrapRelationId, unwrapRelationIds, writeScopedPrefs } from "./state"; export const relationFilter = (field: string, id: string | undefined | null) => id ? `(${field} = "${id}" || ${field} ?= "${id}")` : "false"; export const buildUserContextKey = (userId: string) => `user-${userId}`; const DEFAULT_LABEL_THEME: "light" | "dark" = "dark"; const mapContextToBucket = (context: ShareContext): Bucket => ({ id: context.id, name: context.displayName, color: context.color || "#64748b", description: context.description, policy: context.policy, key: context.key, deletedAt: context.deletedAt ?? null }); const deriveBuckets = (contexts: ShareContext[]) => contexts .filter(context => context.kind === "bucket" && !context.deletedAt) .sort((a, b) => a.displayName.localeCompare(b.displayName)) .map(mapContextToBucket); const deriveSystemTagDefinitions = (contexts: ShareContext[]): TagDefinition[] => contexts .filter(context => !context.deletedAt) .map(context => ({ id: context.id, name: buildShareTag(context.displayName), value: 5, color: context.color, theme: DEFAULT_LABEL_THEME, isUser: context.kind === "user", isBucket: context.kind === "bucket" })); const findContextForShareTag = (tag: { key: string; raw: string }) => store.contexts.find(context => !context.deletedAt && context.key === tag.key) || store.contexts.find(context => !context.deletedAt && context.kind === "user" && normalizeContextKey(context.displayName) === tag.key) || store.contexts.find(context => !context.deletedAt && normalizeContextKey(context.displayName) === tag.key) || store.contexts.find(context => !context.deletedAt && buildShareTag(context.displayName).toLowerCase() === tag.raw.toLowerCase()) || null; const buildNoteContext = (note: Note): ShareContext => ({ id: note.id, key: note.key, displayName: note.key || note.title, kind: "note", policy: getNoteSharePolicy(note), targetUserId: note.user, deletedAt: note.deletedAt ?? null }); export const getContextByRef = (ref: TaskShareRef) => (ref.kind === "note" ? (() => { const note = store.notes.find(existing => !existing.deletedAt && (existing.id === ref.contextId || existing.key === ref.key)); return note ? buildNoteContext(note) : null; })() : null) || store.contexts.find(context => !context.deletedAt && context.id === ref.contextId) || store.contexts.find(context => !context.deletedAt && context.key === ref.key && context.kind === ref.kind) || store.contexts.find(context => !context.deletedAt && context.kind === ref.kind && normalizeContextKey(context.displayName) === ref.key) || store.contexts.find(context => !context.deletedAt && normalizeContextKey(context.displayName) === ref.key) || null; export const getNoteByRef = (ref: NoteRef) => store.notes.find(note => !note.deletedAt && note.id === ref.noteId) || store.notes.find(note => !note.deletedAt && note.key === ref.key); const DEFAULT_SHARE_POLICY_BY_KIND: Record = { user: "collaborative", bucket: "handoff", note: "handoff" }; const NOTE_HANDOFF_POLICY_TAG = "__note_policy_handoff__"; export const getNoteSharePolicy = (note: Pick | null | undefined): "collaborative" | "handoff" => note?.tags?.includes(NOTE_HANDOFF_POLICY_TAG) ? "handoff" : "collaborative"; export const getTaskSharePolicy = (ref: Pick): "collaborative" | "handoff" => { if (ref.kind === "note") { if (ref.policy) return ref.policy; const note = store.notes.find(existing => !existing.deletedAt && (existing.id === ref.contextId || existing.key === ref.key)); return getNoteSharePolicy(note); } const context = getContextByRef(ref as TaskShareRef); if (ref.policy) return ref.policy; if (context?.policy) return context.policy; return DEFAULT_SHARE_POLICY_BY_KIND[ref.kind]; }; export const buildTaskTags = ( labelTags: string[], shareRefs: TaskShareRef[], noteRefs: NoteRef[], favoriteTag?: string ) => { const tags = [ ...labelTags, ...shareRefs.map(ref => { const context = getContextByRef(ref); if (ref.kind === "note") { return buildNoteTag(context?.key || ref.key); } if (context?.displayName) { return buildShareTag(context.displayName); } if (ref.kind === "user" && ref.key.startsWith("user-")) { return null; } return buildShareTag(ref.key); }), ...noteRefs.map(ref => { const note = getNoteByRef(ref); return buildNoteTag(note?.key || ref.key); }) ]; if (favoriteTag && !tags.includes(favoriteTag)) { tags.push(favoriteTag); } return [...new Set(tags.filter((tag): tag is string => !!tag))]; }; export const dedupeShareRefs = (refs: TaskShareRef[]) => { const seen = new Set(); return refs.filter(ref => { const key = `${ref.kind}:${ref.contextId}:${ref.key}`; if (seen.has(key)) return false; seen.add(key); return true; }); }; export const buildTaskShareRef = ( parsedTag: Extract[number], { kind: "share" | "note" }>, existingRefs: TaskShareRef[] = [], shareModeOverrides: Record = {} ): TaskShareRef => { if (parsedTag.kind === "note") { const note = store.notes.find(existing => !existing.deletedAt && existing.key === parsedTag.key); const nextRefBase = { contextId: note?.id || parsedTag.key, key: parsedTag.key, kind: "note" as const }; const existing = existingRefs.find(ref => ref.kind === "note" && (ref.contextId === nextRefBase.contextId || ref.key === nextRefBase.key)); return { ...nextRefBase, policy: shareModeOverrides[parsedTag.raw.toLowerCase()] || existing?.policy || getTaskSharePolicy(nextRefBase) }; } const context = findContextForShareTag(parsedTag); const nextRefBase = { contextId: context?.id || parsedTag.key, key: parsedTag.key, kind: (context?.kind || "bucket") as "user" | "bucket" }; const existing = existingRefs.find(ref => ref.kind === nextRefBase.kind && (ref.contextId === nextRefBase.contextId || ref.key === nextRefBase.key) ); return { ...nextRefBase, policy: shareModeOverrides[parsedTag.raw.toLowerCase()] || existing?.policy || getTaskSharePolicy(nextRefBase) }; }; export const getTaskShareModeForTag = (task: Pick, tag: string): "collaborative" | "handoff" | undefined => { const parsed = parseTags([tag])[0]; if (!parsed || (parsed.kind !== "share" && parsed.kind !== "note")) return undefined; const ref = task.shareRefs.find(existing => existing.key === parsed.key && ( (parsed.kind === "note" && existing.kind === "note") || (parsed.kind === "share" && existing.kind !== "note") ) ); return ref ? getTaskSharePolicy(ref) : undefined; }; export const deriveTaskRelationsFromTags = ( tags: string[], existingShareRefs: TaskShareRef[] = [], shareModeOverrides: Record = {} ) => { const parsedTags = parseTags(tags); const labelTags = parsedTags.filter(tag => tag.kind === "label").map(tag => tag.display); const shareRefs = parsedTags .filter((tag): tag is Extract => tag.kind === "share" || tag.kind === "note") .map(tag => buildTaskShareRef(tag, existingShareRefs, shareModeOverrides)); const noteRefs = parsedTags .filter(tag => tag.kind === "note") .map(tag => { const note = store.notes.find(existing => !existing.deletedAt && existing.key === tag.key); return { noteId: note?.id || tag.key, key: tag.key } satisfies NoteRef; }); return { labelTags, shareRefs: dedupeShareRefs(shareRefs), noteRefs: dedupeNoteRefs(noteRefs) }; }; export const dedupeNoteRefs = (refs: NoteRef[]) => { const seen = new Set(); return refs.filter(ref => { const key = `${ref.noteId}:${ref.key}`; if (seen.has(key)) return false; seen.add(key); return true; }); }; export const setContexts = (contexts: ShareContext[]) => { setStore("contexts", reconcile(contexts)); setStore("buckets", reconcile(deriveBuckets(contexts))); setStore("tasks", tasks => tasks.map(task => { const favoriteTag = task.tags.find(tag => tag.endsWith("_favorite__")); return { ...task, tags: buildTaskTags(task.labelTags, task.shareRefs, task.noteRefs, favoriteTag) }; })); const labelDefinitions = store.tagDefinitions.filter(def => !def.name.startsWith("@")); const mergedDefs = [...labelDefinitions, ...deriveSystemTagDefinitions(contexts)]; setStore("tagDefinitions", reconcile(mergedDefs)); }; export const mapContextToShareRule = (context: ShareContext): ShareRule => ({ id: context.id, ownerId: pb.authStore.model?.id || "", targetUserId: context.targetUserId || pb.authStore.model?.id || "", type: "tag", tagName: buildShareTag(context.displayName), share_mode: context.policy === "handoff" ? "SEND_TASK" : "ADD_USER" }); export const mapRecordToContext = (r: any): ShareContext => ({ id: r.id, key: r.key || normalizeContextKey(r.displayName || r.name || ""), displayName: r.displayName || r.name || r.key, kind: (r.kind || "bucket") as "user" | "bucket", policy: (r.policy || (r.kind === "user" ? "collaborative" : "handoff")) as "collaborative" | "handoff", targetUserId: unwrapRelationId(r.targetUserId), supervisorUserIds: unwrapRelationIds(r.supervisorUserIds), color: r.color, description: r.description, deletedAt: r.deletedAt ? new Date(r.deletedAt).getTime() : null, deletedBy: unwrapRelationId(r.deletedBy) }); export const loadContextsIntoStore = async () => { const contextRecords = await pb.collection(CONTEXTS_COLLECTION).getFullList({ sort: "displayName,key", requestKey: null }).catch(() => []); const nextContexts = contextRecords.map(mapRecordToContext); setContexts(nextContexts); setStore("shareRules", nextContexts.map(mapContextToShareRule)); return nextContexts; }; export const getPersonalContext = (userId = pb.authStore.model?.id) => userId ? store.contexts.find(context => !context.deletedAt && context.kind === "user" && context.targetUserId === userId ) || null : null; const getCurrentUserDisplayName = () => (pb.authStore.model?.name || pb.authStore.model?.email || "").trim() || null; const getPersonalContextTag = (userId = pb.authStore.model?.id) => { const context = getPersonalContext(userId); if (context?.displayName) { return buildShareTag(context.displayName); } if (userId && userId === pb.authStore.model?.id) { const displayName = getCurrentUserDisplayName(); if (displayName) { return buildShareTag(displayName); } } return null; }; const escapeFilterValue = (value: string) => value.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); export const buildPersonalAccessFilter = (userIds: string[], includeLegacyUser = true) => { const clauses = userIds.flatMap(userId => { const nextClauses: string[] = []; const personalTag = getPersonalContextTag(userId); if (personalTag) { nextClauses.push(`tags ~ "${escapeFilterValue(personalTag)}"`); } if (includeLegacyUser) { nextClauses.push(relationFilter("user", userId)); } return nextClauses; }).filter(Boolean); return clauses.length > 0 ? `(${clauses.join(" || ")})` : "false"; }; export const getPersonalContextUserIdFromRef = (ref: TaskShareRef) => { if (ref.kind !== "user") return null; const context = getContextByRef(ref); if (context?.targetUserId) { return context.targetUserId; } if (ref.key.startsWith("user-")) { return ref.key.replace(/^user-/, "") || null; } return null; }; export const getTaskPersonalRefs = (task: Pick) => task.shareRefs.filter(ref => ref.kind === "user" && !!getPersonalContextUserIdFromRef(ref)); const getTaskPersonalUserIds = (task: Pick) => [...new Set(getTaskPersonalRefs(task).map(ref => getPersonalContextUserIdFromRef(ref)).filter((value): value is string => !!value))]; const taskHasLegacyPersonalAccess = (task: Pick, userId: string) => task.ownerId === userId && getTaskPersonalRefs(task).length === 0 && !task.shareRefs.some(ref => ref.kind !== "user" && getTaskSharePolicy(ref) === "handoff"); export const taskHasPersonalContextAccess = (task: Pick, userId: string, allowLegacyFallback = true) => getTaskPersonalUserIds(task).includes(userId) || (allowLegacyFallback && taskHasLegacyPersonalAccess(task, userId)); export const resolveMirroredOwnerId = ( currentOwnerId: string | null | undefined, shareRefs: TaskShareRef[], preferredOwnerId?: string | null ) => { const personalUserIds = [...new Set( shareRefs .filter(ref => ref.kind === "user") .map(ref => getPersonalContextUserIdFromRef(ref)) .filter((value): value is string => !!value) )]; if (personalUserIds.length === 1) { return personalUserIds[0]; } if (personalUserIds.length > 1 && preferredOwnerId && personalUserIds.includes(preferredOwnerId)) { return preferredOwnerId; } return currentOwnerId || null; }; export const withMirroredOwnerId = ( currentOwnerId: string | null | undefined, shareRefs: TaskShareRef[], updates: Partial, preferredOwnerId?: string | null ) => ({ ...updates, ownerId: resolveMirroredOwnerId(currentOwnerId, shareRefs, preferredOwnerId) }); export const withActiveContextTags = (rawTags: string[]) => { const nextTags = [...rawTags]; const ctx = currentTaskContext(); if (ctx === "mine") { const personalContext = getPersonalContext(); if (personalContext) { const personalTag = buildShareTag(personalContext.displayName); if (!nextTags.some(tag => tag.toLowerCase() === personalTag.toLowerCase())) { nextTags.push(personalTag); } } } else if (typeof ctx === "object") { if ("bucketId" in ctx && ctx.name) { const bucketContext = store.contexts.find(context => context.id === ctx.bucketId); const bucketTag = buildShareTag(bucketContext?.displayName || ctx.name); if (!nextTags.some(tag => tag.toLowerCase() === bucketTag.toLowerCase())) { nextTags.push(bucketTag); } } else if ("userId" in ctx && ctx.name) { const userTag = buildShareTag(ctx.name); if (!nextTags.some(tag => tag.toLowerCase() === userTag.toLowerCase())) { nextTags.push(userTag); } } } if (store.isNotepadMode) { const noteId = activeNoteId(); const note = noteId ? store.notes.find(existing => existing.id === noteId) : null; if (note) { const noteTag = buildNoteTag(note.key || note.title); if (!nextTags.some(tag => tag.toLowerCase() === noteTag.toLowerCase())) { nextTags.push(noteTag); } } } return [...new Set(nextTags)]; }; export const isCurrentUserContextTag = (tag: string) => { const normalizedTag = tag.trim().toLowerCase(); if (!normalizedTag.startsWith("@")) return false; const personalContext = getPersonalContext(); const currentUserDisplayName = (pb.authStore.model?.name || pb.authStore.model?.email || "").trim(); const hiddenTags = new Set(); if (personalContext?.displayName) { hiddenTags.add(buildShareTag(personalContext.displayName).toLowerCase()); } if (currentUserDisplayName) { hiddenTags.add(buildShareTag(currentUserDisplayName).toLowerCase()); } return hiddenTags.has(normalizedTag); }; export const isTaskLockedNoteTag = (task: Pick, tag: string) => { const normalizedTag = tag.trim(); if (!normalizedTag.startsWith("#")) return false; const noteKey = normalizeNoteKey(normalizedTag); const matchingNoteRefs = task.noteRefs.filter(ref => ref.key === noteKey || ref.noteId === noteKey); if (matchingNoteRefs.length === 0) return false; const nonNoteContexts = task.shareRefs.filter(ref => ref.kind !== "note"); const uniqueNoteRefs = dedupeNoteRefs(task.noteRefs); return uniqueNoteRefs.length === matchingNoteRefs.length && nonNoteContexts.length === 0; }; export const getVisibleTaskTags = (task: Pick, context: TaskContext = currentTaskContext()) => { const tags = task.tags || []; return tags.filter(tag => { if (tag.endsWith("_favorite__") || isCurrentUserContextTag(tag)) { return false; } if (isTaskLockedNoteTag(task, tag)) { return false; } if (typeof context === "object" && "bucketId" in context) { const bucketContext = store.contexts.find(existing => existing.id === context.bucketId); const bucketTagName = buildShareTag(bucketContext?.displayName || context.name).toLowerCase(); if (tag.toLowerCase() === bucketTagName) { return false; } } return true; }); }; export const getSubscribedBucketContexts = () => store.subscribedBuckets .map(id => store.contexts.find(context => context.id === id)) .filter((context): context is ShareContext => !!context && !context.deletedAt && context.kind === "bucket"); export const shouldTaskBeVisible = (task: Task, currentUserId: string): boolean => { if (taskHasPersonalContextAccess(task, currentUserId)) return true; if (store.supervisedContexts.some(context => taskHasPersonalContextAccess(task, context.ownerUserId))) return true; const hasSubscribedBucket = task.shareRefs.some(ref => ref.kind === "bucket" && ( store.subscribedBuckets.includes(ref.contextId) || store.subscribedBuckets.some(bucketId => store.contexts.find(context => context.id === bucketId)?.key === ref.key) ) ); if (hasSubscribedBucket) return true; const hasAccessibleNote = task.noteRefs.some(ref => { const note = getNoteByRef(ref); return !!note && (!note.isPrivate || note.user === currentUserId); }); if (hasAccessibleNote) return true; return task.shareRefs.some(ref => { if (ref.kind !== "user" || getTaskSharePolicy(ref) !== "collaborative") return false; const context = getContextByRef(ref); return !!context && !context.deletedAt && context.targetUserId === currentUserId; }); }; export const syncUserContexts = async () => { const allUsers = await pb.collection("users").getFullList({ filter: "verified = true", fields: "id,name,email", requestKey: null }); const existingContexts = await pb.collection(CONTEXTS_COLLECTION).getFullList({ requestKey: null }).catch(() => []); let createdAny = false; for (const user of allUsers) { const displayName = user.name || user.email; const existingUserContext = existingContexts.find((context: any) => (context.kind || "bucket") === "user" && unwrapRelationId(context.targetUserId) === user.id ); if (!displayName || existingUserContext) continue; const created = await pb.collection(CONTEXTS_COLLECTION).create({ key: buildUserContextKey(user.id), displayName, kind: "user", policy: "collaborative", targetUserId: user.id }, { requestKey: null }).catch(() => null); if (created) { createdAny = true; } } if (createdAny || store.contexts.length === 0) { await loadContextsIntoStore(); } }; export const syncSupervisedContexts = async () => { const currentUserId = pb.authStore.model?.id; if (!currentUserId) { setStore("supervisedContexts", []); return; } try { const contextRecords = await pb.collection(CONTEXTS_COLLECTION).getFullList({ sort: "displayName,key", requestKey: null }).catch(() => []); const contexts = contextRecords.length > 0 ? contextRecords.map(mapRecordToContext) : store.contexts; if (contextRecords.length > 0) { setContexts(contexts); setStore("shareRules", contexts.map(mapContextToShareRule)); } const nextSupervisedContexts = contexts .filter(context => !context.deletedAt && context.kind === "user" && !!context.targetUserId && context.targetUserId !== currentUserId && (context.supervisorUserIds || []).includes(currentUserId) ) .map(context => ({ ownerUserId: context.targetUserId as string, ownerName: context.displayName, contextId: context.id, contextName: context.displayName })) .sort((a, b) => a.ownerName.localeCompare(b.ownerName)); setStore("supervisedContexts", nextSupervisedContexts); } catch (err) { console.error("Failed to load supervised contexts:", err); } }; export const toggleBucketSubscription = async (bucketId: string) => { const currentSubscribed = store.subscribedBuckets; const isSubscribed = currentSubscribed.includes(bucketId); const newSubscribed = isSubscribed ? currentSubscribed.filter(id => id !== bucketId) : [...currentSubscribed, bucketId]; setStore("subscribedBuckets", newSubscribed); const userId = pb.authStore.model?.id; if (userId) { try { const user = await pb.collection("users").getOne(userId, { requestKey: null }); const rawPrefs = user.Taskgrid_pref || {}; const scopedPrefs = readScopedPrefs(rawPrefs); await pb.collection("users").update(userId, { Taskgrid_pref: writeScopedPrefs(rawPrefs, { ...scopedPrefs, subscribedBuckets: newSubscribed }) }); if (!isSubscribed) { const bucket = store.buckets.find(b => b.id === bucketId); if (bucket) { const { getTaskListFields, mapRecordToTask } = await import("./tasks"); const records = await pb.collection(TASGRID_COLLECTION).getFullList({ filter: `tags ~ "${buildShareTag(bucket.name)}"`, sort: "-created", fields: getTaskListFields() }); const newTasks: Task[] = []; records.forEach((r: any) => { const exists = store.tasks.find(t => t.id === r.id); if (!exists && !r.tags?.includes("__template__")) { newTasks.push(mapRecordToTask(r)); } }); if (newTasks.length > 0) { setStore("tasks", t => [...newTasks, ...t]); } } } } catch (err) { console.error("Failed to update subscriptions:", err); setStore("subscribedBuckets", currentSubscribed); } } }; export const createBucket = async (name: string, color: string, description?: string) => { try { const key = normalizeContextKey(name); await pb.collection(CONTEXTS_COLLECTION).create({ key, displayName: name, kind: "bucket", policy: "handoff", color, description }); toast.success("Bucket created"); } catch (err) { console.error("Failed to create bucket:", err); toast.error("Failed to create bucket"); } }; export const updateBucket = async (id: string, data: { name?: string, color?: string, description?: string, policy?: "collaborative" | "handoff" }) => { try { const payload: any = { ...data }; if (data.name) payload.key = normalizeContextKey(data.name); if (data.name) payload.displayName = data.name; await pb.collection(CONTEXTS_COLLECTION).update(id, payload); toast.success("Bucket updated"); } catch (err) { console.error("Failed to update bucket:", err); toast.error("Failed to update bucket"); } }; export const deleteBucket = async (id: string) => { try { await pb.collection(CONTEXTS_COLLECTION).update(id, { deletedAt: new Date().toISOString(), deletedBy: pb.authStore.model?.id || null }); toast.success("Bucket moved to trash"); } catch (err) { console.error("Failed to delete bucket:", err); toast.error("Failed to delete bucket"); } }; export const updateUserContextPolicy = async (contextId: string, policy: "collaborative" | "handoff") => { const success = await setContextPolicy(contextId, policy); if (success) { toast.success("User context updated"); } }; export const setContextPolicy = async (contextId: string, policy: "collaborative" | "handoff") => { const previousContext = store.contexts.find(context => context.id === contextId); const previousPolicy = previousContext?.policy; if (previousContext && previousPolicy !== policy) { setStore("contexts", context => context.id === contextId, "policy", policy); setStore("shareRules", store.contexts.map(mapContextToShareRule)); } try { await pb.collection(CONTEXTS_COLLECTION).update(contextId, { policy }, { requestKey: null }); return true; } catch (err) { console.error("Failed to update user context:", err); if (previousContext && previousPolicy) { setStore("contexts", context => context.id === contextId, "policy", previousPolicy); setStore("shareRules", store.contexts.map(mapContextToShareRule)); } toast.error("Failed to update user context"); return false; } }; export const setNoteContextPolicy = async (noteId: string, policy: "collaborative" | "handoff") => { if (!pb.authStore.isValid || !noteId) return false; const note = store.notes.find(existing => existing.id === noteId); if (!note) return false; const baseTags = (note.tags || []).filter(tag => tag !== NOTE_HANDOFF_POLICY_TAG); const nextTags = policy === "handoff" ? [...baseTags, NOTE_HANDOFF_POLICY_TAG] : baseTags; const previousTags = note.tags || []; setStore("notes", n => n.id === noteId, "tags", nextTags); try { await pb.collection(NOTES_COLLECTION).update(noteId, { tags: nextTags }, { requestKey: null }); return true; } catch (err) { console.error("Failed to update note context policy:", err); setStore("notes", n => n.id === noteId, "tags", previousTags); toast.error("Failed to update note sharing mode"); return false; } }; export const savePersonalContextSupervisors = async (supervisorUserIds: string[]) => { const userId = pb.authStore.model?.id; if (!userId) return; const uniqueSupervisorIds = [...new Set(supervisorUserIds.filter(Boolean))]; setStore("personalContextSupervisorIds", uniqueSupervisorIds); try { await syncUserContexts(); const personalContext = getPersonalContext(userId); if (!personalContext) { throw new Error("Missing personal context record"); } await pb.collection(CONTEXTS_COLLECTION).update(personalContext.id, { supervisorUserIds: uniqueSupervisorIds }, { requestKey: null }); const user = await pb.collection("users").getOne(userId, { requestKey: null }); const rawPrefs = user.Taskgrid_pref || {}; const scopedPrefs = readScopedPrefs(rawPrefs); await pb.collection("users").update(userId, { Taskgrid_pref: writeScopedPrefs(rawPrefs, { ...scopedPrefs, supervisorUserIds: uniqueSupervisorIds }) }, { requestKey: null }); await syncSupervisedContexts(); toast.success("Personal supervision updated"); } catch (err) { console.error("Failed to update personal supervision:", err); toast.error("Failed to update personal supervision"); } }; export const loadShareRules = async () => { if (!pb.authStore.isValid) return; setStore("shareRules", reconcile(store.contexts.map(mapContextToShareRule))); }; export const addShareRule = async (type: "tag" | "all", targetUserId: string, tagName?: string, share_mode: "ADD_USER" | "SEND_TASK" = "SEND_TASK") => { if (!pb.authStore.isValid) return; if (type !== "tag" || !tagName?.startsWith("@")) { toast.error("Only @tag contexts are supported now."); return; } const existing = store.contexts.find(context => context.targetUserId === targetUserId && buildShareTag(context.displayName) === tagName); if (existing) return; try { await pb.collection(CONTEXTS_COLLECTION).create({ key: normalizeContextKey(tagName), displayName: tagName.replace(/^@/, ""), kind: "user", policy: share_mode === "SEND_TASK" ? "handoff" : "collaborative", targetUserId }, { requestKey: null }); toast.success("Shared context created"); } catch (err) { console.error("Failed to create shared context:", err); toast.error("Failed to create shared context."); } }; export const updateShareRule = async (ruleId: string, updates: Partial) => { if (!pb.authStore.isValid) return; try { const context = store.contexts.find(existing => existing.id === ruleId); if (!context) return; await pb.collection(CONTEXTS_COLLECTION).update(ruleId, { policy: updates.share_mode === "SEND_TASK" ? "handoff" : "collaborative" }, { requestKey: null }); toast.success("Context updated"); } catch (err) { console.error("Failed to update share rule:", err); toast.error("Failed to update share rule."); } }; export const removeShareRule = async (ruleId: string) => { if (!pb.authStore.isValid) return; try { await pb.collection(CONTEXTS_COLLECTION).delete(ruleId); toast.success("Context removed"); } catch (err) { console.error("Failed to delete share rule:", err); toast.error("Failed to delete share rule."); } }; export const syncSystemTagsAndRules = async () => { if (!pb.authStore.isValid) return; setStore("shareRules", store.contexts.map(mapContextToShareRule)); setStore("tagDefinitions", defs => { const plainDefs = defs.filter(def => !def.name.startsWith("@")); return [...plainDefs, ...deriveSystemTagDefinitions(store.contexts)]; }); };