Refine help UX and monthly close layout

This commit is contained in:
2026-08-05 15:56:09 +02:00
parent 3b6f35e31a
commit f8574e5f39
5 changed files with 396 additions and 214 deletions
+41 -19
View File
@@ -69,6 +69,19 @@ import { TimerPage } from "./views/TimerPage";
import type { AuthUser, PeriodType } from "./types";
import type { Dispatch, SetStateAction } from "react";
type NavMenuEntry =
| {
type: "item";
href: string;
label: string;
icon: typeof Timer;
active: boolean;
}
| {
type: "separator";
key: string;
};
function routeFromPath(pathname: string) {
const periodTicketMatch = pathname.match(/^\/analysis\/(month|day)\/([^/]+)\/tickets\/(\d+)$/);
@@ -518,17 +531,20 @@ export function App() {
return <LoginPage onLogin={setCurrentUser} />;
}
const navItems = [
{ href: "/timer", label: "Timer", icon: Timer, active: path.startsWith("/timer") || path === "/" },
{ href: "/analysis", label: "Auswertung", icon: BarChart3, active: path.startsWith("/analysis") },
{ href: "/monthly-close", label: "Monatsabschluss", icon: ClipboardCheck, active: path.startsWith("/monthly-close") },
{ href: "/statistics", label: "Statistiken", icon: ChartNoAxesCombined, active: path.startsWith("/statistics") },
{ 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") },
const navItems: NavMenuEntry[] = [
{ type: "item", href: "/timer", label: "Timer", icon: Timer, active: path.startsWith("/timer") || path === "/" },
{ type: "item", href: "/analysis", label: "Auswertung", icon: BarChart3, active: path.startsWith("/analysis") },
{ type: "separator", key: "after-analysis" },
{ type: "item", href: "/monthly-close", label: "Monatsabschluss", icon: ClipboardCheck, active: path.startsWith("/monthly-close") },
{ type: "separator", key: "after-monthly-close" },
{ type: "item", href: "/statistics", label: "Statistiken", icon: ChartNoAxesCombined, active: path.startsWith("/statistics") },
{ type: "item", href: "/recurring", label: "Fixe Abrechnung", icon: Repeat, active: path.startsWith("/recurring") },
{ type: "item", href: "/profile", label: "Profil", icon: UserCog, active: path.startsWith("/profile") },
...(currentUser.role === "admin"
? [{ href: "/admin/users", label: "Benutzer", icon: Shield, active: path.startsWith("/admin/users") }]
? [{ type: "item" as const, href: "/admin/users", label: "Benutzer", icon: Shield, active: path.startsWith("/admin/users") }]
: []),
{ type: "separator", key: "before-faq" },
{ type: "item", href: "/faq", label: "FAQ", icon: CircleHelp, active: path.startsWith("/faq") },
];
const runningTimer = timers.find((timer) => timer.phase === "running") ?? null;
const runningElapsedSeconds = runningTimer ? Math.floor(activeElapsedMs(runningTimer, tick) / 1000) : 0;
@@ -589,16 +605,22 @@ export function App() {
<SidebarGroupLabel>Workflows</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{navItems.map((item) => (
<SidebarMenuItem key={item.href}>
<SidebarMenuButton asChild tooltip={item.label} isActive={item.active}>
<a href={item.href} onClick={navHandler(item.href)}>
<item.icon />
<span>{item.label}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
))}
{navItems.map((item) =>
item.type === "separator" ? (
<SidebarMenuItem key={item.key} className="py-1 group-data-[collapsible=icon]:hidden">
<Separator />
</SidebarMenuItem>
) : (
<SidebarMenuItem key={item.href}>
<SidebarMenuButton asChild tooltip={item.label} isActive={item.active}>
<a href={item.href} onClick={navHandler(item.href)}>
<item.icon />
<span>{item.label}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
)
)}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
+30 -5
View File
@@ -1,6 +1,8 @@
import { CircleHelp } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { getHelpSection } from "../help-content";
type HelpLinkProps = {
anchor: string;
@@ -9,11 +11,34 @@ type HelpLinkProps = {
};
export function HelpLink({ anchor, label = "Hilfe öffnen", className }: HelpLinkProps) {
const section = getHelpSection(anchor);
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>
<Dialog>
<DialogTrigger asChild>
<Button type="button" variant="ghost" size="icon-sm" className={className} title={label} aria-label={label}>
<CircleHelp className="size-4" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>{section.title}</DialogTitle>
<DialogDescription>{section.description}</DialogDescription>
</DialogHeader>
<ul className="space-y-2 text-sm text-muted-foreground">
{section.quickItems.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>
<DialogFooter>
<Button asChild variant="secondary">
<a href={`/faq#${section.id}`}>Ausführliche FAQ öffnen</a>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+277
View File
@@ -0,0 +1,277 @@
export type HelpSection = {
id: string;
title: string;
description: string;
quickItems: string[];
details: string[];
};
export const helpSections: HelpSection[] = [
{
id: "grundprinzip",
title: "Grundprinzip",
description: "TicketTracker ist die Arbeitszeiterfassung neben Ticketsystem und CRM. Die App misst Arbeit, bewertet Sessions und macht Monatsabschlüsse kontrollierbar.",
quickItems: [
"Sessions sind die getrackten oder nachgetragenen Arbeitszeiten.",
"Teamspace-Werte sind Tageswerte pro Ticket, nicht einzelne Sessions.",
"Abgeschlossen wird nur der Monat."
],
details: [
"TicketTracker ersetzt nicht das Ticketsystem. Ticketnummern dienen als Bezug zur Arbeit, die eigentliche Dokumentation bleibt im vorhandenen Ticketsystem.",
"TicketTracker ersetzt auch nicht das CRM. Die CRM- beziehungsweise Teamspace-Zeit wird als Kontrollwert gepflegt, damit getrackte Zeiten und eingetragene Abrechnung gegenübergestellt werden können.",
"Eine Session ist immer einem Benutzer zugeordnet. In den normalen Auswertungen sieht jeder Benutzer nur seine eigenen Sessions.",
"Ticketdaten wie Organisation und Art können aus bestehenden Tickets übernommen werden, auch wenn ein anderer Benutzer das Ticket schon bearbeitet hat.",
"Tickets werden nicht abgeschlossen. Sie gruppieren Sessions. Relevant für den Abschluss ist nur, ob alle Sessions eines Monats bewertet sind.",
"Der Monatsabschluss sperrt den Monat gegen versehentliche Änderungen. Muss doch noch etwas korrigiert werden, kann der Monat wieder geöffnet werden."
]
},
{
id: "timer",
title: "Timer",
description: "Timer erfassen laufende Arbeit an Tickets. Es können mehrere Timer vorbereitet sein, aber immer nur einer läuft aktiv.",
quickItems: [
"Ticketnummer eingeben und Timer starten.",
"Aktivieren eines Timers pausiert alle anderen Timer automatisch.",
"Beim Beenden stoppt die Zeit sofort; Details werden danach gespeichert."
],
details: [
"Neue Timer werden mit einer Ticketnummer im Format Ticket#XXXXXX gestartet.",
"Alle laufenden und pausierten Timer bleiben in der Seitenleiste sichtbar, damit du auf jeder Unterseite sehen kannst, was vorbereitet oder aktiv ist.",
"Wenn ein anderer Timer aktiviert wird, pausiert TicketTracker die übrigen Timer automatisch. Dadurch kann nie versehentlich auf zwei Tickets gleichzeitig Zeit laufen.",
"Beim Klick auf Beenden wird die Stoppuhr sofort angehalten. Das Speichern der Tätigkeit findet danach statt und verlängert die gemessene Zeit nicht.",
"Existiert das Ticket bereits, übernimmt TicketTracker Organisation und Art. Dann muss beim Beenden nur noch die Tätigkeit eingetragen werden.",
"Existiert das Ticket noch nicht, müssen Organisation, Tätigkeit und Art angegeben werden.",
"Pausen werden von der Gesamtdauer abgezogen. Die gespeicherte Zeit wird auf volle Minuten gerundet.",
"Der Schnellstart im Kopfbereich funktioniert auf allen Unterseiten und nutzt dieselbe Timerlogik."
]
},
{
id: "session-nachtragen",
title: "Session Nachtragen",
description: "Nachtragen ist für vergessene Zeiten gedacht, wenn kein Timer gelaufen ist.",
quickItems: [
"Datum, Von, Bis und Tätigkeit eintragen.",
"Benutzer tragen eigene Sessions nach; Admins können Benutzer auswählen.",
"Tätigkeiten sind mehrzeilig."
],
details: [
"Beim Nachtragen wird aus Datum, Startzeit und Endzeit automatisch die Dauer berechnet.",
"Das Ende muss nach dem Beginn liegen. Übernacht-Sessions sind aktuell nicht vorgesehen und sollten als getrennte Einträge gepflegt werden.",
"Die Organisation muss aus der synchronisierten Organisationsliste gewählt werden. Freitext ist absichtlich gesperrt.",
"Die Tätigkeit ist eine Textarea. Zeilenumbrüche bleiben erhalten und werden in der Ticketdetailansicht wieder ausgegeben.",
"Nachgetragene Sessions starten immer offen. Sie müssen später als Abgerechnet oder Nicht abrechenbar bewertet werden.",
"Admins können im Adminbereich zusätzlich bestimmen, welchem Benutzer die nachgetragene Session gehört.",
"In der Ticketdetailansicht kann eine Session direkt zum geöffneten Ticket nachgetragen werden. Ticketnummer und Stammdaten sind dort schon vorbelegt beziehungsweise aus dem Ticket abgeleitet."
]
},
{
id: "ticketdaten",
title: "Ticketdaten",
description: "Ticketdaten sind Stammdaten für ein Ticket und können nachträglich korrigiert werden.",
quickItems: [
"Ticketnummer, Organisation und Art sind bearbeitbar.",
"Organisationen kommen aus der lokalen Zammad-Synchronisation.",
"Die Art wiederkehrender Sessions kommt aus der Regel."
],
details: [
"Ticketnummern können korrigiert werden, falls beim Starten oder Nachtragen ein falscher Bezug gewählt wurde.",
"Die Organisation wird immer über die Suchliste gewählt. Dadurch sind spätere Auswertungen pro Organisation zuverlässig.",
"Änderungen an Organisation und Art werden auf vorhandene manuelle Sessions dieses Tickets übernommen.",
"Bei wiederkehrenden Sessions ist die Art nicht direkt an der Session änderbar. Sie wird ausschließlich über die fixe Abrechnungsregel gesteuert.",
"Wenn ein Ticket bereits durch einen anderen Benutzer bekannt ist, können dessen Ticketdaten beim Beenden eines Timers übernommen werden. Die Session selbst bleibt trotzdem deinem Benutzer zugeordnet."
]
},
{
id: "organisationen",
title: "Organisationen",
description: "Organisationen kommen aus Zammad und bilden die Grundlage für saubere Kunden- und Monatsauswertungen.",
quickItems: [
"Nur Admins synchronisieren Organisationen.",
"Freitext ist danach nicht mehr möglich.",
"Gespeichert werden Zammad-ID, Name und Zeitstempel."
],
details: [
"Im Adminbereich werden Zammad-URL und API-Key gespeichert. Der API-Key wird danach nicht mehr im Klartext angezeigt.",
"Beim Sync fragt TicketTracker die Organisationen aus Zammad ab und schreibt sie in die lokale Datenbank.",
"Neue Organisationen werden angelegt, geänderte Namen werden aktualisiert.",
"Organisationen, die in Zammad nicht mehr vorhanden sind, werden aus der lokalen Liste entfernt beziehungsweise von vorhandenen Tickets und Sessions gelöst.",
"Alte Sessions oder Tickets mit Freitext-Organisationen müssen manuell korrigiert werden, indem eine Organisation aus der Liste gewählt wird.",
"Die bewusst schlanke Organisationstabelle enthält interne ID, Zammad-ID, Name sowie Erstell- und Änderungszeitpunkte."
]
},
{
id: "auswertung",
title: "Auswertung",
description: "Die Auswertung ist die Arbeitsliste für Monat und Tag: Tickets öffnen, Sessions prüfen, sortieren und bewerten.",
quickItems: [
"Monat oder Tag wählen; die Ansicht aktualisiert automatisch.",
"Tickets können gefiltert, gruppiert und sortiert werden.",
"Die Einstellungen bleiben nach Refresh und Browserneustart erhalten."
],
details: [
"Die Monatsansicht zeigt alle Tickets, die im gewählten Monat mindestens eine Session haben.",
"Die Tagesansicht zeigt dieselben Datenlogiken auf einen einzelnen Tag gefiltert. Es gibt keinen Tagesabschluss.",
"Die Pfeile neben der Zeitraum-Eingabe springen jeweils einen Monat oder einen Tag vor beziehungsweise zurück.",
"Der Gesamtaufwand-Graph zeigt den getrackten Aufwand im Zeitraum. In der Monatsansicht pro Tag, in der Tagesansicht pro Stunde.",
"Die Ticketliste kann nach Typ, Organisation oder Status gruppiert werden. Gruppenüberschriften zeigen Anzahl, Zeit und offene Bewertungen.",
"Filter und Sortierungen bleiben im Browser gespeichert und können über Zurücksetzen wieder auf Standard gestellt werden.",
"Das Copy-Icon neben der Ticketnummer kopiert die Nummer direkt in die Zwischenablage."
]
},
{
id: "ticketdetail",
title: "Ticketdetailansicht",
description: "Die Ticketdetailansicht ist die Bearbeitungsfläche für Sessions, Bewertungen und Teamspace-Tageswerte eines Tickets.",
quickItems: [
"Sessions sind nach Tag gruppiert.",
"Teamspace wird pro Ticket und Tag gespeichert.",
"Monats- und Tagesansicht verwenden denselben Teamspace-Wert."
],
details: [
"Oben steht die Ticketnummer mit Copy-Icon, damit sie schnell in andere Systeme übernommen werden kann.",
"Die Session-Einträge sind nach Kalendertagen gruppiert. Pro Tagesgruppe steht die Summe der getrackten Zeit.",
"Das Teamspace-Feld wird als Stundenwert gepflegt, zum Beispiel 1, 1,5 oder 2.25.",
"Während der Eingabe wird der Teamspace-Wert nicht sofort gespeichert. Sobald eine Änderung vorliegt, erscheint ein Speichern-Button.",
"Ein leeres Teamspace-Feld entfernt den gespeicherten Teamspace-Wert für diesen Ticket-Tag.",
"Der Teamspace-Wert hängt am Ticket, Benutzer und Tag. Deshalb ist er in Monats- und Tagesansicht identisch.",
"Sessions können bewertet, bearbeitet oder gelöscht werden. Gelöschte letzte Sessions können dazu führen, dass ein leeres Ticket entfernt wird."
]
},
{
id: "sessions-bewerten",
title: "Sessions Bewerten",
description: "Bewertungen bestimmen, ob eine Session abgerechnet wurde oder bewusst nicht abrechenbar ist.",
quickItems: [
"Jede Session braucht genau eine Bewertung.",
"Abgerechnet und Nicht abrechenbar können durch erneuten Klick zurückgenommen werden.",
"Offene Bewertungen blockieren den Monatsabschluss."
],
details: [
"Abgerechnet bedeutet: Die Session wurde für die Abrechnung beziehungsweise im CRM berücksichtigt.",
"Nicht abrechenbar bedeutet: Die Session gehört zum Arbeitsnachweis, wird aber nicht abgerechnet.",
"Ein erneuter Klick auf den aktiven Bewertungsbutton entfernt die Bewertung und macht die Session wieder offen.",
"Nicht abrechenbare Sessions zählen als bewertet. Sie blockieren den Monatsabschluss nicht.",
"Wenn ein Monat abgeschlossen ist, sind Bewertungen gesperrt. Öffne den Monat zuerst wieder, wenn du etwas korrigieren musst.",
"Die offenen Bewertungen findest du gebündelt im Monatsabschluss."
]
},
{
id: "sessions-bearbeiten",
title: "Sessions Bearbeiten und Löschen",
description: "Sessions können korrigiert, gelöscht und bei wiederkehrenden Einträgen als Ausnahme behandelt werden.",
quickItems: [
"Bearbeiten läuft über das Stift-Icon.",
"Löschen läuft über das Papierkorb-Icon.",
"Wiederkehrende gelöschte Sessions werden nicht neu erzeugt."
],
details: [
"Beim Bearbeiten sind Datum, Von-Uhrzeit, Bis-Uhrzeit, Organisation und Tätigkeit änderbar.",
"Die Art ist bei manuellen Sessions änderbar. Bei wiederkehrenden Sessions ist sie gesperrt und kommt aus der Regel.",
"Die Dauer wird aus Von und Bis neu berechnet und wieder auf Minuten gerundet.",
"Beim Löschen wird geprüft, ob dadurch ein Ticket für dich oder komplett leer wird. Leere Tickets werden entfernt.",
"Wenn die letzte Session eines Tickets gelöscht wird, springt die Ansicht zurück zur Auswertung.",
"Wiederkehrend erzeugte Sessions werden beim Löschen als Ausnahme gespeichert. Dadurch erscheinen sie beim nächsten Öffnen des Monats nicht wieder automatisch.",
"Wenn eine wiederkehrende Leistung nur an einem anderen Tag erledigt wurde, wird die erzeugte Session bearbeitet beziehungsweise verschoben."
]
},
{
id: "monatsabschluss",
title: "Monatsabschluss",
description: "Der Monatsabschluss ist die Kontrollseite für offene Bewertungen und Teamspace-Punkte vor dem Abschluss.",
quickItems: [
"Bewertungen sind Pflicht für den Abschluss.",
"Tage ohne Teamspace-Wert können zur Kenntnis genommen werden.",
"Der Monat kann wieder geöffnet werden."
],
details: [
"Ein Monat kann abgeschlossen werden, sobald alle Sessions dieses Monats bewertet sind.",
"Tage ohne Teamspace-Wert blockieren den Abschluss nicht automatisch. Sie sind eine Prüfliste für CRM-Abgleich und Nacharbeit.",
"Wenn eine Session bewusst nicht in Teamspace eingetragen wird, kann der fehlende Tageswert zur Kenntnis genommen werden.",
"Zur Kenntnis genommene Einträge können über die Checkbox Zur Kenntnis anzeigen wieder eingeblendet werden.",
"Die Kenntnisnahme kann pro Eintrag über Zurücknehmen rückgängig gemacht werden.",
"Die Einstellung, ob zur Kenntnis genommene Einträge angezeigt werden, bleibt im Browser gespeichert.",
"Offene Bewertungen werden nach Ticket gruppiert. Ein Klick auf Ticket öffnen führt zur Bewertung im Ticket.",
"Es gibt keinen Tagesabschluss. Tagesansichten dienen nur der Kontrolle."
]
},
{
id: "fixe-abrechnungen",
title: "Fixe Abrechnungen",
description: "Fixe Abrechnungen erzeugen wiederkehrende Sessions für planbare regelmäßige Leistungen.",
quickItems: [
"Jeder Benutzer verwaltet eigene fixe Abrechnungen.",
"Ticket ist optional; ohne Ticket entsteht Fix#ID.",
"Feiertage in Bayern werden übersprungen."
],
details: [
"Fixe Abrechnungen sind für regelmäßige Leistungen gedacht, zum Beispiel jeden Donnerstag vier Stunden oder mehrere Wochentage mit unterschiedlichen Dauern.",
"Das Ticketfeld darf leer bleiben. Dann gruppiert TicketTracker die erzeugten Sessions automatisch unter Fix#ID.",
"Die Organisation, Art und Tätigkeit kommen aus der Regel und werden auf erzeugte Sessions angewendet.",
"Beim Muster Wochentage werden einzelne Slots mit Wochentag, Startzeit und Dauer gepflegt.",
"Beim Muster Alle X Wochen zählt das Gültig-von-Datum als Startpunkt. Das Intervall läuft wochenweise.",
"Gültig bis ist optional. Wird es später gesetzt oder geändert, werden erzeugte Sessions außerhalb der Gültigkeit entfernt.",
"Bayerische Feiertage werden bei der automatischen Anlage ausgelassen.",
"Einzelne erzeugte Sessions können wegen Urlaub, Krankheit oder Feiertagsverschiebung gelöscht oder verschoben werden."
]
},
{
id: "statistiken",
title: "Statistiken",
description: "Statistiken vergleichen getrackte Arbeit, bewertete Sessions und Teamspace-Werte.",
quickItems: [
"Sessions und Teamspace haben getrennte Farben.",
"Positive oder gleiche Differenz ist grün.",
"Negative Differenz ist rot."
],
details: [
"Die Statistikseite betrachtet immer einen Monat.",
"Getrackt zeigt die Summe aller Session-Zeiten und die Anzahl der Sessions und Tickets.",
"Teamspace zeigt, wie viel Zeit für dieselben Ticket-Tage im CRM eingetragen wurde.",
"Eine positive Differenz bedeutet: In Teamspace steht mehr Zeit als in den Sessions getrackt wurde.",
"Eine negative Differenz bedeutet: In Teamspace steht weniger Zeit als getrackt wurde.",
"Bewertet umfasst sowohl Abgerechnet als auch Nicht abrechenbar. Offen sind nur Sessions ohne Bewertung.",
"Der Monatsverlauf zeigt getrackte Session-Zeit als Balken und Teamspace als Linie.",
"Organisationen, Typen und Ticket-Hotspots helfen dabei, Auffälligkeiten im Monat schnell zu finden."
]
},
{
id: "profil",
title: "Profil",
description: "Im Profil pflegt jeder Benutzer die eigenen Zugangsdaten.",
quickItems: [
"Benutzername und voller Name sind änderbar.",
"Passwortänderung benötigt das aktuelle Passwort.",
"Der volle Name wird in der Oberfläche angezeigt."
],
details: [
"Der Benutzername wird beim Login verwendet und muss eindeutig sein.",
"Der volle Name erscheint in der App, zum Beispiel im Kopfbereich oder im Adminbereich.",
"Wenn du dein Passwort ändern willst, musst du das aktuelle Passwort sowie das neue Passwort zweimal eingeben.",
"Bleiben die Passwortfelder leer, werden nur Benutzername und voller Name gespeichert.",
"Admins können Benutzer ebenfalls verwalten, aber jeder Benutzer kann die eigenen Daten jederzeit selbst pflegen."
]
},
{
id: "admin",
title: "Adminbereich",
description: "Der Adminbereich verwaltet Benutzer, Session-Zuordnung, manuelle Admin-Nachträge und Zammad-Sync.",
quickItems: [
"Admins legen Benutzer an und bearbeiten Rollen.",
"Sessions können anderen Benutzern zugewiesen werden.",
"Zammad-Organisationen werden über Data-Sync aktualisiert."
],
details: [
"Neue Benutzer bekommen Benutzername, vollen Namen, Passwort, Rolle und Aktivstatus.",
"Admins können bestehende Benutzer bearbeiten, Passwörter setzen und Benutzer deaktivieren.",
"Der eigene Admin-Zugang kann nicht versehentlich deaktiviert oder zur normalen User-Rolle herabgestuft werden.",
"In der Session-Zuordnung sehen Admins die letzten Sessions inklusive Besitzer und können einzelne Sessions umverteilen.",
"Beim Umverteilen werden die betroffenen Monatsabschlüsse für alten und neuen Besitzer wieder geöffnet, damit Bewertungen und Auswertungen konsistent bleiben.",
"Admins können Sessions für andere Benutzer nachtragen, wenn Zeiten vergessen wurden.",
"Der Data-Sync speichert Zammad-Zugangsdaten und synchronisiert Organisationen in die lokale Datenbank."
]
}
];
export function getHelpSection(anchor: string) {
return helpSections.find((section) => section.id === anchor) ?? helpSections[0];
}
+30 -172
View File
@@ -3,163 +3,7 @@ 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."
]
}
];
import { helpSections } from "../help-content";
export function FaqPage() {
return (
@@ -167,9 +11,9 @@ export function FaqPage() {
<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>
<p className="text-sm text-muted-foreground">Ausführliche Anleitung für Timer, Auswertung, Teamspace, fixe Abrechnungen, Monatsabschluss und Adminfunktionen.</p>
</div>
<Badge variant="outline">{faqSections.length} Abschnitt(e)</Badge>
<Badge variant="outline">{helpSections.length} Abschnitt(e)</Badge>
</div>
<Card>
@@ -178,10 +22,10 @@ export function FaqPage() {
<BookOpenCheck className="size-5 text-muted-foreground" />
<CardTitle>Schnellnavigation</CardTitle>
</div>
<CardDescription>Die Fragezeichen in der App springen direkt in diese Abschnitte.</CardDescription>
<CardDescription>Die Fragezeichen in der App öffnen eine Kurzfassung. Von dort gelangst du direkt zum passenden ausführlichen Abschnitt.</CardDescription>
</CardHeader>
<CardContent className="flex flex-wrap gap-2 px-4 pb-4">
{faqSections.map((section) => (
{helpSections.map((section) => (
<Button key={section.id} asChild variant="secondary" size="sm">
<a href={`#${section.id}`}>{section.title}</a>
</Button>
@@ -190,22 +34,36 @@ export function FaqPage() {
</Card>
<div className="grid gap-3 xl:grid-cols-2">
{faqSections.map((section) => (
{helpSections.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">
<CardContent className="space-y-4 px-4 pb-4">
<div className="rounded-md border bg-muted/20 p-3">
<p className="mb-2 text-sm font-medium">Kurzfassung</p>
<ul className="space-y-2 text-sm text-muted-foreground">
{section.quickItems.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>
<div>
<p className="mb-2 text-sm font-medium">Details</p>
<ul className="space-y-2 text-sm text-muted-foreground">
{section.details.map((item) => (
<li key={item} className="flex gap-2">
<span className="mt-2 size-1.5 shrink-0 rounded-full bg-primary/70" />
<span>{item}</span>
</li>
))}
</ul>
</div>
<div className="flex justify-end">
<Button asChild variant="ghost" size="sm">
<a href="#top">
<ArrowUp className="size-4" />
+18 -18
View File
@@ -92,7 +92,7 @@ function groupOpenSessions(sessions: StatisticsOpenSession[]) {
function OpenTicketItem({ group, month, onNavigate }: { group: OpenTicketGroup; month: string; onNavigate: (to: string) => void }) {
return (
<div className="flex min-h-32 flex-col justify-between gap-3 rounded-md border bg-background p-3">
<div className="flex min-h-36 flex-col justify-between gap-3 rounded-md border bg-background p-3">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<span className="font-medium">{group.ticketNumber}</span>
@@ -126,7 +126,7 @@ function CrmDayItem({
const isAcknowledged = Boolean(item.acknowledged);
return (
<div className={`flex min-h-32 flex-col justify-between gap-3 rounded-md border bg-background p-3 ${isAcknowledged ? "opacity-75" : ""}`}>
<div className={`flex min-h-36 flex-col justify-between gap-3 rounded-md border bg-background p-3 ${isAcknowledged ? "opacity-75" : ""}`}>
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<span className="font-medium">{item.ticket_number}</span>
@@ -363,17 +363,27 @@ export function MonthlyClosePage({ onNavigate }: MonthlyClosePageProps) {
<Card>
<CardHeader className="p-4">
<div className="flex items-start justify-between gap-3">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div>
<CardTitle>Aufräumen</CardTitle>
<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 className="flex flex-wrap items-center gap-2">
<div className="inline-flex h-8 items-center gap-2 rounded-md border bg-background px-3 text-sm">
<Checkbox
checked={showAcknowledgedMissingCrm}
aria-label="Zur Kenntnis genommene Teamspace-Tage anzeigen"
onCheckedChange={(checked) => setShowAcknowledgedMissingCrm(checked === true)}
/>
<span>Zur Kenntnis anzeigen ({acknowledgedMissingCrmDays.length})</span>
</div>
<HelpLink anchor="monatsabschluss" label="Hilfe zu offenen Bewertungen und Teamspace-Prüfpunkten" />
</div>
</div>
</CardHeader>
<CardContent className="grid gap-4 px-4 pb-4 xl:grid-cols-2">
<section className="space-y-2">
<div className="flex items-center gap-2 text-sm font-medium">
<div className="flex min-h-8 items-center gap-2 text-sm font-medium">
<FileWarning className="size-4 text-muted-foreground" />
Offene Bewertungen
</div>
@@ -384,19 +394,9 @@ export function MonthlyClosePage({ onNavigate }: MonthlyClosePageProps) {
</section>
<section className="space-y-2">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-2 text-sm font-medium">
<Sparkles className="size-4 text-muted-foreground" />
Tage ohne Teamspace-Wert
</div>
<div className="inline-flex items-center gap-2 rounded-md border bg-background px-3 py-2 text-sm">
<Checkbox
checked={showAcknowledgedMissingCrm}
aria-label="Zur Kenntnis genommene Teamspace-Tage anzeigen"
onCheckedChange={(checked) => setShowAcknowledgedMissingCrm(checked === true)}
/>
<span>Zur Kenntnis anzeigen ({acknowledgedMissingCrmDays.length})</span>
</div>
<div className="flex min-h-8 items-center gap-2 text-sm font-medium">
<Sparkles className="size-4 text-muted-foreground" />
Tage ohne Teamspace-Wert
</div>
{visibleMissingCrmDays.map((item) => (
<CrmDayItem