You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

223 lines
7.0 KiB
TypeScript

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);
const dueBeforeParam = (url.searchParams.get('dueBefore') || '').trim();
const soonDaysParam = parseInt(url.searchParams.get('soonDays') || '', 10);
const soonDays = Number.isFinite(soonDaysParam) && soonDaysParam >= 0 ? Math.min(soonDaysParam, 365) : null;
let dueCutoff: string | null = dueBeforeParam || null;
if (!dueCutoff && soonDays != null) {
const d = new Date();
d.setUTCDate(d.getUTCDate() + soonDays);
dueCutoff = d.toISOString().slice(0, 10);
}
// Acepta "open" (por defecto) o "recent" (completadas <24h)
if (status !== 'open' && status !== 'recent') {
return new Response('Bad Request', { status: 400 });
}
const offset = (page - 1) * limit;
const db = await getDb();
if (status === 'recent') {
// Construir filtros para tareas completadas en <24h asignadas al usuario.
const whereParts = [
`a.user_id = ?`,
`(COALESCE(t.completed, 0) = 1 OR t.completed_at IS NOT NULL)`,
`t.completed_at IS NOT NULL AND t.completed_at >= strftime('%Y-%m-%d %H:%M:%f','now','-24 hours')`,
`(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)))`
];
const params: any[] = [userId, userId];
if (search) {
whereParts.push(`t.description LIKE ? ESCAPE '\\'`);
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 (order by completed_at DESC)
const itemsRows = db
.prepare(
`SELECT t.id, t.description, t.due_date, t.group_id, t.display_code, COALESCE(t.completed,0) as completed, t.completed_at
FROM tasks t
INNER JOIN task_assignments a ON a.task_id = t.id
WHERE ${whereParts.join(' AND ')}
ORDER BY t.completed_at DESC, t.id DESC
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,
completed: Number(r.completed || 0) === 1,
completed_at: r.completed_at ? String(r.completed_at) : null,
assignees: [] as string[]
}));
// Cargar asignados
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' }
});
}
// OPEN (comportamiento existente)
// Construir filtros dinámicos (con gating por grupo permitido y membresía activa)
const whereParts = [
`a.user_id = ?`,
`COALESCE(t.completed, 0) = 0`,
`t.completed_at IS NULL`,
`(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)))`
];
const params: any[] = [userId];
// Añadir userId para el chequeo de membresía en el filtro de gating
params.push(userId);
if (search) {
whereParts.push(`t.description LIKE ? ESCAPE '\\'`);
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(
`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' }
});
};