feat: añadir filtrado, límites y prefetch en grupos y tareas

Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>
webui
borja 2 weeks ago
parent 689e030a83
commit 1bd28380b8

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

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

@ -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 };
};

@ -9,6 +9,8 @@
display_code: number | null;
assignees: string[];
}>;
q?: string | null;
soonDays?: number | null;
};
</script>
@ -22,6 +24,17 @@
</form>
</div>
<form method="GET" action="/app" style="margin: 1rem 0;">
<input type="text" name="q" placeholder="Buscar tareas..." value={data.q ?? ''} />
<select name="soonDays">
<option value="" selected={String(data.soonDays ?? '') === ''}>Todas las fechas</option>
<option value="3" selected={String(data.soonDays ?? '') === '3'}>Próximos 3 días</option>
<option value="7" selected={String(data.soonDays ?? '') === '7'}>Próximos 7 días</option>
<option value="14" selected={String(data.soonDays ?? '') === '14'}>Próximos 14 días</option>
</select>
<button type="submit">Filtrar</button>
</form>
<h2>Mis tareas (abiertas)</h2>
{#if data.tasks.length === 0}
<p>No tienes tareas abiertas.</p>

@ -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<string, any[]> = {};
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 };
};

@ -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<string, TaskItem[]> };
const groups = data.groups || [];
const previews = data.previews || {};
</script>
<svelte:head>
@ -23,6 +30,22 @@
<li>
<strong>{g.name ?? g.id}</strong>
<small style="margin-left:8px;color:#555">(abiertas: {g.counts.open}, sin responsable: {g.counts.unassigned})</small>
{#if previews[g.id]?.length}
<div style="margin-top:6px; padding:6px 8px; background:#f6f6f6; border-radius:6px;">
<em style="color:#333;">Sin responsable (hasta 3):</em>
<ul style="margin:6px 0 0 16px;">
{#each previews[g.id] as t}
<li>
<span>#{t.display_code ?? t.id}{t.description}</span>
{#if t.due_date}
<small> (vence: {t.due_date})</small>
{/if}
</li>
{/each}
</ul>
</div>
{/if}
</li>
{/each}
</ul>

Loading…
Cancel
Save