Use global ticket detail entrypoint
This commit is contained in:
1 parent
b67dbad0b1
commit
018c06dc3e
6 files changed
+94
-25
No files matched your search
@@ -21,7 +21,7 @@ import {
|
||||
updateTicket,
|
||||
updateTicketDayBilling
|
||||
} from "../api";
|
||||
import { currentDay, formatDate, formatDateTime, formatMinutes, formatTimeRange } from "../format";
|
||||
import { currentDay, currentMonth, formatDate, formatDateTime, formatMinutes, formatTimeRange } from "../format";
|
||||
import type { BillingStatus, PeriodType, SessionEntry, TicketDetailPeriodType, TicketPeriod, WorkType } from "../types";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -152,6 +152,7 @@ function isPartialHoursInput(value: string) {
|
||||
}
|
||||
|
||||
export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onOpenTicketChange }: TicketDetailPageProps) {
|
||||
const initialGlobalMonthFilter = periodType === "all" && period !== "all" ? period : currentMonth();
|
||||
const [data, setData] = useState<TicketPeriod | null>(null);
|
||||
const [loadingId, setLoadingId] = useState<string | null>(null);
|
||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||
@@ -173,7 +174,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
const [savingDayBillingKey, setSavingDayBillingKey] = useState<string | null>(null);
|
||||
const [ticketSession, setTicketSession] = useState<TicketSessionFormState>(() => emptyTicketSessionForm());
|
||||
const [savingTicketSession, setSavingTicketSession] = useState(false);
|
||||
const [globalMonthFilter, setGlobalMonthFilter] = useState("all");
|
||||
const [globalMonthFilter, setGlobalMonthFilter] = useState(initialGlobalMonthFilter);
|
||||
const [globalStatusFilter, setGlobalStatusFilter] = useState<"all" | "open" | "billed" | "non_billable">("all");
|
||||
const [globalSourceFilter, setGlobalSourceFilter] = useState<"all" | "manual" | "recurring">("all");
|
||||
|
||||
@@ -199,6 +200,14 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
void load();
|
||||
}, [periodType, period, ticketId, globalMonthFilter, globalStatusFilter, globalSourceFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (periodType !== "all") {
|
||||
return;
|
||||
}
|
||||
|
||||
setGlobalMonthFilter(period !== "all" ? period : currentMonth());
|
||||
}, [periodType, period, ticketId]);
|
||||
|
||||
useEffect(() => {
|
||||
const nextInputs = Object.fromEntries((data?.dayBillings ?? []).map((billing) => [billing.day, formatHoursInput(billing.billed_minutes)]));
|
||||
setDayBillingInputs(nextInputs);
|
||||
@@ -231,6 +240,8 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
: `im Monat ${globalMonthFilter}`
|
||||
: `im ${periodLabel.toLowerCase()} ${period}`;
|
||||
const teamspaceLabel = isGlobalTicket || periodType === "month" ? "Teamspace gesamt" : "Teamspace Tag";
|
||||
const availableMonths = data?.availableMonths ?? [];
|
||||
const selectedMonthHasSessions = availableMonths.some((month) => month.month === globalMonthFilter);
|
||||
const dayBillingMap = useMemo(() => new Map((data?.dayBillings ?? []).map((billing) => [billing.day, billing.billed_minutes])), [data?.dayBillings]);
|
||||
const savedDayBillingInput = (day: string) => formatHoursInput(dayBillingMap.get(day));
|
||||
const isDayBillingDirty = (day: string) => (dayBillingInputs[day] ?? "") !== savedDayBillingInput(day);
|
||||
@@ -282,16 +293,18 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
return;
|
||||
}
|
||||
|
||||
const ticketMonth = isGlobalTicket ? globalMonthFilter : periodType === "day" ? period.slice(0, 7) : period;
|
||||
|
||||
onOpenTicketChange?.({
|
||||
key: isGlobalTicket ? `ticket:${ticket.id}` : `${periodType}:${period}:${ticket.id}`,
|
||||
key: `ticket:${ticket.id}`,
|
||||
ticketId: ticket.id,
|
||||
ticketNumber: ticket.ticket_number,
|
||||
periodType,
|
||||
period,
|
||||
path: isGlobalTicket ? `/tickets/${ticket.id}` : `/analysis/${periodType}/${period}/tickets/${ticket.id}`,
|
||||
periodType: "all",
|
||||
period: ticketMonth,
|
||||
path: `/tickets/${ticket.id}?month=${ticketMonth}`,
|
||||
dirty: hasUnsavedChanges
|
||||
});
|
||||
}, [ticket, periodType, period, isGlobalTicket, hasUnsavedChanges, onOpenTicketChange]);
|
||||
}, [ticket, periodType, period, isGlobalTicket, globalMonthFilter, hasUnsavedChanges, onOpenTicketChange]);
|
||||
|
||||
async function setBilling(sessionId: string, billingStatus: BillingStatus) {
|
||||
if (isPeriodClosed && billingStatus !== null) {
|
||||
@@ -319,6 +332,13 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPeriodClosed) {
|
||||
toast.warning("Monat ist abgeschlossen", {
|
||||
description: "Öffne den Monat zuerst wieder, um Teamspace-Zeiten zu ändern."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedMinutes = parseHoursInput(dayBillingInputs[day] ?? "");
|
||||
|
||||
if (parsedMinutes === undefined) {
|
||||
@@ -542,7 +562,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h2 className="text-xl font-semibold tracking-normal sm:text-2xl">{ticket?.ticket_number ?? "Ticket"}</h2>
|
||||
{ticket?.ticket_number ? <CopyTicketButton ticketNumber={ticket.ticket_number} /> : null}
|
||||
{periodType === "month" && data?.closed ? <Badge variant="outline">Monat geschlossen</Badge> : null}
|
||||
{(periodType === "month" || (isGlobalTicket && globalMonthFilter !== "all")) && data?.closed ? <Badge variant="outline">Monat geschlossen</Badge> : null}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">Sessions {sessionsScopeLabel}</p>
|
||||
</div>
|
||||
@@ -574,7 +594,8 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
onChange={(event) => setGlobalMonthFilter(event.currentTarget.value)}
|
||||
>
|
||||
<option value="all">Alle Monate</option>
|
||||
{(data?.availableMonths ?? []).map((month) => (
|
||||
{globalMonthFilter !== "all" && !selectedMonthHasSessions ? <option value={globalMonthFilter}>{globalMonthFilter} (0)</option> : null}
|
||||
{availableMonths.map((month) => (
|
||||
<option key={month.month} value={month.month}>
|
||||
{month.month} ({month.session_count})
|
||||
</option>
|
||||
@@ -700,7 +721,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
autoComplete="off"
|
||||
inputMode="decimal"
|
||||
value={dayBillingInputs[group.key] ?? ""}
|
||||
disabled={savingDayBillingKey === group.key}
|
||||
disabled={isPeriodClosed || savingDayBillingKey === group.key}
|
||||
placeholder="0"
|
||||
onChange={(event) => {
|
||||
const nextValue = event.currentTarget.value;
|
||||
@@ -725,7 +746,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
type="button"
|
||||
size="sm"
|
||||
className={cn("h-7 w-[104px]", !isDayBillingDirty(group.key) && "invisible")}
|
||||
disabled={!isDayBillingDirty(group.key) || savingDayBillingKey === group.key}
|
||||
disabled={isPeriodClosed || !isDayBillingDirty(group.key) || savingDayBillingKey === group.key}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onClick={() => void saveDayBilling(group.key)}
|
||||
>
|
||||
|
||||
Reference in new issue
Block a user