import { AlertCircle, ArrowDown, ArrowUp, ChevronLeft, ChevronRight, Clock3, Download, ExternalLink, RotateCcw, SlidersHorizontal, Ticket, UserCheck, Users } from "lucide-react"; import type { LucideIcon } from "lucide-react"; import { Fragment, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; import { Alert } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { CopyTicketButton } from "@/components/CopyTicketButton"; import { HelpLink } from "@/components/HelpLink"; import { MultiFilterDropdown, type MultiFilterOption } from "@/components/MultiFilterDropdown"; import { downloadMonthExport, getPeriodOverview } from "../api"; import { currentDay, currentMonth, formatDateTime, formatMinutes } from "../format"; import type { AuthUser, PeriodOverview, PeriodType, TicketSummary } from "../types"; type AnalysisPageProps = { currentUser: AuthUser; onNavigate: (to: string) => void; }; type TicketGroupBy = "none" | "workType" | "organization" | "status"; type TicketSortBy = "ticket" | "workType" | "organization" | "status"; type TicketSortDirection = "asc" | "desc"; type TicketFilterOption = MultiFilterOption; type AnalysisTicketViewSettings = { groupBy: TicketGroupBy; sortBy: TicketSortBy; sortDirection: TicketSortDirection; query: string; filterValues: string[]; organizationValues: string[]; }; type TicketSection = { key: string; label: string; tickets: TicketSummary[]; ticketCount: number; totalMinutes: number; openCount: number; }; const analysisTicketViewSettingsKey = "tickettracker.analysis.ticketViewSettings"; const defaultTicketViewSettings: AnalysisTicketViewSettings = { groupBy: "none", sortBy: "ticket", sortDirection: "asc", query: "", filterValues: [], organizationValues: [] }; const ticketStaticFilterOptions: TicketFilterOption[] = [ { value: "workType:support", label: "Support" }, { value: "workType:consulting", label: "Consulting" }, { value: "status:open", label: "Offen" }, { value: "status:evaluated", label: "Bewertet" } ]; const ticketStaticFilterValues = new Set(ticketStaticFilterOptions.map((option) => option.value)); const groupByOptions: Array<{ value: TicketGroupBy; label: string }> = [ { value: "none", label: "Keine" }, { value: "workType", label: "Typ" }, { value: "organization", label: "Organisation" }, { value: "status", label: "Status" } ]; const sortByOptions: Array<{ value: TicketSortBy; label: string }> = [ { value: "ticket", label: "Ticket" }, { value: "workType", label: "Typ" }, { value: "organization", label: "Organisation" }, { value: "status", label: "Status" } ]; function isKnownTicketFilterValue(value: unknown): value is string { if (typeof value !== "string") { return false; } return ticketStaticFilterValues.has(value); } function normalizedTicketFilterValues(values: unknown) { if (!Array.isArray(values)) { return defaultTicketViewSettings.filterValues; } return Array.from(new Set(values.filter(isKnownTicketFilterValue))); } function normalizedTicketOrganizationValues(values: unknown) { if (!Array.isArray(values)) { return defaultTicketViewSettings.organizationValues; } return Array.from(new Set(values.filter((value): value is string => typeof value === "string" && value.trim() !== ""))); } function legacyTicketFilterValues(parsed: Partial & Record) { const filters: string[] = []; if (parsed.workType === "support" || parsed.workType === "consulting") { filters.push(`workType:${parsed.workType}`); } if (parsed.status === "open" || parsed.status === "evaluated") { filters.push(`status:${parsed.status}`); } return filters; } function legacyTicketOrganizationValues(parsed: Partial & Record) { if (Array.isArray(parsed.organizationValues)) { return normalizedTicketOrganizationValues(parsed.organizationValues); } if (typeof parsed.organization === "string" && parsed.organization !== "all") { return [parsed.organization]; } return defaultTicketViewSettings.organizationValues; } function readStoredTicketViewSettings(): AnalysisTicketViewSettings { if (typeof window === "undefined") { return defaultTicketViewSettings; } try { const raw = window.localStorage.getItem(analysisTicketViewSettingsKey); if (!raw) { return defaultTicketViewSettings; } const parsed = JSON.parse(raw) as Partial; const filterValues = Array.isArray(parsed.filterValues) ? normalizedTicketFilterValues(parsed.filterValues) : legacyTicketFilterValues(parsed as Partial & Record); return { groupBy: groupByOptions.find((option) => option.value === parsed.groupBy)?.value ?? defaultTicketViewSettings.groupBy, sortBy: sortByOptions.find((option) => option.value === parsed.sortBy)?.value ?? defaultTicketViewSettings.sortBy, sortDirection: parsed.sortDirection === "desc" ? "desc" : defaultTicketViewSettings.sortDirection, query: typeof parsed.query === "string" ? parsed.query : defaultTicketViewSettings.query, filterValues, organizationValues: legacyTicketOrganizationValues(parsed as Partial & Record) }; } catch { return defaultTicketViewSettings; } } function TicketStatus({ ticket }: { ticket: TicketSummary }) { const ticketOpenCount = ticket.ticket_open_count ?? ticket.open_count; if (ticketOpenCount > 0) { return {ticketOpenCount} offen; } return bewertet; } function formatSignedMinutes(minutes: number) { if (minutes === 0) { return formatMinutes(0); } return `${minutes > 0 ? "+" : "-"}${formatMinutes(Math.abs(minutes))}`; } function teamspaceMonthDeltaMinutes(ticket: TicketSummary) { const actuals = ticket.teamspace_month_actuals ?? []; if (actuals.length === 0) { return 0; } const tsMinutes = ticket.teamspace_month_billable_minutes ?? actuals.reduce((sum, actual) => sum + actual.teamspace_billable_minutes, 0); return ticket.teamspace_month_delta_minutes ?? tsMinutes - ticket.crm_billed_minutes; } function TeamspaceDeviationIcon({ ticket }: { ticket: TicketSummary }) { const deltaMinutes = teamspaceMonthDeltaMinutes(ticket); if (deltaMinutes === 0) { return