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 @@ +
+ + + +
+

Mis tareas (abiertas)

{#if data.tasks.length === 0}

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 = {}; + const previewLimit = 3; + + for (const g of groups) { + try { + const r = await event.fetch( + `/api/groups/${encodeURIComponent(g.id)}/tasks?unassignedFirst=true&onlyUnassigned=true&limit=${previewLimit}`, + { headers: { 'cache-control': 'no-store' } } + ); + if (r.ok) { + const j = await r.json(); + previews[String(g.id)] = Array.isArray(j?.items) ? j.items : []; + } + } catch { + // ignorar errores de un grupo y continuar + } + } + + return { groups, previews }; }; diff --git a/apps/web/src/routes/app/groups/+page.svelte b/apps/web/src/routes/app/groups/+page.svelte index b56e13e..a512a7a 100644 --- a/apps/web/src/routes/app/groups/+page.svelte +++ b/apps/web/src/routes/app/groups/+page.svelte @@ -4,9 +4,16 @@ name: string | null; counts: { open: number; unassigned: number }; }; + type TaskItem = { + id: number; + description: string; + due_date: string | null; + display_code: number | null; + }; - export let data: { groups: GroupItem[] }; + export let data: { groups: GroupItem[]; previews?: Record }; const groups = data.groups || []; + const previews = data.previews || {}; @@ -23,6 +30,22 @@
  • {g.name ?? g.id} (abiertas: {g.counts.open}, sin responsable: {g.counts.unassigned}) + + {#if previews[g.id]?.length} +
    + Sin responsable (hasta 3): +
      + {#each previews[g.id] as t} +
    • + #{t.display_code ?? t.id} — {t.description} + {#if t.due_date} + (vence: {t.due_date}) + {/if} +
    • + {/each} +
    +
    + {/if}
  • {/each}