diff --git a/backend/src/routes/tasks.js b/backend/src/routes/tasks.js index bdd44d4..d7cdf2d 100644 --- a/backend/src/routes/tasks.js +++ b/backend/src/routes/tasks.js @@ -149,12 +149,18 @@ export async function taskRoutes(app) { app.post('/api/tasks', { preHandler: requireCap('use') }, async (req, reply) => { const { list_id, title, notes, priority, due_at, remind_at, recur_rule, category_id, project_id } = req.body || {} if (!title?.trim()) return reply.status(400).send({ error: 'title required' }) - // Verify the target list belongs to the caller. - const { rows: [list] } = await pool.query( - 'SELECT id FROM lists WHERE id = $1 AND owner_id = $2', - [list_id, req.user.email] - ) - if (!list) return reply.status(400).send({ error: 'invalid list' }) + // list_id is optional — omitting it lands the task "unfiled" (list_id + // NULL), the same state assign/reallocate already use. Projects need this + // so a task can be created straight into a project without first needing + // a personal list, since projects are a cross-cutting grouping, not a + // list replacement. + if (list_id) { + const { rows: [list] } = await pool.query( + 'SELECT id FROM lists WHERE id = $1 AND owner_id = $2', + [list_id, req.user.email] + ) + if (!list) return reply.status(400).send({ error: 'invalid list' }) + } if (priority && !PRIORITIES.includes(priority)) return reply.status(400).send({ error: 'bad priority' }) if (recur_rule && !RECUR_RULES.includes(recur_rule)) return reply.status(400).send({ error: 'bad recur_rule' }) if (category_id) { @@ -166,14 +172,16 @@ export async function taskRoutes(app) { if (!p) return reply.status(400).send({ error: 'invalid project' }) } const { rows: [max] } = await pool.query( - 'SELECT COALESCE(MAX(sort_order), 0) AS m FROM tasks WHERE list_id = $1', - [list_id] + list_id + ? 'SELECT COALESCE(MAX(sort_order), 0) AS m FROM tasks WHERE list_id = $1' + : 'SELECT COALESCE(MAX(sort_order), 0) AS m FROM tasks WHERE list_id IS NULL', + list_id ? [list_id] : [] ) const { rows: [task] } = await pool.query( `INSERT INTO tasks (list_id, owner_id, created_by, title, notes, priority, due_at, remind_at, recur_rule, category_id, project_id, sort_order) VALUES ($1, $2, $2, $3, $4, COALESCE($5, 'medium'), $6, $7, $8, $9, $10, $11) RETURNING *`, - [list_id, req.user.email, title.trim(), notes || null, priority || null, + [list_id || null, req.user.email, title.trim(), notes || null, priority || null, due_at || null, remind_at || null, recur_rule || null, category_id || null, project_id || null, Number(max.m) + 1] ) return task diff --git a/frontend/src/components/TaskDetail.tsx b/frontend/src/components/TaskDetail.tsx index 6e03410..25b7a0c 100644 --- a/frontend/src/components/TaskDetail.tsx +++ b/frontend/src/components/TaskDetail.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { X, Trash2, Plus, Bell, Repeat, CalendarClock, FolderInput, Tag, FolderKanban, UserPlus, Send } from 'lucide-react' +import { X, Trash2, Plus, Bell, Repeat, CalendarClock, FolderInput, Tag, FolderKanban, UserPlus, Send, ChevronDown } from 'lucide-react' import type { TaskDetail as TDetail, Priority, RecurRule } from '../types' import { PRIORITIES, PRIORITY_LABELS, RECUR_RULES, RECUR_LABELS } from '../types' import { @@ -31,6 +31,7 @@ export default function TaskDetail({ taskId, onChange, onClose }: const [tagEmail, setTagEmail] = useState('') const [assignEmail, setAssignEmail] = useState('') const [reallocateEmail, setReallocateEmail] = useState('') + const [sharingOpen, setSharingOpen] = useState(false) useEffect(() => { fetchTask(taskId).then(t => { setTask(t); setTitle(t.title); setNotes(t.notes ?? '') }) @@ -208,49 +209,61 @@ export default function TaskDetail({ taskId, onChange, onClose }: - {/* Sharing — tag (anyone with edit rights) / assign (owner + assign cap) / reallocate (dispatcher cap) */} -