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
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' }
|
||||
});
|
||||
};
|
||||
@ -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…
Reference in New Issue