feat: agregar edición de tareas (claim/unassign y PATCH due_date)
Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>webui
parent
a8a0a3b5f6
commit
a370f8cef6
@ -0,0 +1,112 @@
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getDb } from '$lib/server/db';
|
||||
|
||||
function isValidYmd(input: string): boolean {
|
||||
const m = /^\s*(\d{4})-(\d{2})-(\d{2})\s*$/.exec(input || '');
|
||||
if (!m) return false;
|
||||
const y = Number(m[1]), mo = Number(m[2]), d = Number(m[3]);
|
||||
if (!Number.isFinite(y) || !Number.isFinite(mo) || !Number.isFinite(d)) return false;
|
||||
if (mo < 1 || mo > 12 || d < 1 || d > 31) return false;
|
||||
const dt = new Date(`${String(y).padStart(4, '0')}-${String(mo).padStart(2, '0')}-${String(d).padStart(2, '0')}T00:00:00Z`);
|
||||
// Comprobar que el Date resultante coincide (evita 2025-02-31)
|
||||
return dt.getUTCFullYear() === y && (dt.getUTCMonth() + 1) === mo && dt.getUTCDate() === d;
|
||||
}
|
||||
|
||||
export const PATCH: RequestHandler = async (event) => {
|
||||
const userId = event.locals.userId ?? null;
|
||||
if (!userId) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const idStr = event.params.id || '';
|
||||
const taskId = parseInt(idStr, 10);
|
||||
if (!Number.isFinite(taskId) || taskId <= 0) {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
|
||||
let payload: any = null;
|
||||
try {
|
||||
payload = await event.request.json();
|
||||
} catch {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
|
||||
const due_date_raw = payload?.due_date;
|
||||
if (due_date_raw !== null && due_date_raw !== undefined && typeof due_date_raw !== 'string') {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
const due_date =
|
||||
due_date_raw == null || String(due_date_raw).trim() === ''
|
||||
? null
|
||||
: String(due_date_raw).trim();
|
||||
|
||||
if (due_date !== null && !isValidYmd(due_date)) {
|
||||
return new Response(JSON.stringify({ error: 'invalid_due_date' }), {
|
||||
status: 400,
|
||||
headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' }
|
||||
});
|
||||
}
|
||||
|
||||
const db = await getDb();
|
||||
|
||||
// Cargar tarea y validar abierta
|
||||
const task = db
|
||||
.prepare(
|
||||
`SELECT id, description, due_date, group_id, COALESCE(completed, 0) AS completed, completed_at, display_code
|
||||
FROM tasks
|
||||
WHERE id = ?`
|
||||
)
|
||||
.get(taskId) as any;
|
||||
|
||||
if (!task) {
|
||||
return new Response(JSON.stringify({ status: 'not_found' }), {
|
||||
status: 404,
|
||||
headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' }
|
||||
});
|
||||
}
|
||||
if (Number(task.completed) !== 0 || task.completed_at) {
|
||||
return new Response(JSON.stringify({ status: 'completed' }), {
|
||||
status: 400,
|
||||
headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' }
|
||||
});
|
||||
}
|
||||
|
||||
// Gating: grupo permitido + usuario miembro activo (si tiene group_id)
|
||||
const groupId: string | null = task.group_id ? String(task.group_id) : null;
|
||||
if (groupId) {
|
||||
const allowed = db
|
||||
.prepare(`SELECT 1 FROM allowed_groups WHERE group_id = ? AND status = 'allowed' LIMIT 1`)
|
||||
.get(groupId);
|
||||
const active = db
|
||||
.prepare(
|
||||
`SELECT 1 FROM group_members WHERE group_id = ? AND user_id = ? AND is_active = 1 LIMIT 1`
|
||||
)
|
||||
.get(groupId, userId);
|
||||
|
||||
if (!allowed || !active) {
|
||||
return new Response('Forbidden', { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
// Aplicar actualización
|
||||
db.prepare(`UPDATE tasks SET due_date = ? WHERE id = ?`).run(due_date, taskId);
|
||||
|
||||
const updated = db
|
||||
.prepare(`SELECT id, description, due_date, display_code FROM tasks WHERE id = ?`)
|
||||
.get(taskId) as any;
|
||||
|
||||
const body = {
|
||||
status: 'updated',
|
||||
task: {
|
||||
id: Number(updated.id),
|
||||
description: String(updated.description || ''),
|
||||
due_date: updated.due_date ? String(updated.due_date) : null,
|
||||
display_code: updated.display_code != null ? Number(updated.display_code) : null
|
||||
}
|
||||
};
|
||||
|
||||
return new Response(JSON.stringify(body), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' }
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,99 @@
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getDb } from '$lib/server/db';
|
||||
|
||||
function toIsoSql(d: Date): string {
|
||||
return d.toISOString().replace('T', ' ').replace('Z', '');
|
||||
}
|
||||
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
const userId = event.locals.userId ?? null;
|
||||
if (!userId) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const idStr = event.params.id || '';
|
||||
const taskId = parseInt(idStr, 10);
|
||||
if (!Number.isFinite(taskId) || taskId <= 0) {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
|
||||
const db = await getDb();
|
||||
|
||||
// Cargar tarea y validar abierta
|
||||
const task = db
|
||||
.prepare(
|
||||
`SELECT id, description, due_date, group_id, COALESCE(completed, 0) AS completed, completed_at, display_code
|
||||
FROM tasks
|
||||
WHERE id = ?`
|
||||
)
|
||||
.get(taskId) as any;
|
||||
|
||||
if (!task) {
|
||||
return new Response(JSON.stringify({ status: 'not_found' }), {
|
||||
status: 404,
|
||||
headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' }
|
||||
});
|
||||
}
|
||||
if (Number(task.completed) !== 0 || task.completed_at) {
|
||||
return new Response(JSON.stringify({ status: 'completed' }), {
|
||||
status: 400,
|
||||
headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' }
|
||||
});
|
||||
}
|
||||
|
||||
// Gating: grupo permitido + usuario miembro activo (si tiene group_id)
|
||||
const groupId: string | null = task.group_id ? String(task.group_id) : null;
|
||||
if (groupId) {
|
||||
const allowed = db
|
||||
.prepare(`SELECT 1 FROM allowed_groups WHERE group_id = ? AND status = 'allowed' LIMIT 1`)
|
||||
.get(groupId);
|
||||
const active = db
|
||||
.prepare(
|
||||
`SELECT 1 FROM group_members WHERE group_id = ? AND user_id = ? AND is_active = 1 LIMIT 1`
|
||||
)
|
||||
.get(groupId, userId);
|
||||
|
||||
if (!allowed || !active) {
|
||||
return new Response('Forbidden', { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
// Asegurar existencia del usuario (best-effort)
|
||||
try {
|
||||
db.transaction(() => {
|
||||
db.prepare(
|
||||
`INSERT INTO users (id, first_seen, last_seen)
|
||||
VALUES (?, strftime('%Y-%m-%d %H:%M:%f','now'), strftime('%Y-%m-%d %H:%M:%f','now'))
|
||||
ON CONFLICT(id) DO NOTHING`
|
||||
).run(userId);
|
||||
db.prepare(
|
||||
`UPDATE users SET last_seen = strftime('%Y-%m-%d %H:%M:%f','now') WHERE id = ?`
|
||||
).run(userId);
|
||||
})();
|
||||
} catch {}
|
||||
|
||||
// Reclamar (idempotente)
|
||||
const res = db
|
||||
.prepare(
|
||||
`INSERT OR IGNORE INTO task_assignments (task_id, user_id, assigned_by)
|
||||
VALUES (?, ?, ?)`
|
||||
)
|
||||
.run(taskId, userId, userId) as any;
|
||||
|
||||
const status = Number(res?.changes || 0) > 0 ? 'claimed' : 'already';
|
||||
|
||||
const body = {
|
||||
status,
|
||||
task: {
|
||||
id: Number(task.id),
|
||||
description: String(task.description || ''),
|
||||
due_date: task.due_date ? String(task.due_date) : null,
|
||||
display_code: task.display_code != null ? Number(task.display_code) : null
|
||||
}
|
||||
};
|
||||
|
||||
return new Response(JSON.stringify(body), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' }
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,84 @@
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getDb } from '$lib/server/db';
|
||||
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
const userId = event.locals.userId ?? null;
|
||||
if (!userId) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const idStr = event.params.id || '';
|
||||
const taskId = parseInt(idStr, 10);
|
||||
if (!Number.isFinite(taskId) || taskId <= 0) {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
|
||||
const db = await getDb();
|
||||
|
||||
// Cargar tarea y validar abierta
|
||||
const task = db
|
||||
.prepare(
|
||||
`SELECT id, description, due_date, group_id, COALESCE(completed, 0) AS completed, completed_at, display_code
|
||||
FROM tasks
|
||||
WHERE id = ?`
|
||||
)
|
||||
.get(taskId) as any;
|
||||
|
||||
if (!task) {
|
||||
return new Response(JSON.stringify({ status: 'not_found' }), {
|
||||
status: 404,
|
||||
headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' }
|
||||
});
|
||||
}
|
||||
if (Number(task.completed) !== 0 || task.completed_at) {
|
||||
return new Response(JSON.stringify({ status: 'completed' }), {
|
||||
status: 400,
|
||||
headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' }
|
||||
});
|
||||
}
|
||||
|
||||
// Gating: grupo permitido + usuario miembro activo (si tiene group_id)
|
||||
const groupId: string | null = task.group_id ? String(task.group_id) : null;
|
||||
if (groupId) {
|
||||
const allowed = db
|
||||
.prepare(`SELECT 1 FROM allowed_groups WHERE group_id = ? AND status = 'allowed' LIMIT 1`)
|
||||
.get(groupId);
|
||||
const active = db
|
||||
.prepare(
|
||||
`SELECT 1 FROM group_members WHERE group_id = ? AND user_id = ? AND is_active = 1 LIMIT 1`
|
||||
)
|
||||
.get(groupId, userId);
|
||||
|
||||
if (!allowed || !active) {
|
||||
return new Response('Forbidden', { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
// Eliminar asignación (idempotente)
|
||||
const delRes = db
|
||||
.prepare(`DELETE FROM task_assignments WHERE task_id = ? AND user_id = ?`)
|
||||
.run(taskId, userId) as any;
|
||||
|
||||
const cntRow = db
|
||||
.prepare(`SELECT COUNT(*) AS cnt FROM task_assignments WHERE task_id = ?`)
|
||||
.get(taskId) as any;
|
||||
const remaining = Number(cntRow?.cnt || 0);
|
||||
|
||||
const status = Number(delRes?.changes || 0) > 0 ? 'unassigned' : 'not_assigned';
|
||||
|
||||
const body = {
|
||||
status,
|
||||
now_unassigned: remaining === 0,
|
||||
task: {
|
||||
id: Number(task.id),
|
||||
description: String(task.description || ''),
|
||||
due_date: task.due_date ? String(task.due_date) : null,
|
||||
display_code: task.display_code != null ? Number(task.display_code) : null
|
||||
}
|
||||
};
|
||||
|
||||
return new Response(JSON.stringify(body), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' }
|
||||
});
|
||||
};
|
||||
Loading…
Reference in New Issue