Add FAQ guide and contextual help links

This commit is contained in:
2026-08-05 15:46:06 +02:00
parent 33f0170927
commit 3b6f35e31a
11 changed files with 388 additions and 48 deletions
+10
View File
@@ -4,6 +4,7 @@ import {
ClipboardCheck, ClipboardCheck,
Command, Command,
ChartNoAxesCombined, ChartNoAxesCombined,
CircleHelp,
LayoutGrid, LayoutGrid,
LogOut, LogOut,
Moon, Moon,
@@ -33,6 +34,7 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { HelpLink } from "@/components/HelpLink";
import { import {
Sidebar, Sidebar,
SidebarContent, SidebarContent,
@@ -56,6 +58,7 @@ import { formatTimer } from "./format";
import { activeElapsedMs, pauseEntry, readStoredTimers, resumeEntry, storageKeyForUser, ticketPattern, type TimerEntry } from "./timers"; import { activeElapsedMs, pauseEntry, readStoredTimers, resumeEntry, storageKeyForUser, ticketPattern, type TimerEntry } from "./timers";
import { AdminUsersPage } from "./views/AdminUsersPage"; import { AdminUsersPage } from "./views/AdminUsersPage";
import { AnalysisPage } from "./views/AnalysisPage"; import { AnalysisPage } from "./views/AnalysisPage";
import { FaqPage } from "./views/FaqPage";
import { LoginPage } from "./views/LoginPage"; import { LoginPage } from "./views/LoginPage";
import { MonthlyClosePage } from "./views/MonthlyClosePage"; import { MonthlyClosePage } from "./views/MonthlyClosePage";
import { ProfilePage } from "./views/ProfilePage"; import { ProfilePage } from "./views/ProfilePage";
@@ -97,6 +100,10 @@ function routeFromPath(pathname: string) {
return { page: "statistics" as const }; return { page: "statistics" as const };
} }
if (pathname.startsWith("/faq")) {
return { page: "faq" as const };
}
if (pathname.startsWith("/monthly-close")) { if (pathname.startsWith("/monthly-close")) {
return { page: "monthly-close" as const }; return { page: "monthly-close" as const };
} }
@@ -202,6 +209,7 @@ function QuickTimerStarter({ className, inputId, onStartTimer }: QuickTimerStart
<span className="hidden sm:inline">Timer starten</span> <span className="hidden sm:inline">Timer starten</span>
<span className="sm:hidden">Start</span> <span className="sm:hidden">Start</span>
</Button> </Button>
<HelpLink anchor="timer" label="Hilfe zum schnellen Timerstart" className="hidden md:inline-flex" />
</form> </form>
); );
} }
@@ -516,6 +524,7 @@ export function App() {
{ href: "/monthly-close", label: "Monatsabschluss", icon: ClipboardCheck, active: path.startsWith("/monthly-close") }, { href: "/monthly-close", label: "Monatsabschluss", icon: ClipboardCheck, active: path.startsWith("/monthly-close") },
{ href: "/statistics", label: "Statistiken", icon: ChartNoAxesCombined, active: path.startsWith("/statistics") }, { href: "/statistics", label: "Statistiken", icon: ChartNoAxesCombined, active: path.startsWith("/statistics") },
{ href: "/recurring", label: "Fixe Abrechnung", icon: Repeat, active: path.startsWith("/recurring") }, { href: "/recurring", label: "Fixe Abrechnung", icon: Repeat, active: path.startsWith("/recurring") },
{ href: "/faq", label: "FAQ", icon: CircleHelp, active: path.startsWith("/faq") },
{ href: "/profile", label: "Profil", icon: UserCog, active: path.startsWith("/profile") }, { href: "/profile", label: "Profil", icon: UserCog, active: path.startsWith("/profile") },
...(currentUser.role === "admin" ...(currentUser.role === "admin"
? [{ href: "/admin/users", label: "Benutzer", icon: Shield, active: path.startsWith("/admin/users") }] ? [{ href: "/admin/users", label: "Benutzer", icon: Shield, active: path.startsWith("/admin/users") }]
@@ -702,6 +711,7 @@ export function App() {
{route.page === "monthly-close" ? <MonthlyClosePage onNavigate={navigate} /> : null} {route.page === "monthly-close" ? <MonthlyClosePage onNavigate={navigate} /> : null}
{route.page === "statistics" ? <StatisticsPage onNavigate={navigate} /> : null} {route.page === "statistics" ? <StatisticsPage onNavigate={navigate} /> : null}
{route.page === "recurring" ? <RecurringBillingsPage /> : null} {route.page === "recurring" ? <RecurringBillingsPage /> : null}
{route.page === "faq" ? <FaqPage /> : null}
{route.page === "profile" ? <ProfilePage currentUser={currentUser} onUserUpdated={setCurrentUser} /> : null} {route.page === "profile" ? <ProfilePage currentUser={currentUser} onUserUpdated={setCurrentUser} /> : null}
{route.page === "admin-users" && currentUser.role === "admin" ? <AdminUsersPage currentUser={currentUser} /> : null} {route.page === "admin-users" && currentUser.role === "admin" ? <AdminUsersPage currentUser={currentUser} /> : null}
{route.page === "admin-users" && currentUser.role !== "admin" ? ( {route.page === "admin-users" && currentUser.role !== "admin" ? (
+19
View File
@@ -0,0 +1,19 @@
import { CircleHelp } from "lucide-react";
import { Button } from "@/components/ui/button";
type HelpLinkProps = {
anchor: string;
label?: string;
className?: string;
};
export function HelpLink({ anchor, label = "Hilfe öffnen", className }: HelpLinkProps) {
return (
<Button asChild variant="ghost" size="icon-sm" className={className} title={label}>
<a href={`/faq#${anchor}`} aria-label={label}>
<CircleHelp className="size-4" />
</a>
</Button>
);
}
+16
View File
@@ -8,6 +8,7 @@ import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { HelpLink } from "@/components/HelpLink";
import { OrganizationSelect } from "@/components/OrganizationSelect"; import { OrganizationSelect } from "@/components/OrganizationSelect";
import { import {
createAdminSession, createAdminSession,
@@ -335,10 +336,13 @@ export function AdminUsersPage({ currentUser }: { currentUser: AuthUser }) {
<Card> <Card>
<CardHeader className="p-4"> <CardHeader className="p-4">
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<DatabaseZap className="size-5 text-muted-foreground" /> <DatabaseZap className="size-5 text-muted-foreground" />
<CardTitle>Data-Sync</CardTitle> <CardTitle>Data-Sync</CardTitle>
</div> </div>
<HelpLink anchor="organisationen" label="Hilfe zum Zammad-Sync" />
</div>
<CardDescription>Zammad-Zugang speichern und Organisationen in die lokale TicketTracker-Datenbank übernehmen.</CardDescription> <CardDescription>Zammad-Zugang speichern und Organisationen in die lokale TicketTracker-Datenbank übernehmen.</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="px-4 pb-4"> <CardContent className="px-4 pb-4">
@@ -378,8 +382,13 @@ export function AdminUsersPage({ currentUser }: { currentUser: AuthUser }) {
<Card> <Card>
<CardHeader className="p-4"> <CardHeader className="p-4">
<div className="flex items-start justify-between gap-3">
<div>
<CardTitle>Neuer Benutzer</CardTitle> <CardTitle>Neuer Benutzer</CardTitle>
<CardDescription>Neue Benutzer sehen später nur ihre eigenen Sessions und Abschlüsse.</CardDescription> <CardDescription>Neue Benutzer sehen später nur ihre eigenen Sessions und Abschlüsse.</CardDescription>
</div>
<HelpLink anchor="admin" label="Hilfe zur Benutzerverwaltung" />
</div>
</CardHeader> </CardHeader>
<CardContent className="px-4 pb-4"> <CardContent className="px-4 pb-4">
<form className="grid gap-3 lg:grid-cols-[160px_minmax(180px,1fr)_160px_130px_auto_auto] lg:items-end" onSubmit={createUser}> <form className="grid gap-3 lg:grid-cols-[160px_minmax(180px,1fr)_160px_130px_auto_auto] lg:items-end" onSubmit={createUser}>
@@ -421,10 +430,13 @@ export function AdminUsersPage({ currentUser }: { currentUser: AuthUser }) {
<Card> <Card>
<CardHeader className="p-4"> <CardHeader className="p-4">
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CalendarPlus className="size-5 text-muted-foreground" /> <CalendarPlus className="size-5 text-muted-foreground" />
<CardTitle>Session nachtragen</CardTitle> <CardTitle>Session nachtragen</CardTitle>
</div> </div>
<HelpLink anchor="session-nachtragen" label="Hilfe zum Nachtragen für Benutzer" />
</div>
<CardDescription>Vergessene Zeiten manuell erfassen und direkt einem Benutzer zuweisen.</CardDescription> <CardDescription>Vergessene Zeiten manuell erfassen und direkt einem Benutzer zuweisen.</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="px-4 pb-4"> <CardContent className="px-4 pb-4">
@@ -541,7 +553,10 @@ export function AdminUsersPage({ currentUser }: { currentUser: AuthUser }) {
<CardTitle>Vorhandene Benutzer</CardTitle> <CardTitle>Vorhandene Benutzer</CardTitle>
<CardDescription>{loading ? "Lädt..." : `${users.length} Account(s)`}</CardDescription> <CardDescription>{loading ? "Lädt..." : `${users.length} Account(s)`}</CardDescription>
</div> </div>
<div className="flex items-center gap-2">
<Badge variant="outline">{currentUser.display_name}</Badge> <Badge variant="outline">{currentUser.display_name}</Badge>
<HelpLink anchor="admin" label="Hilfe zu vorhandenen Benutzern" />
</div>
</CardHeader> </CardHeader>
<CardContent className="px-4 pb-4"> <CardContent className="px-4 pb-4">
<div className="hidden xl:block"> <div className="hidden xl:block">
@@ -658,6 +673,7 @@ export function AdminUsersPage({ currentUser }: { currentUser: AuthUser }) {
<CardTitle>Session-Zuordnung</CardTitle> <CardTitle>Session-Zuordnung</CardTitle>
<CardDescription>{loading ? "Lädt..." : `${sessions.length} letzte Session(s), inklusive Besitzer.`}</CardDescription> <CardDescription>{loading ? "Lädt..." : `${sessions.length} letzte Session(s), inklusive Besitzer.`}</CardDescription>
</div> </div>
<HelpLink anchor="admin" label="Hilfe zur Session-Zuordnung" />
</CardHeader> </CardHeader>
<CardContent className="px-4 pb-4"> <CardContent className="px-4 pb-4">
<div className="hidden xl:block"> <div className="hidden xl:block">
+6
View File
@@ -9,6 +9,7 @@ import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { CopyTicketButton } from "@/components/CopyTicketButton"; import { CopyTicketButton } from "@/components/CopyTicketButton";
import { HelpLink } from "@/components/HelpLink";
import { getPeriodOverview } from "../api"; import { getPeriodOverview } from "../api";
import { currentDay, currentMonth, formatMinutes } from "../format"; import { currentDay, currentMonth, formatMinutes } from "../format";
import type { PeriodOverview, PeriodType, TicketSummary } from "../types"; import type { PeriodOverview, PeriodType, TicketSummary } from "../types";
@@ -519,7 +520,10 @@ export function AnalysisPage({ onNavigate }: AnalysisPageProps) {
{periodType === "month" ? "Aufwand pro Tag im ausgewählten Monat." : "Aufwand pro Stunde am ausgewählten Tag."} {periodType === "month" ? "Aufwand pro Tag im ausgewählten Monat." : "Aufwand pro Stunde am ausgewählten Tag."}
</CardDescription> </CardDescription>
</div> </div>
<div className="flex items-center gap-2">
<Badge variant="outline">{formatMinutes(totals?.minutes ?? 0)}</Badge> <Badge variant="outline">{formatMinutes(totals?.minutes ?? 0)}</Badge>
<HelpLink anchor="auswertung" label="Hilfe zur Auswertung" />
</div>
</CardHeader> </CardHeader>
<CardContent className="px-4 pb-4"> <CardContent className="px-4 pb-4">
<div className="h-44 w-full overflow-hidden rounded-md border bg-background"> <div className="h-44 w-full overflow-hidden rounded-md border bg-background">
@@ -605,6 +609,7 @@ export function AnalysisPage({ onNavigate }: AnalysisPageProps) {
{periodType === "month" ? "Sessions prüfen, bewerten und bei Bedarf Tickets öffnen." : "Tagesansicht ohne eigenen Abschluss."} {periodType === "month" ? "Sessions prüfen, bewerten und bei Bedarf Tickets öffnen." : "Tagesansicht ohne eigenen Abschluss."}
</CardDescription> </CardDescription>
</div> </div>
<HelpLink anchor="auswertung" label="Hilfe zu Tickets im Zeitraum" />
</CardHeader> </CardHeader>
<CardContent className="px-4 pb-4"> <CardContent className="px-4 pb-4">
<div className="mb-4 rounded-md border bg-muted/20 p-3"> <div className="mb-4 rounded-md border bg-muted/20 p-3">
@@ -618,6 +623,7 @@ export function AnalysisPage({ onNavigate }: AnalysisPageProps) {
</p> </p>
</div> </div>
{activeFilterCount > 0 ? <Badge variant="outline">{activeFilterCount} aktiv</Badge> : null} {activeFilterCount > 0 ? <Badge variant="outline">{activeFilterCount} aktiv</Badge> : null}
<HelpLink anchor="auswertung" label="Hilfe zu Filtern und Sortierung" />
</div> </div>
<Button <Button
type="button" type="button"
+222
View File
@@ -0,0 +1,222 @@
import { ArrowUp, BookOpenCheck } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
type FaqSection = {
id: string;
title: string;
description: string;
items: string[];
};
const faqSections: FaqSection[] = [
{
id: "grundprinzip",
title: "Grundprinzip",
description: "TicketTracker ersetzt weder Ticketsystem noch CRM. Die App misst Zeiten, bewertet Sessions und bereitet den Monatsabschluss vor.",
items: [
"Session-Zeit ist die gemessene oder manuell erfasste Arbeitszeit einer Tätigkeit.",
"Teamspace-Zeit ist der Tageswert, der für ein Ticket wirklich im CRM eingetragen wurde.",
"Abgeschlossen wird nur ein Monat. Tickets werden nicht geschlossen, sondern nur über ihre Sessions bewertet."
]
},
{
id: "timer",
title: "Timer",
description: "Timer sind für laufende Arbeit an Tickets gedacht.",
items: [
"Ticketnummer im Format Ticket#XXXXXX eingeben und Timer starten.",
"Es können mehrere Timer vorbereitet sein, aber immer nur einer läuft aktiv.",
"Wird ein Timer aktiviert, werden alle anderen automatisch pausiert.",
"Beim Klick auf Beenden stoppt die Zeit sofort. Danach werden Tätigkeit und gegebenenfalls Organisation und Art gespeichert."
]
},
{
id: "session-nachtragen",
title: "Session Nachtragen",
description: "Vergessene Zeiten können nachträglich als Session angelegt werden.",
items: [
"Benutzer tragen eigene Sessions nach. Admins können Sessions auch anderen Benutzern zuweisen.",
"Pflichtangaben sind Ticket, Organisation, Art, Datum, Von, Bis und Tätigkeit.",
"Das Tätigkeitsfeld ist mehrzeilig. Zeilenumbrüche bleiben in den Auswertungen erhalten."
]
},
{
id: "ticketdaten",
title: "Ticketdaten",
description: "Ticketdaten können in der Ticketdetailansicht korrigiert werden.",
items: [
"Bearbeitbar sind Ticketnummer, Organisation und Art.",
"Änderungen an Organisation und Art werden auf vorhandene manuelle Sessions des Tickets übernommen.",
"Bei wiederkehrenden Sessions kommt die Art aus der Regel der fixen Abrechnung."
]
},
{
id: "organisationen",
title: "Organisationen",
description: "Organisationen werden aus Zammad synchronisiert und danach aus der lokalen Liste gewählt.",
items: [
"Freitext ist nicht mehr erlaubt, damit Auswertungen je Organisation sauber möglich sind.",
"Alte Freitexte müssen manuell korrigiert werden.",
"Der Zammad-Sync speichert nur interne ID, Zammad-ID, Name und Zeitstempel."
]
},
{
id: "auswertung",
title: "Auswertung",
description: "Die Auswertung zeigt Monats- oder Tageswerte und listet Tickets im gewählten Zeitraum.",
items: [
"Mit den Pfeilen neben Monat oder Datum wechselst du den Zeitraum. Die Daten aktualisieren sich automatisch.",
"Tickets können gefiltert, gruppiert und sortiert werden. Diese Einstellungen bleiben gespeichert.",
"Der Graph zeigt den Gesamtaufwand des gewählten Zeitraums."
]
},
{
id: "ticketdetail",
title: "Ticketdetailansicht",
description: "Hier werden alle Sessions eines Tickets im gewählten Monat oder Tag bearbeitet.",
items: [
"Sessions sind nach Tagen gruppiert.",
"Pro Tag wird die getrackte Zeit angezeigt.",
"Das Teamspace-Feld speichert die Stunden, die für diesen Ticket-Tag wirklich im CRM eingetragen wurden.",
"Der Teamspace-Wert ist in Monats- und Tagesansicht derselbe."
]
},
{
id: "sessions-bewerten",
title: "Sessions Bewerten",
description: "Jede Session braucht eine Bewertung für die Abrechnung.",
items: [
"Möglich sind Abgerechnet und Nicht abrechenbar.",
"Ein erneuter Klick auf die gewählte Bewertung nimmt sie zurück.",
"Bewertungen können bei abgeschlossenem Monat erst geändert werden, nachdem der Monat wieder geöffnet wurde."
]
},
{
id: "sessions-bearbeiten",
title: "Sessions Bearbeiten und Löschen",
description: "Sessions können korrigiert oder gelöscht werden.",
items: [
"Bearbeitbar sind Datum, Von, Bis, Organisation, Tätigkeit und Art.",
"Bei wiederkehrenden Sessions ist die Art gesperrt und wird über die Regel geändert.",
"Wird die letzte Session eines Tickets gelöscht, wird das leere Ticket entfernt.",
"Gelöschte wiederkehrende Sessions werden als Ausnahme gespeichert und nicht automatisch neu erzeugt."
]
},
{
id: "monatsabschluss",
title: "Monatsabschluss",
description: "Der Monatsabschluss bündelt offene Bewertungen und Teamspace-Prüfpunkte.",
items: [
"Ein Monat kann abgeschlossen werden, sobald alle Sessions des Monats bewertet sind.",
"Tage ohne Teamspace-Wert können zur Kenntnis genommen werden, wenn sie bewusst nicht ins CRM gehören.",
"Zur Kenntnis genommene Teamspace-Punkte können eingeblendet und zurückgenommen werden.",
"Einen Tagesabschluss gibt es nicht."
]
},
{
id: "fixe-abrechnungen",
title: "Fixe Abrechnungen",
description: "Fixe Abrechnungen erzeugen wiederkehrende Sessions für den eigenen Account.",
items: [
"Ticket ist optional. Ohne Ticket wird automatisch Fix#ID verwendet.",
"Muster können Wochentage oder alle X Wochen sein.",
"Gültig bis ist optional. Wird es später gesetzt, werden nicht mehr gültige erzeugte Sessions entfernt.",
"Bayerische Feiertage werden übersprungen."
]
},
{
id: "statistiken",
title: "Statistiken",
description: "Die Statistikseite vergleicht getrackte Sessions mit Teamspace-Zeiten.",
items: [
"Sessions und Teamspace haben eigene Farben.",
"Eine positive oder ausgeglichene Differenz ist grün.",
"Eine negative Differenz ist rot und bedeutet: in Teamspace ist weniger eingetragen als getrackt.",
"Organisationen, Typen und Ticket-Hotspots helfen bei der Monatskontrolle."
]
},
{
id: "profil",
title: "Profil",
description: "Jeder Benutzer kann eigene Accountdaten pflegen.",
items: [
"Benutzername und voller Name können geändert werden.",
"Passwortänderungen benötigen das aktuelle Passwort.",
"Der volle Name wird in der App angezeigt."
]
},
{
id: "admin",
title: "Adminbereich",
description: "Admins verwalten Benutzer, Sessions und Zammad-Organisationen.",
items: [
"Admins legen Benutzer an und können deren Daten bearbeiten.",
"Sessions können zwischen Benutzern umverteilt werden.",
"Admins können Sessions für andere Benutzer nachtragen.",
"Der Zammad-Zugang wird gespeichert und der Organisationssync aktualisiert die lokale Organisationsliste."
]
}
];
export function FaqPage() {
return (
<div id="top" className="space-y-5">
<div className="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
<div>
<h2 className="text-2xl font-semibold tracking-normal">FAQ und Anleitung</h2>
<p className="text-sm text-muted-foreground">Kurzanleitung für Timer, Auswertung, Teamspace, fixe Abrechnungen und Adminfunktionen.</p>
</div>
<Badge variant="outline">{faqSections.length} Abschnitt(e)</Badge>
</div>
<Card>
<CardHeader className="p-4">
<div className="flex items-center gap-2">
<BookOpenCheck className="size-5 text-muted-foreground" />
<CardTitle>Schnellnavigation</CardTitle>
</div>
<CardDescription>Die Fragezeichen in der App springen direkt in diese Abschnitte.</CardDescription>
</CardHeader>
<CardContent className="flex flex-wrap gap-2 px-4 pb-4">
{faqSections.map((section) => (
<Button key={section.id} asChild variant="secondary" size="sm">
<a href={`#${section.id}`}>{section.title}</a>
</Button>
))}
</CardContent>
</Card>
<div className="grid gap-3 xl:grid-cols-2">
{faqSections.map((section) => (
<Card key={section.id} id={section.id} className="scroll-mt-16">
<CardHeader className="p-4">
<CardTitle>{section.title}</CardTitle>
<CardDescription>{section.description}</CardDescription>
</CardHeader>
<CardContent className="px-4 pb-4">
<ul className="space-y-2 text-sm text-muted-foreground">
{section.items.map((item) => (
<li key={item} className="flex gap-2">
<span className="mt-2 size-1.5 shrink-0 rounded-full bg-primary" />
<span>{item}</span>
</li>
))}
</ul>
<div className="mt-4 flex justify-end">
<Button asChild variant="ghost" size="sm">
<a href="#top">
<ArrowUp className="size-4" />
Nach oben
</a>
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</div>
);
}
+11
View File
@@ -19,6 +19,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { CopyTicketButton } from "@/components/CopyTicketButton"; import { CopyTicketButton } from "@/components/CopyTicketButton";
import { HelpLink } from "@/components/HelpLink";
import { acknowledgeMissingTicketDayBilling, closePeriod, getStatisticsOverview, reopenPeriod } from "../api"; import { acknowledgeMissingTicketDayBilling, closePeriod, getStatisticsOverview, reopenPeriod } from "../api";
import { currentMonth, formatDate, formatDateTime, formatMinutes } from "../format"; import { currentMonth, formatDate, formatDateTime, formatMinutes } from "../format";
import type { StatisticsCrmDay, StatisticsOpenSession, StatisticsOverview } from "../types"; import type { StatisticsCrmDay, StatisticsOpenSession, StatisticsOverview } from "../types";
@@ -339,8 +340,13 @@ export function MonthlyClosePage({ onNavigate }: MonthlyClosePageProps) {
<Card> <Card>
<CardHeader className="p-4"> <CardHeader className="p-4">
<div className="flex items-start justify-between gap-3">
<div>
<CardTitle>Checkliste</CardTitle> <CardTitle>Checkliste</CardTitle>
<CardDescription>Bewertungen sind Pflicht. Teamspace-Punkte helfen beim sauberen CRM-Abgleich.</CardDescription> <CardDescription>Bewertungen sind Pflicht. Teamspace-Punkte helfen beim sauberen CRM-Abgleich.</CardDescription>
</div>
<HelpLink anchor="monatsabschluss" label="Hilfe zum Monatsabschluss" />
</div>
</CardHeader> </CardHeader>
<CardContent className="grid gap-2 px-4 pb-4 md:grid-cols-2"> <CardContent className="grid gap-2 px-4 pb-4 md:grid-cols-2">
{checklist.map((item) => ( {checklist.map((item) => (
@@ -357,8 +363,13 @@ export function MonthlyClosePage({ onNavigate }: MonthlyClosePageProps) {
<Card> <Card>
<CardHeader className="p-4"> <CardHeader className="p-4">
<div className="flex items-start justify-between gap-3">
<div>
<CardTitle>Aufräumen</CardTitle> <CardTitle>Aufräumen</CardTitle>
<CardDescription>Alles, was für diesen Monat noch Aufmerksamkeit braucht.</CardDescription> <CardDescription>Alles, was für diesen Monat noch Aufmerksamkeit braucht.</CardDescription>
</div>
<HelpLink anchor="monatsabschluss" label="Hilfe zu offenen Bewertungen und Teamspace-Prüfpunkten" />
</div>
</CardHeader> </CardHeader>
<CardContent className="grid gap-4 px-4 pb-4 xl:grid-cols-2"> <CardContent className="grid gap-4 px-4 pb-4 xl:grid-cols-2">
<section className="space-y-2"> <section className="space-y-2">
+4
View File
@@ -4,6 +4,7 @@ import { toast } from "sonner";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { HelpLink } from "@/components/HelpLink";
import { updateCurrentUser } from "../api"; import { updateCurrentUser } from "../api";
import type { AuthUser } from "../types"; import type { AuthUser } from "../types";
@@ -74,10 +75,13 @@ export function ProfilePage({ currentUser, onUserUpdated }: ProfilePageProps) {
<Card> <Card>
<CardHeader className="p-4"> <CardHeader className="p-4">
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<UserCircle className="size-5 text-muted-foreground" /> <UserCircle className="size-5 text-muted-foreground" />
<CardTitle>Accountdaten</CardTitle> <CardTitle>Accountdaten</CardTitle>
</div> </div>
<HelpLink anchor="profil" label="Hilfe zum Profil" />
</div>
<CardDescription>Der Benutzername wird beim Login verwendet. Dein voller Name wird in der App angezeigt.</CardDescription> <CardDescription>Der Benutzername wird beim Login verwendet. Dein voller Name wird in der App angezeigt.</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="px-4 pb-4"> <CardContent className="px-4 pb-4">
@@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { HelpLink } from "@/components/HelpLink";
import { OrganizationSelect } from "@/components/OrganizationSelect"; import { OrganizationSelect } from "@/components/OrganizationSelect";
import { createRecurringBilling, deleteRecurringBilling, getRecurringBillings, updateRecurringBilling } from "../api"; import { createRecurringBilling, deleteRecurringBilling, getRecurringBillings, updateRecurringBilling } from "../api";
import { currentDay, formatMinutes } from "../format"; import { currentDay, formatMinutes } from "../format";
@@ -248,10 +249,13 @@ export function RecurringBillingsPage() {
<Card className="overflow-hidden"> <Card className="overflow-hidden">
<CardHeader className="border-b bg-muted/30 p-4"> <CardHeader className="border-b bg-muted/30 p-4">
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Repeat className="size-5 text-muted-foreground" /> <Repeat className="size-5 text-muted-foreground" />
<CardTitle>{editingBillingId ? "Regel bearbeiten" : "Neue Regel"}</CardTitle> <CardTitle>{editingBillingId ? "Regel bearbeiten" : "Neue Regel"}</CardTitle>
</div> </div>
<HelpLink anchor="fixe-abrechnungen" label="Hilfe zu fixen Abrechnungen" />
</div>
<CardDescription>Ticket ist optional. Leer bedeutet automatische Gruppierung als Fix#ID.</CardDescription> <CardDescription>Ticket ist optional. Leer bedeutet automatische Gruppierung als Fix#ID.</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="p-4"> <CardContent className="p-4">
@@ -447,8 +451,13 @@ export function RecurringBillingsPage() {
<Card className="overflow-hidden"> <Card className="overflow-hidden">
<CardHeader className="border-b bg-muted/30 p-4"> <CardHeader className="border-b bg-muted/30 p-4">
<div className="flex items-start justify-between gap-3">
<div>
<CardTitle>Regeln</CardTitle> <CardTitle>Regeln</CardTitle>
<CardDescription>Erzeugte Einträge können in der Auswertung verschoben oder gelöscht werden.</CardDescription> <CardDescription>Erzeugte Einträge können in der Auswertung verschoben oder gelöscht werden.</CardDescription>
</div>
<HelpLink anchor="fixe-abrechnungen" label="Hilfe zu erzeugten fixen Sessions" />
</div>
</CardHeader> </CardHeader>
<CardContent className="space-y-2 p-4"> <CardContent className="space-y-2 p-4">
{recurringBillings.map((billing) => ( {recurringBillings.map((billing) => (
+15
View File
@@ -21,6 +21,7 @@ import { Input } from "@/components/ui/input";
import { Pagination, PaginationContent, PaginationItem, PaginationNext, PaginationPrevious } from "@/components/ui/pagination"; import { Pagination, PaginationContent, PaginationItem, PaginationNext, PaginationPrevious } from "@/components/ui/pagination";
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { HelpLink } from "@/components/HelpLink";
import { getStatisticsOverview } from "../api"; import { getStatisticsOverview } from "../api";
import { currentMonth, formatDate, formatMinutes } from "../format"; import { currentMonth, formatDate, formatMinutes } from "../format";
import type { StatisticsDailyBucket, StatisticsGroup, StatisticsOverview, StatisticsTicket, WorkType } from "../types"; import type { StatisticsDailyBucket, StatisticsGroup, StatisticsOverview, StatisticsTicket, WorkType } from "../types";
@@ -535,7 +536,10 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
<CardTitle>Monatsverlauf und Abrechnungsqualität</CardTitle> <CardTitle>Monatsverlauf und Abrechnungsqualität</CardTitle>
<CardDescription>Getrackter Aufwand pro Tag, Teamspace-Linie und Bewertungsstand in einem Überblick.</CardDescription> <CardDescription>Getrackter Aufwand pro Tag, Teamspace-Linie und Bewertungsstand in einem Überblick.</CardDescription>
</div> </div>
<div className="flex items-center gap-2">
<Badge variant="outline">{crmCoverage}% Teamspace-Abdeckung</Badge> <Badge variant="outline">{crmCoverage}% Teamspace-Abdeckung</Badge>
<HelpLink anchor="statistiken" label="Hilfe zu Statistiken" />
</div>
</CardHeader> </CardHeader>
<CardContent className="grid gap-4 px-4 pb-4 xl:grid-cols-[1.55fr_1fr]"> <CardContent className="grid gap-4 px-4 pb-4 xl:grid-cols-[1.55fr_1fr]">
<div className="space-y-3"> <div className="space-y-3">
@@ -613,9 +617,12 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
<CardTitle>Statistik pro Organisation</CardTitle> <CardTitle>Statistik pro Organisation</CardTitle>
<CardDescription>Kundenaufwand inklusive Teamspace-Differenz und Anteil am getrackten Monatsaufwand.</CardDescription> <CardDescription>Kundenaufwand inklusive Teamspace-Differenz und Anteil am getrackten Monatsaufwand.</CardDescription>
</div> </div>
<div className="flex items-center gap-2">
<Badge variant="outline"> <Badge variant="outline">
{organizationRangeStart}-{organizationRangeEnd} von {stats?.organizations.length ?? 0} {organizationRangeStart}-{organizationRangeEnd} von {stats?.organizations.length ?? 0}
</Badge> </Badge>
<HelpLink anchor="statistiken" label="Hilfe zur Organisationsstatistik" />
</div>
</CardHeader> </CardHeader>
<CardContent className="px-4 pb-4"> <CardContent className="px-4 pb-4">
<div className="hidden md:block"> <div className="hidden md:block">
@@ -702,8 +709,13 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
<Card> <Card>
<CardHeader className="p-4"> <CardHeader className="p-4">
<div className="flex items-start justify-between gap-3">
<div>
<CardTitle>Typen</CardTitle> <CardTitle>Typen</CardTitle>
<CardDescription>Support und Consulting im direkten Vergleich.</CardDescription> <CardDescription>Support und Consulting im direkten Vergleich.</CardDescription>
</div>
<HelpLink anchor="statistiken" label="Hilfe zu Typenstatistiken" />
</div>
</CardHeader> </CardHeader>
<CardContent className="space-y-3 px-4 pb-4"> <CardContent className="space-y-3 px-4 pb-4">
{(stats?.workTypes ?? []).map((group) => ( {(stats?.workTypes ?? []).map((group) => (
@@ -738,10 +750,13 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
<CardTitle>Ticket-Hotspots</CardTitle> <CardTitle>Ticket-Hotspots</CardTitle>
<CardDescription>Die größten Zeitblöcke im Monat.</CardDescription> <CardDescription>Die größten Zeitblöcke im Monat.</CardDescription>
</div> </div>
<div className="flex items-center gap-2">
<HelpLink anchor="statistiken" label="Hilfe zu Ticket-Hotspots" />
<Button variant="secondary" size="sm" onClick={() => onNavigate("/analysis")}> <Button variant="secondary" size="sm" onClick={() => onNavigate("/analysis")}>
Auswertung öffnen Auswertung öffnen
<ExternalLink className="size-4" /> <ExternalLink className="size-4" />
</Button> </Button>
</div>
</CardHeader> </CardHeader>
<CardContent className="space-y-2 px-4 pb-4"> <CardContent className="space-y-2 px-4 pb-4">
{(stats?.tickets ?? []).map((ticket) => ( {(stats?.tickets ?? []).map((ticket) => (
+12
View File
@@ -8,6 +8,7 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } f
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { CopyTicketButton } from "@/components/CopyTicketButton"; import { CopyTicketButton } from "@/components/CopyTicketButton";
import { HelpLink } from "@/components/HelpLink";
import { OrganizationSelect } from "@/components/OrganizationSelect"; import { OrganizationSelect } from "@/components/OrganizationSelect";
import { import {
createSession, createSession,
@@ -493,8 +494,13 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate }: T
<Card> <Card>
<CardHeader className="p-4"> <CardHeader className="p-4">
<div className="flex items-start justify-between gap-3">
<div>
<CardTitle>Ticketdaten</CardTitle> <CardTitle>Ticketdaten</CardTitle>
<CardDescription>Änderungen an Organisation und Art werden auf alle vorhandenen Sessions dieses Tickets übernommen.</CardDescription> <CardDescription>Änderungen an Organisation und Art werden auf alle vorhandenen Sessions dieses Tickets übernommen.</CardDescription>
</div>
<HelpLink anchor="ticketdaten" label="Hilfe zu Ticketdaten" />
</div>
</CardHeader> </CardHeader>
<CardContent className="px-4 pb-4"> <CardContent className="px-4 pb-4">
<form className="grid gap-3 lg:grid-cols-[180px_minmax(220px,1fr)_180px_auto] lg:items-end" onSubmit={saveTicketData}> <form className="grid gap-3 lg:grid-cols-[180px_minmax(220px,1fr)_180px_auto] lg:items-end" onSubmit={saveTicketData}>
@@ -544,6 +550,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate }: T
<CardDescription>Wähle für jede Session genau eine Bewertung aus oder lösche falsche Einträge.</CardDescription> <CardDescription>Wähle für jede Session genau eine Bewertung aus oder lösche falsche Einträge.</CardDescription>
</div> </div>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<HelpLink anchor="ticketdetail" label="Hilfe zur Ticketdetailansicht" />
<div className="flex items-center gap-2 rounded-md border bg-muted/30 px-3 py-2 text-sm"> <div className="flex items-center gap-2 rounded-md border bg-muted/30 px-3 py-2 text-sm">
<span className="text-muted-foreground">{periodLabel} gesamt</span> <span className="text-muted-foreground">{periodLabel} gesamt</span>
<span className="font-semibold">{formatMinutes(periodTotalMinutes)}</span> <span className="font-semibold">{formatMinutes(periodTotalMinutes)}</span>
@@ -657,8 +664,13 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate }: T
</Card> </Card>
<Card> <Card>
<CardHeader className="p-4"> <CardHeader className="p-4">
<div className="flex items-start justify-between gap-3">
<div>
<CardTitle>Session nachtragen</CardTitle> <CardTitle>Session nachtragen</CardTitle>
<CardDescription>Neue Zeit direkt diesem Ticket zuordnen.</CardDescription> <CardDescription>Neue Zeit direkt diesem Ticket zuordnen.</CardDescription>
</div>
<HelpLink anchor="session-nachtragen" label="Hilfe zum Nachtragen von Sessions" />
</div>
</CardHeader> </CardHeader>
<CardContent className="px-4 pb-4"> <CardContent className="px-4 pb-4">
<form className="space-y-4" onSubmit={submitTicketSession}> <form className="space-y-4" onSubmit={submitTicketSession}>
+16
View File
@@ -8,6 +8,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { HelpLink } from "@/components/HelpLink";
import { OrganizationSelect } from "@/components/OrganizationSelect"; import { OrganizationSelect } from "@/components/OrganizationSelect";
import { createSession, lookupTicket } from "../api"; import { createSession, lookupTicket } from "../api";
import { formatTimer } from "../format"; import { formatTimer } from "../format";
@@ -300,8 +301,13 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
<Card className="overflow-hidden"> <Card className="overflow-hidden">
<CardHeader className="border-b bg-muted/30 p-4"> <CardHeader className="border-b bg-muted/30 p-4">
<div className="flex items-start justify-between gap-3">
<div>
<CardTitle>Neuen Timer starten</CardTitle> <CardTitle>Neuen Timer starten</CardTitle>
<CardDescription>Ein neuer Timer startet sofort und pausiert alle anderen Timer automatisch.</CardDescription> <CardDescription>Ein neuer Timer startet sofort und pausiert alle anderen Timer automatisch.</CardDescription>
</div>
<HelpLink anchor="timer" label="Hilfe zu Timern" />
</div>
</CardHeader> </CardHeader>
<CardContent className="grid gap-3 p-4 sm:grid-cols-[minmax(220px,320px)_auto] sm:items-end"> <CardContent className="grid gap-3 p-4 sm:grid-cols-[minmax(220px,320px)_auto] sm:items-end">
<div className="space-y-2"> <div className="space-y-2">
@@ -330,6 +336,8 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
<Card className="overflow-hidden"> <Card className="overflow-hidden">
<CardHeader className="border-b bg-muted/30 p-4"> <CardHeader className="border-b bg-muted/30 p-4">
<div className="flex items-start justify-between gap-3">
<div>
<CardTitle>{selectedTimer ? selectedTimer.ticketNumber : "Keine Session ausgewählt"}</CardTitle> <CardTitle>{selectedTimer ? selectedTimer.ticketNumber : "Keine Session ausgewählt"}</CardTitle>
<CardDescription> <CardDescription>
{selectedTimer {selectedTimer
@@ -338,6 +346,9 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
: "Beim Umschalten wird dieser Timer aktiviert und alle anderen werden pausiert." : "Beim Umschalten wird dieser Timer aktiviert und alle anderen werden pausiert."
: "Starte einen Timer, um eine Session zu erfassen."} : "Starte einen Timer, um eine Session zu erfassen."}
</CardDescription> </CardDescription>
</div>
<HelpLink anchor="timer" label="Hilfe zum Timerstatus" />
</div>
</CardHeader> </CardHeader>
<CardContent className="space-y-4 p-4"> <CardContent className="space-y-4 p-4">
<div className="rounded-lg border bg-background p-4 sm:p-5"> <div className="rounded-lg border bg-background p-4 sm:p-5">
@@ -374,8 +385,13 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
<Card className="overflow-hidden"> <Card className="overflow-hidden">
<CardHeader className="border-b bg-muted/30 p-4"> <CardHeader className="border-b bg-muted/30 p-4">
<div className="flex items-start justify-between gap-3">
<div>
<CardTitle>Session nachtragen</CardTitle> <CardTitle>Session nachtragen</CardTitle>
<CardDescription>Vergessene Zeiten für deinen eigenen Account manuell erfassen.</CardDescription> <CardDescription>Vergessene Zeiten für deinen eigenen Account manuell erfassen.</CardDescription>
</div>
<HelpLink anchor="session-nachtragen" label="Hilfe zum Nachtragen von Sessions" />
</div>
</CardHeader> </CardHeader>
<CardContent className="p-4"> <CardContent className="p-4">
<form className="space-y-4" onSubmit={submitManualSession}> <form className="space-y-4" onSubmit={submitManualSession}>