From 5b55cdc06ba6dfd2550d3f196ade0f9024352803 Mon Sep 17 00:00:00 2001 From: brobert Date: Wed, 15 Oct 2025 23:14:22 +0200 Subject: [PATCH] feat: quitar soonDays y adaptar vistas a Fase 1 con orden y grupos Co-authored-by: aider (openrouter/openai/gpt-5) --- apps/web/src/lib/ui/data/TaskItem.svelte | 12 ++ apps/web/src/routes/app/+page.server.ts | 96 ++++++++-- apps/web/src/routes/app/+page.svelte | 173 ++++++++++++------ .../web/src/routes/app/groups/+page.server.ts | 23 +-- apps/web/src/routes/app/groups/+page.svelte | 101 ++++++++-- 5 files changed, 310 insertions(+), 95 deletions(-) diff --git a/apps/web/src/lib/ui/data/TaskItem.svelte b/apps/web/src/lib/ui/data/TaskItem.svelte index fc13563..8728c6d 100644 --- a/apps/web/src/lib/ui/data/TaskItem.svelte +++ b/apps/web/src/lib/ui/data/TaskItem.svelte @@ -18,6 +18,7 @@ export let currentUserId: string | null | undefined = null; export let completed: boolean = false; export let completed_at: string | null = null; + export let groupName: string | null = null; const code = display_code ?? id; const codeStr = String(code).padStart(4, "0"); @@ -26,6 +27,7 @@ $: overdue = !!due_date && compareYmd(due_date, today) < 0; $: imminent = !!due_date && (isToday(due_date) || isTomorrow(due_date)); $: dateDmy = due_date ? ymdToDmy(due_date) : ""; + $: groupLabel = groupName != null ? groupName : "Personal"; let editing = false; let dateValue: string = due_date ?? ""; @@ -227,6 +229,7 @@
+ {groupLabel} {#if due_date} { throw redirect(303, '/'); } + // Parámetros de orden y paginación + const orderParam = (event.url.searchParams.get('order') || 'due').trim().toLowerCase(); + const order: 'due' | 'group' = orderParam === 'group' ? 'group' : 'due'; + const pageStr = (event.url.searchParams.get('page') || '1').trim(); + const page = Math.max(1, parseInt(pageStr, 10) || 1); + // Cargar "mis tareas" desde la API interna let openTasks: Array<{ id: number; @@ -29,36 +35,98 @@ export const load: PageServerLoad = async (event) => { }> = []; let hasMore: boolean = false; - // Filtros desde la query (?q=&soonDays=) - const q = (event.url.searchParams.get('q') || '').trim(); - const soonDaysStr = (event.url.searchParams.get('soonDays') || '').trim(); - const pageStr = (event.url.searchParams.get('page') || '1').trim(); - const page = Math.max(1, parseInt(pageStr, 10) || 1); + // Agregado: "Sin responsable de mis grupos" + let unassignedOpen: Array<{ + id: number; + description: string; + due_date: string | null; + group_id: string | null; + display_code: number | null; + assignees: string[]; + }> = []; + const groupNames: Record = {}; try { + // Mis tareas abiertas (paginadas, orden por fecha en server) let fetchUrl = '/api/me/tasks?limit=20'; - if (q) fetchUrl += `&search=${encodeURIComponent(q)}`; - if (soonDaysStr) fetchUrl += `&soonDays=${encodeURIComponent(soonDaysStr)}`; fetchUrl += `&page=${encodeURIComponent(String(page))}`; - const res = await event.fetch(fetchUrl); + const res = await event.fetch(fetchUrl, { headers: { 'cache-control': 'no-store' } }); if (res.ok) { const json = await res.json(); openTasks = Array.isArray(json?.items) ? json.items : []; hasMore = Boolean(json?.hasMore); } - // Cargar completadas en las últimas 24h (sin paginar por ahora) - let recentUrl = '/api/me/tasks?limit=20&status=recent'; - if (q) recentUrl += `&search=${encodeURIComponent(q)}`; - const resRecent = await event.fetch(recentUrl); + // Completadas en las últimas 24h (sin paginar por ahora) + const resRecent = await event.fetch('/api/me/tasks?limit=20&status=recent', { + headers: { 'cache-control': 'no-store' } + }); if (resRecent.ok) { const jsonRecent = await resRecent.json(); recentTasks = Array.isArray(jsonRecent?.items) ? jsonRecent.items : []; } + + // Mis grupos (para nombres y para recolectar "sin responsable") + const resGroups = await event.fetch('/api/me/groups', { + headers: { 'cache-control': 'no-store' } + }); + if (resGroups.ok) { + const jsonGroups = await resGroups.json(); + const groups = Array.isArray(jsonGroups?.items) ? jsonGroups.items : []; + for (const g of groups) { + const gid = String(g.id); + const gname = g.name != null ? String(g.name) : null; + if (gname) groupNames[gid] = gname; + + // Cargar solo "sin responsable" por grupo (sin límite) + try { + const r = await event.fetch( + `/api/groups/${encodeURIComponent(gid)}/tasks?onlyUnassigned=true&limit=0`, + { headers: { 'cache-control': 'no-store' } } + ); + if (r.ok) { + const j = await r.json(); + const items: any[] = Array.isArray(j?.items) ? j.items : []; + for (const it of items) { + unassignedOpen.push({ + id: Number(it.id), + description: String(it.description || ''), + due_date: it.due_date ? String(it.due_date) : null, + group_id: it.group_id ? String(it.group_id) : null, + display_code: it.display_code != null ? Number(it.display_code) : null, + assignees: Array.isArray(it.assignees) ? it.assignees.map(String) : [] + }); + } + } + } catch { + // ignorar fallos por grupo + } + } + } + + // Orden base por fecha para el agregado (NULL al final) + unassignedOpen.sort((a, b) => { + const ad = a.due_date, bd = b.due_date; + if (ad == null && bd == null) return a.id - b.id; + if (ad == null) return 1; + if (bd == null) return -1; + if (ad < bd) return -1; + if (ad > bd) return 1; + return a.id - b.id; + }); } catch { - // Ignorar errores y dejar lista vacía + // Ignorar errores y dejar listas vacías } - return { userId, openTasks, recentTasks, q, soonDays: soonDaysStr ? Number(soonDaysStr) : null, page, hasMore }; + return { + userId, + openTasks, + recentTasks, + unassignedOpen, + groupNames, + order, + page, + hasMore + }; }; diff --git a/apps/web/src/routes/app/+page.svelte b/apps/web/src/routes/app/+page.svelte index 86a3c7f..04c9e99 100644 --- a/apps/web/src/routes/app/+page.svelte +++ b/apps/web/src/routes/app/+page.svelte @@ -1,38 +1,84 @@

Sesión: {data.userId}

+
+ Orden: + Fecha + Grupo +
+

Mis tareas (abiertas)

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

No tienes tareas abiertas.

@@ -40,7 +86,7 @@
    {#each data.openTasks as t} - + {/each}
@@ -48,15 +94,37 @@ {#if (data.page ?? 1) > 1 || data.hasMore} 1 - ? `/app?${new URLSearchParams({ q: data.q ?? "", soonDays: data.soonDays != null ? String(data.soonDays) : "", page: String((data.page ?? 1) - 1) }).toString()}` - : null} - nextHref={data.hasMore - ? `/app?${new URLSearchParams({ q: data.q ?? "", soonDays: data.soonDays != null ? String(data.soonDays) : "", page: String((data.page ?? 1) + 1) }).toString()}` - : null} + prevHref={(data.page ?? 1) > 1 ? `/app?${buildQuery({ order: data.order, page: (data.page ?? 1) - 1 })}` : null} + nextHref={data.hasMore ? `/app?${buildQuery({ order: data.order, page: (data.page ?? 1) + 1 })}` : null} /> {/if} +

Sin responsable de mis grupos

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

No hay tareas sin responsable en tus grupos.

+{:else} + {#if data.order === 'group'} + {#each groupByGroup(data.unassignedOpen) as g} +

{g.name}

+ +
    + {#each g.tasks as t} + + {/each} +
+
+ {/each} + {:else} + +
    + {#each sortByDue(data.unassignedOpen) as t} + + {/each} +
+
+ {/if} +{/if} +

Completadas (últimas 24 h)

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

No hay tareas completadas recientemente.

@@ -69,58 +137,47 @@ currentUserId={data.userId} completed={true} completed_at={t.completed_at ?? null} + groupName={t.group_id ? (data.groupNames[t.group_id] || t.group_id) : 'Personal'} /> {/each} {/if} -
-
- -
- - -

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