|
|
|
|
@ -23,6 +23,11 @@
|
|
|
|
|
hasMore?: boolean | null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Estado local para permitir actualización sin recargar ni perder scroll
|
|
|
|
|
let openTasks: Task[] = [...data.openTasks];
|
|
|
|
|
let unassignedOpen: Task[] = [...data.unassignedOpen];
|
|
|
|
|
let recentTasks: (Task & { completed?: boolean; completed_at?: string | null })[] = [...data.recentTasks];
|
|
|
|
|
|
|
|
|
|
function buildQuery(params: { order?: 'due' | 'group'; page?: number }) {
|
|
|
|
|
const sp = new URLSearchParams();
|
|
|
|
|
if (params.order) sp.set("order", params.order);
|
|
|
|
|
@ -57,6 +62,104 @@
|
|
|
|
|
// Mantener el orden provisto por el servidor (ya ordenado alfabéticamente con "Personal" al final)
|
|
|
|
|
return groups;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function maintainScrollWhile(mutate: () => void) {
|
|
|
|
|
const y = window.scrollY;
|
|
|
|
|
mutate();
|
|
|
|
|
queueMicrotask(() => window.scrollTo({ top: y }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateTaskInLists(detail: { id: number; action: string; patch: Partial<Task & { completed?: boolean; completed_at?: string | null }> }) {
|
|
|
|
|
const { id, action, patch } = detail;
|
|
|
|
|
|
|
|
|
|
const patchIn = (arr: Task[]) => {
|
|
|
|
|
const idx = arr.findIndex((t) => t.id === id);
|
|
|
|
|
if (idx >= 0) {
|
|
|
|
|
arr[idx] = { ...arr[idx], ...patch };
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (action === 'complete') {
|
|
|
|
|
maintainScrollWhile(() => {
|
|
|
|
|
let moved = false;
|
|
|
|
|
let idx = unassignedOpen.findIndex((t) => t.id === id);
|
|
|
|
|
if (idx >= 0) {
|
|
|
|
|
const [it] = unassignedOpen.splice(idx, 1);
|
|
|
|
|
const completedItem: any = { ...it, ...patch, completed: true };
|
|
|
|
|
recentTasks = [completedItem, ...recentTasks];
|
|
|
|
|
moved = true;
|
|
|
|
|
}
|
|
|
|
|
idx = openTasks.findIndex((t) => t.id === id);
|
|
|
|
|
if (idx >= 0) {
|
|
|
|
|
const [it] = openTasks.splice(idx, 1);
|
|
|
|
|
const completedItem: any = { ...it, ...patch, completed: true };
|
|
|
|
|
recentTasks = [completedItem, ...recentTasks];
|
|
|
|
|
moved = true;
|
|
|
|
|
}
|
|
|
|
|
if (!moved) {
|
|
|
|
|
patchIn(recentTasks as any);
|
|
|
|
|
}
|
|
|
|
|
// Forzar reactividad en listas mutadas
|
|
|
|
|
openTasks = [...openTasks];
|
|
|
|
|
unassignedOpen = [...unassignedOpen];
|
|
|
|
|
recentTasks = [...recentTasks];
|
|
|
|
|
});
|
|
|
|
|
} else if (action === 'uncomplete') {
|
|
|
|
|
maintainScrollWhile(() => {
|
|
|
|
|
const idx = recentTasks.findIndex((t) => t.id === id);
|
|
|
|
|
if (idx >= 0) {
|
|
|
|
|
const [it] = recentTasks.splice(idx, 1);
|
|
|
|
|
const reopened: any = { ...it, ...patch, completed: false };
|
|
|
|
|
openTasks = [reopened, ...openTasks];
|
|
|
|
|
} else {
|
|
|
|
|
patchIn(openTasks);
|
|
|
|
|
}
|
|
|
|
|
openTasks = [...openTasks];
|
|
|
|
|
recentTasks = [...recentTasks];
|
|
|
|
|
});
|
|
|
|
|
} else if (action === 'claim') {
|
|
|
|
|
maintainScrollWhile(() => {
|
|
|
|
|
const idx = unassignedOpen.findIndex((t) => t.id === id);
|
|
|
|
|
if (idx >= 0) {
|
|
|
|
|
const [it] = unassignedOpen.splice(idx, 1);
|
|
|
|
|
const claimed = { ...it, ...patch };
|
|
|
|
|
if (!openTasks.some((x) => x.id === id)) {
|
|
|
|
|
openTasks = [claimed, ...openTasks];
|
|
|
|
|
} else {
|
|
|
|
|
patchIn(openTasks);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
patchIn(openTasks);
|
|
|
|
|
}
|
|
|
|
|
openTasks = [...openTasks];
|
|
|
|
|
unassignedOpen = [...unassignedOpen];
|
|
|
|
|
});
|
|
|
|
|
} else if (action === 'unassign') {
|
|
|
|
|
maintainScrollWhile(() => {
|
|
|
|
|
if (!patchIn(openTasks)) patchIn(unassignedOpen);
|
|
|
|
|
// Si quedó sin responsables, mover a "sin responsable"
|
|
|
|
|
const idx = openTasks.findIndex((t) => t.id === id);
|
|
|
|
|
if (idx >= 0 && (openTasks[idx].assignees || []).length === 0) {
|
|
|
|
|
const [it] = openTasks.splice(idx, 1);
|
|
|
|
|
unassignedOpen = [it, ...unassignedOpen];
|
|
|
|
|
}
|
|
|
|
|
openTasks = [...openTasks];
|
|
|
|
|
unassignedOpen = [...unassignedOpen];
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
// update_due, update_desc u otros parches ligeros
|
|
|
|
|
if (!patchIn(openTasks)) {
|
|
|
|
|
if (!patchIn(unassignedOpen)) {
|
|
|
|
|
patchIn(recentTasks as any);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
openTasks = [...openTasks];
|
|
|
|
|
unassignedOpen = [...unassignedOpen];
|
|
|
|
|
recentTasks = [...recentTasks];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<svelte:head>
|
|
|
|
|
@ -79,13 +182,13 @@
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<h2 class="section-title">Mis tareas (abiertas)</h2>
|
|
|
|
|
{#if data.openTasks.length === 0}
|
|
|
|
|
{#if openTasks.length === 0}
|
|
|
|
|
<p>No tienes tareas asignadas. Crea o reclama una para empezar.</p>
|
|
|
|
|
{:else}
|
|
|
|
|
<Card>
|
|
|
|
|
<ul class="list">
|
|
|
|
|
{#each data.openTasks as t}
|
|
|
|
|
<TaskItem {...t} currentUserId={data.userId} groupName={t.group_id ? (data.groupNames[t.group_id] || t.group_id) : 'Personal'} groupId={t.group_id} />
|
|
|
|
|
{#each openTasks as t (t.id)}
|
|
|
|
|
<TaskItem {...t} currentUserId={data.userId} groupName={t.group_id ? (data.groupNames[t.group_id] || t.group_id) : 'Personal'} groupId={t.group_id} on:changed={(e) => updateTaskInLists(e.detail)} />
|
|
|
|
|
{/each}
|
|
|
|
|
</ul>
|
|
|
|
|
</Card>
|
|
|
|
|
@ -99,16 +202,16 @@
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<h2 class="section-title">Sin responsable de mis grupos</h2>
|
|
|
|
|
{#if data.unassignedOpen.length === 0}
|
|
|
|
|
{#if unassignedOpen.length === 0}
|
|
|
|
|
<p>No hay tareas sin responsable en tus grupos. Crea una nueva o invita a tus compañeros.</p>
|
|
|
|
|
{:else}
|
|
|
|
|
{#if data.order === 'group'}
|
|
|
|
|
{#each groupByGroup(data.unassignedOpen) as g}
|
|
|
|
|
{#each groupByGroup(unassignedOpen) as g (g.id)}
|
|
|
|
|
<h3 class="group-subtitle">{g.name}</h3>
|
|
|
|
|
<Card>
|
|
|
|
|
<ul class="list">
|
|
|
|
|
{#each g.tasks as t}
|
|
|
|
|
<TaskItem {...t} currentUserId={data.userId} groupName={g.name} groupId={t.group_id} />
|
|
|
|
|
{#each g.tasks as t (t.id)}
|
|
|
|
|
<TaskItem {...t} currentUserId={data.userId} groupName={g.name} groupId={t.group_id} on:changed={(e) => updateTaskInLists(e.detail)} />
|
|
|
|
|
{/each}
|
|
|
|
|
</ul>
|
|
|
|
|
</Card>
|
|
|
|
|
@ -116,8 +219,8 @@
|
|
|
|
|
{:else}
|
|
|
|
|
<Card>
|
|
|
|
|
<ul class="list">
|
|
|
|
|
{#each data.unassignedOpen as t}
|
|
|
|
|
<TaskItem {...t} currentUserId={data.userId} groupName={t.group_id ? (data.groupNames[t.group_id] || t.group_id) : 'Personal'} groupId={t.group_id} />
|
|
|
|
|
{#each unassignedOpen as t (t.id)}
|
|
|
|
|
<TaskItem {...t} currentUserId={data.userId} groupName={t.group_id ? (data.groupNames[t.group_id] || t.group_id) : 'Personal'} groupId={t.group_id} on:changed={(e) => updateTaskInLists(e.detail)} />
|
|
|
|
|
{/each}
|
|
|
|
|
</ul>
|
|
|
|
|
</Card>
|
|
|
|
|
@ -125,12 +228,12 @@
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<h2 class="section-title">Completadas (últimas 24 h)</h2>
|
|
|
|
|
{#if data.recentTasks.length === 0}
|
|
|
|
|
{#if recentTasks.length === 0}
|
|
|
|
|
<p>No hay tareas completadas recientemente.</p>
|
|
|
|
|
{:else}
|
|
|
|
|
<Card>
|
|
|
|
|
<ul class="list">
|
|
|
|
|
{#each data.recentTasks as t}
|
|
|
|
|
{#each recentTasks as t (t.id)}
|
|
|
|
|
<TaskItem
|
|
|
|
|
{...t}
|
|
|
|
|
currentUserId={data.userId}
|
|
|
|
|
@ -138,6 +241,7 @@
|
|
|
|
|
completed={true}
|
|
|
|
|
completed_at={t.completed_at ?? null}
|
|
|
|
|
groupName={t.group_id ? (data.groupNames[t.group_id] || t.group_id) : 'Personal'}
|
|
|
|
|
on:changed={(e) => updateTaskInLists(e.detail)}
|
|
|
|
|
/>
|
|
|
|
|
{/each}
|
|
|
|
|
</ul>
|
|
|
|
|
|