From 3d9674bd5a6b6ccd052db06c10ee0654859d959e Mon Sep 17 00:00:00 2001 From: mboehmlaender Date: Wed, 2 Sep 2026 11:28:55 +0200 Subject: [PATCH] Allow project editing from ticket details --- frontend/src/views/TicketDetailPage.tsx | 225 +++++++++++++++++++++++- 1 file changed, 220 insertions(+), 5 deletions(-) diff --git a/frontend/src/views/TicketDetailPage.tsx b/frontend/src/views/TicketDetailPage.tsx index 289fb6a..752bba8 100644 --- a/frontend/src/views/TicketDetailPage.tsx +++ b/frontend/src/views/TicketDetailPage.tsx @@ -22,10 +22,11 @@ import { updateSessionBilling, updateSessionDetails, updateTicket, - updateTicketDayBilling + updateTicketDayBilling, + updateProject } from "../api"; import { currentDay, currentMonth, formatDate, formatDateTime, formatMinutes, formatTimeRange } from "../format"; -import type { BillingStatus, PeriodType, SessionEntry, TicketDetailPeriodType, TicketPeriod, WorkType } from "../types"; +import type { BillingStatus, PeriodType, ProjectSummary, SessionEntry, TicketDetailPeriodType, TicketPeriod, WorkType } from "../types"; import { cn } from "@/lib/utils"; type TicketDetailPageProps = { @@ -54,6 +55,18 @@ type TicketSessionFormState = { endTime: string; }; +type ProjectEditFormState = { + title: string; + organizationId: string; + organizationName: string | null; + teamspaceProject: string; + description: string; + goals: string; + startsOn: string; + endsOn: string; + hourBudget: string; +}; + function emptyTicketSessionForm(): TicketSessionFormState { return { organizationId: "", @@ -66,6 +79,20 @@ function emptyTicketSessionForm(): TicketSessionFormState { }; } +function projectEditForm(project: ProjectSummary): ProjectEditFormState { + return { + title: project.title ?? "", + organizationId: project.organization_id ?? "", + organizationName: project.organization_name ?? project.customer_name ?? null, + teamspaceProject: project.teamspace_project ?? "", + description: project.description, + goals: project.goals, + startsOn: project.starts_on ?? "", + endsOn: project.ends_on ?? "", + hourBudget: formatHoursInput(project.hour_budget_minutes) + }; +} + function BillingButtons({ session, disabled, @@ -182,6 +209,20 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO const [globalStatusFilter, setGlobalStatusFilter] = useState<"all" | "open" | "billed" | "non_billable">("all"); const [globalSourceFilter, setGlobalSourceFilter] = useState<"all" | "manual" | "recurring">("all"); const [creatingProject, setCreatingProject] = useState(false); + const [editingProject, setEditingProject] = useState(false); + const [savingProject, setSavingProject] = useState(false); + const [projectForm, setProjectForm] = useState({ + title: "", + organizationId: "", + organizationName: null, + teamspaceProject: "", + description: "", + goals: "", + startsOn: "", + endsOn: "", + hourBudget: "" + }); + const [todoFilter, setTodoFilter] = useState<"open" | "all" | "done">("open"); async function load() { try { @@ -295,6 +336,13 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO editOrganizationId !== (ticket.organization_id ?? "") || editWorkType !== (ticket.work_type ?? "support")) ); + const todos = data?.todos ?? []; + const visibleTodos = todoFilter === "all" ? todos : todos.filter((todo) => todo.status === todoFilter); + const todoCounts = { + open: todos.filter((todo) => todo.status === "open").length, + done: todos.filter((todo) => todo.status === "done").length, + all: todos.length + }; const hasUnsavedChanges = ticketDataDirty || sessionGroups.some((group) => isDayBillingDirty(group.key)); useEffect(() => { @@ -448,6 +496,61 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO } } + function openProjectEditor() { + if (!project) { + return; + } + + setProjectForm(projectEditForm(project)); + setEditingProject(true); + } + + async function saveProjectData(event: FormEvent) { + event.preventDefault(); + + if (!project || !ticket) { + return; + } + + if (!projectForm.organizationId) { + toast.error("Organisation wählen"); + return; + } + + const hourBudgetMinutes = parseHoursInput(projectForm.hourBudget); + + if (hourBudgetMinutes === undefined) { + toast.error("Stundenkontingent prüfen", { + description: "Bitte Stunden als Zahl eintragen, z.B. 12,5." + }); + return; + } + + setSavingProject(true); + try { + await updateProject(project.id, { + ticketNumber: ticket.ticket_number, + title: projectForm.title || null, + organizationId: projectForm.organizationId, + teamspaceProject: projectForm.teamspaceProject || null, + description: projectForm.description, + goals: projectForm.goals, + startsOn: projectForm.startsOn || null, + endsOn: projectForm.endsOn || null, + hourBudgetMinutes + }); + toast.success("Projekt gespeichert"); + setEditingProject(false); + await load(); + } catch (error) { + toast.error("Projekt konnte nicht gespeichert werden", { + description: error instanceof Error ? error.message : "Unbekannter Fehler" + }); + } finally { + setSavingProject(false); + } + } + async function submitTicketSession(event: FormEvent) { event.preventDefault(); @@ -761,7 +864,13 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO {project.teamspace_project || "Kein Teamspace-Projekt hinterlegt"} - +
+ + +
@@ -790,6 +899,96 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO ) : null} + + + + Projekt bearbeiten + Projektinformationen zum geöffneten Ticket aktualisieren. + +
+
+
+ + setProjectForm({ ...projectForm, title: event.currentTarget.value })} + placeholder="optional" + /> +
+
+ + setProjectForm({ ...projectForm, teamspaceProject: event.currentTarget.value })} + /> +
+
+
+
+ + + setProjectForm({ + ...projectForm, + organizationId: organization.id, + organizationName: organization.name + }) + } + required + /> +
+
+ + setProjectForm({ ...projectForm, startsOn: event.currentTarget.value })} /> +
+
+ + setProjectForm({ ...projectForm, endsOn: event.currentTarget.value })} /> +
+
+ + setProjectForm({ ...projectForm, hourBudget: event.currentTarget.value })} + /> +
+
+
+
+ +