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

+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"}