refactor: extraer TaskCompleteButton y TaskActions y usar en TaskItem

Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>
main
brobert 1 month ago
parent 815f060156
commit 415548cdce

@ -1,27 +1,13 @@
<script lang="ts"> <script lang="ts">
import {
compareYmd,
todayYmdUTC,
ymdToDmy,
isToday,
isTomorrow,
} from "$lib/utils/date";
import { success, error as toastError } from "$lib/stores/toasts"; import { success, error as toastError } from "$lib/stores/toasts";
import { tick, createEventDispatcher } from "svelte"; import { tick, createEventDispatcher } from "svelte";
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
import { normalizeDigits } from "$lib/utils/phone"; import { normalizeDigits } from "$lib/utils/phone";
import { colorForGroup } from "$lib/utils/groupColor"; import { colorForGroup } from "$lib/utils/groupColor";
import DueOkIcon from "$lib/ui/icons/DueOkIcon.svelte";
import DueSoonIcon from "$lib/ui/icons/DueSoonIcon.svelte";
import DueOverdueIcon from "$lib/ui/icons/DueOverdueIcon.svelte";
import AssigneesIcon from "$lib/ui/icons/AssigneesIcon.svelte";
import ClaimIcon from "$lib/ui/icons/ClaimIcon.svelte";
import UnassignIcon from "$lib/ui/icons/UnassignIcon.svelte";
import EditIcon from "$lib/ui/icons/EditIcon.svelte";
import CalendarEditIcon from "$lib/ui/icons/CalendarEditIcon.svelte";
import CheckCircleSuccessIcon from "$lib/ui/icons/CheckCircleSuccessIcon.svelte";
import TaskDueBadge from "$lib/ui/data/task/TaskDueBadge.svelte"; import TaskDueBadge from "$lib/ui/data/task/TaskDueBadge.svelte";
import TaskAssignees from "$lib/ui/data/task/TaskAssignees.svelte"; import TaskAssignees from "$lib/ui/data/task/TaskAssignees.svelte";
import TaskCompleteButton from "$lib/ui/data/task/TaskCompleteButton.svelte";
import TaskActions from "$lib/ui/data/task/TaskActions.svelte";
export let id: number; export let id: number;
export let description: string; export let description: string;
@ -43,10 +29,7 @@
assignees.some( assignees.some(
(a) => normalizeDigits(a) === normalizeDigits(currentUserId), (a) => normalizeDigits(a) === normalizeDigits(currentUserId),
); );
$: today = todayYmdUTC(); // Derivados de fecha ahora los maneja TaskDueBadge
$: overdue = !!due_date && compareYmd(due_date, today) < 0;
$: imminent = !!due_date && (isToday(due_date) || isTomorrow(due_date));
$: dateDmy = due_date ? ymdToDmy(due_date) : "";
$: groupLabel = groupName != null ? groupName : "Personal"; $: groupLabel = groupName != null ? groupName : "Personal";
$: gc = groupId ? colorForGroup(groupId) : null; $: gc = groupId ? colorForGroup(groupId) : null;
@ -167,13 +150,11 @@
} }
} }
async function saveDate() { async function saveDate(value: string | null) {
if (busy) return; if (busy) return;
busy = true; busy = true;
try { try {
const body = { const body = { due_date: value };
due_date: dateValue && dateValue.trim() !== "" ? dateValue.trim() : null,
};
const res = await fetch(`/api/tasks/${id}`, { const res = await fetch(`/api/tasks/${id}`, {
method: "PATCH", method: "PATCH",
headers: { "content-type": "application/json" }, headers: { "content-type": "application/json" },
@ -184,6 +165,7 @@
success("Fecha actualizada"); success("Fecha actualizada");
dispatch("changed", { id, action: "update_due", patch: { due_date } }); dispatch("changed", { id, action: "update_due", patch: { due_date } });
editing = false; editing = false;
dateValue = due_date ?? "";
} else { } else {
const txt = await res.text(); const txt = await res.text();
toastError(txt || "No se pudo actualizar la fecha"); toastError(txt || "No se pudo actualizar la fecha");
@ -204,8 +186,7 @@
function clearDate() { function clearDate() {
if (busy) return; if (busy) return;
if (!confirm("¿Quitar la fecha de vencimiento?")) return; if (!confirm("¿Quitar la fecha de vencimiento?")) return;
dateValue = ""; saveDate(null);
saveDate();
} }
function toggleEditText() { function toggleEditText() {
@ -316,106 +297,35 @@
{/if} {/if}
</div> </div>
<div class="complete"> <div class="complete">
{#if completed} <TaskCompleteButton
<button {completed}
class="btn primary primary-action" {busy}
aria-label="Deshacer completar" on:complete={doComplete}
title="Deshacer completar" on:uncomplete={doUncomplete}
on:click|preventDefault={doUncomplete} />
disabled={busy}
>
↩️ Deshacer
</button>
{:else}
<button
class="btn primary primary-action"
aria-label="Completar"
title="Completar"
on:click|preventDefault={doComplete}
disabled={busy}
><CheckCircleSuccessIcon />
Completar
</button>
{/if}
</div> </div>
<div class="assignees-container"> <div class="assignees-container">
<TaskAssignees {id} {assignees} {currentUserId} /> <TaskAssignees {id} {assignees} {currentUserId} />
</div> </div>
<div class="actions"> <div class="actions">
{#if !completed} <TaskActions
{#if !isAssigned} {isAssigned}
<button {canUnassign}
class="icon-btn secondary-action" {busy}
aria-label="Reclamar" {completed}
on:click|preventDefault={doClaim} editingText={editingText}
disabled={busy} editingDate={editing}
><ClaimIcon /> dateValue={dateValue}
Reclamar</button on:claim={doClaim}
> on:unassign={doUnassign}
{:else} on:toggleEditText={toggleEditText}
<button on:saveText={saveText}
class="icon-btn secondary-action" on:cancelText={cancelText}
aria-label="Soltar" on:toggleEditDate={toggleEdit}
title={canUnassign ? "Soltar" : "No puedes soltar una tarea personal. Márcala como completada para eliminarla"} on:saveDate={(e) => saveDate((e as CustomEvent<{ value: string | null }>).detail.value)}
on:click|preventDefault={doUnassign} on:clearDate={clearDate}
disabled={busy || !canUnassign} on:cancelDate={() => (editing = false)}
><UnassignIcon /> />
Soltar</button
>
{/if}
{#if !editingText}
<button
class="icon-btn secondary-action"
aria-label="Editar texto"
title="Editar texto"
on:click|preventDefault={toggleEditText}
disabled={busy}
><EditIcon />
Editar</button
>
{:else}
<button
class="btn primary secondary-action"
on:click|preventDefault={saveText}
disabled={busy}>Guardar</button
>
<button
class="btn ghost secondary-action"
on:click|preventDefault={cancelText}
disabled={busy}>Cancelar</button
>
{/if}
{#if !editing}
<button
class="icon-btn secondary-action"
aria-label="Editar fecha"
title="Editar fecha"
on:click|preventDefault={toggleEdit}
disabled={busy}
><CalendarEditIcon />
Fecha</button
>
{:else}
<input class="date" type="date" bind:value={dateValue} />
<button
class="btn primary secondary-action"
on:click|preventDefault={saveDate}
disabled={busy}>Guardar</button
>
<button
class="btn danger secondary-action"
on:click|preventDefault={clearDate}
disabled={busy}>Quitar</button
>
<button
class="btn ghost secondary-action"
on:click|preventDefault={toggleEdit}
disabled={busy}>Cancelar</button
>
{/if}
{/if}
</div> </div>
</li> </li>

@ -0,0 +1,103 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import ClaimIcon from "$lib/ui/icons/ClaimIcon.svelte";
import UnassignIcon from "$lib/ui/icons/UnassignIcon.svelte";
import EditIcon from "$lib/ui/icons/EditIcon.svelte";
import CalendarEditIcon from "$lib/ui/icons/CalendarEditIcon.svelte";
export let isAssigned: boolean;
export let canUnassign: boolean;
export let busy: boolean;
export let completed: boolean;
export let editingText: boolean;
export let editingDate: boolean;
export let dateValue: string = "";
const dispatch = createEventDispatcher<{
claim: void;
unassign: void;
toggleEditText: void;
saveText: void;
cancelText: void;
toggleEditDate: void;
saveDate: { value: string | null };
clearDate: void;
cancelDate: void;
}>();
let localDate = dateValue;
$: if (editingDate) {
localDate = dateValue;
}
</script>
{#if !completed}
{#if !isAssigned}
<button
class="icon-btn secondary-action"
aria-label="Reclamar"
on:click|preventDefault={() => dispatch("claim")}
disabled={busy}
>
<ClaimIcon /> Reclamar
</button>
{:else}
<button
class="icon-btn secondary-action"
aria-label="Soltar"
title={canUnassign ? "Soltar" : "No puedes soltar una tarea personal. Márcala como completada para eliminarla"}
on:click|preventDefault={() => dispatch("unassign")}
disabled={busy || !canUnassign}
>
<UnassignIcon /> Soltar
</button>
{/if}
{#if !editingText}
<button
class="icon-btn secondary-action"
aria-label="Editar texto"
title="Editar texto"
on:click|preventDefault={() => dispatch("toggleEditText")}
disabled={busy}
>
<EditIcon /> Editar
</button>
{:else}
<button class="btn primary secondary-action" on:click|preventDefault={() => dispatch("saveText")} disabled={busy}>
Guardar
</button>
<button class="btn ghost secondary-action" on:click|preventDefault={() => dispatch("cancelText")} disabled={busy}>
Cancelar
</button>
{/if}
{#if !editingDate}
<button
class="icon-btn secondary-action"
aria-label="Editar fecha"
title="Editar fecha"
on:click|preventDefault={() => dispatch("toggleEditDate")}
disabled={busy}
>
<CalendarEditIcon /> Fecha
</button>
{:else}
<input class="date" type="date" bind:value={localDate} />
<button
class="btn primary secondary-action"
on:click|preventDefault={() => dispatch("saveDate", { value: (localDate || "").trim() || null })}
disabled={busy}
>
Guardar
</button>
<button class="btn danger secondary-action" on:click|preventDefault={() => dispatch("clearDate")} disabled={busy}>
Quitar
</button>
<button class="btn ghost secondary-action" on:click|preventDefault={() => dispatch("cancelDate")} disabled={busy}>
Cancelar
</button>
{/if}
{/if}

@ -0,0 +1,35 @@
<script lang="ts">
import CheckCircleSuccessIcon from "$lib/ui/icons/CheckCircleSuccessIcon.svelte";
import { createEventDispatcher } from "svelte";
export let completed: boolean;
export let busy: boolean;
const dispatch = createEventDispatcher<{
complete: void;
uncomplete: void;
}>();
</script>
{#if completed}
<button
class="btn primary primary-action"
aria-label="Deshacer completar"
title="Deshacer completar"
on:click|preventDefault={() => dispatch("uncomplete")}
disabled={busy}
>
↩️ Deshacer
</button>
{:else}
<button
class="btn primary primary-action"
aria-label="Completar"
title="Completar"
on:click|preventDefault={() => dispatch("complete")}
disabled={busy}
>
<CheckCircleSuccessIcon />
Completar
</button>
{/if}
Loading…
Cancel
Save