Update
This commit is contained in:
1 parent
07ef8ef025
commit
87d6c9b3ce
10 files changed
+2925
-112
No files matched your search
+189
-11
@@ -42,6 +42,54 @@ type DeleteSessionResult = {
|
||||
};
|
||||
};
|
||||
|
||||
export type ZammadSyncExistingFields = {
|
||||
title: boolean;
|
||||
organization: boolean;
|
||||
type: boolean;
|
||||
projectName: boolean;
|
||||
budget: boolean;
|
||||
};
|
||||
|
||||
export type ZammadSyncChange = {
|
||||
field: string;
|
||||
from: unknown;
|
||||
to: unknown;
|
||||
};
|
||||
|
||||
export type ZammadSyncItem = {
|
||||
ticketNumber: string;
|
||||
action: "update" | "create" | "skip" | "error";
|
||||
reason: string | null;
|
||||
local: Record<string, unknown> | null;
|
||||
zammad: Record<string, unknown> | null;
|
||||
changes: ZammadSyncChange[];
|
||||
};
|
||||
|
||||
export type ZammadSyncResult = {
|
||||
mode: "preview" | "applied";
|
||||
checked: number;
|
||||
updated: number;
|
||||
created: number;
|
||||
skipped: number;
|
||||
missingInZammad: number;
|
||||
organizationMissing: number;
|
||||
markedForCloseReview: number;
|
||||
convertedToConsulting: number;
|
||||
convertedToSupport: number;
|
||||
errors: Array<{ ticketNumber: string; error: string }>;
|
||||
items: ZammadSyncItem[];
|
||||
};
|
||||
|
||||
export type ZammadSyncJob = {
|
||||
id: string;
|
||||
userId: string;
|
||||
status: "running" | "complete" | "error";
|
||||
result: ZammadSyncResult | null;
|
||||
error: string | null;
|
||||
startedAt: string;
|
||||
finishedAt: string | null;
|
||||
};
|
||||
|
||||
export type RestoreValidationResult = {
|
||||
valid: boolean;
|
||||
checkedTables: number;
|
||||
@@ -58,15 +106,46 @@ export class ApiError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
async function request<T>(url: string, options: RequestInit = {}): Promise<T> {
|
||||
const response = await fetch(url, {
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options.headers
|
||||
},
|
||||
...options
|
||||
});
|
||||
type ApiRequestInit = RequestInit & {
|
||||
timeoutMs?: number;
|
||||
};
|
||||
|
||||
async function request<T>(url: string, options: ApiRequestInit = {}): Promise<T> {
|
||||
const { timeoutMs, signal, ...fetchOptions } = options;
|
||||
const controller = timeoutMs ? new AbortController() : null;
|
||||
const timeout = controller
|
||||
? window.setTimeout(() => {
|
||||
controller.abort();
|
||||
}, timeoutMs)
|
||||
: null;
|
||||
|
||||
if (signal && controller) {
|
||||
signal.addEventListener("abort", () => controller.abort(), { once: true });
|
||||
}
|
||||
|
||||
let response: Response;
|
||||
|
||||
try {
|
||||
response = await fetch(url, {
|
||||
signal: controller?.signal ?? signal,
|
||||
...fetchOptions,
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...fetchOptions.headers
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof DOMException && error.name === "AbortError") {
|
||||
throw new ApiError("Anfrage hat zu lange gedauert", 408);
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
if (timeout) {
|
||||
window.clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.json().catch(() => ({}));
|
||||
@@ -266,6 +345,89 @@ export function syncZammadOrganizations(payload: { baseUrl?: string; apiKey?: st
|
||||
});
|
||||
}
|
||||
|
||||
function wait(milliseconds: number) {
|
||||
return new Promise((resolve) => window.setTimeout(resolve, milliseconds));
|
||||
}
|
||||
|
||||
async function waitForZammadSyncJob(jobId: string) {
|
||||
const startedAt = Date.now();
|
||||
const timeoutMs = 10 * 60 * 1000;
|
||||
|
||||
for (;;) {
|
||||
if (Date.now() - startedAt > timeoutMs) {
|
||||
throw new ApiError("Zammad-Sync läuft länger als erwartet. Bitte Vorschau neu starten.", 408);
|
||||
}
|
||||
|
||||
const { job } = await request<{ job: ZammadSyncJob }>(`/api/zammad-sync/jobs/${encodeURIComponent(jobId)}`, { timeoutMs: 20_000 });
|
||||
|
||||
if (job.status === "complete" && job.result) {
|
||||
return job.result;
|
||||
}
|
||||
|
||||
if (job.status === "error") {
|
||||
throw new ApiError(job.error ?? "Zammad-Sync fehlgeschlagen", 500);
|
||||
}
|
||||
|
||||
await wait(1200);
|
||||
}
|
||||
}
|
||||
|
||||
async function runZammadSyncJob(url: string, body: Record<string, unknown>) {
|
||||
const { job } = await request<{ job: ZammadSyncJob }>(url, {
|
||||
method: "POST",
|
||||
timeoutMs: 20_000,
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
const result = await waitForZammadSyncJob(job.id);
|
||||
|
||||
return { jobId: job.id, result };
|
||||
}
|
||||
|
||||
export async function syncExistingZammadTickets(fields: ZammadSyncExistingFields, options: { dryRun?: boolean; ticketNumbers?: string[] } = {}) {
|
||||
const { result } = await runZammadSyncJob("/api/zammad-sync/jobs/existing", {
|
||||
fields,
|
||||
dryRun: options.dryRun ?? true,
|
||||
ticketNumbers: options.ticketNumbers
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function importOwnZammadConsultingTickets(options: { dryRun?: boolean; ticketNumbers?: string[] } = {}) {
|
||||
const { result } = await runZammadSyncJob("/api/zammad-sync/jobs/consulting-import", {
|
||||
dryRun: options.dryRun ?? true,
|
||||
ticketNumbers: options.ticketNumbers
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function previewExistingZammadTickets(fields: ZammadSyncExistingFields) {
|
||||
return runZammadSyncJob("/api/zammad-sync/jobs/existing", {
|
||||
fields,
|
||||
dryRun: true
|
||||
});
|
||||
}
|
||||
|
||||
export function applyExistingZammadTicketsFromPreview(previewJobId: string, fields: ZammadSyncExistingFields, ticketNumbers: string[]) {
|
||||
return runZammadSyncJob(`/api/zammad-sync/jobs/${encodeURIComponent(previewJobId)}/apply-existing`, {
|
||||
fields,
|
||||
ticketNumbers
|
||||
});
|
||||
}
|
||||
|
||||
export function previewOwnZammadConsultingTickets() {
|
||||
return runZammadSyncJob("/api/zammad-sync/jobs/consulting-import", {
|
||||
dryRun: true
|
||||
});
|
||||
}
|
||||
|
||||
export function applyOwnZammadConsultingTicketsFromPreview(previewJobId: string, ticketNumbers: string[]) {
|
||||
return runZammadSyncJob(`/api/zammad-sync/jobs/${encodeURIComponent(previewJobId)}/apply-consulting-import`, {
|
||||
ticketNumbers
|
||||
});
|
||||
}
|
||||
|
||||
export function getRecurringBillings() {
|
||||
return request<{ recurringBillings: RecurringBilling[] }>("/api/recurring-billings");
|
||||
}
|
||||
@@ -324,14 +486,18 @@ export function deleteRecurringBilling(billingId: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export function getProjects(includeClosed = false) {
|
||||
export function getProjects(includeClosed = false, hidden = false) {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (includeClosed) {
|
||||
params.set("includeClosed", "true");
|
||||
}
|
||||
|
||||
return request<{ projects: ProjectSummary[] }>(`/api/projects${params.size ? `?${params}` : ""}`);
|
||||
if (hidden) {
|
||||
params.set("hidden", "true");
|
||||
}
|
||||
|
||||
return request<{ projects: ProjectSummary[]; hiddenCount: number; totalCount: number }>(`/api/projects${params.size ? `?${params}` : ""}`);
|
||||
}
|
||||
|
||||
export function getProject(projectId: string) {
|
||||
@@ -405,6 +571,18 @@ export function deleteProject(projectId: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export function hideProject(projectId: string) {
|
||||
return request<{ hidden: { id: string } }>(`/api/projects/${projectId}/hide`, {
|
||||
method: "POST"
|
||||
});
|
||||
}
|
||||
|
||||
export function unhideProject(projectId: string) {
|
||||
return request<{ shown: { id: string } }>(`/api/projects/${projectId}/hide`, {
|
||||
method: "DELETE"
|
||||
});
|
||||
}
|
||||
|
||||
export function getTodos(status?: TodoStatus | "all") {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
|
||||
Reference in new issue
Block a user