353 lines
9.6 KiB
TypeScript
353 lines
9.6 KiB
TypeScript
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<string | null>(null);
|
|
export const [activeNoteId, setActiveNoteId] = createSignal<string | null>(null);
|
|
export const [currentTaskContext, setCurrentTaskContext] = createSignal<TaskContext>("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<string, string>;
|
|
collapsedUntilUpdatedTasks: Record<string, string>;
|
|
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<string, string>;
|
|
collapsedUntilUpdatedTasks?: Record<string, string>;
|
|
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<TaskStore>({
|
|
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 [];
|
|
};
|