feat: añadir endpoint /api/me/tasks y mostrar tareas en app web

Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>
webui
brobert 2 weeks ago
parent 8a807d8af3
commit da025326b5

@ -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<number, string[]>();
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' }
});
};

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

@ -1,7 +1,48 @@
<script lang="ts">
export let data: { userId: string };
export let data: {
userId: string;
tasks: Array<{
id: number;
description: string;
due_date: string | null;
group_id: string | null;
display_code: number | null;
assignees: string[];
}>;
};
async function logout() {
try {
await fetch('/api/logout', { method: 'POST' });
} catch {}
location.href = '/';
}
</script>
<h1>Panel</h1>
<p>Sesión iniciada como: <strong>{data.userId}</strong></p>
<p>Esta es una página protegida. La cookie de sesión se renueva con cada visita (idle timeout).</p>
<div style="margin: 1rem 0;">
<button on:click={logout}>Cerrar sesión</button>
</div>
<h2>Mis tareas (abiertas)</h2>
{#if data.tasks.length === 0}
<p>No tienes tareas abiertas.</p>
{:else}
<ul>
{#each data.tasks as t}
<li>
<span>#{t.display_code ?? t.id}{t.description}</span>
{#if t.due_date}
<small> (vence: {t.due_date})</small>
{/if}
{#if t.assignees?.length}
<small> — asignados: {t.assignees.join(', ')}</small>
{/if}
</li>
{/each}
</ul>
{/if}
<p style="margin-top:1rem;">La cookie de sesión se renueva con cada visita (idle timeout).</p>

Loading…
Cancel
Save