103 lines
3.9 KiB
TypeScript
103 lines
3.9 KiB
TypeScript
import { ClipboardList } from "lucide-react";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import { toast } from "sonner";
|
|
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { HelpLink } from "@/components/HelpLink";
|
|
import { TodoPanel } from "@/components/TodoPanel";
|
|
import { getTodos } from "../api";
|
|
import type { TicketTodo, TodoStatus } from "../types";
|
|
|
|
type TodosPageProps = {
|
|
onNavigate: (to: string) => void;
|
|
};
|
|
|
|
export function TodosPage({ onNavigate }: TodosPageProps) {
|
|
const [todos, setTodos] = useState<TicketTodo[]>([]);
|
|
const [status, setStatus] = useState<TodoStatus | "all">("open");
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
async function load() {
|
|
setLoading(true);
|
|
try {
|
|
const result = await getTodos(status);
|
|
setTodos(result.todos);
|
|
} catch (error) {
|
|
toast.error("ToDos konnten nicht geladen werden", {
|
|
description: error instanceof Error ? error.message : "Unbekannter Fehler"
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
void load();
|
|
}, [status]);
|
|
|
|
const counts = useMemo(
|
|
() => ({
|
|
open: todos.filter((todo) => todo.status === "open").length,
|
|
done: todos.filter((todo) => todo.status === "done").length,
|
|
urgent: todos.filter((todo) => todo.status === "open" && todo.priority === "urgent").length
|
|
}),
|
|
[todos]
|
|
);
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
|
|
<div>
|
|
<h2 className="text-xl font-semibold tracking-normal sm:text-2xl">ToDos</h2>
|
|
<p className="text-sm text-muted-foreground">Offene Aufgaben aus Projekten und normalen Tickets an einem Ort.</p>
|
|
</div>
|
|
<div className="grid gap-2 text-sm sm:grid-cols-3">
|
|
<div className="rounded-md border bg-muted/20 px-3 py-2">
|
|
<span className="block text-muted-foreground">Offen</span>
|
|
<span className="font-semibold">{counts.open}</span>
|
|
</div>
|
|
<div className="rounded-md border bg-muted/20 px-3 py-2">
|
|
<span className="block text-muted-foreground">Dringend</span>
|
|
<span className="font-semibold">{counts.urgent}</span>
|
|
</div>
|
|
<div className="rounded-md border bg-muted/20 px-3 py-2">
|
|
<span className="block text-muted-foreground">Erledigt</span>
|
|
<span className="font-semibold">{counts.done}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardHeader className="p-4">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<ClipboardList className="size-5 text-muted-foreground" />
|
|
Aufgabenliste
|
|
</CardTitle>
|
|
<CardDescription>ToDos werden im jeweiligen Ticket angelegt und hier gesammelt angezeigt.</CardDescription>
|
|
</div>
|
|
<HelpLink anchor="todos" label="Hilfe zu ToDos" />
|
|
</div>
|
|
<div className="flex flex-wrap gap-2 pt-2">
|
|
{(["open", "all", "done"] as const).map((value) => (
|
|
<button
|
|
key={value}
|
|
type="button"
|
|
className={`rounded-md border px-3 py-1.5 text-sm ${status === value ? "bg-primary text-primary-foreground" : "bg-background hover:bg-muted"}`}
|
|
onClick={() => setStatus(value)}
|
|
>
|
|
{value === "open" ? "Offen" : value === "done" ? "Erledigt" : "Alle"}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="px-4 pb-4">
|
|
<TodoPanel todos={todos} showCreate={false} showTicketMeta onNavigate={onNavigate} onChanged={load} />
|
|
{!loading && todos.length === 0 ? null : null}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|