diff --git a/src/components/TaskCard.tsx b/src/components/TaskCard.tsx
index ed08083..b4c05b6 100644
--- a/src/components/TaskCard.tsx
+++ b/src/components/TaskCard.tsx
@@ -1,7 +1,7 @@
import { type Component, createMemo } from "solid-js";
import { type Task, toggleTask, calculateUrgencyFromDate, setActiveTaskId, store, copyTask } from "@/store";
import { cn } from "@/lib/utils";
-import { CheckCircle2, Circle, Clock, ArrowUpCircle, Copy } from "lucide-solid";
+import { CheckCircle2, Circle, Clock, ArrowUpCircle, Copy, Users2 } from "lucide-solid";
import { Badge } from "@/components/ui/badge";
import { For } from "solid-js";
import { useTheme } from "./ThemeProvider";
@@ -78,6 +78,31 @@ export const TaskCard: Component<{ task: Task }> = (props) => {
P{props.task.priority}
+ {/* Shared Indicator - shows for individual shares OR tag-based ShareRules */}
+ {(() => {
+ const hasIndividualShares = props.task.sharedWith && props.task.sharedWith.length > 0;
+ // Check if any tag-based ShareRules match this task's tags (exclude 'all' type)
+ const hasTagRuleShares = store.shareRules.some(rule =>
+ rule.type === 'tag' &&
+ rule.tagName &&
+ props.task.tags?.includes(rule.tagName)
+ );
+ const isShared = hasIndividualShares || hasTagRuleShares;
+
+ if (!isShared) return null;
+
+ const shareCount = (props.task.sharedWith?.length || 0);
+ const tooltip = hasIndividualShares
+ ? `Shared with ${shareCount} user${shareCount > 1 ? 's' : ''}`
+ : 'Shared via tag rule';
+
+ return (
+
+
+
+ );
+ })()}
+
{/* Tags */}
{props.task.tags && props.task.tags.length > 0 && (
diff --git a/src/components/TaskDetail.tsx b/src/components/TaskDetail.tsx
index 68f790c..e2c3c03 100644
--- a/src/components/TaskDetail.tsx
+++ b/src/components/TaskDetail.tsx
@@ -1,9 +1,9 @@
import { type Component, createEffect, createSignal, For } from "solid-js";
import { Sheet, SheetContent } from "@/components/ui/sheet";
-import { type Task, removeTask, restoreTask, updateTask, saveTaskAsTemplate } from "@/store";
+import { type Task, removeTask, restoreTask, updateTask, saveTaskAsTemplate, shareTask, revokeShare, splitTask } from "@/store";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
-import { ArrowUpCircle, Clock, Calendar, Type, Trash2, X, Copy, MoreHorizontal, Gauge } from "lucide-solid";
+import { ArrowUpCircle, Clock, Calendar, Type, Trash2, X, Copy, MoreHorizontal, Gauge, Share2, UserMinus, GitBranch, Tag, Settings } from "lucide-solid";
import { calculateDateFromUrgency, calculateUrgencyFromDate } from "@/store";
import { Button } from "./ui/button";
import { TagPicker } from "./TagPicker";
@@ -15,6 +15,7 @@ import { toast } from "solid-sonner";
import { lazy, Suspense } from "solid-js";
import { useTheme } from "./ThemeProvider";
import { getThemeAdjustedColor } from "@/lib/colors";
+import { pb } from "@/lib/pocketbase";
const TaskEditor = lazy(() => import("./TaskEditor").then(m => ({ default: m.TaskEditor })));
@@ -468,6 +469,16 @@ export const TaskDetail: Component = (props) => {
Create Template from Task
+
+ {/* Sharing Section */}
+
@@ -536,3 +547,140 @@ const MetadataItem: Component<{
);
};
+
+// User Share Picker Component
+const UserSharePicker: Component<{
+ taskId: string;
+ sharedWith: Array<{ userId: string; access: 'view' | 'edit' }>;
+ tags: string[];
+}> = (props) => {
+ const [users, setUsers] = createSignal>([]);
+ const [selectedUserId, setSelectedUserId] = createSignal("");
+ const [loading, setLoading] = createSignal(true);
+
+ // Get matching ShareRules for this task
+ const matchingRules = () => {
+ const currentUserId = pb.authStore.model?.id;
+ return store.shareRules.filter(rule => {
+ if (rule.ownerId !== currentUserId) return false;
+ if (rule.type === 'all') return true;
+ if (rule.type === 'tag' && props.tags.includes(rule.tagName!)) return true;
+ return false;
+ });
+ };
+
+ const getUserName = (userId: string) => {
+ return users().find(u => u.id === userId)?.name || userId;
+ };
+
+ // Fetch users on mount
+ createEffect(async () => {
+ try {
+ const records = await pb.collection('users').getFullList({
+ fields: 'id,name'
+ });
+ setUsers(records.map(r => ({ id: r.id, name: r.name || r.email || r.id })));
+ } catch (err) {
+ console.error("Failed to load users:", err);
+ } finally {
+ setLoading(false);
+ }
+ });
+
+
+ const availableUsers = () => {
+ const sharedUserIds = props.sharedWith.map(s => s.userId);
+ const currentUserId = pb.authStore.model?.id;
+ return users().filter(u => !sharedUserIds.includes(u.id) && u.id !== currentUserId);
+ };
+
+ return (
+ <>
+
+
+
+
+
+ {(share) => (
+
+
{getUserName(share.userId)}
+
+
+
+
+
+ )}
+
+
+ {/* ShareRule-based sharing */}
+ {matchingRules().length > 0 && (
+
+
Automatic Rule Shares
+
+ {(rule) => (
+
+
+ {rule.type === 'tag' ? : }
+
+
+
+ {getUserName(rule.targetUserId)}
+
+
+ {rule.type === 'all' ? 'All Tasks Rule' : `Matched Tag: ${rule.tagName}`}
+
+
+
+ )}
+
+
+ )}
+ >
+ );
+};
diff --git a/src/store/index.ts b/src/store/index.ts
index e3a17b3..ecdefd6 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -33,6 +33,7 @@ export interface Task {
lastUncompleted?: string; // ISO date of last automatic reset
};
size?: number; // 0-10, task complexity/size
+ sharedWith?: Array<{ userId: string; access: 'view' | 'edit' }>; // Users this task is shared with
}
export const checkRecurringTasks = () => {
@@ -139,6 +140,14 @@ export interface Filter {
editedToday: boolean;
}
+export interface ShareRule {
+ id: string;
+ ownerId: string;
+ targetUserId: string;
+ type: 'tag' | 'all';
+ tagName?: string;
+}
+
interface TaskStore {
tasks: Task[];
pWeight: number;
@@ -148,6 +157,7 @@ interface TaskStore {
tagDefinitions: TagDefinition[];
templates: TaskTemplate[];
filter: Filter;
+ shareRules: ShareRule[];
}
// Initial empty state
@@ -166,7 +176,8 @@ export const [store, setStore] = createStore({
urgencyMin: 1,
urgencyMax: 10,
editedToday: false
- }
+ },
+ shareRules: []
});
export const matchesFilter = (task: Task) => {
@@ -310,10 +321,45 @@ const mapRecordToTask = (r: any): Task => {
created: r.created,
updated: r.updated,
recurrence: r.recurrence,
- size: (r.size === null || r.size === undefined) ? undefined : r.size
+ size: (r.size === null || r.size === undefined) ? undefined : r.size,
+ sharedWith: r.sharedWith
};
};
+const mapRecordToShareRule = (r: any): ShareRule => {
+ return {
+ id: r.id,
+ ownerId: r.ownerId,
+ targetUserId: r.targetUserId,
+ type: r.type as 'tag' | 'all',
+ tagName: r.tagName
+ };
+};
+
+// Helper to check if a task should be visible based on ownership, shares, or rules
+const shouldTaskBeVisible = (task: any, currentUserId: string): boolean => {
+ // Own task
+ if (task.user === currentUserId) return true;
+
+ // Individually shared
+ const sharedWith = task.sharedWith || [];
+ if (sharedWith.some((s: any) => s.userId === currentUserId)) return true;
+
+ // ShareRule match
+ const matchingRule = store.shareRules.find(rule => {
+ if (rule.targetUserId !== currentUserId) return false;
+ if (task.user !== rule.ownerId) return false;
+ if (rule.type === 'all') return true;
+ if (rule.type === 'tag') {
+ const tags = task.tags || [];
+ return tags.includes(rule.tagName);
+ }
+ return false;
+ });
+
+ return !!matchingRule;
+};
+
export const subscribeToRealtime = async () => {
// Unsubscribe first to avoid duplicates
await pb.collection(TASGRID_COLLECTION).unsubscribe('*');
@@ -325,9 +371,6 @@ export const subscribeToRealtime = async () => {
// Check if it's a template
const isTemplate = e.record.tags?.includes("__template__");
if (isTemplate) {
- // We don't auto-add templates to the main task list usually?
- // Wait, `store.templates` is separate.
- // We should handle templates too if we want sync on templates.
let meta: any = {};
try { meta = JSON.parse(e.record.content || "{}"); } catch (e) { }
const newTemplate: TaskTemplate = {
@@ -343,11 +386,17 @@ export const subscribeToRealtime = async () => {
return;
}
- // Regular Task
+ // Regular Task - check if already exists and if it should be visible
const exists = store.tasks.find(t => t.id === e.record.id);
if (!exists) {
- const newTask = mapRecordToTask(e.record);
- setStore("tasks", t => [newTask, ...t]);
+ const currentUserId = pb.authStore.model?.id;
+ const isOwnTask = e.record.user === currentUserId;
+
+ // Only add if it's own task or should be visible via sharing
+ if (isOwnTask || (currentUserId && shouldTaskBeVisible(e.record, currentUserId))) {
+ const newTask = mapRecordToTask(e.record);
+ setStore("tasks", t => [newTask, ...t]);
+ }
}
}
@@ -369,23 +418,36 @@ export const subscribeToRealtime = async () => {
return;
}
- // Normal Task - only apply if incoming record is newer or equal
+ const currentUserId = pb.authStore.model?.id;
const existingTask = store.tasks.find(t => t.id === e.record.id);
+ const isOwnTask = e.record.user === currentUserId;
+ const incomingUpdated = new Date(e.record.updated).getTime();
+
if (existingTask) {
- const incomingUpdated = new Date(e.record.updated).getTime();
const localUpdated = new Date(existingTask.updated).getTime();
// Only apply update if incoming is newer (or we don't have a timestamp)
if (!existingTask.updated || incomingUpdated >= localUpdated) {
+ // For shared tasks, re-evaluate visibility (e.g., tags may have changed)
+ if (!isOwnTask && currentUserId) {
+ if (!shouldTaskBeVisible(e.record, currentUserId)) {
+ // No longer visible, remove it
+ setStore("tasks", t => t.filter(x => x.id !== e.record.id));
+ return;
+ }
+ }
+
const updatedTask = mapRecordToTask(e.record);
setStore("tasks", t => t.id === e.record.id, updatedTask);
} else {
console.log("Skipping stale realtime update for task:", e.record.id);
}
} else {
- // Task doesn't exist locally, add it
- const updatedTask = mapRecordToTask(e.record);
- setStore("tasks", t => [updatedTask, ...t]);
+ // Task doesn't exist locally - check if it should be visible now
+ if (currentUserId && shouldTaskBeVisible(e.record, currentUserId)) {
+ const newTask = mapRecordToTask(e.record);
+ setStore("tasks", t => [newTask, ...t]);
+ }
}
}
@@ -395,6 +457,67 @@ export const subscribeToRealtime = async () => {
setStore("templates", t => (t || []).filter(x => x.id !== e.record.id));
}
});
+
+ // Subscribe to Share Rules
+ await pb.collection(SHARE_RULES_COLLECTION).unsubscribe('*');
+ pb.collection(SHARE_RULES_COLLECTION).subscribe('*', async (e) => {
+ const currentUserId = pb.authStore.model?.id;
+
+ if (e.action === 'create') {
+ const newRule = mapRecordToShareRule(e.record);
+
+ // Add to shareRules store
+ setStore("shareRules", (rules) => {
+ if (rules.some(r => r.id === newRule.id)) return rules;
+ return [...rules, newRule];
+ });
+
+ // If I'm the target, fetch matching tasks from the owner
+ if (newRule.targetUserId === currentUserId) {
+ try {
+ let filter = `user = "${newRule.ownerId}"`;
+ if (newRule.type === 'tag' && newRule.tagName) {
+ filter += ` && tags ~ "${newRule.tagName}"`;
+ }
+
+ const records = await pb.collection(TASGRID_COLLECTION).getFullList({
+ filter,
+ sort: '-created',
+ requestKey: null
+ });
+
+ records.forEach((r: any) => {
+ if (!r.tags?.includes("__template__") && !store.tasks.find(t => t.id === r.id)) {
+ const newTask = mapRecordToTask(r);
+ setStore("tasks", t => [newTask, ...t]);
+ }
+ });
+ } catch (err) {
+ console.warn("Failed to fetch tasks for new share rule:", err);
+ }
+ }
+ }
+
+ if (e.action === 'delete') {
+ const deletedRuleId = e.record.id;
+ const deletedRule = store.shareRules.find(r => r.id === deletedRuleId);
+
+ // Remove from shareRules store
+ setStore("shareRules", (rules) => rules.filter(r => r.id !== deletedRuleId));
+
+ // If I was the target, remove tasks that are no longer visible
+ if (deletedRule && deletedRule.targetUserId === currentUserId) {
+ setStore("tasks", tasks =>
+ tasks.filter(task => shouldTaskBeVisible(task, currentUserId!))
+ );
+ }
+ }
+
+ if (e.action === 'update') {
+ const updatedRule = mapRecordToShareRule(e.record);
+ setStore("shareRules", (rules) => rules.map(r => r.id === updatedRule.id ? updatedRule : r));
+ }
+ });
};
export const initStore = async () => {
@@ -456,16 +579,44 @@ export const initStore = async () => {
console.warn("Failed to load tags:", tagErr);
}
- // 2. Fetch Tasks & Templates
- const records = await pb.collection(TASGRID_COLLECTION).getFullList({
- filter: `user = "${pb.authStore.model?.id}"`,
+ // 2. Fetch Tasks & Templates + Shared Tasks in PARALLEL
+ const currentUserId = pb.authStore.model?.id;
+
+ // Prepare parallel fetch promises (requestKey: null prevents auto-cancellation)
+ const ownTasksPromise = pb.collection(TASGRID_COLLECTION).getFullList({
+ filter: `user = "${currentUserId}"`,
sort: '-created',
+ requestKey: null
});
+ const individuallySharedPromise = currentUserId
+ ? pb.collection(TASGRID_COLLECTION).getFullList({
+ filter: `sharedWith ~ "${currentUserId}"`,
+ sort: '-created',
+ requestKey: null
+ }).catch(err => { console.warn("Failed to load individually shared tasks:", err); return []; })
+ : Promise.resolve([]);
+
+ const shareRulesPromise = currentUserId
+ ? pb.collection(SHARE_RULES_COLLECTION).getFullList({
+ filter: `targetUserId = "${currentUserId}"`,
+ requestKey: null
+ }).catch(err => { console.warn("Failed to load share rules:", err); return []; })
+ : Promise.resolve([]);
+
+ // Wait for all initial fetches
+ const [ownRecords, individuallySharedRecords, incomingRules] = await Promise.all([
+ ownTasksPromise,
+ individuallySharedPromise,
+ shareRulesPromise
+ ]);
+
const allTasks: Task[] = [];
const loadedTemplates: TaskTemplate[] = [];
+ const seenIds = new Set();
- records.forEach(r => {
+ // Process own tasks & templates
+ ownRecords.forEach(r => {
const tags = r.tags || [];
if (tags.includes("__template__")) {
let meta: any = {};
@@ -481,15 +632,72 @@ export const initStore = async () => {
});
} else {
allTasks.push(mapRecordToTask(r));
+ seenIds.add(r.id);
}
});
+ // Process individually shared tasks
+ individuallySharedRecords.forEach((r: any) => {
+ if (!seenIds.has(r.id)) {
+ const tags = r.tags || [];
+ if (!tags.includes("__template__")) {
+ allTasks.push(mapRecordToTask(r));
+ seenIds.add(r.id);
+ }
+ }
+ });
+
+ // Process ShareRules - fetch tasks from each owner in parallel
+ if (incomingRules.length > 0) {
+ // Group rules by owner
+ const ownerRules = new Map();
+ incomingRules.forEach((rule: any) => {
+ const existing = ownerRules.get(rule.ownerId) || [];
+ existing.push({ type: rule.type, tagName: rule.tagName });
+ ownerRules.set(rule.ownerId, existing);
+ });
+
+ // Fetch from all owners in parallel
+ const ownerFetchPromises = Array.from(ownerRules.entries()).map(async ([ownerId, rules]) => {
+ const hasAllRule = rules.some(r => r.type === 'all');
+ const tagRules = rules.filter(r => r.type === 'tag').map(r => r.tagName).filter(Boolean);
+
+ let filter = `user = "${ownerId}"`;
+ if (!hasAllRule && tagRules.length > 0) {
+ const tagFilters = tagRules.map(t => `tags ~ "${t}"`).join(' || ');
+ filter += ` && (${tagFilters})`;
+ }
+
+ try {
+ return await pb.collection(TASGRID_COLLECTION).getFullList({ filter, sort: '-created', requestKey: null });
+ } catch (err) {
+ console.warn(`Failed to fetch tasks from owner ${ownerId}:`, err);
+ return [];
+ }
+ });
+
+ const ownerResults = await Promise.all(ownerFetchPromises);
+ ownerResults.flat().forEach((r: any) => {
+ if (!seenIds.has(r.id)) {
+ const tags = r.tags || [];
+ if (!tags.includes("__template__")) {
+ allTasks.push(mapRecordToTask(r));
+ seenIds.add(r.id);
+ }
+ }
+ });
+ }
+
+ // Set store ONCE with all tasks
setStore("tasks", reconcile(allTasks));
setStore("templates", loadedTemplates);
// Check recurring tasks after load
checkRecurringTasks();
+ // 2.7. Fetch Share Rules (owned by current user)
+ await loadShareRules();
+
// 3. Subscribe to Realtime Updates
await subscribeToRealtime();
@@ -931,6 +1139,136 @@ export const setFilter = (update: Partial) => {
setStore("filter", (f) => ({ ...f, ...update }));
};
+// -- Sharing Functions --
+
+export const shareTask = async (taskId: string, userId: string, access: 'view' | 'edit' = 'view') => {
+ if (!pb.authStore.isValid) return;
+
+ const task = store.tasks.find(t => t.id === taskId);
+ if (!task) return;
+
+ const existingShares = task.sharedWith || [];
+ if (existingShares.some(s => s.userId === userId)) {
+ toast.info(`Task is already shared with this user`);
+ return;
+ }
+
+ const newShares = [...existingShares, { userId, access }];
+
+ // Update task with new sharedWith
+ await updateTask(taskId, { sharedWith: newShares });
+ toast.success(`Task shared successfully`);
+};
+
+export const revokeShare = async (taskId: string, userId: string) => {
+ if (!pb.authStore.isValid) return;
+
+ const task = store.tasks.find(t => t.id === taskId);
+ if (!task) return;
+
+ const existingShares = task.sharedWith || [];
+ const newShares = existingShares.filter(s => s.userId !== userId);
+
+ await updateTask(taskId, { sharedWith: newShares });
+ toast.success(`Share revoked`);
+};
+
+export const splitTask = async (taskId: string, userId: string) => {
+ if (!pb.authStore.isValid) return;
+
+ const task = store.tasks.find(t => t.id === taskId);
+ if (!task) return;
+
+ try {
+ // Create a duplicate task owned by the target user
+ const duplicateData = {
+ user: userId,
+ title: task.title,
+ content: task.content,
+ priority: task.priority,
+ size: task.size,
+ tags: task.tags,
+ startDate: task.startDate,
+ dueDate: task.dueDate,
+ completed: task.completed,
+ deletedAt: null,
+ sharedWith: [], // Start fresh with no shares
+ recurrence: task.recurrence
+ };
+
+ await pb.collection(TASGRID_COLLECTION).create(duplicateData, { requestKey: null });
+
+ // Remove the user from the original task's sharedWith
+ const existingShares = task.sharedWith || [];
+ const newShares = existingShares.filter(s => s.userId !== userId);
+ await updateTask(taskId, { sharedWith: newShares });
+
+ toast.success(`Task split - user now has their own copy`);
+ } catch (err: any) {
+ console.error("Failed to split task:", err);
+ console.error("Error details:", err.response?.data || err.data || err.message);
+ toast.error("Failed to split task: " + (err.response?.data?.message || err.message || "Unknown error"));
+ }
+};
+
+// -- Share Rules (for tag-based and all-tasks sharing) --
+
+const SHARE_RULES_COLLECTION = 'TasGrid_ShareRules';
+
+export const loadShareRules = async () => {
+ if (!pb.authStore.isValid) return;
+
+ try {
+ const records = await pb.collection(SHARE_RULES_COLLECTION).getFullList({
+ filter: `ownerId = "${pb.authStore.model?.id}"`
+ });
+
+ const rules: ShareRule[] = records.map(r => ({
+ id: r.id,
+ ownerId: r.ownerId,
+ targetUserId: r.targetUserId,
+ type: r.type as 'tag' | 'all',
+ tagName: r.tagName
+ }));
+
+ setStore("shareRules", rules);
+ } catch (err) {
+ console.error("Failed to load share rules:", err);
+ // Collection might not exist yet, that's okay
+ }
+};
+
+export const addShareRule = async (type: 'tag' | 'all', targetUserId: string, tagName?: string) => {
+ if (!pb.authStore.isValid) return;
+
+ try {
+ await pb.collection(SHARE_RULES_COLLECTION).create({
+ ownerId: pb.authStore.model?.id,
+ targetUserId,
+ type,
+ tagName: type === 'tag' ? tagName : null
+ });
+ // Realtime subscription will handle adding to store
+ toast.success(`Share rule created`);
+ } catch (err) {
+ console.error("Failed to create share rule:", err);
+ toast.error("Failed to create share rule. Make sure the collection exists in PocketBase.");
+ }
+};
+
+export const removeShareRule = async (ruleId: string) => {
+ if (!pb.authStore.isValid) return;
+
+ try {
+ await pb.collection(SHARE_RULES_COLLECTION).delete(ruleId);
+ setStore("shareRules", (rules) => rules.filter(r => r.id !== ruleId));
+ toast.success("Share rule removed");
+ } catch (err) {
+ console.error("Failed to delete share rule:", err);
+ toast.error("Failed to delete share rule.");
+ }
+};
+
export const clearFilter = () => {
setStore("filter", {
query: "",
diff --git a/src/views/SettingsView.tsx b/src/views/SettingsView.tsx
index 15286a2..09c0ea0 100644
--- a/src/views/SettingsView.tsx
+++ b/src/views/SettingsView.tsx
@@ -1,15 +1,16 @@
import { type Component, For, Show, createSignal, lazy, Suspense } from "solid-js";
import { ThemeToggle } from "../components/ThemeToggle";
-import { store, restoreTask, deleteTaskPermanently, setMatrixScaleDays, addTemplate, removeTemplate, updateTemplate, upsertTagDefinition, removeTagDefinition, renameTagDefinition } from "@/store";
+import { store, restoreTask, deleteTaskPermanently, setMatrixScaleDays, addTemplate, removeTemplate, updateTemplate, upsertTagDefinition, removeTagDefinition, renameTagDefinition, addShareRule, removeShareRule } from "@/store";
import { useTheme } from "@/components/ThemeProvider";
-import { Trash2, Undo2, ArrowLeftRight, Hash, Copy, Plus, ChevronDown, ChevronRight, Type } from "lucide-solid";
+import { Trash2, Undo2, ArrowLeftRight, Hash, Copy, Plus, ChevronDown, ChevronRight, Type, Share2, Users, Tag } from "lucide-solid";
import { TagPicker } from "@/components/TagPicker";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { toast } from "solid-sonner";
-import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { PRIORITY_OPTIONS, URGENCY_OPTIONS } from "@/lib/constants";
import { pb } from "@/lib/pocketbase";
+import { createEffect } from "solid-js";
const TaskEditor = lazy(() => import("../components/TaskEditor").then(m => ({ default: m.TaskEditor })));
@@ -18,7 +19,35 @@ export const SettingsView: Component = () => {
const [isTagsOpen, setIsTagsOpen] = createSignal(false);
const [isTemplatesOpen, setIsTemplatesOpen] = createSignal(false);
const [isTrashOpen, setIsTrashOpen] = createSignal(false);
+ const [isSharingOpen, setIsSharingOpen] = createSignal(false);
const [expandedTemplateId, setExpandedTemplateId] = createSignal(null);
+ const [shareUserId, setShareUserId] = createSignal("");
+ const [shareType, setShareType] = createSignal<'all' | 'tag'>('all');
+ const [shareTag, setShareTag] = createSignal("");
+ const [allUsers, setAllUsers] = createSignal>([]);
+ const [usersLoading, setUsersLoading] = createSignal(false);
+
+ // Fetch users when sharing section opens
+ createEffect(() => {
+ if (isSharingOpen() && allUsers().length === 0 && !usersLoading()) {
+ setUsersLoading(true);
+ pb.collection('users').getFullList({ fields: 'id,name,email' })
+ .then(records => {
+ setAllUsers(records.map((r: any) => ({ id: r.id, name: r.name || r.email || r.id })));
+ })
+ .catch(err => console.error("Failed to load users:", err))
+ .finally(() => setUsersLoading(false));
+ }
+ });
+
+ const getUserName = (userId: string) => {
+ return allUsers().find(u => u.id === userId)?.name || userId;
+ };
+
+ const availableUsers = () => {
+ const currentUserId = pb.authStore.model?.id;
+ return allUsers().filter(u => u.id !== currentUserId);
+ };
return (
@@ -53,6 +82,132 @@ export const SettingsView: Component = () => {
+ {/* Sharing */}
+
+ setIsSharingOpen(!isSharingOpen())}
+ >
+
+
+
+ Task Sharing
+
+
Share all tasks or tasks with specific tags with others.
+
+
+ {isSharingOpen() ? : }
+
+
+
+
+
+ {/* Add new share rule */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Active share rules */}
+
+ No active share rules.
+
+ }>
+ {(rule) => (
+
+
+ {rule.type === 'all' ?
:
}
+
+
{getUserName(rule.targetUserId)}
+
+ {rule.type === 'all' ? 'All Tasks' : `Tag: ${rule.tagName}`}
+
+
+
+
+
+ )}
+
+
+
+
+
{/* Matrix Configuration */}