Files
verdande/web/src/routes/faerdige/+page.svelte
T
Kristian WindandClaude Opus 5 dee2d5e30b
Release / Build and publish image (push) Failing after 9m56s
fix(faerdige): en opgave lukket efter midnat lå under "I går"
Fundet af prøverne, som for første gang kørte forbi midnat.

Dagen blev læst som `completed_at.slice(0, 10)` — de ti første tegn af et
tidsstempel, der er i UTC. Overskriften sammenlignede med *lokal* dato. I dansk
sommertid er de to ikke den samme dag i de første to timer efter midnat, så en
opgave lukket 00.30 blev sat under "I går", indtil klokken blev to. Tyve timer i
døgnet er de ens, hvilket er derfor prøven har været grøn hver eneste gang før.
Dagen udregnes nu lokalt, som den overskrift den skal svare til.

Og prøven for invitationslinket kunne kun bestå seks dage om ugen. Den ledte
efter "males inden fredag" som én tekst, men hurtig tilføjelse læser "fredag" som
forfaldsdatoen og tager ordet ud af titlen — tilbage står "males inden" plus et
mærke, hvis ord skifter med ugedagen. Kørt på en torsdag hedder mærket "I
morgen", og så fandt den ingenting. Prøven handler om invitationer, ikke om
datoer, så den ser nu på titlen alene.

Uddraget i notelisten viste `![](/api/v1/attachments/01a0…` for hver note med et
billede i — den eneste tekst i listen, ingen havde skrevet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 00:17:50 +02:00

141 lines
4.0 KiB
Svelte

<script>
/**
* Færdige: everything that has been closed, newest first.
*
* Completing a task is one click on a small circle beside a row you were only
* reading, and it takes the row off the screen — so the mistake and the evidence
* of it leave together. Somebody who clicked the wrong one had no name to search
* for and nowhere to look: the project page can show its own closed tasks, but
* only if you already know which project it was in.
*
* The toast that appears on completing offers an undo, which covers the mistake
* you notice at once. This covers the one you notice tomorrow.
*
* Newest first, decided by the server. A record of what has been finished is
* read backwards from now, not by a due date the task no longer has.
*/
import { api } from '$lib/api.js';
import { app } from '$lib/stores.svelte.js';
import { t, tag } from '$lib/i18n.svelte.js';
import TaskRow from '$lib/components/TaskRow.svelte';
let loaded = $state(false);
$effect(() => {
app.loadTasks({ completed: 'only', limit: 200 }).then(() => (loaded = true));
});
// Read back out of the store, so a task reopened from here leaves the list at
// once rather than on the next load — which is the whole reason to be here.
let done = $derived(app.tasks.filter((task) => task.completed));
/**
* Grouped by the day it was closed.
*
* A flat list of two hundred rows says when nothing. "I dag" and "I går" are
* how anybody looking for something they just closed would describe it.
*/
/**
* Dagen en opgave blev lukket, målt hvor personen står.
*
* Ikke `completed_at.slice(0, 10)`. Tidsstemplet er i UTC, og de ti første tegn
* er derfor UTC's dato — mens overskriften nedenfor sammenligner med *lokal*
* dato. I to timer efter midnat i dansk sommertid er de to ikke den samme dag,
* så en opgave lukket 00.30 lå under "I går", indtil klokken blev to.
*
* Fundet af prøven, som kørte forbi midnat for første gang. Den havde været
* grøn hver eneste gang før, fordi de to datoer er ens tyve timer i døgnet.
*/
const localDay = (iso) => {
if (!iso) return '';
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return '';
const pad = (n) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
};
let days = $derived.by(() => {
const groups = new Map();
for (const task of done) {
const key = localDay(task.completed_at);
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(task);
}
return [...groups.entries()].map(([date, tasks]) => ({ date, tasks }));
});
function heading(date) {
if (!date) return t('done.unknownDay');
const day = new Date(date + 'T00:00:00');
const today = new Date(new Date().toDateString());
const diff = Math.round((day - today) / 86400000);
if (diff === 0) return t('task.today');
if (diff === -1) return t('task.yesterday');
return day.toLocaleDateString(tag(), { weekday: 'long', day: 'numeric', month: 'long' });
}
</script>
<svelte:head><title>{t('done.title')} — verdande</title></svelte:head>
<div class="view">
<header>
<h1>{t('done.title')}</h1>
<p>{t('done.hint')}</p>
</header>
{#each days as day (day.date)}
<section>
<h2>{heading(day.date)}</h2>
{#each day.tasks as task (task.id)}
<TaskRow {task} />
{/each}
</section>
{/each}
{#if loaded && !done.length}
<p class="empty">{t('done.empty')}</p>
{/if}
</div>
<style>
.view {
max-width: var(--content-max);
margin: 0 auto;
padding: var(--s6) var(--s4) var(--s8);
}
header {
margin-bottom: var(--s5);
}
h1 {
font-size: var(--text-2xl);
}
header p {
margin: var(--s1) 0 0;
font-size: var(--text-sm);
color: var(--ink-muted);
}
section {
margin-bottom: var(--s5);
}
h2 {
font-size: var(--text-xs);
font-weight: 560;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--ink-faint);
padding-bottom: var(--s2);
border-bottom: 1px solid var(--line);
margin-bottom: var(--s2);
}
.empty {
font-size: var(--text-sm);
color: var(--ink-faint);
}
</style>