From e8382cf85c68c866bf87ba7b97166ac3695c164a Mon Sep 17 00:00:00 2001 From: brobert Date: Tue, 14 Oct 2025 22:18:35 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20ampliar=20PATCH=20de=20tareas=20para=20?= =?UTF-8?q?descripci=C3=B3n=20e=20edici=C3=B3n=20en=20l=C3=ADnea?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: aider (openrouter/openai/gpt-5) --- apps/web/src/lib/ui/data/TaskItem.svelte | 95 ++++++++++++++++++- apps/web/src/routes/api/tasks/[id]/+server.ts | 46 ++++++++- 2 files changed, 136 insertions(+), 5 deletions(-) diff --git a/apps/web/src/lib/ui/data/TaskItem.svelte b/apps/web/src/lib/ui/data/TaskItem.svelte index 2f37666..39f4b43 100644 --- a/apps/web/src/lib/ui/data/TaskItem.svelte +++ b/apps/web/src/lib/ui/data/TaskItem.svelte @@ -2,6 +2,7 @@ import Badge from '$lib/ui/atoms/Badge.svelte'; import { dueStatus } from '$lib/utils/date'; import { success, error as toastError } from '$lib/stores/toasts'; + import { tick } from 'svelte'; export let id: number; export let description: string; @@ -18,6 +19,10 @@ let dateValue: string = due_date ?? ''; let busy = false; + // Edición de texto (inline) + let editingText = false; + let descEl: HTMLElement | null = null; + async function doClaim() { if (busy) return; busy = true; @@ -82,6 +87,7 @@ function toggleEdit() { editing = !editing; + if (editing) editingText = false; dateValue = due_date ?? ''; } @@ -91,12 +97,91 @@ dateValue = ''; saveDate(); } + + function toggleEditText() { + editingText = !editingText; + if (editingText) { + editing = false; + // Asegurar que el elemento refleja el texto actual y enfocarlo + if (descEl) { + descEl.textContent = description; + } + tick().then(() => { + if (descEl) { + descEl.focus(); + placeCaretAtEnd(descEl); + } + }); + } + } + + function placeCaretAtEnd(el: HTMLElement) { + const range = document.createRange(); + range.selectNodeContents(el); + range.collapse(false); + const sel = window.getSelection(); + sel?.removeAllRanges(); + sel?.addRange(range); + } + + async function saveText() { + if (busy) return; + const raw = (descEl?.textContent || '').replace(/\s+/g, ' ').trim(); + if (raw.length < 1 || raw.length > 1000) { + toastError('La descripción debe tener entre 1 y 1000 caracteres.'); + return; + } + if (raw === description) { + editingText = false; + return; + } + busy = true; + try { + const res = await fetch(`/api/tasks/${id}`, { + method: 'PATCH', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ description: raw }) + }); + if (res.ok) { + description = raw; + success('Descripción actualizada'); + editingText = false; + } else { + const txt = await res.text(); + toastError(txt || 'No se pudo actualizar la descripción'); + } + } catch { + toastError('Error de red'); + } finally { + busy = false; + } + } + + function cancelText() { + if (descEl) { + descEl.textContent = description; + } + editingText = false; + }
  • #{code} - {description} + { + if (e.key === 'Escape') { e.preventDefault(); cancelText(); } + else if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); saveText(); } + else if (e.key === 'Enter') { e.preventDefault(); } + }} + >{description} {#if due_date} {#if status === 'overdue'} vence: {due_date} @@ -121,6 +206,13 @@ {/if} + {#if !editingText} + + {:else} + + + {/if} + {#if !editing} {:else} @@ -154,6 +246,7 @@ color: var(--color-text-muted); } .desc { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .desc.editing { outline: 2px solid var(--color-primary); outline-offset: 2px; background: var(--color-surface); padding: 2px 4px; border-radius: 4px; white-space: normal; text-overflow: clip; } .meta { justify-self: end; } .muted { color: var(--color-text-muted); } diff --git a/apps/web/src/routes/api/tasks/[id]/+server.ts b/apps/web/src/routes/api/tasks/[id]/+server.ts index cfc329b..38efdcc 100644 --- a/apps/web/src/routes/api/tasks/[id]/+server.ts +++ b/apps/web/src/routes/api/tasks/[id]/+server.ts @@ -31,22 +31,54 @@ export const PATCH: RequestHandler = async (event) => { return new Response('Bad Request', { status: 400 }); } + // Validar que al menos se envíe algún campo editable + const hasDueField = Object.prototype.hasOwnProperty.call(payload ?? {}, 'due_date'); + const hasDescField = Object.prototype.hasOwnProperty.call(payload ?? {}, 'description'); + if (!hasDueField && !hasDescField) { + return new Response('Bad Request', { status: 400 }); + } + + // due_date (opcional) const due_date_raw = payload?.due_date; - if (due_date_raw !== null && due_date_raw !== undefined && typeof due_date_raw !== 'string') { + if (hasDueField && 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() === '' + !hasDueField || due_date_raw == null || String(due_date_raw).trim() === '' ? null : String(due_date_raw).trim(); - if (due_date !== null && !isValidYmd(due_date)) { + if (hasDueField && 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' } }); } + // description (opcional) + let description: string | undefined = undefined; + if (hasDescField) { + const descRaw = payload?.description; + if (descRaw !== null && descRaw !== undefined && typeof descRaw !== 'string') { + return new Response('Bad Request', { status: 400 }); + } + if (descRaw == null) { + // No permitimos null en description (columna NOT NULL) + return new Response(JSON.stringify({ error: 'invalid_description' }), { + status: 400, + headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' } + }); + } + const normalized = String(descRaw).replace(/\s+/g, ' ').trim(); + if (normalized.length < 1 || normalized.length > 1000) { + return new Response(JSON.stringify({ error: 'invalid_description' }), { + status: 400, + headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' } + }); + } + description = normalized; + } + const db = await getDb(); // Cargar tarea y validar abierta @@ -89,7 +121,13 @@ export const PATCH: RequestHandler = async (event) => { } // Aplicar actualización - db.prepare(`UPDATE tasks SET due_date = ? WHERE id = ?`).run(due_date, taskId); + if (hasDescField && hasDueField) { + db.prepare(`UPDATE tasks SET description = ?, due_date = ? WHERE id = ?`).run(description!, due_date, taskId); + } else if (hasDescField) { + db.prepare(`UPDATE tasks SET description = ? WHERE id = ?`).run(description!, taskId); + } else if (hasDueField) { + 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 = ?`)