Update
This commit is contained in:
1 parent
235c19e51b
commit
227644cfeb
8 files changed
+1041
-309
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useSyncExternalStore } from "react";
|
||||
import { useEffect, useMemo, useState, useSyncExternalStore } from "react";
|
||||
import { AlertCircle, CheckCircle2, DatabaseZap, DownloadCloud, RefreshCw } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -14,11 +14,14 @@ import {
|
||||
applyOwnZammadConsultingTicketsFromPreview,
|
||||
previewExistingZammadTickets,
|
||||
previewOwnZammadConsultingTickets,
|
||||
waitForZammadSyncJob,
|
||||
type ZammadSyncExistingFields,
|
||||
type ZammadSyncChange,
|
||||
type ZammadSyncJob,
|
||||
type ZammadSyncItem,
|
||||
type ZammadSyncResult
|
||||
} from "../api";
|
||||
import { getLocalStorageValue, setLocalStorageValue } from "@/lib/local-storage.client";
|
||||
import type { AuthUser } from "../types";
|
||||
import { zammadPriorityLabel, zammadStateLabel } from "../zammad-meta";
|
||||
|
||||
@@ -27,6 +30,7 @@ type ZammadSyncPageProps = {
|
||||
};
|
||||
|
||||
type SelectionMap = Record<string, boolean>;
|
||||
type RunningJobKind = "preview" | "apply";
|
||||
|
||||
const defaultFields: ZammadSyncExistingFields = {
|
||||
title: true,
|
||||
@@ -51,25 +55,39 @@ type ZammadSyncPageState = {
|
||||
fields: ZammadSyncExistingFields;
|
||||
existingResult: ZammadSyncResult | null;
|
||||
existingPreviewJobId: string | null;
|
||||
existingRunningJobId: string | null;
|
||||
existingRunningJobKind: RunningJobKind | null;
|
||||
importResult: ZammadSyncResult | null;
|
||||
importPreviewJobId: string | null;
|
||||
importRunningJobId: string | null;
|
||||
importRunningJobKind: RunningJobKind | null;
|
||||
existingSelection: SelectionMap;
|
||||
importSelection: SelectionMap;
|
||||
syncingExisting: boolean;
|
||||
importingConsulting: boolean;
|
||||
};
|
||||
|
||||
let zammadSyncPageState: ZammadSyncPageState = {
|
||||
fields: defaultFields,
|
||||
existingResult: null,
|
||||
existingPreviewJobId: null,
|
||||
importResult: null,
|
||||
importPreviewJobId: null,
|
||||
existingSelection: {},
|
||||
importSelection: {},
|
||||
syncingExisting: false,
|
||||
importingConsulting: false
|
||||
};
|
||||
function emptyZammadSyncPageState(): ZammadSyncPageState {
|
||||
return {
|
||||
fields: defaultFields,
|
||||
existingResult: null,
|
||||
existingPreviewJobId: null,
|
||||
existingRunningJobId: null,
|
||||
existingRunningJobKind: null,
|
||||
importResult: null,
|
||||
importPreviewJobId: null,
|
||||
importRunningJobId: null,
|
||||
importRunningJobKind: null,
|
||||
existingSelection: {},
|
||||
importSelection: {},
|
||||
syncingExisting: false,
|
||||
importingConsulting: false
|
||||
};
|
||||
}
|
||||
|
||||
let zammadSyncPageState: ZammadSyncPageState = emptyZammadSyncPageState();
|
||||
let hydratedZammadSyncUserId: string | null = null;
|
||||
let activeZammadSyncStorageUserId: string | null = null;
|
||||
|
||||
const zammadSyncPageListeners = new Set<() => void>();
|
||||
|
||||
@@ -89,9 +107,104 @@ function setZammadSyncPageState(update: Partial<ZammadSyncPageState> | ((current
|
||||
...zammadSyncPageState,
|
||||
...(typeof update === "function" ? update(zammadSyncPageState) : update)
|
||||
};
|
||||
|
||||
if (activeZammadSyncStorageUserId) {
|
||||
saveZammadSyncPageState(activeZammadSyncStorageUserId, zammadSyncPageState);
|
||||
}
|
||||
|
||||
zammadSyncPageListeners.forEach((listener) => listener());
|
||||
}
|
||||
|
||||
function zammadSyncStorageKey(userId: string) {
|
||||
return `tickettracker.zammad-sync.${userId}`;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function selectionMap(value: unknown): SelectionMap {
|
||||
if (!isRecord(value)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return Object.fromEntries(Object.entries(value).filter(([, selected]) => typeof selected === "boolean")) as SelectionMap;
|
||||
}
|
||||
|
||||
function syncResult(value: unknown): ZammadSyncResult | null {
|
||||
if (!isRecord(value) || (value.mode !== "preview" && value.mode !== "applied") || !Array.isArray(value.items)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return value as ZammadSyncResult;
|
||||
}
|
||||
|
||||
function syncFields(value: unknown): ZammadSyncExistingFields {
|
||||
const source = isRecord(value) ? value : {};
|
||||
|
||||
return {
|
||||
...defaultFields,
|
||||
title: typeof source.title === "boolean" ? source.title : defaultFields.title,
|
||||
organization: typeof source.organization === "boolean" ? source.organization : defaultFields.organization,
|
||||
type: typeof source.type === "boolean" ? source.type : defaultFields.type,
|
||||
projectName: typeof source.projectName === "boolean" ? source.projectName : defaultFields.projectName,
|
||||
budget: typeof source.budget === "boolean" ? source.budget : defaultFields.budget,
|
||||
overrideBudget: typeof source.overrideBudget === "boolean" ? source.overrideBudget : defaultFields.overrideBudget,
|
||||
pendingTodo: typeof source.pendingTodo === "boolean" ? source.pendingTodo : defaultFields.pendingTodo
|
||||
};
|
||||
}
|
||||
|
||||
function nullableString(value: unknown) {
|
||||
return typeof value === "string" && value.trim() ? value : null;
|
||||
}
|
||||
|
||||
function runningJobKind(value: unknown): RunningJobKind | null {
|
||||
return value === "preview" || value === "apply" ? value : null;
|
||||
}
|
||||
|
||||
function loadZammadSyncPageState(userId: string): ZammadSyncPageState | null {
|
||||
const raw = getLocalStorageValue(zammadSyncStorageKey(userId));
|
||||
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
|
||||
if (!isRecord(parsed)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const existingRunningJobId = nullableString(parsed.existingRunningJobId);
|
||||
const existingRunningJobKind = runningJobKind(parsed.existingRunningJobKind);
|
||||
const importRunningJobId = nullableString(parsed.importRunningJobId);
|
||||
const importRunningJobKind = runningJobKind(parsed.importRunningJobKind);
|
||||
|
||||
return {
|
||||
fields: syncFields(parsed.fields),
|
||||
existingResult: syncResult(parsed.existingResult),
|
||||
existingPreviewJobId: nullableString(parsed.existingPreviewJobId),
|
||||
existingRunningJobId,
|
||||
existingRunningJobKind,
|
||||
importResult: syncResult(parsed.importResult),
|
||||
importPreviewJobId: nullableString(parsed.importPreviewJobId),
|
||||
importRunningJobId,
|
||||
importRunningJobKind,
|
||||
existingSelection: selectionMap(parsed.existingSelection),
|
||||
importSelection: selectionMap(parsed.importSelection),
|
||||
syncingExisting: Boolean(existingRunningJobId && existingRunningJobKind),
|
||||
importingConsulting: Boolean(importRunningJobId && importRunningJobKind)
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function saveZammadSyncPageState(userId: string, state: ZammadSyncPageState) {
|
||||
setLocalStorageValue(zammadSyncStorageKey(userId), JSON.stringify(state));
|
||||
}
|
||||
|
||||
function useZammadSyncPageState() {
|
||||
return useSyncExternalStore(subscribeZammadSyncPageState, getZammadSyncPageState, getZammadSyncPageState);
|
||||
}
|
||||
@@ -206,7 +319,7 @@ function formatChangeValue(field: string, value: unknown) {
|
||||
return formatMinutes(value);
|
||||
}
|
||||
|
||||
if (field === "Teamspace-Projekte") {
|
||||
if (field === "Teamspace-Projekte" || field === "Teamspace-Prüfung") {
|
||||
return formatTeamspaceProjectList(value) || "-";
|
||||
}
|
||||
|
||||
@@ -262,6 +375,10 @@ function changeAllowedByFields(item: ZammadSyncItem, change: ZammadSyncChange, f
|
||||
return fields.projectName && consultingProjectCanBeTouched;
|
||||
}
|
||||
|
||||
if (change.field === "Teamspace-Prüfung") {
|
||||
return fields.projectName && consultingProjectCanBeTouched;
|
||||
}
|
||||
|
||||
if (change.field === "Teamspace-Budget") {
|
||||
return fields.budget && consultingProjectCanBeTouched;
|
||||
}
|
||||
@@ -293,7 +410,7 @@ function cachedStringList(source: Record<string, unknown> | null | undefined, ke
|
||||
|
||||
function formatTeamspaceProjectList(value: unknown) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean).join(" + ");
|
||||
return normalizedTeamspaceProjectList(value).join(" + ");
|
||||
}
|
||||
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
@@ -303,6 +420,50 @@ function splitTeamspaceProjects(value: unknown) {
|
||||
return typeof value === "string" ? value.match(/[^\s+]+/g) ?? [] : [];
|
||||
}
|
||||
|
||||
function normalizedTeamspaceProjectList(value: unknown) {
|
||||
const entries = Array.isArray(value) ? value : splitTeamspaceProjects(value);
|
||||
const seen = new Set<string>();
|
||||
const normalized: string[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const teamspaceProject = typeof entry === "string" ? entry.trim() : "";
|
||||
|
||||
if (!teamspaceProject || seen.has(teamspaceProject)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
seen.add(teamspaceProject);
|
||||
normalized.push(teamspaceProject);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function teamspaceProjectDiff(localProjects: unknown, zammadProjects: unknown) {
|
||||
const local = normalizedTeamspaceProjectList(localProjects);
|
||||
const zammad = normalizedTeamspaceProjectList(zammadProjects);
|
||||
const localSet = new Set(local);
|
||||
const zammadSet = new Set(zammad);
|
||||
|
||||
return {
|
||||
local,
|
||||
additions: zammad.filter((project) => !localSet.has(project)),
|
||||
missingInZammad: local.filter((project) => !zammadSet.has(project))
|
||||
};
|
||||
}
|
||||
|
||||
function addCachedTeamspaceProjectChanges(changes: ZammadSyncChange[], localProjects: unknown, zammadProjects: unknown) {
|
||||
const diff = teamspaceProjectDiff(localProjects, zammadProjects);
|
||||
|
||||
if (diff.additions.length > 0) {
|
||||
changes.push({ field: "Teamspace-Projekte", from: diff.local, to: [...diff.local, ...diff.additions] });
|
||||
}
|
||||
|
||||
if (diff.missingInZammad.length > 0) {
|
||||
changes.push({ field: "Teamspace-Prüfung", from: diff.missingInZammad, to: "nicht mehr aus Zammad; bitte manuell prüfen" });
|
||||
}
|
||||
}
|
||||
|
||||
function zammadPendingTodoPreview(zammad: Record<string, unknown>) {
|
||||
const stateId = numericValue(cachedValue(zammad, "stateId"));
|
||||
const pendingTime = cachedValue(zammad, "pendingTime");
|
||||
@@ -355,8 +516,8 @@ function comparablePreviewValueForField(field: string, value: unknown) {
|
||||
return comparablePreviewIdValue(value);
|
||||
}
|
||||
|
||||
if (field === "Teamspace-Projekte" && Array.isArray(value)) {
|
||||
return value.map((entry) => comparablePreviewValue(entry));
|
||||
if (field === "Teamspace-Projekte") {
|
||||
return normalizedTeamspaceProjectList(value).sort((left, right) => left.localeCompare(right, "de", { numeric: true, sensitivity: "base" }));
|
||||
}
|
||||
|
||||
return comparablePreviewValue(value);
|
||||
@@ -368,7 +529,7 @@ function cachedValueChanged(field: string, left: unknown, right: unknown) {
|
||||
|
||||
function addCachedChange(changes: ZammadSyncChange[], field: string, from: unknown, to: unknown) {
|
||||
if (cachedValueChanged(field, from, to)) {
|
||||
changes.push({ field, from: comparablePreviewValue(from), to: comparablePreviewValue(to) });
|
||||
changes.push({ field, from: comparablePreviewValueForField(field, from), to: comparablePreviewValueForField(field, to) });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,6 +548,7 @@ function changesFromExistingPreviewCache(item: ZammadSyncItem, fields: ZammadSyn
|
||||
const localWorkType = cachedValue(local, "workType");
|
||||
const zammadWorkType = cachedValue(zammad, "workType");
|
||||
const zammadStateId = numericValue(cachedValue(zammad, "stateId"));
|
||||
const pendingTodoExists = cachedValue(local, "zammadPendingTodoExists");
|
||||
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");
|
||||
@@ -398,7 +560,7 @@ function changesFromExistingPreviewCache(item: ZammadSyncItem, fields: ZammadSyn
|
||||
addCachedChange(changes, "Zammad-Status", cachedValue(local, "zammadStateId"), cachedValue(zammad, "stateId"));
|
||||
|
||||
const pendingTodo = zammadPendingTodoPreview(zammad);
|
||||
if (fields.pendingTodo && pendingTodo) {
|
||||
if (fields.pendingTodo && pendingTodo && pendingTodoExists === false) {
|
||||
changes.push({ field: "ToDo", from: null, to: pendingTodo });
|
||||
}
|
||||
|
||||
@@ -430,7 +592,7 @@ function changesFromExistingPreviewCache(item: ZammadSyncItem, fields: ZammadSyn
|
||||
}
|
||||
|
||||
if (fields.projectName) {
|
||||
addCachedChange(changes, "Teamspace-Projekte", localTeamspaceProjectList, zammadTeamspaceProjectList);
|
||||
addCachedTeamspaceProjectChanges(changes, localTeamspaceProjectList, zammadTeamspaceProjectList);
|
||||
}
|
||||
|
||||
if (canSyncBudget) {
|
||||
@@ -587,6 +749,7 @@ function ResultSummary({
|
||||
const selectable = selectableItems(result);
|
||||
const selectedCount = selection ? selectable.filter((item) => itemIsSelected(item, selection)).length : 0;
|
||||
const allSelected = selectable.length > 0 && selectedCount === selectable.length;
|
||||
const listedItems = result.items.filter((item) => item.action !== "skip" || visibleTicketTrackerChanges(item).length > 0);
|
||||
const metrics = [
|
||||
["Geprüft", result.checked],
|
||||
[result.mode === "preview" ? "Würde ändern" : "Aktualisiert", result.updated],
|
||||
@@ -642,14 +805,14 @@ function ResultSummary({
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{result.items.length === 0 ? (
|
||||
{listedItems.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={selection ? 5 : 4} className="py-8 text-center text-sm text-muted-foreground">
|
||||
Keine Datensätze im Lauf.
|
||||
Keine Änderungen für die aktuelle Auswahl.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
result.items.map((item) => {
|
||||
listedItems.map((item) => {
|
||||
const action = formatAction(item);
|
||||
const isSelectable = item.action === "update" || item.action === "create";
|
||||
const visibleChanges = visibleTicketTrackerChanges(item);
|
||||
@@ -717,20 +880,155 @@ function ResultSummary({
|
||||
}
|
||||
|
||||
export function ZammadSyncPage({ currentUser }: ZammadSyncPageProps) {
|
||||
const pageState = useZammadSyncPageState();
|
||||
const {
|
||||
fields,
|
||||
existingResult,
|
||||
existingRunningJobId,
|
||||
existingRunningJobKind,
|
||||
importResult,
|
||||
importRunningJobId,
|
||||
importRunningJobKind,
|
||||
existingSelection,
|
||||
importSelection,
|
||||
syncingExisting,
|
||||
importingConsulting
|
||||
} = useZammadSyncPageState();
|
||||
} = pageState;
|
||||
const [storageReady, setStorageReady] = useState(false);
|
||||
const displayedExistingResult = useMemo(() => applyFieldsToExistingPreview(existingResult, fields), [existingResult, fields]);
|
||||
const selectedExistingCount = useMemo(() => selectedTicketNumbers(displayedExistingResult, existingSelection).length, [displayedExistingResult, existingSelection]);
|
||||
const selectedImportCount = useMemo(() => selectedTicketNumbers(importResult, importSelection).length, [importResult, importSelection]);
|
||||
const zammadReady = currentUser.has_zammad_api_token && Boolean(currentUser.zammad_user_id);
|
||||
|
||||
useEffect(() => {
|
||||
setStorageReady(false);
|
||||
activeZammadSyncStorageUserId = currentUser.id;
|
||||
const restored = loadZammadSyncPageState(currentUser.id);
|
||||
|
||||
if (restored) {
|
||||
setZammadSyncPageState(restored);
|
||||
} else if (hydratedZammadSyncUserId !== currentUser.id) {
|
||||
setZammadSyncPageState(emptyZammadSyncPageState());
|
||||
}
|
||||
|
||||
hydratedZammadSyncUserId = currentUser.id;
|
||||
setStorageReady(true);
|
||||
}, [currentUser.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!storageReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
saveZammadSyncPageState(currentUser.id, pageState);
|
||||
}, [currentUser.id, pageState, storageReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!storageReady || !existingRunningJobId || !existingRunningJobKind) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
void waitForZammadSyncJob(existingRunningJobId)
|
||||
.then((result) => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (existingRunningJobKind === "preview") {
|
||||
setZammadSyncPageState((current) => ({
|
||||
existingResult: result,
|
||||
existingPreviewJobId: existingRunningJobId,
|
||||
existingSelection: Object.keys(current.existingSelection).length ? current.existingSelection : selectionFromResult(result),
|
||||
existingRunningJobId: null,
|
||||
existingRunningJobKind: null,
|
||||
syncingExisting: false
|
||||
}));
|
||||
} else {
|
||||
setZammadSyncPageState({
|
||||
existingResult: result,
|
||||
existingPreviewJobId: null,
|
||||
existingSelection: {},
|
||||
existingRunningJobId: null,
|
||||
existingRunningJobKind: null,
|
||||
syncingExisting: false
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setZammadSyncPageState({
|
||||
existingRunningJobId: null,
|
||||
existingRunningJobKind: null,
|
||||
syncingExisting: false
|
||||
});
|
||||
toast.error("Zammad-Lauf konnte nicht fortgesetzt werden", {
|
||||
description: error instanceof Error ? error.message : "Unbekannter Fehler"
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [existingRunningJobId, existingRunningJobKind, storageReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!storageReady || !importRunningJobId || !importRunningJobKind) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
void waitForZammadSyncJob(importRunningJobId)
|
||||
.then((result) => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (importRunningJobKind === "preview") {
|
||||
setZammadSyncPageState((current) => ({
|
||||
importResult: result,
|
||||
importPreviewJobId: importRunningJobId,
|
||||
importSelection: Object.keys(current.importSelection).length ? current.importSelection : selectionFromResult(result),
|
||||
importRunningJobId: null,
|
||||
importRunningJobKind: null,
|
||||
importingConsulting: false
|
||||
}));
|
||||
} else {
|
||||
setZammadSyncPageState({
|
||||
importResult: result,
|
||||
importPreviewJobId: null,
|
||||
importSelection: {},
|
||||
importRunningJobId: null,
|
||||
importRunningJobKind: null,
|
||||
importingConsulting: false
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setZammadSyncPageState({
|
||||
importRunningJobId: null,
|
||||
importRunningJobKind: null,
|
||||
importingConsulting: false
|
||||
});
|
||||
toast.error("Zammad-Importlauf konnte nicht fortgesetzt werden", {
|
||||
description: error instanceof Error ? error.message : "Unbekannter Fehler"
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [importRunningJobId, importRunningJobKind, storageReady]);
|
||||
|
||||
function setField(key: keyof ZammadSyncExistingFields, checked: boolean) {
|
||||
setZammadSyncPageState((current) => ({
|
||||
fields: { ...current.fields, [key]: checked, ...(key === "budget" && !checked ? { overrideBudget: false } : {}) }
|
||||
@@ -740,7 +1038,11 @@ export function ZammadSyncPage({ currentUser }: ZammadSyncPageProps) {
|
||||
function resetRunningState() {
|
||||
setZammadSyncPageState({
|
||||
syncingExisting: false,
|
||||
importingConsulting: false
|
||||
importingConsulting: false,
|
||||
existingRunningJobId: null,
|
||||
existingRunningJobKind: null,
|
||||
importRunningJobId: null,
|
||||
importRunningJobKind: null
|
||||
});
|
||||
toast.info("Laufstatus zurückgesetzt");
|
||||
}
|
||||
@@ -751,13 +1053,17 @@ export function ZammadSyncPage({ currentUser }: ZammadSyncPageProps) {
|
||||
}
|
||||
|
||||
const syncFields = zammadSyncPageState.fields;
|
||||
setZammadSyncPageState({ syncingExisting: true });
|
||||
setZammadSyncPageState({ syncingExisting: true, existingRunningJobId: null, existingRunningJobKind: null });
|
||||
try {
|
||||
const { jobId, result } = await previewExistingZammadTickets(syncFields);
|
||||
const { jobId, result } = await previewExistingZammadTickets(syncFields, (job: ZammadSyncJob) => {
|
||||
setZammadSyncPageState({ syncingExisting: true, existingRunningJobId: job.id, existingRunningJobKind: "preview" });
|
||||
});
|
||||
setZammadSyncPageState({
|
||||
existingResult: result,
|
||||
existingPreviewJobId: jobId,
|
||||
existingSelection: selectionFromResult(result)
|
||||
existingSelection: selectionFromResult(result),
|
||||
existingRunningJobId: null,
|
||||
existingRunningJobKind: null
|
||||
});
|
||||
toast.success("Vorschau erstellt", {
|
||||
description: `${result.updated + result.created} Datensatz/Datensätze können übernommen werden.`
|
||||
@@ -767,7 +1073,7 @@ export function ZammadSyncPage({ currentUser }: ZammadSyncPageProps) {
|
||||
description: error instanceof Error ? error.message : "Unbekannter Fehler"
|
||||
});
|
||||
} finally {
|
||||
setZammadSyncPageState({ syncingExisting: false });
|
||||
setZammadSyncPageState({ syncingExisting: false, existingRunningJobId: null, existingRunningJobKind: null });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -790,13 +1096,17 @@ export function ZammadSyncPage({ currentUser }: ZammadSyncPageProps) {
|
||||
}
|
||||
|
||||
const syncFields = zammadSyncPageState.fields;
|
||||
setZammadSyncPageState({ syncingExisting: true });
|
||||
setZammadSyncPageState({ syncingExisting: true, existingRunningJobId: null, existingRunningJobKind: null });
|
||||
try {
|
||||
const { result } = await applyExistingZammadTicketsFromPreview(zammadSyncPageState.existingPreviewJobId, syncFields, ticketNumbers);
|
||||
const { result } = await applyExistingZammadTicketsFromPreview(zammadSyncPageState.existingPreviewJobId, syncFields, ticketNumbers, (job: ZammadSyncJob) => {
|
||||
setZammadSyncPageState({ syncingExisting: true, existingRunningJobId: job.id, existingRunningJobKind: "apply" });
|
||||
});
|
||||
setZammadSyncPageState({
|
||||
existingResult: result,
|
||||
existingPreviewJobId: null,
|
||||
existingSelection: {}
|
||||
existingSelection: {},
|
||||
existingRunningJobId: null,
|
||||
existingRunningJobKind: null
|
||||
});
|
||||
toast.success("Auswahl synchronisiert", {
|
||||
description: `${result.updated} aktualisiert, ${result.skipped} ohne Transfer.`
|
||||
@@ -806,7 +1116,7 @@ export function ZammadSyncPage({ currentUser }: ZammadSyncPageProps) {
|
||||
description: error instanceof Error ? error.message : "Unbekannter Fehler"
|
||||
});
|
||||
} finally {
|
||||
setZammadSyncPageState({ syncingExisting: false });
|
||||
setZammadSyncPageState({ syncingExisting: false, existingRunningJobId: null, existingRunningJobKind: null });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -815,13 +1125,17 @@ export function ZammadSyncPage({ currentUser }: ZammadSyncPageProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
setZammadSyncPageState({ importingConsulting: true });
|
||||
setZammadSyncPageState({ importingConsulting: true, importRunningJobId: null, importRunningJobKind: null });
|
||||
try {
|
||||
const { jobId, result } = await previewOwnZammadConsultingTickets();
|
||||
const { jobId, result } = await previewOwnZammadConsultingTickets((job: ZammadSyncJob) => {
|
||||
setZammadSyncPageState({ importingConsulting: true, importRunningJobId: job.id, importRunningJobKind: "preview" });
|
||||
});
|
||||
setZammadSyncPageState({
|
||||
importResult: result,
|
||||
importPreviewJobId: jobId,
|
||||
importSelection: selectionFromResult(result)
|
||||
importSelection: selectionFromResult(result),
|
||||
importRunningJobId: null,
|
||||
importRunningJobKind: null
|
||||
});
|
||||
toast.success("Import-Vorschau erstellt", {
|
||||
description: `${result.created} Consulting-Ticket(s) können angelegt werden.`
|
||||
@@ -831,7 +1145,7 @@ export function ZammadSyncPage({ currentUser }: ZammadSyncPageProps) {
|
||||
description: error instanceof Error ? error.message : "Unbekannter Fehler"
|
||||
});
|
||||
} finally {
|
||||
setZammadSyncPageState({ importingConsulting: false });
|
||||
setZammadSyncPageState({ importingConsulting: false, importRunningJobId: null, importRunningJobKind: null });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -852,13 +1166,17 @@ export function ZammadSyncPage({ currentUser }: ZammadSyncPageProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
setZammadSyncPageState({ importingConsulting: true });
|
||||
setZammadSyncPageState({ importingConsulting: true, importRunningJobId: null, importRunningJobKind: null });
|
||||
try {
|
||||
const { result } = await applyOwnZammadConsultingTicketsFromPreview(zammadSyncPageState.importPreviewJobId, ticketNumbers);
|
||||
const { result } = await applyOwnZammadConsultingTicketsFromPreview(zammadSyncPageState.importPreviewJobId, ticketNumbers, (job: ZammadSyncJob) => {
|
||||
setZammadSyncPageState({ importingConsulting: true, importRunningJobId: job.id, importRunningJobKind: "apply" });
|
||||
});
|
||||
setZammadSyncPageState({
|
||||
importResult: result,
|
||||
importPreviewJobId: null,
|
||||
importSelection: {}
|
||||
importSelection: {},
|
||||
importRunningJobId: null,
|
||||
importRunningJobKind: null
|
||||
});
|
||||
toast.success("Auswahl importiert", {
|
||||
description: `${result.created} angelegt, ${result.skipped} ohne Transfer.`
|
||||
@@ -868,7 +1186,7 @@ export function ZammadSyncPage({ currentUser }: ZammadSyncPageProps) {
|
||||
description: error instanceof Error ? error.message : "Unbekannter Fehler"
|
||||
});
|
||||
} finally {
|
||||
setZammadSyncPageState({ importingConsulting: false });
|
||||
setZammadSyncPageState({ importingConsulting: false, importRunningJobId: null, importRunningJobKind: null });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -950,8 +1268,8 @@ export function ZammadSyncPage({ currentUser }: ZammadSyncPageProps) {
|
||||
<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 className="block text-sm font-medium">Budget-Schutz im Sync ignorieren</span>
|
||||
<span className="block text-xs text-muted-foreground">Aus: nur ungeschützte Budgets aktualisieren. An: Zammad überschreibt auch geschützte Budgets.</span>
|
||||
</span>
|
||||
</label>
|
||||
) : null}
|
||||
|
||||
Reference in new issue
Block a user