Files
tickettracker/frontend/src/api.ts
T

423 lines
11 KiB
TypeScript

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<T>(url: string, options: RequestInit = {}): Promise<T> {
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<T>;
}
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<RestoreValidationResult>("/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<RestoreValidationResult>("/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<PeriodOverview>(`/api/periods/${periodPath(type)}/${period}/overview`);
}
export function getStatisticsOverview(month: string) {
return request<StatisticsOverview>(`/api/statistics/months/${month}`);
}
export function getTicketPeriod(type: PeriodType, period: string, ticketId: string) {
return request<TicketPeriod>(`/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<DeleteSessionResult>(`/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"
});
}