|
|
|
@ -2,6 +2,7 @@
|
|
|
|
import Badge from '$lib/ui/atoms/Badge.svelte';
|
|
|
|
import Badge from '$lib/ui/atoms/Badge.svelte';
|
|
|
|
import { dueStatus } from '$lib/utils/date';
|
|
|
|
import { dueStatus } from '$lib/utils/date';
|
|
|
|
import { success, error as toastError } from '$lib/stores/toasts';
|
|
|
|
import { success, error as toastError } from '$lib/stores/toasts';
|
|
|
|
|
|
|
|
import { tick } from 'svelte';
|
|
|
|
|
|
|
|
|
|
|
|
export let id: number;
|
|
|
|
export let id: number;
|
|
|
|
export let description: string;
|
|
|
|
export let description: string;
|
|
|
|
@ -18,6 +19,10 @@
|
|
|
|
let dateValue: string = due_date ?? '';
|
|
|
|
let dateValue: string = due_date ?? '';
|
|
|
|
let busy = false;
|
|
|
|
let busy = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Edición de texto (inline)
|
|
|
|
|
|
|
|
let editingText = false;
|
|
|
|
|
|
|
|
let descEl: HTMLElement | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
async function doClaim() {
|
|
|
|
async function doClaim() {
|
|
|
|
if (busy) return;
|
|
|
|
if (busy) return;
|
|
|
|
busy = true;
|
|
|
|
busy = true;
|
|
|
|
@ -82,6 +87,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
function toggleEdit() {
|
|
|
|
function toggleEdit() {
|
|
|
|
editing = !editing;
|
|
|
|
editing = !editing;
|
|
|
|
|
|
|
|
if (editing) editingText = false;
|
|
|
|
dateValue = due_date ?? '';
|
|
|
|
dateValue = due_date ?? '';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -91,12 +97,91 @@
|
|
|
|
dateValue = '';
|
|
|
|
dateValue = '';
|
|
|
|
saveDate();
|
|
|
|
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;
|
|
|
|
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<li class="task">
|
|
|
|
<li class="task">
|
|
|
|
<div class="left">
|
|
|
|
<div class="left">
|
|
|
|
<span class="code">#{code}</span>
|
|
|
|
<span class="code">#{code}</span>
|
|
|
|
<span class="desc">{description}</span>
|
|
|
|
<span
|
|
|
|
|
|
|
|
class="desc"
|
|
|
|
|
|
|
|
class:editing={editingText}
|
|
|
|
|
|
|
|
contenteditable={editingText}
|
|
|
|
|
|
|
|
role="textbox"
|
|
|
|
|
|
|
|
aria-label="Descripción de la tarea"
|
|
|
|
|
|
|
|
spellcheck="true"
|
|
|
|
|
|
|
|
bind:this={descEl}
|
|
|
|
|
|
|
|
on:keydown={(e) => {
|
|
|
|
|
|
|
|
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}</span>
|
|
|
|
{#if due_date}
|
|
|
|
{#if due_date}
|
|
|
|
{#if status === 'overdue'}
|
|
|
|
{#if status === 'overdue'}
|
|
|
|
<Badge tone="danger">vence: {due_date}</Badge>
|
|
|
|
<Badge tone="danger">vence: {due_date}</Badge>
|
|
|
|
@ -121,6 +206,13 @@
|
|
|
|
<button class="btn" on:click|preventDefault={doUnassign} disabled={busy}>Soltar</button>
|
|
|
|
<button class="btn" on:click|preventDefault={doUnassign} disabled={busy}>Soltar</button>
|
|
|
|
{/if}
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{#if !editingText}
|
|
|
|
|
|
|
|
<button class="btn secondary" on:click|preventDefault={toggleEditText} disabled={busy}>Editar texto</button>
|
|
|
|
|
|
|
|
{:else}
|
|
|
|
|
|
|
|
<button class="btn primary" on:click|preventDefault={saveText} disabled={busy}>Guardar</button>
|
|
|
|
|
|
|
|
<button class="btn ghost" on:click|preventDefault={cancelText} disabled={busy}>Cancelar</button>
|
|
|
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
|
|
{#if !editing}
|
|
|
|
{#if !editing}
|
|
|
|
<button class="btn secondary" on:click|preventDefault={toggleEdit} disabled={busy}>Editar fecha</button>
|
|
|
|
<button class="btn secondary" on:click|preventDefault={toggleEdit} disabled={busy}>Editar fecha</button>
|
|
|
|
{:else}
|
|
|
|
{:else}
|
|
|
|
@ -154,6 +246,7 @@
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.desc { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
|
.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; }
|
|
|
|
.meta { justify-self: end; }
|
|
|
|
.muted { color: var(--color-text-muted); }
|
|
|
|
.muted { color: var(--color-text-muted); }
|
|
|
|
|
|
|
|
|
|
|
|
|