Update
This commit is contained in:
1 parent
fa6db04370
commit
0c14a3acbd
5 files changed
+79
-7
No files matched your search
@@ -111,11 +111,14 @@ Beim Nachtragen werden angegeben:
|
||||
- Bis-Uhrzeit
|
||||
- Bewertung: `---`, `Abrechenbar` oder `Nicht abrechenbar`
|
||||
- Taetigkeit
|
||||
- optional interne Zammad-Notiz
|
||||
|
||||
Das Taetigkeitsfeld ist mehrzeilig. Zeilenumbrueche bleiben in der Auswertung erhalten.
|
||||
|
||||
Mit Bewertung `---` bleibt eine nachgetragene Session offen. `Abrechenbar` und `Nicht abrechenbar` werden direkt an der neuen Session gespeichert.
|
||||
|
||||
Wenn die interne Zammad-Notiz aktiviert ist, wird die Taetigkeit auch beim Nachtragen als interner Zammad-Article uebertragen. Bei Zammad-Fehlern wird der Eintrag wie beim Timerabschluss geparkt und kann spaeter im Profil gesendet werden.
|
||||
|
||||
Das Ende muss nach dem Beginn liegen. Die Dauer wird automatisch berechnet und auf Minuten gerundet.
|
||||
|
||||
In der Ticketdetailansicht gibt es ebenfalls `Session nachtragen`. Dort wird die neue Session direkt dem geoeffneten Ticket zugeordnet.
|
||||
@@ -225,8 +228,11 @@ In der Ticketdetailansicht sieht man:
|
||||
- ToDos zu diesem Ticket oder Projekt
|
||||
- Session-Eintraege nach Tagen gruppiert
|
||||
- Teamspace-Wert pro Ticket und Tag
|
||||
- Moeglichkeit, direkt einen Timer fuer dieses Ticket zu starten
|
||||
- Moeglichkeit, direkt eine Session zu diesem Ticket nachzutragen
|
||||
|
||||
`Timer starten` nutzt dieselbe Timerlogik wie der globale Schnellstart. Existiert bereits ein Timer fuer das Ticket, wird dieser aktiviert.
|
||||
|
||||
Geoeffnete Tickets erscheinen in der Seitenleiste unter `Geoeffnete Tickets`. Die Liste bleibt nach Aktualisierung und Browserneustart erhalten. Mit einem Klick auf die Ticketnummer gelangt man direkt zur Ticketdetailansicht zurueck. Die Reihenfolge bleibt beim Anklicken unveraendert und kann nur per Drag and Drop angepasst werden. Der Schliessen-Button entfernt das Ticket aus der Seitenleiste. Wenn im aktuell geoeffneten Ticket noch ungespeicherte Werte vorhanden sind, muss zuerst gespeichert werden.
|
||||
|
||||
## Projekte und ToDos
|
||||
|
||||
@@ -1504,6 +1504,8 @@ export function App() {
|
||||
period={route.period}
|
||||
ticketId={route.ticketId}
|
||||
onNavigate={navigate}
|
||||
onStartTimer={startQuickTimer}
|
||||
currentUser={currentUser}
|
||||
onOpenTicketChange={updateOpenTicket}
|
||||
returnPath={ticketReturnPaths[route.ticketId] ?? openTickets.find((ticket) => ticket.ticketId === route.ticketId)?.returnPath ?? "/analysis"}
|
||||
/>
|
||||
|
||||
@@ -146,6 +146,7 @@ export const helpSections: HelpSection[] = [
|
||||
"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.",
|
||||
"Mit Bewertung --- bleibt die Session offen. Abrechenbar oder Nicht abrechenbar werden direkt an der neuen Session gespeichert.",
|
||||
"Die interne Zammad-Notiz kann auch beim Nachtragen gesetzt werden. Bei Zammad-Fehlern wird sie geparkt und kann später im Profil gesendet werden.",
|
||||
"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.",
|
||||
"Bei Consulting-Projekten mit mehreren Teamspace-Einträgen ist der passende Teamspace-Eintrag Pflicht. Bei nur einem Eintrag wird er automatisch gesetzt."
|
||||
]
|
||||
@@ -218,6 +219,7 @@ export const helpSections: HelpSection[] = [
|
||||
],
|
||||
details: [
|
||||
"Oben steht die Ticketnummer mit Copy-Icon, damit sie schnell in andere Systeme übernommen werden kann.",
|
||||
"Über Timer starten kann direkt für dieses Ticket ein Timer gestartet oder ein vorhandener Timer aktiviert werden.",
|
||||
"Die Session-Einträge sind nach Kalendertagen gruppiert. Pro Tagesgruppe steht die Summe der abgerechneten Zeit.",
|
||||
"Abrechenbare, nicht abrechenbare und offene Zeiten werden neben der abgerechneten Tageszeit separat angezeigt.",
|
||||
"Bei Projekt-Tickets mit mehreren Teamspace-Einträgen zeigt jede zugeordnete Session den gewählten Teamspace-Eintrag. Nicht zugeordnete Sessions werden hervorgehoben.",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ArrowLeft, CheckCircle2, ChevronDown, ChevronUp, ClipboardList, Copy, EyeOff, FolderKanban, Pencil, Plus, RotateCcw, Save, Star, Trash2 } from "lucide-react";
|
||||
import { ArrowLeft, CheckCircle2, ChevronDown, ChevronUp, ClipboardList, Copy, EyeOff, FolderKanban, Pencil, Play, Plus, RotateCcw, Save, Star, Trash2 } from "lucide-react";
|
||||
import { FormEvent, useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Alert } from "@/components/ui/alert";
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
} from "../api";
|
||||
import { currentDay, currentMonth, formatDate, formatDateTime, formatMinutes, formatTimeRange } from "../format";
|
||||
import { ticketHasRecurringBilling } from "../recurring-billings";
|
||||
import type { BillingStatus, PeriodType, ProjectSummary, ProjectTeamspaceEntry, SessionEntry, TicketDetailPeriodType, TicketPeriod, WorkType } from "../types";
|
||||
import type { AuthUser, BillingStatus, PeriodType, ProjectSummary, ProjectTeamspaceEntry, SessionEntry, TicketDetailPeriodType, TicketPeriod, WorkType } from "../types";
|
||||
import { ZammadPriorityBadge, ZammadStateBadge } from "../zammad-meta";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -41,6 +41,8 @@ type TicketDetailPageProps = {
|
||||
period: string;
|
||||
ticketId: string;
|
||||
onNavigate: (to: string) => void;
|
||||
onStartTimer: (ticketNumber: string) => Promise<boolean>;
|
||||
currentUser: AuthUser;
|
||||
onOpenTicketChange?: (ticket: {
|
||||
key: string;
|
||||
ticketId: string;
|
||||
@@ -60,6 +62,7 @@ type TicketSessionFormState = {
|
||||
projectTeamspaceEntryId: string;
|
||||
activity: string;
|
||||
billingStatus: BillingStatus;
|
||||
createZammadArticle: boolean;
|
||||
workType: WorkType;
|
||||
day: string;
|
||||
startTime: string;
|
||||
@@ -92,6 +95,7 @@ function emptyTicketSessionForm(): TicketSessionFormState {
|
||||
projectTeamspaceEntryId: "",
|
||||
activity: "",
|
||||
billingStatus: null,
|
||||
createZammadArticle: false,
|
||||
workType: "support",
|
||||
day: currentDay(),
|
||||
startTime: "09:00",
|
||||
@@ -480,7 +484,7 @@ function ProjectTeamspaceEntriesEditor({
|
||||
);
|
||||
}
|
||||
|
||||
export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onOpenTicketChange, returnPath }: TicketDetailPageProps) {
|
||||
export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onStartTimer, currentUser, onOpenTicketChange, returnPath }: TicketDetailPageProps) {
|
||||
const initialGlobalMonthFilter = periodType === "all" && period !== "all" ? period : currentMonth();
|
||||
const [data, setData] = useState<TicketPeriod | null>(null);
|
||||
const [loadingId, setLoadingId] = useState<string | null>(null);
|
||||
@@ -593,6 +597,19 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
}));
|
||||
}, [data?.ticket, data?.project]);
|
||||
|
||||
useEffect(() => {
|
||||
setTicketSession((current) => {
|
||||
if (current.activity || current.billingStatus || current.createZammadArticle === currentUser.zammad_session_note_default_enabled) {
|
||||
return current;
|
||||
}
|
||||
|
||||
return {
|
||||
...current,
|
||||
createZammadArticle: currentUser.zammad_session_note_default_enabled
|
||||
};
|
||||
});
|
||||
}, [currentUser.zammad_session_note_default_enabled]);
|
||||
|
||||
useEffect(() => {
|
||||
setProjectDescriptionExpanded(false);
|
||||
}, [data?.project?.id]);
|
||||
@@ -1023,7 +1040,8 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
endedAt: endedAt.toISOString(),
|
||||
durationSeconds,
|
||||
projectTeamspaceEntryId: effectiveWorkType === "consulting" && requiresProjectTeamspaceEntry ? ticketSession.projectTeamspaceEntryId : null,
|
||||
billingStatus: ticketSession.billingStatus
|
||||
billingStatus: ticketSession.billingStatus,
|
||||
createZammadArticle: ticketSession.createZammadArticle
|
||||
});
|
||||
toast.success("Session nachgetragen", {
|
||||
description: "Der Eintrag wurde diesem Ticket zugeordnet. Betroffene Abschlüsse wurden wieder geöffnet."
|
||||
@@ -1035,7 +1053,8 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
projectTeamspaceEntryId: "",
|
||||
workType: ticket.work_type ?? "support",
|
||||
day: ticketSession.day,
|
||||
billingStatus: null
|
||||
billingStatus: null,
|
||||
createZammadArticle: currentUser.zammad_session_note_default_enabled
|
||||
});
|
||||
await load();
|
||||
} catch (error) {
|
||||
@@ -1264,6 +1283,12 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{ticket?.ticket_number ? (
|
||||
<Button type="button" size="sm" onClick={() => void onStartTimer(ticket.ticket_number)}>
|
||||
<Play className="size-4" />
|
||||
Timer starten
|
||||
</Button>
|
||||
) : null}
|
||||
{ticket?.ticket_number ? (
|
||||
<Button type="button" size="sm" variant={isFavorite ? "default" : "secondary"} disabled={savingFavorite} onClick={() => void toggleTicketFavorite()}>
|
||||
<Star className={cn("size-4", isFavorite && "fill-current")} />
|
||||
@@ -1974,6 +1999,16 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<label className="flex min-h-10 items-start gap-2 rounded-md border bg-background p-3 text-sm lg:col-span-5">
|
||||
<Checkbox
|
||||
checked={ticketSession.createZammadArticle}
|
||||
onCheckedChange={(checked) => setTicketSession({ ...ticketSession, createZammadArticle: checked === true })}
|
||||
/>
|
||||
<span>
|
||||
<span className="block font-medium">Interne Zammad-Notiz erstellen</span>
|
||||
<span className="block text-xs text-muted-foreground">Überträgt die Tätigkeit inklusive time_unit nach Zammad oder parkt sie bei Fehlern.</span>
|
||||
</span>
|
||||
</label>
|
||||
<Button type="submit" disabled={savingTicketSession}>
|
||||
<Save className="size-4" />
|
||||
{savingTicketSession ? "Speichert..." : "Nachtragen"}
|
||||
|
||||
@@ -24,6 +24,7 @@ type ManualSessionFormState = {
|
||||
projectTeamspaceEntryId: string;
|
||||
activity: string;
|
||||
billingStatus: BillingStatus;
|
||||
createZammadArticle: boolean;
|
||||
workType: WorkType;
|
||||
day: string;
|
||||
startTime: string;
|
||||
@@ -43,6 +44,7 @@ function emptyManualSession(): ManualSessionFormState {
|
||||
projectTeamspaceEntryId: "",
|
||||
activity: "",
|
||||
billingStatus: null,
|
||||
createZammadArticle: false,
|
||||
workType: "support",
|
||||
day: currentDay(),
|
||||
startTime: "09:00",
|
||||
@@ -221,6 +223,19 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
|
||||
};
|
||||
}, [manualSession.ticketNumber]);
|
||||
|
||||
useEffect(() => {
|
||||
setManualSession((current) => {
|
||||
if (current.ticketNumber || current.activity || current.organizationId || current.billingStatus || current.createZammadArticle === currentUser.zammad_session_note_default_enabled) {
|
||||
return current;
|
||||
}
|
||||
|
||||
return {
|
||||
...current,
|
||||
createZammadArticle: currentUser.zammad_session_note_default_enabled
|
||||
};
|
||||
});
|
||||
}, [currentUser.zammad_session_note_default_enabled]);
|
||||
|
||||
async function addTimer(source: "local" | "zammad" = "local") {
|
||||
const normalizedTicketNumber = ticketNumber.trim();
|
||||
|
||||
@@ -596,14 +611,16 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
|
||||
durationSeconds,
|
||||
zammadLookup: Boolean(manualLookupTicket?.zammad_ticket_id),
|
||||
projectTeamspaceEntryId: requiresTeamspaceEntry(manualLookupTicket) ? manualSession.projectTeamspaceEntryId : null,
|
||||
billingStatus: manualSession.billingStatus
|
||||
billingStatus: manualSession.billingStatus,
|
||||
createZammadArticle: manualSession.createZammadArticle
|
||||
});
|
||||
toast.success("Session nachgetragen", {
|
||||
description: manualLookupTicket?.is_project ? "Der Eintrag wurde dem Projekt zugeordnet." : "Der Eintrag ist in deiner Ticketliste offen."
|
||||
});
|
||||
setManualSession({
|
||||
...emptyManualSession(),
|
||||
day: manualSession.day
|
||||
day: manualSession.day,
|
||||
createZammadArticle: currentUser.zammad_session_note_default_enabled
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error("Session konnte nicht nachgetragen werden", {
|
||||
@@ -908,6 +925,16 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<label className="flex min-h-10 items-start gap-2 rounded-md border bg-background p-3 text-sm lg:col-span-5">
|
||||
<Checkbox
|
||||
checked={manualSession.createZammadArticle}
|
||||
onCheckedChange={(checked) => setManualSession({ ...manualSession, createZammadArticle: checked === true })}
|
||||
/>
|
||||
<span>
|
||||
<span className="block font-medium">Interne Zammad-Notiz erstellen</span>
|
||||
<span className="block text-xs text-muted-foreground">Überträgt die Tätigkeit inklusive time_unit nach Zammad oder parkt sie bei Fehlern.</span>
|
||||
</span>
|
||||
</label>
|
||||
<Button type="submit" disabled={savingManualSession || manualTicketIsRecurringBilling}>
|
||||
<Upload className="size-4" />
|
||||
{savingManualSession ? "Speichert..." : "Nachtragen"}
|
||||
|
||||
Reference in new issue
Block a user