Add project and todo management
This commit is contained in:
1 parent
2792051a16
commit
3da436423f
12 files changed
+1865
-43
No files matched your search
@@ -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}>
|
||||
|
||||
Reference in new issue
Block a user