import type { AdminUser, AuthUser, BillingStatus, Organization, PeriodOverview, PeriodType, RecurringBilling, StatisticsOverview, TicketMeta, TicketPeriod, WorkType } from "./types"; type CreateSessionPayload = { ticketNumber: string; organizationId: string; activity: string; workType: WorkType; startedAt: string; endedAt: string; durationSeconds: number; }; type DeleteSessionResult = { deleted: { id: string; ticket_id: string; started_at: string; userTicketEmpty: boolean; ticketDeleted: boolean; }; }; export type RestoreValidationResult = { valid: boolean; checkedTables: number; issues: string[]; }; export class ApiError extends Error { constructor( message: string, public status: number ) { super(message); this.name = "ApiError"; } } async function request(url: string, options: RequestInit = {}): Promise { const response = await fetch(url, { credentials: "same-origin", headers: { "Content-Type": "application/json", ...options.headers }, ...options }); if (!response.ok) { const body = await response.json().catch(() => ({})); throw new ApiError(body.error ?? "Anfrage fehlgeschlagen", response.status); } return response.json() as Promise; } export function login(payload: { username: string; password: string }) { return request<{ user: AuthUser }>("/api/auth/login", { method: "POST", body: JSON.stringify(payload) }); } export function logout() { return request<{ ok: boolean }>("/api/auth/logout", { method: "POST" }); } export function getCurrentUser() { return request<{ user: AuthUser }>("/api/auth/me"); } export function updateCurrentUser(payload: { username: string; displayName: string; currentPassword?: string; newPassword?: string; }) { return request<{ user: AuthUser }>("/api/auth/me", { method: "PATCH", body: JSON.stringify(payload) }); } export function getSetupStatus() { return request<{ required: boolean }>("/api/setup/status"); } export function createSetupAdmin(payload: { username: string; displayName: string; password: string }) { return request<{ user: AuthUser }>("/api/setup/admin", { method: "POST", body: JSON.stringify(payload) }); } export function restoreSetupBackup(sql: string) { return request<{ ok: boolean }>("/api/setup/restore", { method: "POST", body: JSON.stringify({ sql }) }); } export function validateSetupBackup(sql: string) { return request("/api/setup/restore/validate", { method: "POST", body: JSON.stringify({ sql }) }); } export function getAdminUsers() { return request<{ users: AdminUser[] }>("/api/admin/users"); } export function createAdminUser(payload: { username: string; displayName: string; password: string; active: boolean; }) { return request<{ user: AdminUser }>("/api/admin/users", { method: "POST", body: JSON.stringify(payload) }); } export function updateAdminUser( userId: string, payload: { username: string; displayName: string; password?: string; active: boolean; } ) { return request<{ user: AdminUser }>(`/api/admin/users/${userId}`, { method: "PATCH", body: JSON.stringify(payload) }); } export async function downloadDatabaseExport() { const response = await fetch("/api/admin/export/database", { credentials: "same-origin" }); if (!response.ok) { const body = await response.json().catch(() => ({})); throw new ApiError(body.error ?? "Export fehlgeschlagen", response.status); } const contentDisposition = response.headers.get("content-disposition") ?? ""; const filenameMatch = contentDisposition.match(/filename="([^"]+)"/); return { blob: await response.blob(), filename: filenameMatch?.[1] ?? `tickettracker-backup-${new Date().toISOString().replace(/[:.]/g, "-")}.sql` }; } export function restoreDatabaseBackup(sql: string) { return request<{ ok: boolean }>("/api/admin/restore/database", { method: "POST", body: JSON.stringify({ sql }) }); } export function validateDatabaseBackup(sql: string) { return request("/api/admin/restore/database/validate", { method: "POST", body: JSON.stringify({ sql }) }); } export async function downloadMonthExport(month: string) { const response = await fetch(`/api/export/months/${month}`, { credentials: "same-origin" }); if (!response.ok) { const body = await response.json().catch(() => ({})); throw new ApiError(body.error ?? "Export fehlgeschlagen", response.status); } const contentDisposition = response.headers.get("content-disposition") ?? ""; const filenameMatch = contentDisposition.match(/filename="([^"]+)"/); return { blob: await response.blob(), filename: filenameMatch?.[1] ?? `tickettracker-monat-${month}.csv` }; } export function getOrganizations(search = "") { const params = new URLSearchParams(); if (search.trim()) { params.set("search", search.trim()); } return request<{ organizations: Organization[] }>(`/api/organizations${params.size ? `?${params}` : ""}`); } export function getZammadSettings() { return request<{ settings: { baseUrl: string; hasApiKey: boolean } }>("/api/admin/zammad/settings"); } export function saveZammadSettings(payload: { baseUrl: string; apiKey?: string }) { return request<{ settings: { baseUrl: string; hasApiKey: boolean } }>("/api/admin/zammad/settings", { method: "PUT", body: JSON.stringify(payload) }); } export function syncZammadOrganizations(payload: { baseUrl?: string; apiKey?: string }) { return request<{ synced: number; skipped: number; removed: number; unlinkedTickets: number; unlinkedSessions: number }>("/api/admin/zammad/organizations/sync", { method: "POST", body: JSON.stringify(payload) }); } export function getRecurringBillings() { return request<{ recurringBillings: RecurringBilling[] }>("/api/recurring-billings"); } export function createRecurringBilling(payload: { ticketNumber?: string | null; organizationId: string; activity: string; workType: WorkType; recurrenceType: "weekly" | "every_n_weeks"; intervalValue: number; validFrom: string; validUntil?: string | null; slots: Array<{ weekday: number | null; startTime: string; durationMinutes: number; }>; }) { return request<{ recurringBillings: RecurringBilling[] }>("/api/recurring-billings", { method: "POST", body: JSON.stringify(payload) }); } export function updateRecurringBilling( billingId: string, payload: | { active: boolean } | { ticketNumber?: string | null; organizationId: string; activity: string; workType: WorkType; recurrenceType: "weekly" | "every_n_weeks"; intervalValue: number; validFrom: string; validUntil?: string | null; slots: Array<{ id?: string; weekday: number | null; startTime: string; durationMinutes: number; }>; } ) { return request<{ recurringBillings: RecurringBilling[] }>(`/api/recurring-billings/${billingId}`, { method: "PATCH", body: JSON.stringify(payload) }); } export function deleteRecurringBilling(billingId: string) { return request<{ recurringBillings: RecurringBilling[] }>(`/api/recurring-billings/${billingId}`, { method: "DELETE" }); } export function createSession(payload: CreateSessionPayload) { return request("/api/sessions", { method: "POST", body: JSON.stringify(payload) }); } export function lookupTicket(ticketNumber: string) { return request<{ ticket: TicketMeta | null }>(`/api/tickets/lookup?ticketNumber=${encodeURIComponent(ticketNumber)}`); } export function updateTicket( ticketId: string, payload: { ticketNumber: string; organizationId: string; workType: WorkType; } ) { return request<{ ticket: TicketMeta }>(`/api/tickets/${ticketId}`, { method: "PATCH", body: JSON.stringify(payload) }); } export function updateSessionDetails( sessionId: string, payload: { organizationId: string; activity: string; workType: WorkType; startedAt: string; endedAt: string; } ) { return request(`/api/sessions/${sessionId}/details`, { method: "PATCH", body: JSON.stringify(payload) }); } export function updateTicketDayBilling(ticketId: string, day: string, billedMinutes: number | null) { return request(`/api/tickets/${ticketId}/day-billings/${day}`, { method: "PATCH", body: JSON.stringify({ billedMinutes }) }); } export function acknowledgeMissingTicketDayBilling(ticketId: string, day: string, acknowledged = true) { return request(`/api/tickets/${ticketId}/day-billings/${day}/acknowledgement`, { method: "PATCH", body: JSON.stringify({ acknowledged }) }); } export function getMonthOverview(month: string) { return getPeriodOverview("month", month); } export function getTicketMonth(month: string, ticketId: string) { return getTicketPeriod("month", month, ticketId); } function periodPath(type: PeriodType) { return type === "month" ? "months" : "days"; } export function getPeriodOverview(type: PeriodType, period: string) { return request(`/api/periods/${periodPath(type)}/${period}/overview`); } export function getStatisticsOverview(month: string) { return request(`/api/statistics/months/${month}`); } export function getTicketPeriod(type: PeriodType, period: string, ticketId: string) { return request(`/api/periods/${periodPath(type)}/${period}/tickets/${ticketId}`); } export function updateSessionBilling(sessionId: string, billingStatus: BillingStatus) { return request(`/api/sessions/${sessionId}/billing`, { method: "PATCH", body: JSON.stringify({ billingStatus }) }); } export function deleteSession(sessionId: string) { return request(`/api/sessions/${sessionId}`, { method: "DELETE" }); } export function closeTicketMonth(month: string, ticketId: string) { return closeTicketPeriod("month", month, ticketId); } export function reopenTicketMonth(month: string, ticketId: string) { return reopenTicketPeriod("month", month, ticketId); } export function closeMonth(month: string) { return closePeriod("month", month); } export function reopenMonth(month: string) { return reopenPeriod("month", month); } export function closeTicketPeriod(type: PeriodType, period: string, ticketId: string) { return request(`/api/periods/${periodPath(type)}/${period}/tickets/${ticketId}/close`, { method: "POST" }); } export function reopenTicketPeriod(type: PeriodType, period: string, ticketId: string) { return request(`/api/periods/${periodPath(type)}/${period}/tickets/${ticketId}/reopen`, { method: "POST" }); } export function closePeriod(type: PeriodType, period: string) { return request(`/api/periods/${periodPath(type)}/${period}/close`, { method: "POST" }); } export function reopenPeriod(type: PeriodType, period: string) { return request(`/api/periods/${periodPath(type)}/${period}/reopen`, { method: "POST" }); }