From da025326b5d4c7aa03db537b2f96d8090fe92cb9 Mon Sep 17 00:00:00 2001 From: brobert Date: Mon, 13 Oct 2025 00:38:10 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20a=C3=B1adir=20endpoint=20/api/me/tasks?= =?UTF-8?q?=20y=20mostrar=20tareas=20en=20app=20web?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: aider (openrouter/openai/gpt-5) --- apps/web/src/routes/api/me/tasks/+server.ts | 115 ++++++++++++++++++++ apps/web/src/routes/app/+page.server.ts | 23 +++- apps/web/src/routes/app/+page.svelte | 45 +++++++- 3 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/routes/api/me/tasks/+server.ts diff --git a/apps/web/src/routes/api/me/tasks/+server.ts b/apps/web/src/routes/api/me/tasks/+server.ts new file mode 100644 index 0000000..65f8a06 --- /dev/null +++ b/apps/web/src/routes/api/me/tasks/+server.ts @@ -0,0 +1,115 @@ +import type { RequestHandler } from './$types'; +import { getDb } from '$lib/server/db'; + +function clamp(n: number, min: number, max: number) { + return Math.max(min, Math.min(max, n)); +} + +export const GET: RequestHandler = async (event) => { + // Requiere sesión + const userId = event.locals.userId ?? null; + if (!userId) { + return new Response('Unauthorized', { status: 401 }); + } + + const url = new URL(event.request.url); + const search = (url.searchParams.get('search') || '').trim(); + 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); + + // Por ahora solo "open" + if (status !== 'open') { + return new Response('Bad Request', { status: 400 }); + } + + const offset = (page - 1) * limit; + + const db = await getDb(); + + // Construir filtros dinámicos + const whereParts = [ + `a.user_id = ?`, + `COALESCE(t.completed, 0) = 0`, + `t.completed_at IS NULL` + ]; + const params: any[] = [userId]; + + if (search) { + whereParts.push(`t.description LIKE ?`); + params.push(`%${search.replace(/%/g, '\\%').replace(/_/g, '\\_')}%`); + } + + // Total + const totalRow = db + .prepare( + `SELECT COUNT(*) AS cnt + FROM tasks t + INNER JOIN task_assignments a ON a.task_id = t.id + WHERE ${whereParts.join(' AND ')}` + ) + .get(...params) as any; + const total = Number(totalRow?.cnt || 0); + + // Items + const itemsRows = db + .prepare( + `SELECT t.id, t.description, t.due_date, t.group_id, t.display_code + FROM tasks t + INNER JOIN task_assignments a ON a.task_id = t.id + WHERE ${whereParts.join(' AND ')} + ORDER BY + CASE WHEN t.due_date IS NULL THEN 1 ELSE 0 END, + t.due_date ASC, + t.id ASC + LIMIT ? OFFSET ?` + ) + .all(...params, limit, offset) as any[]; + + const items = itemsRows.map((r) => ({ + id: Number(r.id), + description: String(r.description || ''), + due_date: r.due_date ? String(r.due_date) : null, + group_id: r.group_id ? String(r.group_id) : null, + display_code: r.display_code != null ? Number(r.display_code) : null, + assignees: [] as string[] + })); + + // Cargar asignados de todas las tareas recuperadas (si hay) + if (items.length > 0) { + const ids = items.map((it) => it.id); + const placeholders = ids.map(() => '?').join(','); + const assignRows = db + .prepare( + `SELECT task_id, user_id + FROM task_assignments + WHERE task_id IN (${placeholders}) + ORDER BY assigned_at ASC` + ) + .all(...ids) as any[]; + + const map = new Map(); + for (const row of assignRows) { + const tid = Number(row.task_id); + const uid = String(row.user_id); + if (!map.has(tid)) map.set(tid, []); + map.get(tid)!.push(uid); + } + for (const it of items) { + it.assignees = map.get(it.id) || []; + } + } + + const body = { + items, + page, + limit, + total, + hasMore: offset + items.length < total + }; + + return new Response(JSON.stringify(body), { + status: 200, + headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' } + }); +}; diff --git a/apps/web/src/routes/app/+page.server.ts b/apps/web/src/routes/app/+page.server.ts index 57a97a1..be1fc7e 100644 --- a/apps/web/src/routes/app/+page.server.ts +++ b/apps/web/src/routes/app/+page.server.ts @@ -7,5 +7,26 @@ export const load: PageServerLoad = async (event) => { // No hay sesión: redirigir a la home throw redirect(303, '/'); } - return { userId }; + + // Cargar "mis tareas" desde la API interna + let tasks: Array<{ + id: number; + description: string; + due_date: string | null; + group_id: string | null; + display_code: number | null; + assignees: string[]; + }> = []; + + try { + const res = await event.fetch('/api/me/tasks?limit=20'); + if (res.ok) { + const json = await res.json(); + tasks = Array.isArray(json?.items) ? json.items : []; + } + } catch { + // Ignorar errores y dejar lista vacía + } + + return { userId, tasks }; }; diff --git a/apps/web/src/routes/app/+page.svelte b/apps/web/src/routes/app/+page.svelte index 4470e52..e1bfec9 100644 --- a/apps/web/src/routes/app/+page.svelte +++ b/apps/web/src/routes/app/+page.svelte @@ -1,7 +1,48 @@

Panel

Sesión iniciada como: {data.userId}

-

Esta es una página protegida. La cookie de sesión se renueva con cada visita (idle timeout).

+ +
+ +
+ +

Mis tareas (abiertas)

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

No tienes tareas abiertas.

+{:else} +
    + {#each data.tasks as t} +
  • + #{t.display_code ?? t.id} — {t.description} + {#if t.due_date} + (vence: {t.due_date}) + {/if} + {#if t.assignees?.length} + — asignados: {t.assignees.join(', ')} + {/if} +
  • + {/each} +
+{/if} + +

La cookie de sesión se renueva con cada visita (idle timeout).