Update
This commit is contained in:
1 parent
3d9674bd5a
commit
d1da86beab
20 files changed
+6109
-923
No files matched your search
@@ -10,10 +10,11 @@ import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { HelpLink } from "@/components/HelpLink";
|
||||
import { OrganizationSelect } from "@/components/OrganizationSelect";
|
||||
import { createSession, lookupTicket } from "../api";
|
||||
import { createSession, getRecurringBillings, lookupTicket } from "../api";
|
||||
import { formatTimer } from "../format";
|
||||
import { isRecurringBillingTicketNumber, ticketHasRecurringBilling } from "../recurring-billings";
|
||||
import { activeElapsedMs, pauseEntry, resumeEntry, ticketPattern, type TimerEntry } from "../timers";
|
||||
import type { TicketMeta, WorkType } from "../types";
|
||||
import type { RecurringBilling, TicketMeta, WorkType } from "../types";
|
||||
|
||||
type ManualSessionFormState = {
|
||||
ticketNumber: string;
|
||||
@@ -75,14 +76,41 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
|
||||
const [savingManualSession, setSavingManualSession] = useState(false);
|
||||
const [manualLookupStatus, setManualLookupStatus] = useState<"idle" | "loading" | "found" | "not-found">("idle");
|
||||
const [manualLookupTicket, setManualLookupTicket] = useState<TicketMeta | null>(null);
|
||||
const [recurringBillings, setRecurringBillings] = useState<RecurringBilling[]>([]);
|
||||
|
||||
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 manualTicketIsRecurringBilling =
|
||||
ticketHasRecurringBilling(manualLookupTicket, recurringBillings) || isRecurringBillingTicketNumber(manualSession.ticketNumber.trim(), recurringBillings);
|
||||
const runningTimer = timers.find((timer) => timer.phase === "running") ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
async function loadRecurringBillings() {
|
||||
try {
|
||||
const result = await getRecurringBillings();
|
||||
|
||||
if (!cancelled) {
|
||||
setRecurringBillings(result.recurringBillings);
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
setRecurringBillings([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void loadRecurringBillings();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const normalizedTicketNumber = manualSession.ticketNumber.trim();
|
||||
|
||||
@@ -104,7 +132,7 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
|
||||
|
||||
const timeout = window.setTimeout(async () => {
|
||||
try {
|
||||
const result = await lookupTicket(normalizedTicketNumber);
|
||||
const result = await lookupTicket(normalizedTicketNumber);
|
||||
|
||||
if (cancelled) {
|
||||
return;
|
||||
@@ -149,6 +177,16 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
|
||||
return;
|
||||
}
|
||||
|
||||
const ticketResult = await lookupTicket(ticketNumber).catch(() => ({ ticket: null }));
|
||||
|
||||
if (ticketHasRecurringBilling(ticketResult.ticket, recurringBillings) || isRecurringBillingTicketNumber(ticketNumber, recurringBillings)) {
|
||||
setTimers((current) => current.filter((timer) => timer.ticketNumber !== ticketNumber));
|
||||
toast.error("Fixe Abrechnung", {
|
||||
description: "Für fixe Abrechnungen dürfen keine manuellen Sessions gestartet werden. Bitte die Definition bearbeiten."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (timers.some((timer) => timer.ticketNumber === ticketNumber)) {
|
||||
toast.error("Timer existiert bereits", {
|
||||
description: "Für dieses Ticket läuft oder pausiert schon ein Timer."
|
||||
@@ -157,7 +195,6 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const ticketResult = await lookupTicket(ticketNumber).catch(() => ({ ticket: null }));
|
||||
const newTimer: TimerEntry = {
|
||||
id: crypto.randomUUID(),
|
||||
ticketNumber,
|
||||
@@ -221,6 +258,14 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
|
||||
const result = await lookupTicket(timer.ticketNumber);
|
||||
const ticket = result.ticket;
|
||||
|
||||
if (ticketHasRecurringBilling(ticket, recurringBillings) || isRecurringBillingTicketNumber(timer.ticketNumber, recurringBillings)) {
|
||||
setTimers((current) => current.filter((entry) => entry.id !== timer.id));
|
||||
toast.error("Fixe Abrechnung", {
|
||||
description: "Für fixe Abrechnungen dürfen keine manuellen Sessions gespeichert werden. Bitte die Definition bearbeiten."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setOrganizationId(ticket?.organization_id ?? "");
|
||||
setOrganizationName(ticket?.organization_name ?? null);
|
||||
setWorkType(ticket?.work_type ?? "support");
|
||||
@@ -250,6 +295,14 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
|
||||
return;
|
||||
}
|
||||
|
||||
if (ticketHasRecurringBilling(finished.ticket, recurringBillings) || isRecurringBillingTicketNumber(finished.ticketNumber, recurringBillings)) {
|
||||
toast.error("Fixe Abrechnung", {
|
||||
description: "Für fixe Abrechnungen dürfen keine manuellen Sessions gespeichert werden. Bitte die Definition bearbeiten."
|
||||
});
|
||||
setFinished(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const existingTicketHasDefaults = Boolean(finished.ticket?.organization_id && finished.ticket?.work_type);
|
||||
const effectiveOrganizationId = existingTicketHasDefaults ? finished.ticket!.organization_id! : organizationId;
|
||||
const effectiveWorkType = existingTicketHasDefaults ? finished.ticket!.work_type! : workType;
|
||||
@@ -307,6 +360,13 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
|
||||
return;
|
||||
}
|
||||
|
||||
if (manualTicketIsRecurringBilling) {
|
||||
toast.error("Fixe Abrechnung", {
|
||||
description: "Für fixe Abrechnungen dürfen keine Sessions nachgetragen werden. Bitte die Definition bearbeiten."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const startedAt = new Date(`${manualSession.day}T${manualSession.startTime}:00`);
|
||||
const endedAt = new Date(`${manualSession.day}T${manualSession.endTime}:00`);
|
||||
|
||||
@@ -335,7 +395,7 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
|
||||
durationSeconds
|
||||
});
|
||||
toast.success("Session nachgetragen", {
|
||||
description: manualLookupTicket?.is_project ? "Der Eintrag wurde dem Projekt zugeordnet." : "Der Eintrag ist in deiner Auswertung offen."
|
||||
description: manualLookupTicket?.is_project ? "Der Eintrag wurde dem Projekt zugeordnet." : "Der Eintrag ist in deiner Ticketliste offen."
|
||||
});
|
||||
setManualSession({
|
||||
...emptyManualSession(),
|
||||
@@ -488,6 +548,9 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
|
||||
{manualLookupStatus === "found" && !manualTicketDefaultsLocked ? (
|
||||
<p className="text-xs text-muted-foreground">Ticket gefunden, aber Stammdaten sind unvollständig. Bitte ergänzen.</p>
|
||||
) : null}
|
||||
{manualTicketIsRecurringBilling ? (
|
||||
<p className="text-xs text-destructive">Fixe Abrechnung: Sessions kommen ausschließlich aus der Definition.</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">
|
||||
@@ -562,7 +625,7 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" disabled={savingManualSession}>
|
||||
<Button type="submit" disabled={savingManualSession || manualTicketIsRecurringBilling}>
|
||||
<Upload className="size-4" />
|
||||
{savingManualSession ? "Speichert..." : "Nachtragen"}
|
||||
</Button>
|
||||
|
||||
Reference in new issue
Block a user