This commit is contained in:
michael committed 2026-09-29 13:26:18 +02:00
1 parent c89d4825e2
commit 62794f72f1
8 files changed
+402 -78

No files matched your search

+41 -14
View File
@@ -12,7 +12,7 @@ import { Textarea } from "@/components/ui/textarea";
import { CopyTicketButton } from "@/components/CopyTicketButton";
import { HelpLink } from "@/components/HelpLink";
import { OrganizationSelect } from "@/components/OrganizationSelect";
import { RichActivityEditor } from "@/components/RichActivityEditor";
import { readRichActivityEditorValue, RichActivityEditor } from "@/components/RichActivityEditor";
import { TodoPanel } from "@/components/TodoPanel";
import {
createSession,
@@ -240,14 +240,18 @@ function budgetedProjectTeamspaceEntries(project: ProjectSummary) {
return projectTeamspaceEntries(project).filter((entry) => Number(entry.hour_budget_minutes ?? 0) > 0);
}
function teamspaceEntryOptionLabel(entry: { teamspace_project: string | null; is_default: boolean }) {
const label = entry.teamspace_project || "Ohne Teamspace-ID";
function teamspaceEntryDisplayLabel(entry: { teamspace_project: string | null; teamspace_description?: string | null; is_default: boolean }) {
return entry.teamspace_description?.trim() || entry.teamspace_project || "Ohne Teamspace-ID";
}
function teamspaceEntryOptionLabel(entry: { teamspace_project: string | null; teamspace_description?: string | null; is_default: boolean }) {
const label = teamspaceEntryDisplayLabel(entry);
return entry.is_default ? `${label} (Standard)` : label;
}
function teamspaceEntryDescription(entry: { teamspace_description?: string | null } | null | undefined) {
return entry?.teamspace_description?.trim() || "";
function teamspaceEntryDescription(entry: { teamspace_project?: string | null; teamspace_description?: string | null } | null | undefined) {
return entry?.teamspace_description?.trim() ? entry.teamspace_project?.trim() || "" : "";
}
function teamspaceBillingKey(day: string, projectTeamspaceEntryId: string | null | undefined) {
@@ -495,6 +499,19 @@ function teamspacePreparationText(ticket: TicketPeriod["ticket"] | null | undefi
return [`${ticketNumber} - ${customerName}`, "", activities, "", "Tickethandling und Dokumentation"].join("\n");
}
function SessionActivityBody({ session }: { session: SessionEntry }) {
if (session.zammad_article_body) {
return (
<div
className="break-words text-sm font-medium [&_blockquote]:border-l-2 [&_blockquote]:pl-3 [&_br]:block [&_code]:rounded-sm [&_code]:bg-muted [&_code]:px-1 [&_h1]:text-xl [&_h1]:font-semibold [&_h2]:text-lg [&_h2]:font-semibold [&_h3]:text-base [&_h3]:font-semibold [&_img]:my-2 [&_img]:max-h-80 [&_img]:max-w-full [&_img]:rounded-md [&_ol]:ml-5 [&_ol]:list-decimal [&_p]:my-1 [&_pre]:whitespace-pre-wrap [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:p-2 [&_strike]:line-through [&_ul]:ml-5 [&_ul]:list-disc"
dangerouslySetInnerHTML={{ __html: session.zammad_article_body }}
/>
);
}
return <p className="whitespace-pre-wrap break-words text-sm font-medium">{session.activity || "Keine Tätigkeit erfasst."}</p>;
}
function toDateLocalValue(value: string) {
const date = new Date(value);
const offsetMs = date.getTimezoneOffset() * 60_000;
@@ -921,7 +938,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
return [
...projectEntries.map((entry) => ({
id: entry.id,
label: entry.teamspace_project || (entry.is_default ? "Standard" : "Ohne Teamspace-ID"),
label: teamspaceEntryDisplayLabel(entry),
description: teamspaceEntryDescription(entry),
entry
})),
@@ -1446,18 +1463,22 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
setSavingTicketSession(true);
try {
const activityValue = readRichActivityEditorValue("ticket-session-activity") ?? {
activity: ticketSession.activity,
zammadArticleBody: ticketSession.zammadArticleBody
};
const result = await createSession({
ticketNumber: ticket.ticket_number,
organizationId: effectiveOrganizationId,
activity: ticketSession.activity,
zammadArticleBody: ticketSession.zammadArticleBody || null,
activity: activityValue.activity,
zammadArticleBody: activityValue.zammadArticleBody || null,
workType: effectiveWorkType,
startedAt: startedAt.toISOString(),
endedAt: endedAt.toISOString(),
durationSeconds,
projectTeamspaceEntryId: effectiveWorkType === "consulting" && requiresProjectTeamspaceEntry ? ticketSession.projectTeamspaceEntryId : null,
billingStatus: ticketSession.billingStatus,
createZammadArticle: ticketSession.createZammadArticle,
createZammadArticle: ticketSession.createZammadArticle && (activityValue.activity.trim().length > 0 || Boolean(activityValue.zammadArticleBody)),
zammadArticleTypeId: ticketSession.zammadArticleTypeId
});
toast.success("Session nachgetragen", {
@@ -1563,10 +1584,14 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
setSavingSessionDetails(true);
try {
const activityValue = readRichActivityEditorValue("session-activity") ?? {
activity: editSessionActivity,
zammadArticleBody: editSessionZammadArticleBody
};
await updateSessionDetails(editingSession.id, {
organizationId: editSessionOrganizationId,
activity: editSessionActivity,
zammadArticleBody: editSessionZammadArticleBody || null,
activity: activityValue.activity,
zammadArticleBody: activityValue.zammadArticleBody || null,
workType: editSessionWorkType,
removeProjectData,
projectTeamspaceEntryId: editSessionWorkType === "consulting" && requiresProjectTeamspaceEntry ? editSessionProjectTeamspaceEntryId : null,
@@ -1855,7 +1880,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
? `Eigentümer: ${project.owner_display_name}`
: showBudgetedProjectEntries
? `${budgetedProjectEntries.length} budgetierte Teamspace-Einträge`
: budgetedProjectEntries[0]?.teamspace_project || project.teamspace_project || "Kein Teamspace-Projekt hinterlegt"}
: budgetedProjectEntries[0] ? teamspaceEntryDisplayLabel(budgetedProjectEntries[0]) : project.teamspace_project || "Kein Teamspace-Projekt hinterlegt"}
</CardDescription>
</div>
<div className="flex shrink-0 items-center gap-1">
@@ -1927,7 +1952,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
{budgetedProjectEntries.map((entry) => {
const consumption = Number(entry.consumption_minutes ?? 0);
const percent = projectConsumptionPercent(consumption, entry.hour_budget_minutes);
const label = entry.teamspace_project || (entry.is_default ? "Standard" : "Ohne Teamspace-ID");
const label = teamspaceEntryDisplayLabel(entry);
return (
<div key={entry.id} className="rounded-md border bg-background/50 px-2 py-1">
@@ -2371,7 +2396,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
</div>
{expanded ? (
<div className="border-t px-3 pb-3 pt-2">
<p className="whitespace-pre-wrap break-words text-sm font-medium">{session.activity || "Keine Tätigkeit erfasst."}</p>
<SessionActivityBody session={session} />
<div className="mt-2 flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground">
<span>{session.organization_name ?? session.customer_name}</span>
<span>{session.work_type === "support" ? "Support" : "Consulting"}</span>
@@ -2566,6 +2591,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
Tätigkeit
</label>
<RichActivityEditor
key={`ticket-session-${ticketId}`}
id="ticket-session-activity"
value={ticketSession.activity}
htmlValue={ticketSession.zammadArticleBody}
@@ -2727,6 +2753,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
<div className="space-y-2">
<label className="text-sm font-medium" htmlFor="session-activity">Tätigkeit</label>
<RichActivityEditor
key={editingSession?.id ?? "session-activity-empty"}
id="session-activity"
value={editSessionActivity}
htmlValue={editSessionZammadArticleBody}