import { getApiBaseUrlSync } from '@/src/lib/server-config'; export type Tenant = { id: number; name: string; short?: string | null; [key: string]: unknown; }; export type TaskStatus = 'Offen' | 'In Bearbeitung' | 'Abgeschlossen'; export type Task = { id: number; name: string; description?: string | null; categorie?: string | null; userId?: string | null; user_id?: string | null; profile?: string | null; project?: number | { id?: number; name?: string } | null; customer?: number | { id?: number; name?: string } | null; plant?: number | { id?: number; name?: string } | null; archived?: boolean; [key: string]: unknown; }; export type TenantProfile = { id?: number | string; user_id?: string; full_name?: string; fullName?: string; email?: string; [key: string]: unknown; }; export type StaffTimeSpan = { id: string | null; eventIds: string[]; state: string; started_at: string; stopped_at: string | null; duration_minutes: number; user_id: string | null; type: string; description: string; }; export type Project = { id: number; name: string; notes?: string | null; projectNumber?: string | null; archived?: boolean; [key: string]: unknown; }; export type ProjectFile = { id: string; name?: string | null; path?: string | null; project?: number | { id?: number; name?: string }; customer?: number | { id?: number; name?: string }; plant?: number | { id?: number; name?: string }; createddocument?: number | { id?: number; documentNumber?: string }; mimeType?: string | null; url?: string; archived?: boolean; [key: string]: unknown; }; export type Customer = { id: number; name: string; customerNumber?: string | null; notes?: string | null; archived?: boolean; [key: string]: unknown; }; export type Plant = { id: number; name: string; description?: string | null; customer?: number | { id?: number; name?: string }; archived?: boolean; [key: string]: unknown; }; export type CustomerInventoryItem = { id: number; name: string; customer?: number | { id?: number; name?: string } | null; customerInventoryId?: string | null; serialNumber?: string | null; description?: string | null; manufacturer?: string | null; manufacturerNumber?: string | null; quantity?: number | null; archived?: boolean; [key: string]: unknown; }; export type CreatedDocument = { id: number; documentNumber?: string | null; title?: string | null; type?: string | null; state?: string | null; documentDate?: string | null; customer?: number | { id?: number; name?: string }; archived?: boolean; [key: string]: unknown; }; export type MeResponse = { user: { id: string; email: string; must_change_password?: boolean; [key: string]: unknown; }; tenants: Tenant[]; activeTenant: number | string | null; profile: Record | null; permissions: string[]; }; export type EncodedLabelRow = { dataType: 'pixels' | 'void' | 'check'; rowNumber: number; repeat: number; rowData?: Uint8Array | number[] | Record; blackPixelsCount: number; }; export type EncodedLabelImage = { cols: number; rows: number; rowsData: EncodedLabelRow[]; }; export type PrintLabelResponse = { encoded: EncodedLabelImage; base64?: string; }; export type WikiTreeItem = { id: string; parentId?: string | null; title: string; isFolder?: boolean; isVirtual?: boolean; sortOrder?: number; entityType?: string | null; entityId?: number | null; entityUuid?: string | null; updatedAt?: string; [key: string]: unknown; }; export type WikiPage = { id: string; title: string; content?: unknown; parentId?: string | null; isFolder?: boolean; entityType?: string | null; entityId?: number | null; entityUuid?: string | null; updatedAt?: string; [key: string]: unknown; }; type RequestOptions = { method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; token?: string | null; body?: unknown; }; function buildUrl(path: string): string { if (path.startsWith('http://') || path.startsWith('https://')) { return path; } const normalizedPath = path.startsWith('/') ? path : `/${path}`; return `${getApiBaseUrlSync()}${normalizedPath}`; } async function parseJson(response: Response): Promise { const text = await response.text(); if (!text) return null; try { return JSON.parse(text); } catch { return null; } } export async function apiRequest(path: string, options: RequestOptions = {}): Promise { const response = await fetch(buildUrl(path), { method: options.method || 'GET', headers: { Accept: 'application/json', 'Content-Type': 'application/json', ...(options.token ? { Authorization: `Bearer ${options.token}` } : {}), }, body: options.body ? JSON.stringify(options.body) : undefined, }); const payload = await parseJson(response); if (!response.ok) { const message = (payload as { message?: string; error?: string } | null)?.message || (payload as { message?: string; error?: string } | null)?.error || `Request failed (${response.status}) for ${path}`; throw new Error(message); } return payload as T; } export async function checkBackendHealth(): Promise<{ status: string; [key: string]: unknown }> { return apiRequest<{ status: string; [key: string]: unknown }>('/health'); } export async function renderPrintLabel( token: string, context: Record, width = 584, height = 354 ): Promise { return apiRequest('/api/print/label', { method: 'POST', token, body: { context, width, height, }, }); } export async function loginWithEmailPassword(email: string, password: string): Promise { const payload = await apiRequest<{ token?: string }>('/auth/login', { method: 'POST', body: { email, password }, }); if (!payload?.token) { throw new Error('Login did not return a token.'); } return payload.token; } export async function fetchMe(token: string): Promise { return apiRequest('/api/me', { token, }); } export async function switchTenantRequest(tenantId: number, token: string): Promise { const payload = await apiRequest<{ token?: string }>('/api/tenant/switch', { method: 'POST', token, body: { tenant_id: String(tenantId) }, }); if (!payload?.token) { throw new Error('Tenant switch did not return a token.'); } return payload.token; } export async function fetchTasks(token: string): Promise { const tasks = await apiRequest('/api/resource/tasks', { token }); return (tasks || []).filter((task) => !task.archived); } export async function createTask( token: string, payload: { name: string; description?: string | null; categorie?: TaskStatus; userId?: string | null; project?: number | null; customer?: number | null; plant?: number | null; } ): Promise { return apiRequest('/api/resource/tasks', { method: 'POST', token, body: payload, }); } export async function updateTask(token: string, taskId: number, payload: Partial): Promise { return apiRequest(`/api/resource/tasks/${taskId}`, { method: 'PUT', token, body: payload, }); } export async function fetchTenantProfiles(token: string): Promise { const response = await apiRequest<{ data?: TenantProfile[] }>('/api/tenant/profiles', { token }); return response?.data || []; } export async function fetchStaffTimeSpans( token: string, targetUserId?: string ): Promise { const query = targetUserId ? `?targetUserId=${encodeURIComponent(targetUserId)}` : ''; const spans = await apiRequest(`/api/staff/time/spans${query}`, { token }); return (spans || []) .map((span) => { const started = span.startedAt ? new Date(span.startedAt) : null; const ended = span.endedAt ? new Date(span.endedAt) : new Date(); const durationMinutes = started && ended ? Math.max(0, Math.floor((ended.getTime() - started.getTime()) / 60000)) : 0; return { id: span.sourceEventIds?.[0] ?? null, eventIds: span.sourceEventIds || [], state: span.status || 'draft', started_at: span.startedAt, stopped_at: span.endedAt || null, duration_minutes: durationMinutes, user_id: targetUserId || null, type: span.type || 'work', description: span.payload?.description || '', } as StaffTimeSpan; }) .sort((a, b) => new Date(b.started_at).getTime() - new Date(a.started_at).getTime()); } export async function createStaffTimeEvent( token: string, payload: { eventtype: string; eventtime: string; user_id: string; description?: string; } ): Promise { await apiRequest('/api/staff/time/event', { method: 'POST', token, body: { eventtype: payload.eventtype, eventtime: payload.eventtime, user_id: payload.user_id, payload: payload.description ? { description: payload.description } : undefined, }, }); } export async function submitStaffTime(token: string, eventIds: string[]): Promise { await apiRequest('/api/staff/time/submit', { method: 'POST', token, body: { eventIds }, }); } export async function approveStaffTime( token: string, eventIds: string[], employeeUserId: string ): Promise { await apiRequest('/api/staff/time/approve', { method: 'POST', token, body: { eventIds, employeeUserId }, }); } export async function rejectStaffTime( token: string, eventIds: string[], employeeUserId: string, reason: string ): Promise { await apiRequest('/api/staff/time/reject', { method: 'POST', token, body: { eventIds, employeeUserId, reason }, }); } export async function fetchProjects(token: string, includeArchived = false): Promise { const projects = await apiRequest('/api/resource/projects', { token }); if (includeArchived) return projects || []; return (projects || []).filter((project) => !project.archived); } export async function createProject( token: string, payload: { name: string; projectNumber?: string | null; customer?: number | null; plant?: number | null; notes?: string | null; } ): Promise { return apiRequest('/api/resource/projects', { method: 'POST', token, body: payload, }); } export async function fetchCustomers(token: string, includeArchived = false): Promise { const customers = await apiRequest('/api/resource/customers', { token }); if (includeArchived) return customers || []; return (customers || []).filter((customer) => !customer.archived); } export async function createCustomer( token: string, payload: { name: string; customerNumber?: string | null; notes?: string | null; } ): Promise { return apiRequest('/api/resource/customers', { method: 'POST', token, body: payload, }); } export async function fetchCustomerById(token: string, customerId: number): Promise { return apiRequest(`/api/resource/customers/${customerId}`, { token }); } function resolveCustomerIdFromCustomerInventoryItem(item: CustomerInventoryItem): number | null { const rawCustomer = item.customer; if (!rawCustomer) return null; if (typeof rawCustomer === 'object') { return rawCustomer.id ? Number(rawCustomer.id) : null; } return Number(rawCustomer); } export async function fetchCustomerInventoryItems( token: string, customerId: number, includeArchived = false ): Promise { const rows = await apiRequest('/api/resource/customerinventoryitems', { token }); return (rows || []).filter((item) => { if (!includeArchived && item.archived) return false; return resolveCustomerIdFromCustomerInventoryItem(item) === Number(customerId); }); } export async function fetchAllCustomerInventoryItems( token: string, includeArchived = false ): Promise { const rows = await apiRequest('/api/resource/customerinventoryitems', { token }); if (includeArchived) return rows || []; return (rows || []).filter((item) => !item.archived); } export async function createCustomerInventoryItem( token: string, payload: { customer: number; name: string; customerInventoryId?: string | null; serialNumber?: string | null; description?: string | null; quantity?: number | null; } ): Promise { const autoInventoryId = `MOB-${Date.now()}`; return apiRequest('/api/resource/customerinventoryitems', { method: 'POST', token, body: { customer: payload.customer, name: payload.name, customerInventoryId: payload.customerInventoryId?.trim() || autoInventoryId, serialNumber: payload.serialNumber?.trim() || null, description: payload.description?.trim() || null, quantity: Number.isFinite(Number(payload.quantity)) ? Number(payload.quantity) : 1, }, }); } export async function fetchPlants(token: string, includeArchived = false): Promise { const plants = await apiRequest('/api/resource/plants', { token }); if (includeArchived) return plants || []; return (plants || []).filter((plant) => !plant.archived); } export async function createPlant( token: string, payload: { name: string; description?: string | null; customer?: number | null; } ): Promise { return apiRequest('/api/resource/plants', { method: 'POST', token, body: payload, }); } export async function fetchPlantById(token: string, plantId: number): Promise { return apiRequest(`/api/resource/plants/${plantId}`, { token }); } function toQueryString(params: Record): string { const searchParams = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value === undefined || value === null || value === '') return; searchParams.append(key, String(value)); }); const query = searchParams.toString(); return query ? `?${query}` : ''; } export async function fetchWikiTree( token: string, filters: { entityType?: string; entityId?: number | null; entityUuid?: string | null; } = {} ): Promise { const query = toQueryString({ entityType: filters.entityType, entityId: filters.entityId, entityUuid: filters.entityUuid, }); return apiRequest(`/api/wiki/tree${query}`, { token }); } export async function fetchWikiPageById(token: string, pageId: string): Promise { return apiRequest(`/api/wiki/${encodeURIComponent(pageId)}`, { token }); } export async function createWikiPage( token: string, payload: { title: string; parentId?: string | null; isFolder?: boolean; entityType?: string; entityId?: number | null; entityUuid?: string | null; } ): Promise { return apiRequest('/api/wiki', { method: 'POST', token, body: payload, }); } export async function updateWikiPage( token: string, pageId: string, payload: { title?: string; content?: unknown; parentId?: string | null; sortOrder?: number; isFolder?: boolean; } ): Promise { return apiRequest(`/api/wiki/${encodeURIComponent(pageId)}`, { method: 'PATCH', token, body: payload, }); } export async function deleteWikiPage(token: string, pageId: string): Promise<{ success: boolean; deletedId?: string }> { return apiRequest<{ success: boolean; deletedId?: string }>(`/api/wiki/${encodeURIComponent(pageId)}`, { method: 'DELETE', token, }); } export async function fetchProjectById(token: string, projectId: number): Promise { return apiRequest(`/api/resource/projects/${projectId}`, { token }); } function resolveProjectIdFromTask(task: Task): number | null { const rawProject = task.project; if (!rawProject) return null; if (typeof rawProject === 'object') { return rawProject.id ? Number(rawProject.id) : null; } return Number(rawProject); } export async function fetchProjectTasks(token: string, projectId: number): Promise { const tasks = await fetchTasks(token); return (tasks || []).filter((task) => resolveProjectIdFromTask(task) === Number(projectId)); } export async function createProjectTask( token: string, payload: { projectId: number; name: string; description?: string | null; userId?: string | null; categorie?: TaskStatus; } ): Promise { return createTask(token, { name: payload.name, description: payload.description || null, userId: payload.userId || null, categorie: payload.categorie || 'Offen', project: payload.projectId, }); } function resolveProjectIdFromFile(file: ProjectFile): number | null { const rawProject = file.project; if (!rawProject) return null; if (typeof rawProject === 'object') { return rawProject.id ? Number(rawProject.id) : null; } return Number(rawProject); } function resolveCustomerIdFromFile(file: ProjectFile): number | null { const rawCustomer = file.customer; if (!rawCustomer) return null; if (typeof rawCustomer === 'object') { return rawCustomer.id ? Number(rawCustomer.id) : null; } return Number(rawCustomer); } function resolvePlantIdFromFile(file: ProjectFile): number | null { const rawPlant = file.plant; if (!rawPlant) return null; if (typeof rawPlant === 'object') { return rawPlant.id ? Number(rawPlant.id) : null; } return Number(rawPlant); } function resolveCreatedDocumentIdFromFile(file: ProjectFile): number | null { const rawCreatedDocument = file.createddocument; if (!rawCreatedDocument) return null; if (typeof rawCreatedDocument === 'object') { return rawCreatedDocument.id ? Number(rawCreatedDocument.id) : null; } return Number(rawCreatedDocument); } function resolveCustomerIdFromCreatedDocument(doc: CreatedDocument): number | null { const rawCustomer = doc.customer; if (!rawCustomer) return null; if (typeof rawCustomer === 'object') { return rawCustomer.id ? Number(rawCustomer.id) : null; } return Number(rawCustomer); } export async function fetchProjectFiles(token: string, projectId: number): Promise { const files = await apiRequest('/api/resource/files', { token }); const projectFiles = (files || []).filter((file) => { if (file.archived) return false; return resolveProjectIdFromFile(file) === Number(projectId); }); if (projectFiles.length === 0) return []; const presigned = await apiRequest<{ files?: ProjectFile[] }>('/api/files/presigned', { method: 'POST', token, body: { ids: projectFiles.map((file) => file.id) }, }); return presigned.files || []; } export async function fetchCustomerFiles(token: string, customerId: number): Promise { const files = await apiRequest('/api/resource/files', { token }); const customerFiles = (files || []).filter((file) => { if (file.archived) return false; return resolveCustomerIdFromFile(file) === Number(customerId); }); if (customerFiles.length === 0) return []; const presigned = await apiRequest<{ files?: ProjectFile[] }>('/api/files/presigned', { method: 'POST', token, body: { ids: customerFiles.map((file) => file.id) }, }); return presigned.files || []; } export async function fetchPlantFiles(token: string, plantId: number): Promise { const files = await apiRequest('/api/resource/files', { token }); const plantFiles = (files || []).filter((file) => { if (file.archived) return false; return resolvePlantIdFromFile(file) === Number(plantId); }); if (plantFiles.length === 0) return []; const presigned = await apiRequest<{ files?: ProjectFile[] }>('/api/files/presigned', { method: 'POST', token, body: { ids: plantFiles.map((file) => file.id) }, }); return presigned.files || []; } export async function fetchCustomerCreatedDocuments( token: string, customerId: number ): Promise { const docs = await apiRequest('/api/resource/createddocuments', { token }); return (docs || []) .filter((doc) => !doc.archived && resolveCustomerIdFromCreatedDocument(doc) === Number(customerId)) .sort((a, b) => { const dateA = new Date(String(a.documentDate || '')).getTime(); const dateB = new Date(String(b.documentDate || '')).getTime(); return dateB - dateA; }); } export async function fetchCreatedDocumentFiles( token: string, createdDocumentId: number ): Promise { const files = await apiRequest('/api/resource/files', { token }); const createdDocumentFiles = (files || []).filter((file) => { if (file.archived) return false; return resolveCreatedDocumentIdFromFile(file) === Number(createdDocumentId); }); if (createdDocumentFiles.length === 0) return []; const presigned = await apiRequest<{ files?: ProjectFile[] }>('/api/files/presigned', { method: 'POST', token, body: { ids: createdDocumentFiles.map((file) => file.id) }, }); return presigned.files || []; } export async function uploadProjectFile( token: string, payload: { projectId: number; uri: string; filename: string; mimeType?: string; } ): Promise { const formData = new FormData(); formData.append( 'file', { uri: payload.uri, name: payload.filename, type: payload.mimeType || 'application/octet-stream', } as any ); formData.append( 'meta', JSON.stringify({ project: payload.projectId, name: payload.filename, mimeType: payload.mimeType || 'application/octet-stream', }) ); const response = await fetch(buildUrl('/api/files/upload'), { method: 'POST', headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), Accept: 'application/json', }, body: formData, }); const parsed = await parseJson(response); if (!response.ok) { const message = (parsed as { message?: string; error?: string } | null)?.message || (parsed as { message?: string; error?: string } | null)?.error || 'Upload fehlgeschlagen.'; throw new Error(message); } return parsed as ProjectFile; } export async function uploadCustomerFile( token: string, payload: { customerId: number; uri: string; filename: string; mimeType?: string; } ): Promise { const formData = new FormData(); formData.append( 'file', { uri: payload.uri, name: payload.filename, type: payload.mimeType || 'application/octet-stream', } as any ); formData.append( 'meta', JSON.stringify({ customer: payload.customerId, name: payload.filename, mimeType: payload.mimeType || 'application/octet-stream', }) ); const response = await fetch(buildUrl('/api/files/upload'), { method: 'POST', headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), Accept: 'application/json', }, body: formData, }); const parsed = await parseJson(response); if (!response.ok) { const message = (parsed as { message?: string; error?: string } | null)?.message || (parsed as { message?: string; error?: string } | null)?.error || 'Upload fehlgeschlagen.'; throw new Error(message); } return parsed as ProjectFile; } export async function uploadPlantFile( token: string, payload: { plantId: number; uri: string; filename: string; mimeType?: string; } ): Promise { const formData = new FormData(); formData.append( 'file', { uri: payload.uri, name: payload.filename, type: payload.mimeType || 'application/octet-stream', } as any ); formData.append( 'meta', JSON.stringify({ plant: payload.plantId, name: payload.filename, mimeType: payload.mimeType || 'application/octet-stream', }) ); const response = await fetch(buildUrl('/api/files/upload'), { method: 'POST', headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), Accept: 'application/json', }, body: formData, }); const parsed = await parseJson(response); if (!response.ok) { const message = (parsed as { message?: string; error?: string } | null)?.message || (parsed as { message?: string; error?: string } | null)?.error || 'Upload fehlgeschlagen.'; throw new Error(message); } return parsed as ProjectFile; }