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

+57 -30
View File
@@ -9,6 +9,7 @@ import { HelpLink } from "@/components/HelpLink";
import { getZammadSessionArticleQueue, retryZammadSessionArticleQueue, updateCurrentUser } from "../api";
import { formatDateTime, formatMinutes } from "../format";
import type { AuthUser, ZammadSessionArticleQueueItem } from "../types";
import { zammadArticleTypeFromValue, zammadArticleTypeLabel, zammadArticleTypeOptions, zammadArticleTypeValue, type ZammadArticleTypeId } from "../zammad-article-types";
type ProfilePageProps = {
currentUser: AuthUser;
@@ -28,6 +29,7 @@ export function ProfilePage({ currentUser, onUserUpdated }: ProfilePageProps) {
const [zammadSessionNoteDefaultEnabled, setZammadSessionNoteDefaultEnabled] = useState(currentUser.zammad_session_note_default_enabled);
const [zammadTimeUnitRoundingEnabled, setZammadTimeUnitRoundingEnabled] = useState(currentUser.zammad_time_unit_rounding_enabled);
const [zammadTimeUnitRoundingMinutes, setZammadTimeUnitRoundingMinutes] = useState(currentUser.zammad_time_unit_rounding_minutes);
const [zammadArticleTypeId, setZammadArticleTypeId] = useState<ZammadArticleTypeId>(currentUser.zammad_article_type_id ?? 6);
const [zammadQueue, setZammadQueue] = useState<ZammadSessionArticleQueueItem[]>([]);
const [loadingZammadQueue, setLoadingZammadQueue] = useState(false);
const [retryingZammadQueue, setRetryingZammadQueue] = useState(false);
@@ -43,6 +45,7 @@ export function ProfilePage({ currentUser, onUserUpdated }: ProfilePageProps) {
setZammadSessionNoteDefaultEnabled(currentUser.zammad_session_note_default_enabled);
setZammadTimeUnitRoundingEnabled(currentUser.zammad_time_unit_rounding_enabled);
setZammadTimeUnitRoundingMinutes(currentUser.zammad_time_unit_rounding_minutes);
setZammadArticleTypeId(currentUser.zammad_article_type_id ?? 6);
}, [currentUser]);
async function loadZammadQueue() {
@@ -125,7 +128,8 @@ export function ProfilePage({ currentUser, onUserUpdated }: ProfilePageProps) {
sessionBillingPromptEnabled,
zammadSessionNoteDefaultEnabled,
zammadTimeUnitRoundingEnabled,
zammadTimeUnitRoundingMinutes
zammadTimeUnitRoundingMinutes,
zammadArticleTypeId
}
: {})
};
@@ -334,41 +338,63 @@ export function ProfilePage({ currentUser, onUserUpdated }: ProfilePageProps) {
/>
</div>
{zammadSessionNoteDefaultEnabled ? (
<div className="mt-3 rounded-md border bg-background p-3">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
<Clock3 className="size-4 text-muted-foreground" />
<div>
<label className="text-sm font-medium" htmlFor="profile-zammad-time-rounding">
Zammad-Zeit aufrunden
</label>
<p className="text-xs text-muted-foreground">Ohne Rundung wird die konkrete Zeit als Stundenwert mit 2 Nachkommastellen gesendet.</p>
</div>
</div>
<Switch
id="profile-zammad-time-rounding"
checked={zammadTimeUnitRoundingEnabled}
onCheckedChange={setZammadTimeUnitRoundingEnabled}
/>
</div>
{zammadTimeUnitRoundingEnabled ? (
<div className="mt-3 grid gap-2 sm:w-56">
<label className="text-xs font-medium text-muted-foreground" htmlFor="profile-zammad-rounding-minutes">
Rundungsintervall
<div className="mt-3 space-y-3">
<div className="rounded-md border bg-background p-3">
<div className="grid gap-2 sm:max-w-lg">
<label className="text-sm font-medium" htmlFor="profile-zammad-article-type">
Standard-Typ
</label>
<select
id="profile-zammad-rounding-minutes"
id="profile-zammad-article-type"
className="h-9 rounded-md border border-input bg-background px-3 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
value={zammadTimeUnitRoundingMinutes}
onChange={(event) => setZammadTimeUnitRoundingMinutes(Number(event.currentTarget.value))}
value={zammadArticleTypeValue(zammadArticleTypeId)}
onChange={(event) => setZammadArticleTypeId(zammadArticleTypeFromValue(event.currentTarget.value))}
>
<option value={5}>5 Minuten</option>
<option value={10}>10 Minuten</option>
<option value={15}>15 Minuten</option>
<option value={30}>30 Minuten</option>
{zammadArticleTypeOptions.map((option) => (
<option key={option.value || "empty"} value={option.value}>
{option.label}
</option>
))}
</select>
<p className="text-xs text-muted-foreground">Wird beim Erstellen interner Zammad-Notizen vorbelegt.</p>
</div>
) : null}
</div>
<div className="rounded-md border bg-background p-3">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
<Clock3 className="size-4 text-muted-foreground" />
<div>
<label className="text-sm font-medium" htmlFor="profile-zammad-time-rounding">
Zammad-Zeit aufrunden
</label>
<p className="text-xs text-muted-foreground">Ohne Rundung wird die konkrete Zeit als Stundenwert mit 2 Nachkommastellen gesendet.</p>
</div>
</div>
<Switch
id="profile-zammad-time-rounding"
checked={zammadTimeUnitRoundingEnabled}
onCheckedChange={setZammadTimeUnitRoundingEnabled}
/>
</div>
{zammadTimeUnitRoundingEnabled ? (
<div className="mt-3 grid gap-2 sm:w-56">
<label className="text-xs font-medium text-muted-foreground" htmlFor="profile-zammad-rounding-minutes">
Rundungsintervall
</label>
<select
id="profile-zammad-rounding-minutes"
className="h-9 rounded-md border border-input bg-background px-3 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
value={zammadTimeUnitRoundingMinutes}
onChange={(event) => setZammadTimeUnitRoundingMinutes(Number(event.currentTarget.value))}
>
<option value={5}>5 Minuten</option>
<option value={10}>10 Minuten</option>
<option value={15}>15 Minuten</option>
<option value={30}>30 Minuten</option>
</select>
</div>
) : null}
</div>
</div>
) : null}
<div className="mt-3 rounded-md border bg-background p-3">
@@ -401,6 +427,7 @@ export function ProfilePage({ currentUser, onUserUpdated }: ProfilePageProps) {
<span className="text-muted-foreground">{formatDateTime(item.started_at)}</span>
<span className="text-muted-foreground">{formatMinutes(item.rounded_minutes)}</span>
<span className="text-muted-foreground">time_unit {item.time_unit}</span>
<span className="text-muted-foreground">{zammadArticleTypeLabel(item.zammad_article_type_id)}</span>
</div>
{item.last_error ? <p className="mt-1 line-clamp-2 text-destructive">{item.last_error}</p> : null}
</div>
+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"}
+95 -24
View File
@@ -12,10 +12,12 @@ import { Textarea } from "@/components/ui/textarea";
import { HelpLink } from "@/components/HelpLink";
import { OrganizationSelect } from "@/components/OrganizationSelect";
import { createSession, getRecurringBillings, lookupTicket } from "../api";
import type { CreateSessionResult } from "../api";
import { formatTimer } from "../format";
import { isRecurringBillingTicketNumber, ticketHasRecurringBilling } from "../recurring-billings";
import { activeElapsedMs, createClientId, pauseEntry, resumeEntry, ticketPattern, type TimerEntry } from "../timers";
import type { AuthUser, BillingStatus, RecurringBilling, TicketMeta, WorkType } from "../types";
import { zammadArticleTypeFromValue, zammadArticleTypeOptions, zammadArticleTypeValue, type ZammadArticleTypeId } from "../zammad-article-types";
type ManualSessionFormState = {
ticketNumber: string;
@@ -25,6 +27,7 @@ type ManualSessionFormState = {
activity: string;
billingStatus: BillingStatus;
createZammadArticle: boolean;
zammadArticleTypeId: ZammadArticleTypeId;
workType: WorkType;
day: string;
startTime: string;
@@ -45,6 +48,7 @@ function emptyManualSession(): ManualSessionFormState {
activity: "",
billingStatus: null,
createZammadArticle: false,
zammadArticleTypeId: 6,
workType: "support",
day: currentDay(),
startTime: "09:00",
@@ -60,6 +64,7 @@ type FinishedSession = {
billingStatus: BillingStatus;
promptBillingStatus: boolean;
createZammadArticle: boolean;
zammadArticleTypeId: ZammadArticleTypeId;
startedAt: string;
endedAt: string;
durationSeconds: number;
@@ -115,6 +120,19 @@ function BillingStatusSelect({
);
}
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."
});
}
}
export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimerId, tick, currentUser }: TimerPageProps) {
const [ticketNumber, setTicketNumber] = useState("");
const [finished, setFinished] = useState<FinishedSession | null>(null);
@@ -225,16 +243,23 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
useEffect(() => {
setManualSession((current) => {
if (current.ticketNumber || current.activity || current.organizationId || current.billingStatus || current.createZammadArticle === currentUser.zammad_session_note_default_enabled) {
if (
current.ticketNumber ||
current.activity ||
current.organizationId ||
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]);
async function addTimer(source: "local" | "zammad" = "local") {
const normalizedTicketNumber = ticketNumber.trim();
@@ -464,6 +489,7 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
billingStatus: timer.billingStatus,
promptBillingStatus: currentUser.session_billing_prompt_enabled && timer.billingStatus === null,
createZammadArticle: currentUser.zammad_session_note_default_enabled,
zammadArticleTypeId: currentUser.zammad_article_type_id,
startedAt: new Date(stoppedTimer.startedAt).toISOString(),
endedAt: new Date(now).toISOString(),
durationSeconds,
@@ -522,7 +548,7 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
setSaving(true);
try {
await createSession({
const result = await createSession({
ticketNumber: finished.ticketNumber,
startedAt: finished.startedAt,
endedAt: finished.endedAt,
@@ -533,12 +559,14 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
zammadLookup: Boolean(finished.ticket?.zammad_ticket_id),
projectTeamspaceEntryId: finishedRequiresTeamspaceEntry ? projectTeamspaceEntryId : null,
billingStatus: finished.billingStatus,
createZammadArticle: finished.createZammadArticle
createZammadArticle: finished.createZammadArticle,
zammadArticleTypeId: finished.zammadArticleTypeId
});
toast.success("Session gespeichert", {
description: `${finished.ticketNumber} wurde mit ${finished.roundedMinutes} Minute(n) erfasst.`
});
notifyZammadArticleResult(result);
setTimers((current) => current.filter((timer) => timer.id !== finished.timerId));
setFinished(null);
setOrganizationId("");
@@ -601,7 +629,7 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
setSavingManualSession(true);
try {
await createSession({
const result = await createSession({
ticketNumber: manualSession.ticketNumber.trim(),
organizationId: manualSession.organizationId,
activity: manualSession.activity,
@@ -612,15 +640,18 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
zammadLookup: Boolean(manualLookupTicket?.zammad_ticket_id),
projectTeamspaceEntryId: requiresTeamspaceEntry(manualLookupTicket) ? manualSession.projectTeamspaceEntryId : null,
billingStatus: manualSession.billingStatus,
createZammadArticle: manualSession.createZammadArticle
createZammadArticle: manualSession.createZammadArticle,
zammadArticleTypeId: manualSession.zammadArticleTypeId
});
toast.success("Session nachgetragen", {
description: manualLookupTicket?.is_project ? "Der Eintrag wurde dem Projekt zugeordnet." : "Der Eintrag ist in deiner Ticketliste offen."
});
notifyZammadArticleResult(result);
setManualSession({
...emptyManualSession(),
day: manualSession.day,
createZammadArticle: currentUser.zammad_session_note_default_enabled
createZammadArticle: currentUser.zammad_session_note_default_enabled,
zammadArticleTypeId: currentUser.zammad_article_type_id
});
} catch (error) {
toast.error("Session konnte nicht nachgetragen werden", {
@@ -935,6 +966,25 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
<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>
{manualSession.createZammadArticle ? (
<div className="space-y-2 lg:col-span-5">
<label className="text-sm font-medium" htmlFor="manual-zammad-article-type">
Zammad-Typ
</label>
<select
id="manual-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(manualSession.zammadArticleTypeId)}
onChange={(event) => setManualSession({ ...manualSession, 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={savingManualSession || manualTicketIsRecurringBilling}>
<Upload className="size-4" />
{savingManualSession ? "Speichert..." : "Nachtragen"}
@@ -1006,23 +1056,44 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
/>
</div>
{finished ? (
<div className="flex items-start gap-3 rounded-md border bg-muted/30 p-3">
<Checkbox
id="finished-zammad-article"
checked={finished.createZammadArticle}
onCheckedChange={(checked) => setFinished((current) => (current ? { ...current, createZammadArticle: checked === true } : current))}
/>
<div className="space-y-1">
<label className="text-sm font-medium" htmlFor="finished-zammad-article">
Tätigkeit als interne Zammad-Notiz übertragen
</label>
<p className="text-xs text-muted-foreground">
Erstellt einen internen Zammad-Article mit Tätigkeit und getrackter Zeit.
</p>
{!finished.ticket?.zammad_ticket_id ? (
<p className="text-xs text-muted-foreground">Falls die Zammad-ID noch fehlt, wird sie beim Speichern oder späteren Nachsenden erneut gesucht.</p>
) : null}
<div className="space-y-3 rounded-md border bg-muted/30 p-3">
<div className="flex items-start gap-3">
<Checkbox
id="finished-zammad-article"
checked={finished.createZammadArticle}
onCheckedChange={(checked) => setFinished((current) => (current ? { ...current, createZammadArticle: checked === true } : current))}
/>
<div className="space-y-1">
<label className="text-sm font-medium" htmlFor="finished-zammad-article">
Tätigkeit als interne Zammad-Notiz übertragen
</label>
<p className="text-xs text-muted-foreground">
Erstellt einen internen Zammad-Article mit Tätigkeit und getrackter Zeit.
</p>
{!finished.ticket?.zammad_ticket_id ? (
<p className="text-xs text-muted-foreground">Falls die Zammad-ID noch fehlt, wird sie beim Speichern oder späteren Nachsenden erneut gesucht.</p>
) : null}
</div>
</div>
{finished.createZammadArticle ? (
<div className="space-y-2">
<label className="text-sm font-medium" htmlFor="finished-zammad-article-type">
Zammad-Typ
</label>
<select
id="finished-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(finished.zammadArticleTypeId)}
onChange={(event) => setFinished((current) => (current ? { ...current, zammadArticleTypeId: zammadArticleTypeFromValue(event.currentTarget.value) } : current))}
>
{zammadArticleTypeOptions.map((option) => (
<option key={option.value || "empty"} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
) : null}
</div>
) : null}
{finished?.promptBillingStatus ? (