This commit is contained in:
michael committed 2026-09-15 09:51:30 +00:00
1 parent b7f7d7e793
commit c01d747352
14 files changed
+987 -294

No files matched your search

+61 -33
View File
@@ -20,6 +20,7 @@ import {
type ZammadSyncResult
} from "../api";
import type { AuthUser } from "../types";
import { zammadPriorityLabel, zammadStateLabel } from "../zammad-meta";
type ZammadSyncPageProps = {
currentUser: AuthUser;
@@ -32,10 +33,11 @@ const defaultFields: ZammadSyncExistingFields = {
organization: true,
type: true,
projectName: true,
budget: true
budget: true,
overrideBudget: false
};
const fieldOptions: Array<{ key: keyof ZammadSyncExistingFields; label: string; description: string }> = [
const fieldOptions: Array<{ key: Exclude<keyof ZammadSyncExistingFields, "overrideBudget">; label: string; description: string }> = [
{ key: "title", label: "Titel", description: "Ticket-Titel aus Zammad übernehmen." },
{ key: "organization", label: "Organisation", description: "Lokale Organisation anhand der Zammad-ID setzen." },
{ key: "type", label: "Typ", description: "Support/Consulting übernehmen, Projekte anlegen oder entfernen." },
@@ -164,16 +166,8 @@ function formatZammadGroup(value: unknown) {
function formatZammadPriority(value: unknown) {
const priorityId = numericValue(value);
if (priorityId === 1) {
return "1 (Niedrig)";
}
if (priorityId === 2) {
return "2 (Normal)";
}
if (priorityId === 3) {
return "3 (Hoch)";
if (priorityId !== null) {
return `${priorityId} (${zammadPriorityLabel(priorityId)})`;
}
return formatValue(value);
@@ -182,20 +176,8 @@ function formatZammadPriority(value: unknown) {
function formatZammadState(value: unknown) {
const stateId = numericValue(value);
if (stateId === 2) {
return "2 (offen)";
}
if (stateId === 3) {
return "3 (warten auf Erinnerung)";
}
if (stateId === 4) {
return "4 (geschlossen)";
}
if (stateId === 7) {
return "7 (warten auf schließen)";
if (stateId !== null) {
return `${stateId} (${zammadStateLabel(stateId)})`;
}
return formatValue(value);
@@ -222,6 +204,10 @@ function formatChangeValue(field: string, value: unknown) {
return formatMinutes(value);
}
if (field === "Teamspace-Projekte") {
return formatTeamspaceProjectList(value) || "-";
}
if (field === "Typ") {
return formatWorkType(value);
}
@@ -242,7 +228,7 @@ function formatChangeValue(field: string, value: unknown) {
}
function visibleTicketTrackerChanges(item: ZammadSyncItem) {
return item.changes.filter((change) => !change.field.startsWith("Zammad-"));
return item.changes.filter((change) => !change.field.startsWith("Zammad-") || change.field === "Zammad-Status" || change.field === "Zammad-Priorität");
}
function changeAllowedByFields(item: ZammadSyncItem, change: ZammadSyncChange, fields: ZammadSyncExistingFields) {
@@ -270,6 +256,10 @@ function changeAllowedByFields(item: ZammadSyncItem, change: ZammadSyncChange, f
return fields.projectName && consultingProjectCanBeTouched;
}
if (change.field === "Teamspace-Projekte") {
return fields.projectName && consultingProjectCanBeTouched;
}
if (change.field === "Teamspace-Budget") {
return fields.budget && consultingProjectCanBeTouched;
}
@@ -290,6 +280,23 @@ function cachedObject(source: Record<string, unknown> | null | undefined, key: s
return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : null;
}
function cachedStringList(source: Record<string, unknown> | null | undefined, key: string) {
const value = cachedValue(source, key);
return Array.isArray(value) ? value.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean) : [];
}
function formatTeamspaceProjectList(value: unknown) {
if (Array.isArray(value)) {
return value.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean).join(" + ");
}
return typeof value === "string" ? value.trim() : "";
}
function splitTeamspaceProjects(value: unknown) {
return typeof value === "string" ? value.split("+").map((entry) => entry.trim()).filter(Boolean) : [];
}
function comparablePreviewValue(value: unknown) {
if (typeof value === "string" && value.trim().length === 0) {
return null;
@@ -317,10 +324,14 @@ function comparablePreviewIdValue(value: unknown) {
}
function comparablePreviewValueForField(field: string, value: unknown) {
if (field === "Organisation") {
if (field === "Organisation" || field === "Zammad-Priorität" || field === "Zammad-Status") {
return comparablePreviewIdValue(value);
}
if (field === "Teamspace-Projekte" && Array.isArray(value)) {
return value.map((entry) => comparablePreviewValue(entry));
}
return comparablePreviewValue(value);
}
@@ -349,8 +360,16 @@ function changesFromExistingPreviewCache(item: ZammadSyncItem, fields: ZammadSyn
const localWorkType = cachedValue(local, "workType");
const zammadWorkType = cachedValue(zammad, "workType");
const zammadStateId = numericValue(cachedValue(zammad, "stateId"));
const localTeamspaceProjects = cachedStringList(project, "teamspaceProjects");
const zammadTeamspaceProjects = cachedStringList(zammad, "teamspaceProjects");
const localTeamspaceProjectList = localTeamspaceProjects.length ? localTeamspaceProjects : [cachedValue(project, "teamspaceProject")].filter((entry): entry is string => typeof entry === "string");
const zammadTeamspaceProjectList = zammadTeamspaceProjects.length ? zammadTeamspaceProjects : splitTeamspaceProjects(cachedValue(zammad, "teamspaceProject"));
const canSyncBudget = fields.budget && (cachedValue(project, "defaultBudgetSyncDisabled") !== true || fields.overrideBudget);
const changes: ZammadSyncChange[] = [];
addCachedChange(changes, "Zammad-Priorität", cachedValue(local, "zammadPriorityId"), cachedValue(zammad, "priorityId"));
addCachedChange(changes, "Zammad-Status", cachedValue(local, "zammadStateId"), cachedValue(zammad, "stateId"));
if (zammadWorkType === "support" && zammadStateId === 4) {
return changes;
}
@@ -379,11 +398,11 @@ function changesFromExistingPreviewCache(item: ZammadSyncItem, fields: ZammadSyn
}
if (fields.projectName) {
addCachedChange(changes, "Teamspace-Projekt", cachedValue(project, "teamspaceProject"), cachedValue(zammad, "teamspaceProject"));
addCachedChange(changes, "Teamspace-Projekte", localTeamspaceProjectList, zammadTeamspaceProjectList);
}
if (fields.budget) {
addCachedChange(changes, "Teamspace-Budget", cachedValue(project, "hourBudgetMinutes"), cachedValue(zammad, "teamspaceBudgetMinutes"));
if (canSyncBudget) {
addCachedChange(changes, "Teamspace-Budget", cachedValue(project, "defaultHourBudgetMinutes") ?? cachedValue(project, "hourBudgetMinutes"), cachedValue(zammad, "teamspaceBudgetMinutes"));
}
if (zammadStateId === 4 && cachedValue(project, "closedAt") === null && cachedValue(project, "closeReviewRequired") !== true) {
@@ -516,7 +535,7 @@ function zammadDetails(item: ZammadSyncItem) {
`Owner ${formatValue(zammad.ownerId)}`,
`Org ${formatValue(zammad.organizationId)}`,
`Titel ${formatValue(zammad.title)}`,
`Projekt ${formatValue(zammad.teamspaceProject)}`,
`Projekt ${formatValue(formatTeamspaceProjectList(zammad.teamspaceProjects) || zammad.teamspaceProject)}`,
`Budget ${formatMinutes(zammad.teamspaceBudgetMinutes)}`
];
}
@@ -681,7 +700,7 @@ export function ZammadSyncPage({ currentUser }: ZammadSyncPageProps) {
function setField(key: keyof ZammadSyncExistingFields, checked: boolean) {
setZammadSyncPageState((current) => ({
fields: { ...current.fields, [key]: checked }
fields: { ...current.fields, [key]: checked, ...(key === "budget" && !checked ? { overrideBudget: false } : {}) }
}));
}
@@ -894,6 +913,15 @@ export function ZammadSyncPage({ currentUser }: ZammadSyncPageProps) {
</label>
))}
</div>
{fields.budget ? (
<label className={cn("flex min-w-0 gap-3 rounded-md border p-3", fields.overrideBudget ? "bg-muted/30" : "bg-background")}>
<Checkbox checked={fields.overrideBudget} onCheckedChange={(checked) => setField("overrideBudget", checked === true)} />
<span className="min-w-0">
<span className="block text-sm font-medium">Manuelle Budget-Änderungen überschreiben</span>
<span className="block text-xs text-muted-foreground">Ausgeschaltet: geschützte Budgets bleiben in TicketTracker unverändert.</span>
</span>
</label>
) : null}
<div className="flex flex-wrap justify-end gap-2">
{syncingExisting ? (
<Button type="button" variant="outline" onClick={resetRunningState}>