feat: añadir /api/me/tasks/overview y adaptar /app para consumirla

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

@ -0,0 +1,113 @@
import type { RequestHandler } from './$types';
import { getDb } from '$lib/server/db';
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 orderParam = (url.searchParams.get('order') || 'due').trim().toLowerCase();
const order = orderParam === 'group_then_due' ? 'group_then_due' : 'due';
const db = await getDb();
// Orden para "assigned"
const assignedOrder =
order === 'group_then_due'
? `(t.group_id IS NULL) ASC, g.name COLLATE NOCASE ASC, CASE WHEN t.due_date IS NULL THEN 1 ELSE 0 END, t.due_date ASC, t.id ASC`
: `CASE WHEN t.due_date IS NULL THEN 1 ELSE 0 END, t.due_date ASC, t.id ASC`;
// Tareas asignadas al usuario (abiertas)
const assignedRows = db
.prepare(
`SELECT t.id, t.description, t.due_date, t.group_id, t.display_code, g.name AS group_name
FROM tasks t
LEFT JOIN groups g ON g.id = t.group_id
INNER JOIN task_assignments a ON a.task_id = t.id
WHERE a.user_id = ?
AND COALESCE(t.completed, 0) = 0
AND t.completed_at IS NULL
AND (
t.group_id IS NULL OR (
EXISTS (SELECT 1 FROM allowed_groups ag WHERE ag.group_id = t.group_id AND ag.status = 'allowed')
AND EXISTS (SELECT 1 FROM group_members gm WHERE gm.group_id = t.group_id AND gm.user_id = ? AND gm.is_active = 1)
)
)
ORDER BY ${assignedOrder}`
)
.all(userId, userId) as any[];
const assigned = assignedRows.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,
group_name: r.group_name != null ? String(r.group_name) : null, // personales => null
display_code: r.display_code != null ? Number(r.display_code) : null,
assignees: [] as string[]
}));
// Cargar asignados completos para "assigned"
if (assigned.length > 0) {
const ids = assigned.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 assigned) {
it.assignees = map.get(it.id) || [];
}
}
// Orden para "unassigned"
const unassignedOrder =
order === 'group_then_due'
? `(t.group_id IS NULL) ASC, g.name COLLATE NOCASE ASC, CASE WHEN t.due_date IS NULL THEN 1 ELSE 0 END, t.due_date ASC, t.id ASC`
: `CASE WHEN t.due_date IS NULL THEN 1 ELSE 0 END, t.due_date ASC, t.id ASC`;
// Tareas sin responsable (solo de grupos permitidos donde soy miembro activo)
const unassignedRows = db
.prepare(
`SELECT t.id, t.description, t.due_date, t.group_id, t.display_code, g.name AS group_name
FROM tasks t
LEFT JOIN groups g ON g.id = t.group_id
WHERE t.group_id IS NOT NULL
AND COALESCE(t.completed, 0) = 0
AND t.completed_at IS NULL
AND NOT EXISTS (SELECT 1 FROM task_assignments a WHERE a.task_id = t.id)
AND EXISTS (SELECT 1 FROM allowed_groups ag WHERE ag.group_id = t.group_id AND ag.status = 'allowed')
AND EXISTS (SELECT 1 FROM group_members gm WHERE gm.group_id = t.group_id AND gm.user_id = ? AND gm.is_active = 1)
ORDER BY ${unassignedOrder}`
)
.all(userId) as any[];
const unassigned = unassignedRows.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,
group_name: r.group_name != null ? String(r.group_name) : null,
display_code: r.display_code != null ? Number(r.display_code) : null,
assignees: [] as string[] // por definición, vacío
}));
return new Response(JSON.stringify({ assigned, unassigned }), {
status: 200,
headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' }
});
};

@ -67,6 +67,25 @@ export const load: PageServerLoad = async (event) => {
recentTasks = Array.isArray(jsonRecent?.items) ? jsonRecent.items : [];
}
// Overview: obtener "sin responsable" de todos los grupos en una sola llamada
const overviewOrder = order === 'group' ? 'group_then_due' : 'due';
const resOverview = await event.fetch(
`/api/me/tasks/overview?order=${encodeURIComponent(overviewOrder)}`,
{ headers: { 'cache-control': 'no-store' } }
);
if (resOverview.ok) {
const jsonOv = await resOverview.json();
const items: any[] = Array.isArray(jsonOv?.unassigned) ? jsonOv.unassigned : [];
unassignedOpen = items.map((it) => ({
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) : []
}));
}
// Mis grupos (para nombres y para recolectar "sin responsable")
const resGroups = await event.fetch('/api/me/groups', {
headers: { 'cache-control': 'no-store' }
@ -78,30 +97,6 @@ export const load: PageServerLoad = async (event) => {
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
}
}
}

Loading…
Cancel
Save