370 lines
9.3 KiB
TypeScript
370 lines
9.3 KiB
TypeScript
import type {
|
|
AdminSession,
|
|
AdminUser,
|
|
AuthUser,
|
|
BillingStatus,
|
|
Organization,
|
|
PeriodOverview,
|
|
PeriodType,
|
|
RecurringBilling,
|
|
StatisticsOverview,
|
|
TicketMeta,
|
|
TicketPeriod,
|
|
UserRole,
|
|
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 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 getAdminUsers() {
|
|
return request<{ users: AdminUser[] }>("/api/admin/users");
|
|
}
|
|
|
|
export function createAdminUser(payload: {
|
|
username: string;
|
|
displayName: string;
|
|
password: string;
|
|
role: UserRole;
|
|
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;
|
|
role: UserRole;
|
|
active: boolean;
|
|
}
|
|
) {
|
|
return request<{ user: AdminUser }>(`/api/admin/users/${userId}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(payload)
|
|
});
|
|
}
|
|
|
|
export function getAdminSessions() {
|
|
return request<{ sessions: AdminSession[] }>("/api/admin/sessions");
|
|
}
|
|
|
|
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 createAdminSession(payload: {
|
|
userId: string;
|
|
ticketNumber: string;
|
|
organizationId: string;
|
|
activity: string;
|
|
workType: WorkType;
|
|
startedAt: string;
|
|
endedAt: string;
|
|
}) {
|
|
return request<{ session: AdminSession }>("/api/admin/sessions", {
|
|
method: "POST",
|
|
body: JSON.stringify(payload)
|
|
});
|
|
}
|
|
|
|
export function reassignAdminSession(sessionId: string, userId: string) {
|
|
return request<{ session: AdminSession }>(`/api/admin/sessions/${sessionId}/owner`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify({ userId })
|
|
});
|
|
}
|
|
|
|
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"
|
|
});
|
|
}
|