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
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' }
|
|
});
|
|
};
|