Start page (AuDHD-oriented, per the agreed "Heute zuerst" layout): - One highlighted next step: started work first (easy re-entry after an interruption), then overdue, due today, then urgent undated tasks. Primary action "Erledigt", secondary "Beginnen", link to the related colony/apiary/queen. - "Außerdem heute": at most five further tasks with a large honey-cell checkbox; undated automatic suggestions are included so a queenless colony does not sit unseen in the full list. - Completing is the one authored motion moment: the cell fills, the tick draws, the row folds away and the next step arrives with a short focus pull. Every completion can be undone from the toast (10 s), restoring the previous status. - Status is shown as icon plus word (never colour alone); tasks created by automation carry a "Vorschlag" badge, recurring and inspection tasks their own - a suggestion never looks like work already done. - Friendly empty, loading and error states; key figures moved into a compact overview below; overdue tasks are no longer listed twice. Motion: per-device setting Voll / Dezent / Aus in the profile (default Voll); the OS reduce-motion setting always lowers Voll to Dezent. New 44 px "touch" button size for gloves at the apiary; themed text selection and caret. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
93 lines
3.2 KiB
TypeScript
93 lines
3.2 KiB
TypeScript
import { CheckCircle2, Circle, ClipboardList, PlayCircle, Repeat, Sparkles, XCircle, type LucideIcon } from "lucide-react";
|
||
import { Task, TaskStatus } from "@/lib/tasks/types";
|
||
import { TASK_STATUS_LABELS } from "@/lib/tasks/labels";
|
||
import { DueState } from "@/lib/tasks/today";
|
||
import { cn } from "@/lib/utils";
|
||
|
||
const STATUS_META: Record<TaskStatus, { icon: LucideIcon; className: string }> = {
|
||
OPEN: { icon: Circle, className: "bg-muted text-foreground/80" },
|
||
IN_PROGRESS: {
|
||
icon: PlayCircle,
|
||
className: "bg-[color-mix(in_oklch,var(--chart-5)_16%,var(--card))] text-foreground",
|
||
},
|
||
DONE: { icon: CheckCircle2, className: "bg-success/15 text-foreground" },
|
||
CANCELLED: { icon: XCircle, className: "bg-muted text-muted-foreground line-through" },
|
||
};
|
||
|
||
/** Status as icon AND word - never colour alone. */
|
||
export function TaskStatusPill({ status, className }: { status: TaskStatus; className?: string }) {
|
||
const meta = STATUS_META[status];
|
||
const Icon = meta.icon;
|
||
return (
|
||
<span
|
||
className={cn(
|
||
"inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium",
|
||
meta.className,
|
||
className,
|
||
)}
|
||
>
|
||
<Icon className="size-3.5" aria-hidden />
|
||
{TASK_STATUS_LABELS[status]}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Where a task came from. An automatic task is a suggestion to look at
|
||
* something - it says nothing about work having been done.
|
||
*/
|
||
export function TaskSourceBadge({
|
||
task,
|
||
}: {
|
||
task: Pick<Task, "sourceAutomationTrigger" | "sourceTemplateId" | "sourceInspectionId">;
|
||
}) {
|
||
const source = task.sourceAutomationTrigger
|
||
? { icon: Sparkles, label: "Vorschlag", title: "Automatisch vorgeschlagen – noch nichts erledigt" }
|
||
: task.sourceTemplateId
|
||
? { icon: Repeat, label: "Wiederkehrend", title: "Aus einer wiederkehrenden Vorlage" }
|
||
: task.sourceInspectionId
|
||
? { icon: ClipboardList, label: "Aus Durchsicht", title: "Bei einer Durchsicht angelegt" }
|
||
: null;
|
||
if (!source) return null;
|
||
const Icon = source.icon;
|
||
return (
|
||
<span
|
||
title={source.title}
|
||
className="inline-flex items-center gap-1 rounded-full border border-dashed border-primary/45 px-2 py-0.5 text-xs text-foreground/80"
|
||
>
|
||
<Icon className="size-3.5 text-primary" aria-hidden />
|
||
{source.label}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
const DUE_LABEL: Record<Exclude<DueState, "none">, string> = {
|
||
overdue: "Überfällig",
|
||
today: "Heute",
|
||
upcoming: "Geplant",
|
||
};
|
||
|
||
export function DueChip({ state, dueDate }: { state: DueState; dueDate: string | null }) {
|
||
if (state === "none" || !dueDate) {
|
||
return <span className="text-xs text-muted-foreground">ohne Termin</span>;
|
||
}
|
||
const date = new Date(dueDate).toLocaleDateString("de-DE", {
|
||
day: "numeric",
|
||
month: "short",
|
||
timeZone: "UTC",
|
||
});
|
||
return (
|
||
<span
|
||
className={cn(
|
||
"inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium",
|
||
state === "overdue" && "bg-warning/20 text-foreground",
|
||
state === "today" && "bg-primary/12 text-foreground",
|
||
state === "upcoming" && "bg-muted text-muted-foreground",
|
||
)}
|
||
>
|
||
{DUE_LABEL[state]}
|
||
{state !== "today" ? <span className="font-normal">· {date}</span> : null}
|
||
</span>
|
||
);
|
||
}
|