refactor: extrae TaskDueBadge y TaskAssignees y actualiza TaskItem

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

@ -7,10 +7,9 @@
isTomorrow,
} from "$lib/utils/date";
import { success, error as toastError } from "$lib/stores/toasts";
import { tick, onDestroy, createEventDispatcher } from "svelte";
import { tick, createEventDispatcher } from "svelte";
import { fade } from "svelte/transition";
import Popover from "$lib/ui/feedback/Popover.svelte";
import { normalizeDigits, buildWaMeUrl } from "$lib/utils/phone";
import { normalizeDigits } from "$lib/utils/phone";
import { colorForGroup } from "$lib/utils/groupColor";
import DueOkIcon from "$lib/ui/icons/DueOkIcon.svelte";
import DueSoonIcon from "$lib/ui/icons/DueSoonIcon.svelte";
@ -21,6 +20,8 @@
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 TaskAssignees from "$lib/ui/data/task/TaskAssignees.svelte";
export let id: number;
export let description: string;
@ -54,19 +55,8 @@
let busy = false;
// Popover de responsables
let showAssignees = false;
let assigneesButtonEl: HTMLButtonElement | null = null;
$: assigneesCount = Array.isArray(assignees) ? assignees.length : 0;
$: canUnassign = !(groupId == null && assigneesCount === 1 && isAssigned);
$: assigneesAria =
assigneesCount === 0
? "Sin responsables"
: `${assigneesCount} responsable${assigneesCount === 1 ? "" : "s"}${isAssigned ? "; tú incluido" : ""}`;
onDestroy(() => {
// Cerrar popover si se desmonta el item (por navegación o filtrado)
showAssignees = false;
});
// Edición de texto (inline)
let editingText = false;
@ -322,21 +312,7 @@
: undefined}>{groupLabel}</span
>
{#if due_date}
<span
class="date-badge"
class:overdue
class:soon={imminent}
title={overdue ? "Vencida" : imminent ? "Próxima" : "Fecha"}
>
{#if !overdue && !imminent}
<DueOkIcon />
{:else if imminent}
<DueSoonIcon />
{:else}
<DueOverdueIcon />
{/if}
{dateDmy}
</span>
<TaskDueBadge {due_date} />
{/if}
</div>
<div class="complete">
@ -363,38 +339,7 @@
{/if}
</div>
<div class="assignees-container">
{#if assigneesCount === 0}
<button
class="assignees-badge empty"
type="button"
aria-haspopup="dialog"
aria-expanded="false"
aria-controls={"assignees-popover-" + id}
title="Sin responsables"
disabled
bind:this={assigneesButtonEl}
>
🙅
</button>
{:else}
<button
class="assignees-badge"
class:mine={isAssigned}
type="button"
aria-haspopup="dialog"
aria-expanded={showAssignees}
aria-controls={"assignees-popover-" + id}
title={assigneesAria}
aria-label={assigneesAria}
on:click|preventDefault={() => (showAssignees = true)}
bind:this={assigneesButtonEl}
>
<span class="icon" aria-hidden="true">
<AssigneesIcon />
</span>
<span class="count">{assigneesCount}</span>
</button>
{/if}
<TaskAssignees {id} {assignees} {currentUserId} />
</div>
<div class="actions">
{#if !completed}
@ -472,38 +417,6 @@
{/if}
{/if}
</div>
<Popover
bind:open={showAssignees}
ariaLabel="Responsables"
id={"assignees-popover-" + id}
>
<h3 class="popover-title">Responsables</h3>
{#if assigneesCount === 0}
<p class="muted">No hay responsables asignados.</p>
{:else}
<ul class="assignees-list">
{#each assignees as a}
<li>
<a
href={buildWaMeUrl(normalizeDigits(a))}
target="_blank"
rel="noopener noreferrer nofollow"
>
{normalizeDigits(a)}
</a>
{#if currentUserId && normalizeDigits(a) === normalizeDigits(currentUserId)}
<span class="you-pill"></span>
{/if}
</li>
{/each}
</ul>
{/if}
<div class="popover-actions">
<button class="btn ghost" on:click={() => (showAssignees = false)}
>Cerrar</button
>
</div>
</Popover>
</li>
<style>
@ -574,25 +487,6 @@
color: var(--gc-text, inherit);
font-size: 12px;
}
.date-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 6px;
border-radius: 6px;
border: 1px solid transparent;
font-size: 12px;
}
.date-badge img {
max-height: 1rem;
min-width: 1.2rem;
}
.date-badge.overdue {
border-color: var(--color-danger);
}
.date-badge.soon {
border-color: var(--color-warning);
}
.assignees-container {
grid-row: 4/5;
grid-column: 1/2;
@ -699,92 +593,7 @@
}
/* Badge de responsables */
.assignees-badge {
display: inline-flex;
align-items: center;
justify-self: start;
gap: 8px;
padding: 2px 8px;
border-radius: 6px;
border: 1px solid var(--color-border);
background: var(--color-surface);
font-size: 12px;
cursor: pointer;
box-shadow: 0 0 4px 4px var(--color-border);
}
.assignees-badge .icon {
font-size: 16px;
line-height: 1;
}
.assignees-badge .count {
font-size: 12px;
line-height: 1;
}
.assignees-badge.mine {
border-color: var(--color-surface);
}
.assignees-badge.mine .icon {
position: relative;
}
.assignees-badge.mine .icon::after {
content: "";
position: absolute;
right: -6px;
top: -6px;
width: 8px;
height: 8px;
background: var(--color-primary);
border: 1px solid var(--color-surface);
border-radius: 50%;
}
.assignees-badge[aria-expanded="true"] {
border-color: var(--color-primary);
}
.assignees-badge:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.assignees-badge.empty {
padding: 2px 6px;
gap: 0;
}
.assignees-list {
list-style: none;
margin: 8px 0;
padding: 0;
}
.assignees-list li {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
}
.assignees-list a {
color: var(--color-primary);
text-decoration: none;
}
.assignees-list a:hover,
.assignees-list a:focus-visible {
text-decoration: underline;
}
.popover-actions {
display: flex;
justify-content: flex-end;
margin-top: 8px;
}
.popover-title {
margin: 0 0 4px 0;
font-size: 0.95rem;
}
.you-pill {
margin-left: 6px;
padding: 1px 6px;
border-radius: 999px;
background: rgba(37, 99, 235, 0.12);
color: var(--color-primary);
font-size: 11px;
}
.icon-btn-svg {
fill-rule: evenodd;
clip-rule: evenodd;

@ -0,0 +1,144 @@
<script lang="ts">
import { normalizeDigits, buildWaMeUrl } from "$lib/utils/phone";
import Popover from "$lib/ui/feedback/Popover.svelte";
import AssigneesIcon from "$lib/ui/icons/AssigneesIcon.svelte";
import { onDestroy } from "svelte";
export let id: number;
export let assignees: string[] = [];
export let currentUserId: string | null | undefined = null;
let open = false;
$: assigneesCount = Array.isArray(assignees) ? assignees.length : 0;
$: isAssigned =
!!currentUserId &&
(assignees || []).some((a) => normalizeDigits(a) === normalizeDigits(currentUserId!));
$: assigneesAria =
assigneesCount === 0
? "Sin responsables"
: `${assigneesCount} responsable${assigneesCount === 1 ? "" : "s"}${isAssigned ? "; tú incluido" : ""}`;
onDestroy(() => { open = false; });
</script>
{#if assigneesCount === 0}
<button
class="assignees-badge empty"
type="button"
aria-haspopup="dialog"
aria-expanded="false"
aria-controls={"assignees-popover-" + id}
title="Sin responsables"
disabled
>
🙅
</button>
{:else}
<button
class="assignees-badge"
class:mine={isAssigned}
type="button"
aria-haspopup="dialog"
aria-expanded={open}
aria-controls={"assignees-popover-" + id}
title={assigneesAria}
aria-label={assigneesAria}
on:click={() => (open = true)}
>
<span class="icon" aria-hidden="true">
<AssigneesIcon />
</span>
<span class="count">{assigneesCount}</span>
</button>
{/if}
<Popover bind:open={open} ariaLabel="Responsables" id={"assignees-popover-" + id}>
<h3 class="popover-title">Responsables</h3>
{#if assigneesCount === 0}
<p class="muted">No hay responsables asignados.</p>
{:else}
<ul class="assignees-list">
{#each assignees as a}
<li>
<a href={buildWaMeUrl(normalizeDigits(a))} target="_blank" rel="noopener noreferrer nofollow">
{normalizeDigits(a)}
</a>
{#if currentUserId && normalizeDigits(a) === normalizeDigits(currentUserId)}
<span class="you-pill"></span>
{/if}
</li>
{/each}
</ul>
{/if}
<div class="popover-actions">
<button class="btn ghost" on:click={() => (open = false)}>Cerrar</button>
</div>
</Popover>
<style>
.muted { color: var(--color-text-muted); }
.assignees-badge {
display: inline-flex;
align-items: center;
justify-self: start;
gap: 8px;
padding: 2px 8px;
border-radius: 6px;
border: 1px solid var(--color-border);
background: var(--color-surface);
font-size: 12px;
cursor: pointer;
box-shadow: 0 0 4px 4px var(--color-border);
}
.assignees-badge .icon {
font-size: 16px;
line-height: 1;
}
.assignees-badge .count {
font-size: 12px;
line-height: 1;
}
.assignees-badge.mine { border-color: var(--color-surface); }
.assignees-badge.mine .icon { position: relative; }
.assignees-badge.mine .icon::after {
content: "";
position: absolute;
right: -6px;
top: -6px;
width: 8px;
height: 8px;
background: var(--color-primary);
border: 1px solid var(--color-surface);
border-radius: 50%;
}
.assignees-badge[aria-expanded="true"] { border-color: var(--color-primary); }
.assignees-badge:disabled { cursor: not-allowed; opacity: 0.6; }
.assignees-badge.empty { padding: 2px 6px; gap: 0; }
.assignees-list { list-style: none; margin: 8px 0; padding: 0; }
.assignees-list li {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
}
.assignees-list a { color: var(--color-primary); text-decoration: none; }
.assignees-list a:hover, .assignees-list a:focus-visible { text-decoration: underline; }
.popover-actions {
display: flex;
justify-content: flex-end;
margin-top: 8px;
}
.popover-title { margin: 0 0 4px 0; font-size: 0.95rem; }
.you-pill {
margin-left: 6px;
padding: 1px 6px;
border-radius: 999px;
background: rgba(37, 99, 235, 0.12);
color: var(--color-primary);
font-size: 11px;
}
</style>

@ -0,0 +1,45 @@
<script lang="ts">
import { compareYmd, todayYmdUTC, ymdToDmy, isToday, isTomorrow } from "$lib/utils/date";
import DueOkIcon from "$lib/ui/icons/DueOkIcon.svelte";
import DueSoonIcon from "$lib/ui/icons/DueSoonIcon.svelte";
import DueOverdueIcon from "$lib/ui/icons/DueOverdueIcon.svelte";
export let due_date: string | null;
$: today = todayYmdUTC();
$: overdue = !!due_date && compareYmd(due_date, today) < 0;
$: imminent = !!due_date && (isToday(due_date!) || isTomorrow(due_date!));
$: dateDmy = due_date ? ymdToDmy(due_date) : "";
$: title = overdue ? "Vencida" : imminent ? "Próxima" : "Fecha";
</script>
{#if due_date}
<span class="date-badge" class:overdue class:soon={imminent} {title}>
{#if !overdue && !imminent}
<DueOkIcon />
{:else if imminent}
<DueSoonIcon />
{:else}
<DueOverdueIcon />
{/if}
{dateDmy}
</span>
{/if}
<style>
.date-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 6px;
border-radius: 6px;
border: 1px solid transparent;
font-size: 12px;
}
.date-badge.overdue {
border-color: var(--color-danger);
}
.date-badge.soon {
border-color: var(--color-warning);
}
</style>
Loading…
Cancel
Save