This commit is contained in:
michael committed 2026-09-15 14:59:30 +00:00
1 parent 3ebcea0b3d
commit 312dd3b3ad
3 files changed
+65 -55

No files matched your search

+61 -52
View File
@@ -30,7 +30,7 @@ import {
UserCircle, UserCircle,
X, X,
} from "lucide-react"; } from "lucide-react";
import { FormEvent, MouseEvent, useCallback, useEffect, useMemo, useState } from "react"; import { FormEvent, MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useShallow } from "zustand/react/shallow"; import { useShallow } from "zustand/react/shallow";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -781,6 +781,7 @@ export function App() {
const [ticketReturnPaths, setTicketReturnPaths] = useState<Record<string, string>>({}); const [ticketReturnPaths, setTicketReturnPaths] = useState<Record<string, string>>({});
const [draggedOpenTicketKey, setDraggedOpenTicketKey] = useState<string | null>(null); const [draggedOpenTicketKey, setDraggedOpenTicketKey] = useState<string | null>(null);
const [sidebarTodos, setSidebarTodos] = useState<TicketTodo[]>([]); const [sidebarTodos, setSidebarTodos] = useState<TicketTodo[]>([]);
const openTicketsListRef = useRef<HTMLDivElement | null>(null);
const navigate = useCallback( const navigate = useCallback(
(to: string) => { (to: string) => {
@@ -875,6 +876,11 @@ export function App() {
); );
}, [currentUser, openTickets]); }, [currentUser, openTickets]);
useEffect(() => {
const activeTicket = openTicketsListRef.current?.querySelector<HTMLElement>("[data-open-ticket-active='true']");
activeTicket?.scrollIntoView({ block: "nearest" });
}, [path, openTickets]);
useEffect(() => { useEffect(() => {
if (!currentUser || currentUser.role === "admin" || !timersLoaded || timerOwnerId !== currentUser.id) { if (!currentUser || currentUser.role === "admin" || !timersLoaded || timerOwnerId !== currentUser.id) {
return; return;
@@ -1301,62 +1307,65 @@ export function App() {
)} )}
</SidebarMenu> </SidebarMenu>
</SidebarGroupContent> </SidebarGroupContent>
</SidebarGroup> </SidebarGroup>
{!isAdmin ? <SidebarTodayTodos todos={sidebarTodos} onNavigate={navigate} /> : null} {!isAdmin ? <SidebarTodayTodos todos={sidebarTodos} onNavigate={navigate} /> : null}
{!isAdmin && openTickets.length > 0 ? ( {!isAdmin && openTickets.length > 0 ? (
<SidebarGroup className="pt-0 group-data-[collapsible=icon]:hidden"> <SidebarGroup className="min-h-0 flex-1 pt-0 group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel className="h-6 px-2 text-xs">Geöffnete Tickets</SidebarGroupLabel> <SidebarGroupLabel className="h-6 px-2 text-xs">Geöffnete Tickets</SidebarGroupLabel>
<SidebarGroupContent> <SidebarGroupContent className="min-h-0 flex-1 overflow-hidden">
<SidebarMenu> <div ref={openTicketsListRef} className="max-h-[clamp(8rem,calc(100svh-31rem),24rem)] overflow-y-auto overscroll-contain pr-1 [scrollbar-gutter:stable] [scrollbar-width:thin]">
{openTickets.map((ticket) => { <SidebarMenu>
const isActive = pathWithoutSearch(path) === pathWithoutSearch(ticket.path); {openTickets.map((ticket) => {
const isActive = pathWithoutSearch(path) === pathWithoutSearch(ticket.path);
return ( return (
<SidebarMenuItem key={ticket.key}> <SidebarMenuItem key={ticket.key}>
<div <div
draggable data-open-ticket-active={isActive ? "true" : undefined}
onDragStart={(event) => startOpenTicketDrag(event, ticket)} draggable
onDragOver={(event) => overOpenTicket(event, ticket)} onDragStart={(event) => startOpenTicketDrag(event, ticket)}
onDrop={(event) => dropOpenTicket(event, ticket)} onDragOver={(event) => overOpenTicket(event, ticket)}
onDragEnd={() => setDraggedOpenTicketKey(null)} onDrop={(event) => dropOpenTicket(event, ticket)}
className={cn( onDragEnd={() => setDraggedOpenTicketKey(null)}
"mx-2 flex cursor-grab items-center gap-1 rounded-md px-2 py-1.5 active:cursor-grabbing", className={cn(
isActive ? "bg-primary text-primary-foreground" : "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", "mx-2 flex cursor-grab items-center gap-1 rounded-md px-2 py-1.5 active:cursor-grabbing",
draggedOpenTicketKey === ticket.key && "opacity-60", isActive ? "bg-primary text-primary-foreground" : "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
)} draggedOpenTicketKey === ticket.key && "opacity-60",
> )}
<GripVertical className="size-3.5 shrink-0 text-muted-foreground" />
<a
href={ticket.path}
className="flex min-w-0 flex-1 items-center gap-2 text-sm"
title={
ticket.periodType === "all"
? `${ticket.ticketNumber} (alle Sessions)`
: `${ticket.ticketNumber} (${ticket.periodType === "month" ? "Monat" : "Tag"} ${ticket.period})`
}
onClick={navHandler(ticket.path)}
> >
<span className={cn("size-2 shrink-0 rounded-full", ticket.dirty ? "bg-amber-400" : isActive ? "bg-primary-foreground" : "bg-muted-foreground/70")} /> <GripVertical className="size-3.5 shrink-0 text-muted-foreground" />
<FileText className="size-4 shrink-0" /> <a
<span className="truncate">{ticket.ticketNumber}</span> href={ticket.path}
</a> className="flex min-w-0 flex-1 items-center gap-2 text-sm"
<Button title={
type="button" ticket.periodType === "all"
size="icon" ? `${ticket.ticketNumber} (alle Sessions)`
variant="ghost" : `${ticket.ticketNumber} (${ticket.periodType === "month" ? "Monat" : "Tag"} ${ticket.period})`
className={cn("size-6 shrink-0", isActive ? "hover:bg-primary-foreground/15 hover:text-primary-foreground" : "")} }
onClick={() => closeOpenTicket(ticket)} onClick={navHandler(ticket.path)}
aria-label={`${ticket.ticketNumber} schließen`} >
> <span className={cn("size-2 shrink-0 rounded-full", ticket.dirty ? "bg-amber-400" : isActive ? "bg-primary-foreground" : "bg-muted-foreground/70")} />
<X className="size-3.5" /> <FileText className="size-4 shrink-0" />
</Button> <span className="truncate">{ticket.ticketNumber}</span>
</div> </a>
</SidebarMenuItem> <Button
); type="button"
})} size="icon"
</SidebarMenu> variant="ghost"
className={cn("size-6 shrink-0", isActive ? "hover:bg-primary-foreground/15 hover:text-primary-foreground" : "")}
onClick={() => closeOpenTicket(ticket)}
aria-label={`${ticket.ticketNumber} schließen`}
>
<X className="size-3.5" />
</Button>
</div>
</SidebarMenuItem>
);
})}
</SidebarMenu>
</div>
</SidebarGroupContent> </SidebarGroupContent>
</SidebarGroup> </SidebarGroup>
) : null} ) : null}
+1
View File
@@ -53,6 +53,7 @@
--font-heading: var(--font-sans); --font-heading: var(--font-sans);
--font-sans: var(--font-sans); --font-sans: var(--font-sans);
--breakpoint-xxl: 108.125rem; --breakpoint-xxl: 108.125rem;
--breakpoint-details: 85rem;
} }
+3 -3
View File
@@ -1242,7 +1242,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
</Alert> </Alert>
) : null} ) : null}
<div className={cn("grid gap-4", isGlobalTicket && "xxl:grid-cols-2")}> <div className={cn("grid gap-4", isGlobalTicket && "details:grid-cols-2")}>
{isGlobalTicket ? ( {isGlobalTicket ? (
<Card> <Card>
<CardHeader className="p-4"> <CardHeader className="p-4">
@@ -1300,7 +1300,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="px-4 pb-4"> <CardContent className="px-4 pb-4">
<form className="grid gap-3 md:grid-cols-2" onSubmit={saveTicketData}> <form className="grid grid-cols-2 gap-3 [@media_(min-width:85rem)_and_(max-width:108.124rem)]:grid-cols-1 xxl:grid-cols-2" onSubmit={saveTicketData}>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium" htmlFor="edit-ticket-number">Ticketnummer</label> <label className="text-sm font-medium" htmlFor="edit-ticket-number">Ticketnummer</label>
<Input id="edit-ticket-number" value={editTicketNumber} disabled={!canEditTicketData} onChange={(event) => setEditTicketNumber(event.currentTarget.value)} required /> <Input id="edit-ticket-number" value={editTicketNumber} disabled={!canEditTicketData} onChange={(event) => setEditTicketNumber(event.currentTarget.value)} required />
@@ -1338,7 +1338,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
required required
/> />
</div> </div>
<Button type="submit" className="md:col-span-2 md:justify-self-end" disabled={!canEditTicketData || savingTicket}> <Button type="submit" className="col-span-2 justify-self-end [@media_(min-width:85rem)_and_(max-width:108.124rem)]:col-span-1 xxl:col-span-2" disabled={!canEditTicketData || savingTicket}>
<Save className="size-4" /> <Save className="size-4" />
{savingTicket ? "Speichert..." : "Speichern"} {savingTicket ? "Speichert..." : "Speichern"}
</Button> </Button>