diff --git a/frontend/src/views/TicketDetailPage.tsx b/frontend/src/views/TicketDetailPage.tsx index ea74ea2..d03003b 100644 --- a/frontend/src/views/TicketDetailPage.tsx +++ b/frontend/src/views/TicketDetailPage.tsx @@ -1,4 +1,4 @@ -import { ArrowLeft, CheckCircle2, ChevronDown, ChevronUp, ClipboardList, EyeOff, FolderKanban, Pencil, Plus, RotateCcw, Save, Star, Trash2 } from "lucide-react"; +import { ArrowLeft, CheckCircle2, ChevronDown, ChevronUp, ClipboardList, Copy, EyeOff, FolderKanban, Pencil, Plus, RotateCcw, Save, Star, Trash2 } from "lucide-react"; import { FormEvent, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { Alert } from "@/components/ui/alert"; @@ -204,6 +204,47 @@ function sessionDayKey(value: string) { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`; } +async function writeTextToClipboard(value: string) { + if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(value); + return; + } + + if (typeof document === "undefined") { + throw new Error("Zwischenablage nicht verfügbar"); + } + + const textarea = document.createElement("textarea"); + textarea.value = value; + textarea.style.position = "fixed"; + textarea.style.left = "-9999px"; + textarea.setAttribute("readonly", ""); + document.body.appendChild(textarea); + textarea.focus(); + textarea.select(); + + try { + if (!document.execCommand("copy")) { + throw new Error("Zwischenablage nicht verfügbar"); + } + } finally { + document.body.removeChild(textarea); + } +} + +function teamspacePreparationText(ticket: TicketPeriod["ticket"] | null | undefined, sessions: SessionEntry[]) { + const ticketNumber = ticket?.ticket_number ?? "Ticket#-"; + const customerName = ticket?.organization_name ?? ticket?.customer_name ?? "-"; + const activities = sessions + .slice() + .sort((left, right) => new Date(left.started_at).getTime() - new Date(right.started_at).getTime()) + .map((session) => session.activity.trim()) + .filter(Boolean) + .join("\n"); + + return [`${ticketNumber} - ${customerName}`, "", activities, "", "Tickethandling und Dokumentation"].join("\n"); +} + function toDateLocalValue(value: string) { const date = new Date(value); const offsetMs = date.getTimezoneOffset() * 60_000; @@ -448,6 +489,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO const [todoFilter, setTodoFilter] = useState<"open" | "all" | "done">("open"); const [expandedSessionIds, setExpandedSessionIds] = useState([]); const [projectDescriptionExpanded, setProjectDescriptionExpanded] = useState(false); + const [teamspacePreparation, setTeamspacePreparation] = useState<{ day: string; label: string; text: string } | null>(null); async function load() { try { @@ -964,6 +1006,31 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO setEditSessionEndTime(toTimeLocalValue(session.ended_at)); } + function openTeamspacePreparation(group: (typeof sessionGroups)[number]) { + setTeamspacePreparation({ + day: group.key, + label: group.label, + text: teamspacePreparationText(ticket, group.sessions) + }); + } + + async function copyTeamspacePreparation() { + if (!teamspacePreparation) { + return; + } + + try { + await writeTextToClipboard(teamspacePreparation.text); + toast.success("TS Aufbereitung kopiert", { + description: teamspacePreparation.label + }); + } catch (error) { + toast.error("TS Aufbereitung konnte nicht kopiert werden", { + description: error instanceof Error ? error.message : "Zwischenablage nicht verfügbar" + }); + } + } + async function saveSessionData(event: FormEvent) { event.preventDefault(); @@ -1130,11 +1197,12 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO

{ticket?.title || ticket?.ticket_number || "Ticket"}

- {ticket?.ticket_number ? : null} + {isProject ? Projekt : null} {(periodType === "month" || (isGlobalTicket && globalMonthFilter !== "all")) && data?.closed ? Monat geschlossen : null} + {ticket?.ticket_number ? : null}

{ticket?.title ? `${ticket.ticket_number} · ` : ""} @@ -1199,19 +1267,6 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO -

- - -
) : null} @@ -1579,6 +1634,10 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO

{group.sessions.length} Session(s)

+ Abgerechnet {formatMinutes(group.totalMinutes)} {group.nonBillableMinutes > 0 ? Nicht {formatMinutes(group.nonBillableMinutes)} : null} {group.openMinutes > 0 ? Offen {formatMinutes(group.openMinutes)} : null} @@ -1847,6 +1906,40 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO

) : null} + !open && setTeamspacePreparation(null)}> + + + TS Aufbereitung + {teamspacePreparation?.label ?? "Sessiontätigkeiten des Tages"} + +
+