feat(aufgaben): Datenmodell und Regeln für Aufgaben und Projekte
Aufgabe mit Typ, Status, Priorität, Aufwand, Fälligkeit und Herkunft; Projekt als eigene Klammer darum. Zwei Entwurfsentscheidungen: - Die Inbox ist kein eigener Zustand, sondern schlicht eine Aufgabe ohne Bereich. Einsortieren heißt: einen Bereich setzen. Ein zusätzliches Feld könnte mit der Zuordnung auseinanderlaufen. - Projekt ist ein eigenes Modell, kein Aufgabentyp. Ein Projekt hat selbst weder Aufwand noch Fälligkeit, sondern leitet beides aus dem ab, was drinsteckt. ARBEITSPAKET und MEILENSTEIN sind Typen der Aufgaben *innerhalb* eines Projekts. @@unique([quelle, quelleRef]) ist die Grundlage für die Integrationen ab M6: sie upserten darüber, statt zu doppeln. Von Hand erfasste Aufgaben tragen quelleRef = null; PostgreSQL sieht NULL-Werte als verschieden an, sie behindern sich also nicht. Verworfene Aufgaben zählen bewusst gar nicht in den Fortschritt eines Projekts — sonst sähe es fertig aus, weil die Hälfte gestrichen wurde. Fälligkeit wird in Kalendertagen in Europe/Berlin gerechnet, nicht in 24-Stunden-Schritten: was heute um 23:00 fällig ist, ist heute fällig. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
704c490368
commit
63c3c15a3c
@@ -0,0 +1,119 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { faelligkeitFarbe, fuerAnzeige, type AufgabeRoh } from './aufgabe-anzeige'
|
||||
|
||||
// Dienstag, 17. März 2026, 10:00 Uhr Berliner Zeit.
|
||||
const jetzt = new Date('2026-03-17T09:00:00Z')
|
||||
|
||||
function roh(teil: Partial<AufgabeRoh> = {}): AufgabeRoh {
|
||||
return {
|
||||
id: 'a1',
|
||||
titel: 'Angebot schreiben',
|
||||
notiz: null,
|
||||
typ: 'AUFGABE',
|
||||
status: 'OFFEN',
|
||||
prioritaet: 3,
|
||||
aufwandMinuten: 60,
|
||||
faelligAm: null,
|
||||
quelle: 'MANUELL',
|
||||
quelleUrl: null,
|
||||
bereich: null,
|
||||
projekt: null,
|
||||
...teil,
|
||||
}
|
||||
}
|
||||
|
||||
describe('fuerAnzeige', () => {
|
||||
it('lässt den Datumswert leer, wenn kein Termin gesetzt ist', () => {
|
||||
const anzeige = fuerAnzeige(roh(), jetzt)
|
||||
expect(anzeige.faelligWert).toBe('')
|
||||
expect(anzeige.faelligDatum).toBeNull()
|
||||
expect(anzeige.faelligText).toBe('ohne Termin')
|
||||
expect(anzeige.ueberfaellig).toBe(false)
|
||||
})
|
||||
|
||||
it('gibt den Datumswert im Format des Eingabefelds zurück', () => {
|
||||
const anzeige = fuerAnzeige(
|
||||
roh({ faelligAm: new Date('2026-03-20T12:00:00Z') }),
|
||||
jetzt,
|
||||
)
|
||||
expect(anzeige.faelligWert).toBe('2026-03-20')
|
||||
})
|
||||
|
||||
it('nimmt den Berliner Kalendertag, nicht den UTC-Tag', () => {
|
||||
// 23:30 UTC am 19. ist in Berlin bereits der 20. um 00:30.
|
||||
const anzeige = fuerAnzeige(
|
||||
roh({ faelligAm: new Date('2026-03-19T23:30:00Z') }),
|
||||
jetzt,
|
||||
)
|
||||
expect(anzeige.faelligWert).toBe('2026-03-20')
|
||||
})
|
||||
|
||||
it('markiert eine offene Aufgabe von gestern als überfällig', () => {
|
||||
const anzeige = fuerAnzeige(
|
||||
roh({ faelligAm: new Date('2026-03-16T09:00:00Z') }),
|
||||
jetzt,
|
||||
)
|
||||
expect(anzeige.ueberfaellig).toBe(true)
|
||||
expect(anzeige.faelligText).toBe('seit gestern überfällig')
|
||||
})
|
||||
|
||||
it('markiert eine erledigte Aufgabe von gestern nicht als überfällig', () => {
|
||||
const anzeige = fuerAnzeige(
|
||||
roh({ faelligAm: new Date('2026-03-16T09:00:00Z'), status: 'ERLEDIGT' }),
|
||||
jetzt,
|
||||
)
|
||||
expect(anzeige.ueberfaellig).toBe(false)
|
||||
})
|
||||
|
||||
it('schreibt das Datum ausgeschrieben für den Hinweis', () => {
|
||||
const anzeige = fuerAnzeige(
|
||||
roh({ faelligAm: new Date('2026-03-20T12:00:00Z') }),
|
||||
jetzt,
|
||||
)
|
||||
expect(anzeige.faelligDatum).toBe('Fr., 20.03.2026')
|
||||
})
|
||||
|
||||
it('reicht Bereich und Projekt unverändert durch', () => {
|
||||
const anzeige = fuerAnzeige(
|
||||
roh({
|
||||
bereich: { id: 'b1', name: 'Gewerbe', farbe: '#7c3aed' },
|
||||
projekt: { id: 'p1', name: 'Website neu' },
|
||||
}),
|
||||
jetzt,
|
||||
)
|
||||
expect(anzeige.bereich?.name).toBe('Gewerbe')
|
||||
expect(anzeige.projekt?.name).toBe('Website neu')
|
||||
})
|
||||
})
|
||||
|
||||
describe('faelligkeitFarbe', () => {
|
||||
it('warnt rot bei Überfälligkeit', () => {
|
||||
const anzeige = fuerAnzeige(
|
||||
roh({ faelligAm: new Date('2026-03-15T09:00:00Z') }),
|
||||
jetzt,
|
||||
)
|
||||
expect(faelligkeitFarbe(anzeige)).toBe('gefahr')
|
||||
})
|
||||
|
||||
it('warnt gelb für heute und morgen', () => {
|
||||
expect(
|
||||
faelligkeitFarbe(
|
||||
fuerAnzeige(roh({ faelligAm: new Date('2026-03-17T15:00:00Z') }), jetzt),
|
||||
),
|
||||
).toBe('warnung')
|
||||
expect(
|
||||
faelligkeitFarbe(
|
||||
fuerAnzeige(roh({ faelligAm: new Date('2026-03-18T15:00:00Z') }), jetzt),
|
||||
),
|
||||
).toBe('warnung')
|
||||
})
|
||||
|
||||
it('bleibt bei allem Weiteren unauffällig', () => {
|
||||
expect(
|
||||
faelligkeitFarbe(
|
||||
fuerAnzeige(roh({ faelligAm: new Date('2026-04-01T09:00:00Z') }), jetzt),
|
||||
),
|
||||
).toBe('neutral')
|
||||
expect(faelligkeitFarbe(fuerAnzeige(roh(), jetzt))).toBe('neutral')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user