Files
FEDEO/mobile/src/lib/api.ts
florianfederspiel 409db82368
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 2m50s
Build and Push Docker Images / build-frontend (push) Successful in 1m13s
Mobile Dev
2026-02-21 21:21:39 +01:00

917 lines
24 KiB
TypeScript

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<string, unknown> | null;
permissions: string[];
};
export type EncodedLabelRow = {
dataType: 'pixels' | 'void' | 'check';
rowNumber: number;
repeat: number;
rowData?: Uint8Array | number[] | Record<string, number>;
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<unknown> {
const text = await response.text();
if (!text) return null;
try {
return JSON.parse(text);
} catch {
return null;
}
}
export async function apiRequest<T>(path: string, options: RequestOptions = {}): Promise<T> {
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<string, unknown>,
width = 584,
height = 354
): Promise<PrintLabelResponse> {
return apiRequest<PrintLabelResponse>('/api/print/label', {
method: 'POST',
token,
body: {
context,
width,
height,
},
});
}
export async function loginWithEmailPassword(email: string, password: string): Promise<string> {
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<MeResponse> {
return apiRequest<MeResponse>('/api/me', {
token,
});
}
export async function switchTenantRequest(tenantId: number, token: string): Promise<string> {
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<Task[]> {
const tasks = await apiRequest<Task[]>('/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<Task> {
return apiRequest<Task>('/api/resource/tasks', {
method: 'POST',
token,
body: payload,
});
}
export async function updateTask(token: string, taskId: number, payload: Partial<Task>): Promise<Task> {
return apiRequest<Task>(`/api/resource/tasks/${taskId}`, {
method: 'PUT',
token,
body: payload,
});
}
export async function fetchTenantProfiles(token: string): Promise<TenantProfile[]> {
const response = await apiRequest<{ data?: TenantProfile[] }>('/api/tenant/profiles', { token });
return response?.data || [];
}
export async function fetchStaffTimeSpans(
token: string,
targetUserId?: string
): Promise<StaffTimeSpan[]> {
const query = targetUserId ? `?targetUserId=${encodeURIComponent(targetUserId)}` : '';
const spans = await apiRequest<any[]>(`/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<void> {
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<void> {
await apiRequest('/api/staff/time/submit', {
method: 'POST',
token,
body: { eventIds },
});
}
export async function approveStaffTime(
token: string,
eventIds: string[],
employeeUserId: string
): Promise<void> {
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<void> {
await apiRequest('/api/staff/time/reject', {
method: 'POST',
token,
body: { eventIds, employeeUserId, reason },
});
}
export async function fetchProjects(token: string, includeArchived = false): Promise<Project[]> {
const projects = await apiRequest<Project[]>('/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<Project> {
return apiRequest<Project>('/api/resource/projects', {
method: 'POST',
token,
body: payload,
});
}
export async function fetchCustomers(token: string, includeArchived = false): Promise<Customer[]> {
const customers = await apiRequest<Customer[]>('/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<Customer> {
return apiRequest<Customer>('/api/resource/customers', {
method: 'POST',
token,
body: payload,
});
}
export async function fetchCustomerById(token: string, customerId: number): Promise<Customer> {
return apiRequest<Customer>(`/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<CustomerInventoryItem[]> {
const rows = await apiRequest<CustomerInventoryItem[]>('/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<CustomerInventoryItem[]> {
const rows = await apiRequest<CustomerInventoryItem[]>('/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<CustomerInventoryItem> {
const autoInventoryId = `MOB-${Date.now()}`;
return apiRequest<CustomerInventoryItem>('/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<Plant[]> {
const plants = await apiRequest<Plant[]>('/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<Plant> {
return apiRequest<Plant>('/api/resource/plants', {
method: 'POST',
token,
body: payload,
});
}
export async function fetchPlantById(token: string, plantId: number): Promise<Plant> {
return apiRequest<Plant>(`/api/resource/plants/${plantId}`, { token });
}
function toQueryString(params: Record<string, unknown>): 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<WikiTreeItem[]> {
const query = toQueryString({
entityType: filters.entityType,
entityId: filters.entityId,
entityUuid: filters.entityUuid,
});
return apiRequest<WikiTreeItem[]>(`/api/wiki/tree${query}`, { token });
}
export async function fetchWikiPageById(token: string, pageId: string): Promise<WikiPage> {
return apiRequest<WikiPage>(`/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<WikiPage> {
return apiRequest<WikiPage>('/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<WikiPage> {
return apiRequest<WikiPage>(`/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<Project> {
return apiRequest<Project>(`/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<Task[]> {
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<Task> {
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<ProjectFile[]> {
const files = await apiRequest<ProjectFile[]>('/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<ProjectFile[]> {
const files = await apiRequest<ProjectFile[]>('/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<ProjectFile[]> {
const files = await apiRequest<ProjectFile[]>('/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<CreatedDocument[]> {
const docs = await apiRequest<CreatedDocument[]>('/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<ProjectFile[]> {
const files = await apiRequest<ProjectFile[]>('/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<ProjectFile> {
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<ProjectFile> {
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<ProjectFile> {
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;
}