This commit is contained in:
michael committed 2026-09-23 13:34:38 +00:00
1 parent 0c14a3acbd
commit 2105a0c5f7
11 files changed
+442 -86

No files matched your search

+61 -8
View File
@@ -1,4 +1,4 @@
import { ArrowLeft, CheckCircle2, ChevronDown, ChevronUp, ClipboardList, Copy, EyeOff, FolderKanban, Pencil, Play, Plus, RotateCcw, Save, Star, Trash2 } from "lucide-react";
import { ArrowLeft, CheckCircle2, ChevronDown, ChevronUp, ClipboardList, Copy, EyeOff, FolderKanban, Pencil, Play, Plus, RotateCcw, Save, Send, Star, Trash2 } from "lucide-react";
import { FormEvent, useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import { Alert } from "@/components/ui/alert";
@@ -30,10 +30,12 @@ import {
updateProject,
updateProjectStatus
} from "../api";
import type { CreateSessionResult } from "../api";
import { currentDay, currentMonth, formatDate, formatDateTime, formatMinutes, formatTimeRange } from "../format";
import { ticketHasRecurringBilling } from "../recurring-billings";
import type { AuthUser, BillingStatus, PeriodType, ProjectSummary, ProjectTeamspaceEntry, SessionEntry, TicketDetailPeriodType, TicketPeriod, WorkType } from "../types";
import { ZammadPriorityBadge, ZammadStateBadge } from "../zammad-meta";
import { zammadArticleTypeFromValue, zammadArticleTypeLabel, zammadArticleTypeOptions, zammadArticleTypeValue, type ZammadArticleTypeId } from "../zammad-article-types";
import { cn } from "@/lib/utils";
type TicketDetailPageProps = {
@@ -63,6 +65,7 @@ type TicketSessionFormState = {
activity: string;
billingStatus: BillingStatus;
createZammadArticle: boolean;
zammadArticleTypeId: ZammadArticleTypeId;
workType: WorkType;
day: string;
startTime: string;
@@ -96,6 +99,7 @@ function emptyTicketSessionForm(): TicketSessionFormState {
activity: "",
billingStatus: null,
createZammadArticle: false,
zammadArticleTypeId: 6,
workType: "support",
day: currentDay(),
startTime: "09:00",
@@ -103,6 +107,19 @@ function emptyTicketSessionForm(): TicketSessionFormState {
};
}
function notifyZammadArticleResult(result: CreateSessionResult) {
if (result.zammadArticle?.status === "sent") {
toast.success("Zammad-Notiz übertragen");
return;
}
if (result.zammadArticle?.status === "queued") {
toast.warning("Zammad-Notiz geparkt", {
description: result.zammadArticle.error ?? "Die Session wurde gespeichert. Die Notiz kann im Profil später erneut gesendet werden."
});
}
}
function projectEntryId() {
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
return crypto.randomUUID();
@@ -599,16 +616,21 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
useEffect(() => {
setTicketSession((current) => {
if (current.activity || current.billingStatus || current.createZammadArticle === currentUser.zammad_session_note_default_enabled) {
if (
current.activity ||
current.billingStatus ||
(current.createZammadArticle === currentUser.zammad_session_note_default_enabled && current.zammadArticleTypeId === currentUser.zammad_article_type_id)
) {
return current;
}
return {
...current,
createZammadArticle: currentUser.zammad_session_note_default_enabled
createZammadArticle: currentUser.zammad_session_note_default_enabled,
zammadArticleTypeId: currentUser.zammad_article_type_id
};
});
}, [currentUser.zammad_session_note_default_enabled]);
}, [currentUser.zammad_article_type_id, currentUser.zammad_session_note_default_enabled]);
useEffect(() => {
setProjectDescriptionExpanded(false);
@@ -1031,7 +1053,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
setSavingTicketSession(true);
try {
await createSession({
const result = await createSession({
ticketNumber: ticket.ticket_number,
organizationId: effectiveOrganizationId,
activity: ticketSession.activity,
@@ -1041,11 +1063,13 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
durationSeconds,
projectTeamspaceEntryId: effectiveWorkType === "consulting" && requiresProjectTeamspaceEntry ? ticketSession.projectTeamspaceEntryId : null,
billingStatus: ticketSession.billingStatus,
createZammadArticle: ticketSession.createZammadArticle
createZammadArticle: ticketSession.createZammadArticle,
zammadArticleTypeId: ticketSession.zammadArticleTypeId
});
toast.success("Session nachgetragen", {
description: "Der Eintrag wurde diesem Ticket zugeordnet. Betroffene Abschlüsse wurden wieder geöffnet."
});
notifyZammadArticleResult(result);
setTicketSession({
...emptyTicketSessionForm(),
organizationId: ticket.organization_id ?? "",
@@ -1054,7 +1078,8 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
workType: ticket.work_type ?? "support",
day: ticketSession.day,
billingStatus: null,
createZammadArticle: currentUser.zammad_session_note_default_enabled
createZammadArticle: currentUser.zammad_session_note_default_enabled,
zammadArticleTypeId: currentUser.zammad_article_type_id
});
await load();
} catch (error) {
@@ -1842,7 +1867,16 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
<p className="whitespace-pre-wrap break-words text-sm font-medium">{session.activity}</p>
<div className="mt-2 flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground">
<span>{session.organization_name ?? session.customer_name}</span>
<span>{session.work_type === "support" ? "Support" : "Consulting"}</span>
{session.zammad_article_status ? (
<span className="inline-flex items-center gap-1">
{session.zammad_article_status === "sent" ? (
<Send className="size-3.5 text-emerald-500" aria-hidden="true" />
) : null}
<span>{zammadArticleTypeLabel(session.zammad_article_type_id)}</span>
</span>
) : (
<span>{session.work_type === "support" ? "Support" : "Consulting"}</span>
)}
<span>Erfasst {formatDateTime(session.created_at)}</span>
</div>
</div>
@@ -2009,6 +2043,25 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
<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>
{ticketSession.createZammadArticle ? (
<div className="space-y-2 lg:col-span-5">
<label className="text-sm font-medium" htmlFor="ticket-session-zammad-article-type">
Zammad-Typ
</label>
<select
id="ticket-session-zammad-article-type"
className="h-10 w-full rounded-md border border-input bg-background px-3 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
value={zammadArticleTypeValue(ticketSession.zammadArticleTypeId)}
onChange={(event) => setTicketSession({ ...ticketSession, zammadArticleTypeId: zammadArticleTypeFromValue(event.currentTarget.value) })}
>
{zammadArticleTypeOptions.map((option) => (
<option key={option.value || "empty"} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
) : null}
<Button type="submit" disabled={savingTicketSession}>
<Save className="size-4" />
{savingTicketSession ? "Speichert..." : "Nachtragen"}