proj/Frontend/components/tickets/ticket-table.tsx
2026-01-21 11:24:53 +01:00

215 lines
8.6 KiB
TypeScript

"use client"
import { useState, useMemo } from "react"
import type { Ticket, TicketStatus } from "@/lib/types"
import { StatusBadge } from "./status-badge"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { ArrowUpDown, ArrowDown, ArrowUp, Calendar, MapPin, FileText } from "lucide-react"
import { Input } from "@/components/ui/input"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { TicketComments } from "./ticket-comments"
interface TicketTableProps {
tickets: Ticket[]
showStatusUpdate?: boolean
onStatusUpdate?: (ticketId: number, status: TicketStatus) => void
}
type SortDirection = "asc" | "desc"
export function TicketTable({ tickets, showStatusUpdate = false, onStatusUpdate }: TicketTableProps) {
const [sortConfig, setSortConfig] = useState<{ key: keyof Ticket | "room" | "owner"; direction: SortDirection }>({ key: "createdAt", direction: "desc" })
const [search, setSearch] = useState("")
const [statusFilter, setStatusFilter] = useState<TicketStatus | "ALL">("ALL")
const [selectedTicket, setSelectedTicket] = useState<Ticket | null>(null)
// Advanced Filtering & Sorting
const processedTickets = useMemo(() => {
let filtered = [...tickets]
// Search
if (search.trim()) {
const q = search.toLowerCase()
filtered = filtered.filter(t =>
t.title.toLowerCase().includes(q) ||
t.description.toLowerCase().includes(q) ||
t.room.name.toLowerCase().includes(q)
)
}
// Filter
if (statusFilter !== "ALL") {
filtered = filtered.filter(t => t.status === statusFilter)
}
// Sort
return filtered.sort((a, b) => {
let aValue: any = a[sortConfig.key as keyof Ticket]
let bValue: any = b[sortConfig.key as keyof Ticket]
// Nested keys
if (sortConfig.key === "room") {
aValue = a.room.name
bValue = b.room.name
} else if (sortConfig.key === "owner") {
aValue = a.owner.name
bValue = b.owner.name
}
if (aValue < bValue) return sortConfig.direction === "asc" ? -1 : 1
if (aValue > bValue) return sortConfig.direction === "asc" ? 1 : -1
return 0
})
}, [tickets, search, statusFilter, sortConfig])
const handleSort = (key: keyof Ticket | "room" | "owner") => {
setSortConfig(current => ({
key,
direction: current.key === key && current.direction === "asc" ? "desc" : "asc"
}))
}
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString("de-DE", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit"
})
}
return (
<div className="space-y-4">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<Input
placeholder="Search tickets..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="max-w-sm"
/>
<div className="flex items-center gap-2">
<Select value={statusFilter} onValueChange={(v) => setStatusFilter(v as TicketStatus | "ALL")}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Filter Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="ALL">All Statuses</SelectItem>
<SelectItem value="OPEN">Open</SelectItem>
<SelectItem value="IN_PROGRESS">In Progress</SelectItem>
<SelectItem value="CLOSED">Closed</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="rounded-lg border border-border bg-card overflow-hidden">
<Table>
<TableHeader>
<TableRow className="bg-muted/50 hover:bg-muted/50">
<TableHead className="font-semibold cursor-pointer" onClick={() => handleSort("room")}>
<div className="flex items-center gap-2">
<MapPin className="h-4 w-4" />
Room {sortConfig.key === "room" && (sortConfig.direction === "asc" ? <ArrowUp className="h-3 w-3" /> : <ArrowDown className="h-3 w-3" />)}
</div>
</TableHead>
<TableHead className="font-semibold cursor-pointer" onClick={() => handleSort("title")}>
Title {sortConfig.key === "title" && (sortConfig.direction === "asc" ? <ArrowUp className="h-3 w-3" /> : <ArrowDown className="h-3 w-3" />)}
</TableHead>
<TableHead className="font-semibold hidden md:table-cell">Description</TableHead>
<TableHead className="font-semibold cursor-pointer" onClick={() => handleSort("createdAt")}>
<div className="flex items-center gap-1">
<Calendar className="h-4 w-4" />
Date {sortConfig.key === "createdAt" && (sortConfig.direction === "asc" ? <ArrowUp className="h-3 w-3" /> : <ArrowDown className="h-3 w-3" />)}
</div>
</TableHead>
<TableHead className="font-semibold cursor-pointer" onClick={() => handleSort("status")}>
Status {sortConfig.key === "status" && (sortConfig.direction === "asc" ? <ArrowUp className="h-3 w-3" /> : <ArrowDown className="h-3 w-3" />)}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{processedTickets.length === 0 ? (
<TableRow>
<TableCell colSpan={5} className="h-24 text-center">
No results found.
</TableCell>
</TableRow>
) : (
processedTickets.map((ticket) => (
<TableRow
key={ticket.id}
className="group cursor-pointer hover:bg-muted/50"
onClick={() => setSelectedTicket(ticket)}
>
<TableCell className="font-medium">{ticket.room.name}</TableCell>
<TableCell className="font-medium">{ticket.title}</TableCell>
<TableCell className="hidden md:table-cell max-w-xs">
<p className="truncate text-muted-foreground">{ticket.description}</p>
</TableCell>
<TableCell className="text-muted-foreground">{formatDate(ticket.createdAt)}</TableCell>
<TableCell onClick={(e) => e.stopPropagation()}>
{showStatusUpdate && onStatusUpdate ? (
<Select
value={ticket.status}
onValueChange={(value: TicketStatus) => onStatusUpdate(ticket.id, value)}
>
<SelectTrigger className="w-32 h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="OPEN"><StatusBadge status="OPEN" /></SelectItem>
<SelectItem value="IN_PROGRESS"><StatusBadge status="IN_PROGRESS" /></SelectItem>
<SelectItem value="CLOSED"><StatusBadge status="CLOSED" /></SelectItem>
</SelectContent>
</Select>
) : (
<StatusBadge status={ticket.status} />
)}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{selectedTicket && (
<Dialog open={!!selectedTicket} onOpenChange={(open) => !open && setSelectedTicket(null)}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>{selectedTicket.title}</DialogTitle>
<DialogDescription>
{selectedTicket.room.name} {formatDate(selectedTicket.createdAt)} {selectedTicket.owner.name}
</DialogDescription>
</DialogHeader>
<div className="grid gap-6">
<div>
<h3 className="font-semibold mb-2">Description</h3>
<div className="p-3 bg-muted rounded-md text-sm">
{selectedTicket.description}
</div>
</div>
<div>
<StatusBadge status={selectedTicket.status} />
</div>
<div className="border-t pt-4">
<TicketComments ticketId={selectedTicket.id} />
</div>
</div>
</DialogContent>
</Dialog>
)}
</div>
)
}