diff --git a/apps/web/src/routes/api/groups/[id]/tasks/+server.ts b/apps/web/src/routes/api/groups/[id]/tasks/+server.ts index bb193c0..ddacd86 100644 --- a/apps/web/src/routes/api/groups/[id]/tasks/+server.ts +++ b/apps/web/src/routes/api/groups/[id]/tasks/+server.ts @@ -16,6 +16,11 @@ export const GET: RequestHandler = async (event) => { const url = new URL(event.request.url); const unassignedFirst = (url.searchParams.get('unassignedFirst') || '').trim().toLowerCase() === 'true'; + const onlyUnassigned = + (url.searchParams.get('onlyUnassigned') || '').trim().toLowerCase() === 'true'; + let limit = parseInt(url.searchParams.get('limit') || '', 10); + if (!Number.isFinite(limit) || limit <= 0) limit = 0; + if (limit > 100) limit = 100; const db = await getDb(); @@ -45,16 +50,28 @@ export const GET: RequestHandler = async (event) => { `t.id ASC` ); - const rows = db - .prepare( - `SELECT t.id, t.description, t.due_date, t.group_id, t.display_code + const whereParts = [ + `t.group_id = ?`, + `COALESCE(t.completed, 0) = 0`, + `t.completed_at IS NULL` + ]; + if (onlyUnassigned) { + whereParts.push( + `NOT EXISTS (SELECT 1 FROM task_assignments a WHERE a.task_id = t.id)` + ); + } + + const params: any[] = [groupId]; + + const sql = ` + SELECT t.id, t.description, t.due_date, t.group_id, t.display_code FROM tasks t - WHERE t.group_id = ? - AND COALESCE(t.completed, 0) = 0 - AND t.completed_at IS NULL - ORDER BY ${orderParts.join(', ')}` - ) - .all(groupId) as any[]; + WHERE ${whereParts.join(' AND ')} + ORDER BY ${orderParts.join(', ')}${limit > 0 ? ' LIMIT ?' : ''}`; + + if (limit > 0) params.push(limit); + + const rows = db.prepare(sql).all(...params) as any[]; let items = rows.map((r) => ({ id: Number(r.id), @@ -66,7 +83,7 @@ export const GET: RequestHandler = async (event) => { })); // Cargar asignados - if (items.length > 0) { + if (items.length > 0 && !onlyUnassigned) { const ids = items.map((it) => it.id); const placeholders = ids.map(() => '?').join(','); const assignRows = db diff --git a/apps/web/src/routes/api/me/tasks/+server.ts b/apps/web/src/routes/api/me/tasks/+server.ts index 6cd763b..1ea5850 100644 --- a/apps/web/src/routes/api/me/tasks/+server.ts +++ b/apps/web/src/routes/api/me/tasks/+server.ts @@ -17,6 +17,14 @@ export const GET: RequestHandler = async (event) => { const status = (url.searchParams.get('status') || 'open').trim().toLowerCase(); const page = clamp(parseInt(url.searchParams.get('page') || '1', 10) || 1, 1, 100000); const limit = clamp(parseInt(url.searchParams.get('limit') || '20', 10) || 20, 1, 100); + const dueBeforeParam = (url.searchParams.get('dueBefore') || '').trim(); + const soonDaysParam = parseInt(url.searchParams.get('soonDays') || '', 10); + let dueCutoff: string | null = dueBeforeParam || null; + if (!dueCutoff && Number.isFinite(soonDaysParam) && soonDaysParam >= 0) { + const d = new Date(); + d.setUTCDate(d.getUTCDate() + soonDaysParam); + dueCutoff = d.toISOString().slice(0, 10); + } // Por ahora solo "open" if (status !== 'open') { @@ -44,6 +52,11 @@ export const GET: RequestHandler = async (event) => { params.push(`%${search.replace(/%/g, '\\%').replace(/_/g, '\\_')}%`); } + if (dueCutoff) { + whereParts.push(`t.due_date IS NOT NULL AND t.due_date <= ?`); + params.push(dueCutoff); + } + // Total const totalRow = db .prepare( diff --git a/apps/web/src/routes/app/+page.server.ts b/apps/web/src/routes/app/+page.server.ts index be1fc7e..922d61c 100644 --- a/apps/web/src/routes/app/+page.server.ts +++ b/apps/web/src/routes/app/+page.server.ts @@ -18,8 +18,16 @@ export const load: PageServerLoad = async (event) => { assignees: string[]; }> = []; + // Filtros desde la query (?q=&soonDays=) + const q = (event.url.searchParams.get('q') || '').trim(); + const soonDaysStr = (event.url.searchParams.get('soonDays') || '').trim(); + try { - const res = await event.fetch('/api/me/tasks?limit=20'); + let fetchUrl = '/api/me/tasks?limit=20'; + if (q) fetchUrl += `&search=${encodeURIComponent(q)}`; + if (soonDaysStr) fetchUrl += `&soonDays=${encodeURIComponent(soonDaysStr)}`; + + const res = await event.fetch(fetchUrl); if (res.ok) { const json = await res.json(); tasks = Array.isArray(json?.items) ? json.items : []; @@ -28,5 +36,5 @@ export const load: PageServerLoad = async (event) => { // Ignorar errores y dejar lista vacía } - return { userId, tasks }; + return { userId, tasks, q, soonDays: soonDaysStr ? Number(soonDaysStr) : null }; }; diff --git a/apps/web/src/routes/app/+page.svelte b/apps/web/src/routes/app/+page.svelte index f0283bd..e9eacb8 100644 --- a/apps/web/src/routes/app/+page.svelte +++ b/apps/web/src/routes/app/+page.svelte @@ -9,6 +9,8 @@ display_code: number | null; assignees: string[]; }>; + q?: string | null; + soonDays?: number | null; }; @@ -22,6 +24,17 @@ +
+No tienes tareas abiertas.
diff --git a/apps/web/src/routes/app/groups/+page.server.ts b/apps/web/src/routes/app/groups/+page.server.ts index 4c3c1fa..0799258 100644 --- a/apps/web/src/routes/app/groups/+page.server.ts +++ b/apps/web/src/routes/app/groups/+page.server.ts @@ -4,9 +4,29 @@ export const load: PageServerLoad = async (event) => { const res = await event.fetch('/api/me/groups', { headers: { 'cache-control': 'no-store' } }); if (!res.ok) { // El gate del layout debería impedir llegar aquí sin sesión; devolvemos vacío como salvaguarda. - return { groups: [] }; + return { groups: [], previews: {} }; } const data = await res.json(); const groups = Array.isArray(data?.items) ? data.items : []; - return { groups }; + + // Prefetch de "sin responsable" por grupo (ligero) + const previews: Record