Add project and todo management

This commit is contained in:
michael committed 2026-09-02 11:03:50 +02:00
1 parent 2792051a16
commit 3da436423f
12 files changed
+1865 -43

No files matched your search

+72 -5
View File
@@ -1,4 +1,4 @@
import { ArrowLeft, Pencil, Save, Trash2 } from "lucide-react";
import { ArrowLeft, ClipboardList, FolderKanban, Pencil, Save, Target, Trash2 } from "lucide-react";
import { FormEvent, useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import { Alert } from "@/components/ui/alert";
@@ -11,6 +11,7 @@ import { Textarea } from "@/components/ui/textarea";
import { CopyTicketButton } from "@/components/CopyTicketButton";
import { HelpLink } from "@/components/HelpLink";
import { OrganizationSelect } from "@/components/OrganizationSelect";
import { TodoPanel } from "@/components/TodoPanel";
import {
createSession,
deleteSession,
@@ -231,6 +232,8 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
}, [data?.ticket]);
const ticket = data?.ticket;
const project = data?.project ?? null;
const isProject = Boolean(project);
const isGlobalTicket = periodType === "all";
const isPeriodClosed = Boolean(data?.closed);
const periodLabel = periodType === "month" ? "Monat" : periodType === "day" ? "Tag" : "Ticket";
@@ -385,7 +388,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
await updateTicket(ticket.id, {
ticketNumber: editTicketNumber,
organizationId: editOrganizationId,
workType: editWorkType
workType: isProject ? "consulting" : editWorkType
});
toast.success("Ticketdaten gespeichert", {
description: "Organisation und Art wurden auf die vorhandenen Sessions übernommen."
@@ -408,7 +411,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
}
const effectiveOrganizationId = ticket.organization_id ?? ticketSession.organizationId;
const effectiveWorkType = ticket.work_type ?? ticketSession.workType;
const effectiveWorkType = isProject ? "consulting" : ticket.work_type ?? ticketSession.workType;
if (!effectiveOrganizationId) {
toast.error("Organisation wählen");
@@ -503,7 +506,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
await updateSessionDetails(editingSession.id, {
organizationId: editSessionOrganizationId,
activity: editSessionActivity,
workType: editSessionWorkType,
workType: isProject ? "consulting" : editSessionWorkType,
startedAt: startedAt.toISOString(),
endedAt: endedAt.toISOString()
});
@@ -670,11 +673,13 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
id="edit-ticket-work-type"
className="h-8 w-full rounded-lg border border-input bg-background px-2 text-sm focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/50"
value={editWorkType}
disabled={isProject}
onChange={(event) => setEditWorkType(event.currentTarget.value as WorkType)}
>
<option value="support">Support</option>
<option value="consulting">Consulting</option>
</select>
{isProject ? <p className="text-xs text-muted-foreground">Projekte sind immer Consulting.</p> : null}
</div>
<Button type="submit" disabled={savingTicket}>
<Save className="size-4" />
@@ -684,6 +689,67 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
</CardContent>
</Card>
{project ? (
<Card>
<CardHeader className="p-4">
<div className="flex items-start justify-between gap-3">
<div>
<CardTitle className="flex items-center gap-2">
<FolderKanban className="size-5 text-muted-foreground" />
Projekt
</CardTitle>
<CardDescription>{project.teamspace_project || "Kein Teamspace-Projekt hinterlegt"}</CardDescription>
</div>
<HelpLink anchor="projekte" label="Hilfe zu Projekten" />
</div>
</CardHeader>
<CardContent className="grid gap-3 px-4 pb-4 lg:grid-cols-[1fr_1fr_220px]">
<div className="rounded-md border bg-muted/20 p-3">
<div className="mb-1 flex items-center gap-2 text-sm font-medium">
<FolderKanban className="size-4 text-muted-foreground" />
Beschreibung
</div>
<p className="whitespace-pre-wrap text-sm text-muted-foreground">{project.description || "Noch keine Beschreibung."}</p>
</div>
<div className="rounded-md border bg-muted/20 p-3">
<div className="mb-1 flex items-center gap-2 text-sm font-medium">
<Target className="size-4 text-muted-foreground" />
Ziele
</div>
<p className="whitespace-pre-wrap text-sm text-muted-foreground">{project.goals || "Noch keine Ziele."}</p>
</div>
<div className="space-y-2 rounded-md border bg-muted/20 p-3 text-sm">
<div>
<span className="block text-xs text-muted-foreground">Zeitraum</span>
<span className="font-medium">{project.starts_on || "offen"} - {project.ends_on || "offen"}</span>
</div>
<div>
<span className="block text-xs text-muted-foreground">Kontingent</span>
<span className="font-medium">{project.hour_budget_minutes ? formatMinutes(project.hour_budget_minutes) : "offen"}</span>
</div>
</div>
</CardContent>
</Card>
) : null}
<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" />
ToDos
</CardTitle>
<CardDescription>Aufgaben zu diesem Ticket oder Projekt verwalten.</CardDescription>
</div>
<HelpLink anchor="todos" label="Hilfe zu ToDos" />
</div>
</CardHeader>
<CardContent className="px-4 pb-4">
<TodoPanel ticketId={ticket?.id} todos={data?.todos ?? []} onChanged={load} />
</CardContent>
</Card>
<Card>
<CardHeader className="gap-3 p-4 sm:flex-row sm:items-start sm:justify-between sm:space-y-0">
<div>
@@ -994,12 +1060,13 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
id="session-work-type"
className="h-8 w-full rounded-lg border border-input bg-background px-2 text-sm focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/50"
value={editSessionWorkType}
disabled={Boolean(editingSession?.recurring_billing_id)}
disabled={Boolean(editingSession?.recurring_billing_id) || isProject}
onChange={(event) => setEditSessionWorkType(event.currentTarget.value as WorkType)}
>
<option value="support">Support</option>
<option value="consulting">Consulting</option>
</select>
{isProject ? <p className="text-xs text-muted-foreground">Projekt-Sessions bleiben Consulting.</p> : null}
</div>
<div className="flex justify-end">
<Button type="submit" disabled={savingSessionDetails}>