Auto-fill manual sessions from existing tickets

This commit is contained in:
michael committed 2026-09-01 11:18:47 +02:00
1 parent 018c06dc3e
commit 2792051a16
2 files changed
+87 -6

No files matched your search

+2 -2
View File
@@ -2785,7 +2785,7 @@ app.post("/api/sessions", requireUser, async (req, res) => {
[ticketNumber]
);
const existingTicket = existingTicketResult.rows[0] ?? null;
const effectiveOrganizationId = requestedOrganizationId ?? (existingTicket?.organization_id ? Number(existingTicket.organization_id) : null);
const effectiveOrganizationId = existingTicket?.organization_id ? Number(existingTicket.organization_id) : requestedOrganizationId;
if (!effectiveOrganizationId) {
throw badRequest("organizationId is required");
@@ -2812,7 +2812,7 @@ app.post("/api/sessions", requireUser, async (req, res) => {
);
const ticket = ticketResult.rows[0];
const workType = requestedWorkType ?? ticket.work_type;
const workType = ticket.work_type ?? requestedWorkType;
if (!workType) {
throw badRequest("workType must be support or consulting");
+85 -4
View File
@@ -1,4 +1,4 @@
import { FormEvent, useMemo, useState } from "react";
import { FormEvent, useEffect, useMemo, useState } from "react";
import type { Dispatch, SetStateAction } from "react";
import { Pause, Play, Plus, RotateCcw, Square, Upload } from "lucide-react";
import { toast } from "sonner";
@@ -73,12 +73,74 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
const [saving, setSaving] = useState(false);
const [manualSession, setManualSession] = useState<ManualSessionFormState>(() => emptyManualSession());
const [savingManualSession, setSavingManualSession] = useState(false);
const [manualLookupStatus, setManualLookupStatus] = useState<"idle" | "loading" | "found" | "not-found">("idle");
const [manualLookupTicket, setManualLookupTicket] = useState<TicketMeta | null>(null);
const selectedTimer = timers.find((timer) => timer.id === selectedTimerId) ?? timers[0] ?? null;
const elapsedSeconds = useMemo(() => Math.floor(activeElapsedMs(selectedTimer, tick) / 1000), [selectedTimer, tick]);
const isValidTicket = ticketPattern.test(ticketNumber);
const isManualTicketValid = ticketPattern.test(manualSession.ticketNumber.trim());
const manualTicketDefaultsLocked = Boolean(manualLookupTicket?.organization_id && manualLookupTicket.work_type);
const runningTimer = timers.find((timer) => timer.phase === "running") ?? null;
useEffect(() => {
const normalizedTicketNumber = manualSession.ticketNumber.trim();
if (!normalizedTicketNumber) {
setManualLookupStatus("idle");
setManualLookupTicket(null);
return;
}
if (!ticketPattern.test(normalizedTicketNumber)) {
setManualLookupStatus("idle");
setManualLookupTicket(null);
return;
}
let cancelled = false;
setManualLookupStatus("loading");
setManualLookupTicket(null);
const timeout = window.setTimeout(async () => {
try {
const result = await lookupTicket(normalizedTicketNumber);
if (cancelled) {
return;
}
setManualLookupTicket(result.ticket);
setManualLookupStatus(result.ticket ? "found" : "not-found");
if (result.ticket) {
setManualSession((current) => {
if (current.ticketNumber.trim() !== normalizedTicketNumber) {
return current;
}
return {
...current,
organizationId: result.ticket?.organization_id ?? "",
organizationName: result.ticket?.organization_name ?? result.ticket?.customer_name ?? null,
workType: result.ticket?.work_type ?? current.workType
};
});
}
} catch {
if (!cancelled) {
setManualLookupStatus("idle");
setManualLookupTicket(null);
}
}
}, 250);
return () => {
cancelled = true;
window.clearTimeout(timeout);
};
}, [manualSession.ticketNumber]);
async function addTimer() {
if (!isValidTicket) {
toast.error("Ticketnummer prüfen", {
@@ -231,7 +293,7 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
async function submitManualSession(event: FormEvent) {
event.preventDefault();
if (!ticketPattern.test(manualSession.ticketNumber)) {
if (!isManualTicketValid) {
toast.error("Ticketnummer prüfen", {
description: "Das Format muss Ticket#XXXXXX sein."
});
@@ -262,7 +324,7 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
try {
await createSession({
ticketNumber: manualSession.ticketNumber,
ticketNumber: manualSession.ticketNumber.trim(),
organizationId: manualSession.organizationId,
activity: manualSession.activity,
workType: manualSession.workType,
@@ -403,15 +465,33 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
id="manual-ticket-number"
placeholder="Ticket#123456"
value={manualSession.ticketNumber}
onChange={(event) => setManualSession({ ...manualSession, ticketNumber: event.currentTarget.value })}
onChange={(event) =>
setManualSession({
...manualSession,
ticketNumber: event.currentTarget.value,
organizationId: "",
organizationName: null,
workType: "support"
})
}
required
/>
{manualSession.ticketNumber.length > 0 && !isManualTicketValid ? <p className="text-xs text-destructive">Format: Ticket#XXXXXX</p> : null}
{manualLookupStatus === "loading" ? <p className="text-xs text-muted-foreground">Prüfe vorhandenes Ticket...</p> : null}
{manualLookupStatus === "found" && manualTicketDefaultsLocked ? (
<p className="text-xs text-muted-foreground">Ticket gefunden. Organisation und Art wurden übernommen.</p>
) : null}
{manualLookupStatus === "found" && !manualTicketDefaultsLocked ? (
<p className="text-xs text-muted-foreground">Ticket gefunden, aber Stammdaten sind unvollständig. Bitte ergänzen.</p>
) : null}
{manualLookupStatus === "not-found" ? <p className="text-xs text-muted-foreground">Neues Ticket. Organisation und Art bitte wählen.</p> : null}
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Organisation</label>
<OrganizationSelect
value={manualSession.organizationId}
selectedName={manualSession.organizationName}
disabled={manualTicketDefaultsLocked}
onChange={(organization) =>
setManualSession({
...manualSession,
@@ -428,6 +508,7 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
id="manual-work-type"
className="h-9 w-full rounded-lg border border-input bg-background px-2 text-sm focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/50"
value={manualSession.workType}
disabled={manualTicketDefaultsLocked}
onChange={(event) => setManualSession({ ...manualSession, workType: event.currentTarget.value as WorkType })}
>
<option value="support">Support</option>