Update
This commit is contained in:
1 parent
0c14a3acbd
commit
2105a0c5f7
11 files changed
+442
-86
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
Reference in new issue
Block a user