Historie: Task-Notizen + Link sichtbar, Navigation zu Tagen mit Einträgen

- Notizfeld pro Task in der Reflexion (optional, jeder Status) — nutzt das
  bestehende dropReason-Feld (Limit 200->500). Wird in der Historie bei JEDEM
  Status angezeigt (bisher nur bei "verworfen").
- Optionaler Tages-Link wird in der Historie jetzt angezeigt.
- Historie-Pfeile springen zum naechsten/vorherigen Tag MIT Eintraegen statt
  starr ueber Werktage — Wochenend-Arbeit (z. B. Sa 6.6.) ist nun erreichbar.
  Default-Tag = juengster Tag vor heute mit Eintrag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Raffael SheikhandClaude Opus 4.8 committed 2026-06-07 13:04:52 +02:00
1 parent 6e7a910ed3
commit 6acf9977ec
4 files changed
+116 -56

No files matched your search

+33 -18
View File
@@ -68,9 +68,10 @@ export function HistorieRoute() {
<div className="flex flex-wrap items-center gap-2">
<button
type="button"
onClick={() => setDate(prevDate)}
aria-label="Vorheriger Werktag"
className="inline-flex h-9 w-9 items-center justify-center rounded-[var(--radius-md)] border border-[var(--color-border)] text-[var(--color-ink-muted)] hover:border-[var(--color-ink)] hover:text-[var(--color-ink)]"
onClick={() => prevDate && setDate(prevDate)}
disabled={!prevDate}
aria-label="Vorheriger Tag mit Eintrag"
className="inline-flex h-9 w-9 items-center justify-center rounded-[var(--radius-md)] border border-[var(--color-border)] text-[var(--color-ink-muted)] hover:border-[var(--color-ink)] hover:text-[var(--color-ink)] disabled:opacity-40 disabled:hover:border-[var(--color-border)]"
>
<ChevronLeft size={16} />
</button>
@@ -79,7 +80,7 @@ export function HistorieRoute() {
type="button"
onClick={() => nextDate && setDate(nextDate)}
disabled={!nextDate}
aria-label="Nächster Werktag"
aria-label="Nächster Tag mit Eintrag"
className="inline-flex h-9 w-9 items-center justify-center rounded-[var(--radius-md)] border border-[var(--color-border)] text-[var(--color-ink-muted)] hover:border-[var(--color-ink)] hover:text-[var(--color-ink)] disabled:opacity-40 disabled:hover:border-[var(--color-border)]"
>
<ChevronRight size={16} />
@@ -154,21 +155,25 @@ function PersonCard({ person: p }: { person: HistoryPerson }) {
: 'bg-[var(--color-border-strong)]',
)}
/>
<span
className={clsx(
t.status === 'dropped' && 'text-[var(--color-ink-muted)] line-through',
)}
>
{t.title}
{t.unplanned && (
<span className="ml-2 rounded-full bg-black/5 px-1.5 py-0.5 text-[10px] text-[var(--color-ink-muted)]">
ungeplant
<div className="flex-1">
<span
className={clsx(
t.status === 'dropped' && 'text-[var(--color-ink-muted)] line-through',
)}
>
{t.title}
{t.unplanned && (
<span className="ml-2 rounded-full bg-black/5 px-1.5 py-0.5 text-[10px] text-[var(--color-ink-muted)]">
ungeplant
</span>
)}
</span>
{t.dropReason && (
<span className="mt-0.5 block whitespace-pre-wrap text-xs text-[var(--color-ink-muted)]">
{t.dropReason}
</span>
)}
{t.status === 'dropped' && t.dropReason && (
<span className="text-xs text-[var(--color-ink-faint)]"> — {t.dropReason}</span>
)}
</span>
</div>
</li>
))}
</ul>
@@ -177,7 +182,17 @@ function PersonCard({ person: p }: { person: HistoryPerson }) {
{p.reflectionText && (
<p className="whitespace-pre-wrap text-sm text-[var(--color-ink)]">{p.reflectionText}</p>
)}
{p.todos.length === 0 && !p.reflectionText && (
{p.url && (
<a
href={p.url}
target="_blank"
rel="noopener noreferrer"
className="block truncate text-sm text-[var(--color-ink-muted)] underline hover:text-[var(--color-ink)]"
>
{p.url}
</a>
)}
{p.todos.length === 0 && !p.reflectionText && !p.url && (
<p className="text-xs text-[var(--color-ink-faint)]">Keine Details.</p>
)}
</Card.Body>
+47 -31
View File
@@ -51,12 +51,13 @@ function ReflectForm({
status: TodoStatus;
bezugKind: 'project' | 'deal' | 'campaign' | 'allgemein' | null;
unplanned?: boolean;
dropReason?: string | null;
}>;
onDone: () => void;
}) {
const [todoStates, setTodoStates] = useState<
Record<string, { status: TodoStatus; reason?: string }>
>(Object.fromEntries(todos.map((t) => [t.id, { status: t.status }])));
>(Object.fromEntries(todos.map((t) => [t.id, { status: t.status, reason: t.dropReason ?? '' }])));
const [reflectionText, setReflectionText] = useState('');
const [ampel, setAmpel] = useState<Ampel | null>(null);
const [trend, setTrend] = useState<Trend | null>(null);
@@ -86,6 +87,10 @@ function ReflectForm({
setTodoStates((prev) => ({ ...prev, [id]: { ...prev[id], status } }));
}
function setTodoReason(id: string, reason: string) {
setTodoStates((prev) => ({ ...prev, [id]: { status: prev[id]?.status ?? 'open', reason } }));
}
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!ampel) {
@@ -107,7 +112,7 @@ function ReflectForm({
todos: todos.map((t) => ({
id: t.id,
status: todoStates[t.id]?.status ?? t.status,
dropReason: todoStates[t.id]?.reason,
dropReason: todoStates[t.id]?.reason?.trim() || undefined,
})),
reflectionText,
ampel,
@@ -138,36 +143,47 @@ function ReflectForm({
{todos.map((t) => {
const status = todoStates[t.id]?.status ?? t.status;
return (
<li key={t.id} className="flex items-center gap-2">
<EditableTodoTitle id={t.id} title={t.title} />
{t.unplanned && (
<span className="rounded-full bg-black/5 px-1.5 py-0.5 text-[10px] text-[var(--color-ink-muted)]">
ungeplant
</span>
)}
<div className="flex gap-1">
<StatusBtn
active={status === 'done'}
onClick={() => setTodoStatus(t.id, 'done')}
label="Erledigt"
>
<Check size={14} />
</StatusBtn>
<StatusBtn
active={status === 'open'}
onClick={() => setTodoStatus(t.id, 'open')}
label="Offen"
>
<Minus size={14} />
</StatusBtn>
<StatusBtn
active={status === 'dropped'}
onClick={() => setTodoStatus(t.id, 'dropped')}
label="Verworfen"
>
<X size={14} />
</StatusBtn>
<li key={t.id} className="space-y-1">
<div className="flex items-center gap-2">
<EditableTodoTitle id={t.id} title={t.title} />
{t.unplanned && (
<span className="rounded-full bg-black/5 px-1.5 py-0.5 text-[10px] text-[var(--color-ink-muted)]">
ungeplant
</span>
)}
<div className="flex gap-1">
<StatusBtn
active={status === 'done'}
onClick={() => setTodoStatus(t.id, 'done')}
label="Erledigt"
>
<Check size={14} />
</StatusBtn>
<StatusBtn
active={status === 'open'}
onClick={() => setTodoStatus(t.id, 'open')}
label="Offen"
>
<Minus size={14} />
</StatusBtn>
<StatusBtn
active={status === 'dropped'}
onClick={() => setTodoStatus(t.id, 'dropped')}
label="Verworfen"
>
<X size={14} />
</StatusBtn>
</div>
</div>
<input
type="text"
value={todoStates[t.id]?.reason ?? ''}
onChange={(e) => setTodoReason(t.id, e.target.value)}
placeholder="Notiz (optional) — Ergebnis, Kontext, Link …"
maxLength={500}
aria-label={`Notiz zu ${t.title}`}
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-2 py-1 text-xs focus:border-[var(--color-ink)] focus:outline-none"
/>
</li>
);
})}