This commit is contained in:
michael committed 2026-09-23 11:37:33 +00:00
1 parent fa6db04370
commit 0c14a3acbd
5 files changed
+79 -7

No files matched your search

+2
View File
@@ -1504,6 +1504,8 @@ export function App() {
period={route.period}
ticketId={route.ticketId}
onNavigate={navigate}
onStartTimer={startQuickTimer}
currentUser={currentUser}
onOpenTicketChange={updateOpenTicket}
returnPath={ticketReturnPaths[route.ticketId] ?? openTickets.find((ticket) => ticket.ticketId === route.ticketId)?.returnPath ?? "/analysis"}
/>
+2
View File
@@ -146,6 +146,7 @@ export const helpSections: HelpSection[] = [
"Die Organisation muss aus der synchronisierten Organisationsliste gewählt werden. Freitext ist absichtlich gesperrt.",
"Die Tätigkeit ist eine Textarea. Zeilenumbrüche bleiben erhalten und werden in der Ticketdetailansicht wieder ausgegeben.",
"Mit Bewertung --- bleibt die Session offen. Abrechenbar oder Nicht abrechenbar werden direkt an der neuen Session gespeichert.",
"Die interne Zammad-Notiz kann auch beim Nachtragen gesetzt werden. Bei Zammad-Fehlern wird sie geparkt und kann später im Profil gesendet werden.",
"In der Ticketdetailansicht kann eine Session direkt zum geöffneten Ticket nachgetragen werden. Ticketnummer und Stammdaten sind dort schon vorbelegt beziehungsweise aus dem Ticket abgeleitet.",
"Bei Consulting-Projekten mit mehreren Teamspace-Einträgen ist der passende Teamspace-Eintrag Pflicht. Bei nur einem Eintrag wird er automatisch gesetzt."
]
@@ -218,6 +219,7 @@ export const helpSections: HelpSection[] = [
],
details: [
"Oben steht die Ticketnummer mit Copy-Icon, damit sie schnell in andere Systeme übernommen werden kann.",
"Über Timer starten kann direkt für dieses Ticket ein Timer gestartet oder ein vorhandener Timer aktiviert werden.",
"Die Session-Einträge sind nach Kalendertagen gruppiert. Pro Tagesgruppe steht die Summe der abgerechneten Zeit.",
"Abrechenbare, nicht abrechenbare und offene Zeiten werden neben der abgerechneten Tageszeit separat angezeigt.",
"Bei Projekt-Tickets mit mehreren Teamspace-Einträgen zeigt jede zugeordnete Session den gewählten Teamspace-Eintrag. Nicht zugeordnete Sessions werden hervorgehoben.",
+40 -5
View File
@@ -1,4 +1,4 @@
import { ArrowLeft, CheckCircle2, ChevronDown, ChevronUp, ClipboardList, Copy, EyeOff, FolderKanban, Pencil, Plus, RotateCcw, Save, Star, Trash2 } from "lucide-react";
import { ArrowLeft, CheckCircle2, ChevronDown, ChevronUp, ClipboardList, Copy, EyeOff, FolderKanban, Pencil, Play, 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";
@@ -32,7 +32,7 @@ import {
} from "../api";
import { currentDay, currentMonth, formatDate, formatDateTime, formatMinutes, formatTimeRange } from "../format";
import { ticketHasRecurringBilling } from "../recurring-billings";
import type { BillingStatus, PeriodType, ProjectSummary, ProjectTeamspaceEntry, SessionEntry, TicketDetailPeriodType, TicketPeriod, WorkType } from "../types";
import type { AuthUser, BillingStatus, PeriodType, ProjectSummary, ProjectTeamspaceEntry, SessionEntry, TicketDetailPeriodType, TicketPeriod, WorkType } from "../types";
import { ZammadPriorityBadge, ZammadStateBadge } from "../zammad-meta";
import { cn } from "@/lib/utils";
@@ -41,6 +41,8 @@ type TicketDetailPageProps = {
period: string;
ticketId: string;
onNavigate: (to: string) => void;
onStartTimer: (ticketNumber: string) => Promise<boolean>;
currentUser: AuthUser;
onOpenTicketChange?: (ticket: {
key: string;
ticketId: string;
@@ -60,6 +62,7 @@ type TicketSessionFormState = {
projectTeamspaceEntryId: string;
activity: string;
billingStatus: BillingStatus;
createZammadArticle: boolean;
workType: WorkType;
day: string;
startTime: string;
@@ -92,6 +95,7 @@ function emptyTicketSessionForm(): TicketSessionFormState {
projectTeamspaceEntryId: "",
activity: "",
billingStatus: null,
createZammadArticle: false,
workType: "support",
day: currentDay(),
startTime: "09:00",
@@ -480,7 +484,7 @@ function ProjectTeamspaceEntriesEditor({
);
}
export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onOpenTicketChange, returnPath }: TicketDetailPageProps) {
export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onStartTimer, currentUser, onOpenTicketChange, returnPath }: TicketDetailPageProps) {
const initialGlobalMonthFilter = periodType === "all" && period !== "all" ? period : currentMonth();
const [data, setData] = useState<TicketPeriod | null>(null);
const [loadingId, setLoadingId] = useState<string | null>(null);
@@ -593,6 +597,19 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
}));
}, [data?.ticket, data?.project]);
useEffect(() => {
setTicketSession((current) => {
if (current.activity || current.billingStatus || current.createZammadArticle === currentUser.zammad_session_note_default_enabled) {
return current;
}
return {
...current,
createZammadArticle: currentUser.zammad_session_note_default_enabled
};
});
}, [currentUser.zammad_session_note_default_enabled]);
useEffect(() => {
setProjectDescriptionExpanded(false);
}, [data?.project?.id]);
@@ -1023,7 +1040,8 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
endedAt: endedAt.toISOString(),
durationSeconds,
projectTeamspaceEntryId: effectiveWorkType === "consulting" && requiresProjectTeamspaceEntry ? ticketSession.projectTeamspaceEntryId : null,
billingStatus: ticketSession.billingStatus
billingStatus: ticketSession.billingStatus,
createZammadArticle: ticketSession.createZammadArticle
});
toast.success("Session nachgetragen", {
description: "Der Eintrag wurde diesem Ticket zugeordnet. Betroffene Abschlüsse wurden wieder geöffnet."
@@ -1035,7 +1053,8 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
projectTeamspaceEntryId: "",
workType: ticket.work_type ?? "support",
day: ticketSession.day,
billingStatus: null
billingStatus: null,
createZammadArticle: currentUser.zammad_session_note_default_enabled
});
await load();
} catch (error) {
@@ -1264,6 +1283,12 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
</div>
<div className="flex flex-wrap items-center gap-2">
{ticket?.ticket_number ? (
<Button type="button" size="sm" onClick={() => void onStartTimer(ticket.ticket_number)}>
<Play className="size-4" />
Timer starten
</Button>
) : null}
{ticket?.ticket_number ? (
<Button type="button" size="sm" variant={isFavorite ? "default" : "secondary"} disabled={savingFavorite} onClick={() => void toggleTicketFavorite()}>
<Star className={cn("size-4", isFavorite && "fill-current")} />
@@ -1974,6 +1999,16 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
required
/>
</div>
<label className="flex min-h-10 items-start gap-2 rounded-md border bg-background p-3 text-sm lg:col-span-5">
<Checkbox
checked={ticketSession.createZammadArticle}
onCheckedChange={(checked) => setTicketSession({ ...ticketSession, createZammadArticle: checked === true })}
/>
<span>
<span className="block font-medium">Interne Zammad-Notiz erstellen</span>
<span className="block text-xs text-muted-foreground">Überträgt die Tätigkeit inklusive time_unit nach Zammad oder parkt sie bei Fehlern.</span>
</span>
</label>
<Button type="submit" disabled={savingTicketSession}>
<Save className="size-4" />
{savingTicketSession ? "Speichert..." : "Nachtragen"}
+29 -2
View File
@@ -24,6 +24,7 @@ type ManualSessionFormState = {
projectTeamspaceEntryId: string;
activity: string;
billingStatus: BillingStatus;
createZammadArticle: boolean;
workType: WorkType;
day: string;
startTime: string;
@@ -43,6 +44,7 @@ function emptyManualSession(): ManualSessionFormState {
projectTeamspaceEntryId: "",
activity: "",
billingStatus: null,
createZammadArticle: false,
workType: "support",
day: currentDay(),
startTime: "09:00",
@@ -221,6 +223,19 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
};
}, [manualSession.ticketNumber]);
useEffect(() => {
setManualSession((current) => {
if (current.ticketNumber || current.activity || current.organizationId || current.billingStatus || current.createZammadArticle === currentUser.zammad_session_note_default_enabled) {
return current;
}
return {
...current,
createZammadArticle: currentUser.zammad_session_note_default_enabled
};
});
}, [currentUser.zammad_session_note_default_enabled]);
async function addTimer(source: "local" | "zammad" = "local") {
const normalizedTicketNumber = ticketNumber.trim();
@@ -596,14 +611,16 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
durationSeconds,
zammadLookup: Boolean(manualLookupTicket?.zammad_ticket_id),
projectTeamspaceEntryId: requiresTeamspaceEntry(manualLookupTicket) ? manualSession.projectTeamspaceEntryId : null,
billingStatus: manualSession.billingStatus
billingStatus: manualSession.billingStatus,
createZammadArticle: manualSession.createZammadArticle
});
toast.success("Session nachgetragen", {
description: manualLookupTicket?.is_project ? "Der Eintrag wurde dem Projekt zugeordnet." : "Der Eintrag ist in deiner Ticketliste offen."
});
setManualSession({
...emptyManualSession(),
day: manualSession.day
day: manualSession.day,
createZammadArticle: currentUser.zammad_session_note_default_enabled
});
} catch (error) {
toast.error("Session konnte nicht nachgetragen werden", {
@@ -908,6 +925,16 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
required
/>
</div>
<label className="flex min-h-10 items-start gap-2 rounded-md border bg-background p-3 text-sm lg:col-span-5">
<Checkbox
checked={manualSession.createZammadArticle}
onCheckedChange={(checked) => setManualSession({ ...manualSession, createZammadArticle: checked === true })}
/>
<span>
<span className="block font-medium">Interne Zammad-Notiz erstellen</span>
<span className="block text-xs text-muted-foreground">Überträgt die Tätigkeit inklusive time_unit nach Zammad oder parkt sie bei Fehlern.</span>
</span>
</label>
<Button type="submit" disabled={savingManualSession || manualTicketIsRecurringBilling}>
<Upload className="size-4" />
{savingManualSession ? "Speichert..." : "Nachtragen"}