Files
web-cad/frontend/src/services/api.ts
T

499 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* API Service Backend communication for projects, drawings, elements, layers, blocks
*/
import type { CADElement, CADLayer, BlockDefinition } from '../types/cad.types';
const API_BASE = import.meta.env.VITE_API_BASE || '';
function authHeaders(token: string): Record<string, string> {
return {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
};
}
// ─── Types ──────────────────────────────────────────────
export interface Project {
id: string;
name: string;
description: string | null;
owner_id: string;
created_at: string;
updated_at: string;
}
export interface Drawing {
id: string;
project_id: string;
name: string;
created_at: string;
updated_at: string;
}
// ─── Auth ───────────────────────────────────────────────
export async function login(email: string, password: string): Promise<{ user: any; session: { token: string } }> {
const res = await fetch(`${API_BASE}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!res.ok) throw new Error('Login failed');
return res.json();
}
export async function register(email: string, password: string, name: string): Promise<{ user: any; session: { token: string } }> {
const res = await fetch(`${API_BASE}/api/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, name }),
});
if (!res.ok) throw new Error('Registration failed');
return res.json();
}
export async function getMe(token: string): Promise<any> {
const res = await fetch(`${API_BASE}/api/auth/me`, {
headers: authHeaders(token),
});
if (!res.ok) throw new Error('Not authenticated');
return res.json();
}
// ─── Projects ───────────────────────────────────────────
export async function getProjects(token: string): Promise<Project[]> {
const res = await fetch(`${API_BASE}/api/projects`, { headers: authHeaders(token) });
if (!res.ok) throw new Error('Failed to load projects');
return res.json();
}
export async function createProject(token: string, name: string, description?: string): Promise<Project> {
const res = await fetch(`${API_BASE}/api/projects`, {
method: 'POST',
headers: authHeaders(token),
body: JSON.stringify({ name, description: description || null }),
});
if (!res.ok) throw new Error('Failed to create project');
return res.json();
}
export async function deleteProject(token: string, id: string): Promise<void> {
await fetch(`${API_BASE}/api/projects/${id}`, {
method: 'DELETE',
headers: authHeaders(token),
});
}
// ─── Drawings ───────────────────────────────────────────
export async function getDrawings(token: string, projectId: string): Promise<Drawing[]> {
const res = await fetch(`${API_BASE}/api/projects/${projectId}/drawings`, { headers: authHeaders(token) });
if (!res.ok) throw new Error('Failed to load drawings');
return res.json();
}
export async function createDrawing(token: string, projectId: string, name: string): Promise<Drawing> {
const res = await fetch(`${API_BASE}/api/projects/${projectId}/drawings`, {
method: 'POST',
headers: authHeaders(token),
body: JSON.stringify({ name }),
});
if (!res.ok) throw new Error('Failed to create drawing');
return res.json();
}
// ─── Elements ───────────────────────────────────────────
export async function getElements(token: string, drawingId: string): Promise<CADElement[]> {
const res = await fetch(`${API_BASE}/api/drawings/${drawingId}/elements`, { headers: authHeaders(token) });
if (!res.ok) throw new Error('Failed to load elements');
return res.json();
}
export async function createElement(token: string, drawingId: string, el: CADElement): Promise<CADElement> {
const res = await fetch(`${API_BASE}/api/drawings/${drawingId}/elements`, {
method: 'POST',
headers: authHeaders(token),
body: JSON.stringify(el),
});
if (!res.ok) throw new Error('Failed to create element');
return res.json();
}
export async function updateElement(token: string, elementId: string, patch: Partial<CADElement>): Promise<CADElement> {
const res = await fetch(`${API_BASE}/api/elements/${elementId}`, {
method: 'PATCH',
headers: authHeaders(token),
body: JSON.stringify(patch),
});
if (!res.ok) throw new Error('Failed to update element');
return res.json();
}
export async function deleteElement(token: string, elementId: string): Promise<void> {
await fetch(`${API_BASE}/api/elements/${elementId}`, {
method: 'DELETE',
headers: authHeaders(token),
});
}
// ─── Layers ─────────────────────────────────────────────
export async function getLayers(token: string, drawingId: string): Promise<CADLayer[]> {
const res = await fetch(`${API_BASE}/api/drawings/${drawingId}/layers`, { headers: authHeaders(token) });
if (!res.ok) throw new Error('Failed to load layers');
return res.json();
}
export async function createLayer(token: string, drawingId: string, layer: CADLayer): Promise<CADLayer> {
const res = await fetch(`${API_BASE}/api/drawings/${drawingId}/layers`, {
method: 'POST',
headers: authHeaders(token),
body: JSON.stringify(layer),
});
if (!res.ok) throw new Error('Failed to create layer');
return res.json();
}
export async function updateLayer(token: string, layerId: string, patch: Partial<CADLayer>): Promise<CADLayer> {
const res = await fetch(`${API_BASE}/api/layers/${layerId}`, {
method: 'PATCH',
headers: authHeaders(token),
body: JSON.stringify(patch),
});
if (!res.ok) throw new Error('Failed to update layer');
return res.json();
}
export async function deleteLayer(token: string, layerId: string): Promise<void> {
await fetch(`${API_BASE}/api/layers/${layerId}`, {
method: 'DELETE',
headers: authHeaders(token),
});
}
// ─── Blocks ─────────────────────────────────────────────
export async function getBlocks(token: string, drawingId: string): Promise<BlockDefinition[]> {
const res = await fetch(`${API_BASE}/api/drawings/${drawingId}/blocks`, { headers: authHeaders(token) });
if (!res.ok) throw new Error('Failed to load blocks');
return res.json();
}
export async function createBlock(token: string, drawingId: string, block: BlockDefinition): Promise<BlockDefinition> {
const res = await fetch(`${API_BASE}/api/drawings/${drawingId}/blocks`, {
method: 'POST',
headers: authHeaders(token),
body: JSON.stringify(block),
});
if (!res.ok) throw new Error('Failed to create block');
return res.json();
}
export async function updateBlock(token: string, blockId: string, patch: Partial<BlockDefinition>): Promise<BlockDefinition> {
const res = await fetch(`${API_BASE}/api/blocks/${blockId}`, {
method: 'PATCH',
headers: authHeaders(token),
body: JSON.stringify(patch),
});
if (!res.ok) throw new Error('Failed to update block');
return res.json();
}
export async function deleteBlock(token: string, blockId: string): Promise<void> {
await fetch(`${API_BASE}/api/blocks/${blockId}`, {
method: 'DELETE',
headers: authHeaders(token),
});
}
// ─── Composite: Load full project data ───────────────────
export interface ProjectData {
project: Project;
drawing: Drawing | null;
elements: CADElement[];
layers: CADLayer[];
blocks: BlockDefinition[];
}
export async function loadProjectData(token: string, projectId: string): Promise<ProjectData> {
const drawings = await getDrawings(token, projectId);
const project = (await getProjects(token)).find(p => p.id === projectId);
if (!project) throw new Error('Project not found');
// Use first drawing or create one
let drawing = drawings[0] || null;
if (!drawing) {
drawing = await createDrawing(token, projectId, 'Hauptzeichnung');
}
const [elements, layers, blocks] = await Promise.all([
getElements(token, drawing.id),
getLayers(token, drawing.id),
getBlocks(token, drawing.id),
]);
return { project, drawing, elements, layers, blocks };
}
// ─── Format Conversion: DB (snake_case) ↔ Frontend (camelCase) ─────
function dbElementToFrontend(dbEl: any): CADElement {
return {
id: dbEl.id,
type: dbEl.type,
layerId: dbEl.layer_id,
x: dbEl.x,
y: dbEl.y,
width: dbEl.width,
height: dbEl.height,
properties: typeof dbEl.properties_json === 'string'
? JSON.parse(dbEl.properties_json)
: (dbEl.properties_json || {}),
};
}
function frontendElementToDb(el: CADElement, drawingId: string): any {
return {
id: el.id,
drawing_id: drawingId,
layer_id: el.layerId,
type: el.type,
x: el.x,
y: el.y,
width: el.width,
height: el.height,
properties_json: JSON.stringify(el.properties),
};
}
function dbLayerToFrontend(dbLayer: any): CADLayer {
return {
id: dbLayer.id,
name: dbLayer.name,
visible: !!dbLayer.visible,
locked: !!dbLayer.locked,
color: dbLayer.color,
lineType: dbLayer.line_type as 'solid' | 'dashed' | 'dotted',
transparency: dbLayer.transparency,
sortOrder: dbLayer.sort_order,
parentId: dbLayer.parent_id,
};
}
function frontendLayerToDb(layer: CADLayer, drawingId: string): any {
return {
id: layer.id,
drawing_id: drawingId,
name: layer.name,
visible: layer.visible ? 1 : 0,
locked: layer.locked ? 1 : 0,
color: layer.color,
line_type: layer.lineType,
transparency: layer.transparency,
sort_order: layer.sortOrder,
parent_id: layer.parentId,
};
}
function dbBlockToFrontend(dbBlock: any): BlockDefinition {
return {
id: dbBlock.id,
name: dbBlock.name,
description: dbBlock.description || '',
category: dbBlock.category,
elements: typeof dbBlock.elements_json === 'string'
? JSON.parse(dbBlock.elements_json)
: (dbBlock.elements_json || []),
thumbnail: dbBlock.thumbnail || undefined,
};
}
function frontendBlockToDb(block: BlockDefinition, drawingId: string): any {
return {
id: block.id,
drawing_id: drawingId,
name: block.name,
description: block.description,
category: block.category,
elements_json: JSON.stringify(block.elements),
thumbnail: block.thumbnail || null,
};
}
// ─── Typed API calls with conversion ─────────────────────
export async function getElementsTyped(token: string, drawingId: string): Promise<CADElement[]> {
const raw = await getElements(token, drawingId);
return raw.map(dbElementToFrontend);
}
export async function createElementTyped(token: string, drawingId: string, el: CADElement): Promise<CADElement> {
const raw = await createElement(token, drawingId, frontendElementToDb(el, drawingId));
return dbElementToFrontend(raw);
}
export async function getLayersTyped(token: string, drawingId: string): Promise<CADLayer[]> {
const raw = await getLayers(token, drawingId);
return raw.map(dbLayerToFrontend);
}
export async function createLayerTyped(token: string, drawingId: string, layer: CADLayer): Promise<CADLayer> {
const raw = await createLayer(token, drawingId, frontendLayerToDb(layer, drawingId));
return dbLayerToFrontend(raw);
}
export async function getBlocksTyped(token: string, drawingId: string): Promise<BlockDefinition[]> {
const raw = await getBlocks(token, drawingId);
return raw.map(dbBlockToFrontend);
}
export async function createBlockTyped(token: string, drawingId: string, block: BlockDefinition): Promise<BlockDefinition> {
const raw = await createBlock(token, drawingId, frontendBlockToDb(block, drawingId));
return dbBlockToFrontend(raw);
}
const projectLoadCache = new Map<string, Promise<ProjectData>>();
export async function loadProjectDataTyped(token: string, projectId: string): Promise<ProjectData> {
// Dedup concurrent calls (React StrictMode double-render)
const existing = projectLoadCache.get(projectId);
if (existing) return existing;
const promise = (async () => {
const drawings = await getDrawings(token, projectId);
const projects = await getProjects(token);
const project = projects.find(p => p.id === projectId);
if (!project) throw new Error('Project not found');
let drawing = drawings[0] || null;
if (!drawing) {
drawing = await createDrawing(token, projectId, 'Hauptzeichnung');
}
const [elementsRaw, layersRaw, blocksRaw] = await Promise.all([
getElements(token, drawing.id),
getLayers(token, drawing.id),
getBlocks(token, drawing.id),
]);
return {
project,
drawing,
elements: elementsRaw.map(dbElementToFrontend),
layers: layersRaw.map(dbLayerToFrontend),
blocks: blocksRaw.map(dbBlockToFrontend),
};
})();
projectLoadCache.set(projectId, promise);
promise.finally(() => projectLoadCache.delete(projectId));
return promise;
}
// ─── Notifications ───────────────────────────────────────
export interface NotificationItem {
id: string;
user_id: string;
type: string;
title: string;
message: string;
read: number;
created_at: string;
}
export async function getNotifications(token: string): Promise<NotificationItem[]> {
const res = await fetch(`${API_BASE}/api/notifications`, { headers: authHeaders(token) });
if (!res.ok) throw new Error('Failed to load notifications');
return res.json();
}
export async function createNotification(token: string, data: { type?: string; title: string; message: string }): Promise<NotificationItem> {
const res = await fetch(`${API_BASE}/api/notifications`, {
method: 'POST',
headers: { ...authHeaders(token), 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error('Failed to create notification');
return res.json();
}
export async function markNotificationRead(token: string, id: string): Promise<void> {
const res = await fetch(`${API_BASE}/api/notifications/${id}/read`, {
method: 'PATCH',
headers: authHeaders(token),
});
if (!res.ok) throw new Error('Failed to mark notification as read');
}
export async function deleteNotification(token: string, id: string): Promise<void> {
const res = await fetch(`${API_BASE}/api/notifications/${id}`, {
method: 'DELETE',
headers: authHeaders(token),
});
if (!res.ok) throw new Error('Failed to delete notification');
}
// ─── Project Shares ──────────────────────────────────────
export interface ProjectShare {
id: string;
project_id: string;
shared_with_email: string;
shared_by: string;
permission: string;
share_token: string | null;
created_at: string;
}
export async function getProjectShares(token: string, projectId: string): Promise<ProjectShare[]> {
const res = await fetch(`${API_BASE}/api/projects/${projectId}/shares`, { headers: authHeaders(token) });
if (!res.ok) throw new Error('Failed to load shares');
return res.json();
}
export async function createProjectShare(token: string, projectId: string, data: { shared_with_email: string; permission?: string }): Promise<ProjectShare> {
const res = await fetch(`${API_BASE}/api/projects/${projectId}/shares`, {
method: 'POST',
headers: { ...authHeaders(token), 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error('Failed to create share');
return res.json();
}
export async function deleteProjectShare(token: string, shareId: string): Promise<void> {
const res = await fetch(`${API_BASE}/api/shares/${shareId}`, {
method: 'DELETE',
headers: authHeaders(token),
});
if (!res.ok) throw new Error('Failed to delete share');
}
export { API_BASE };
// ─── AI Copilot ─────────────────────────────────────────
export interface AIChatMessage {
role: string;
content: string;
}
export interface AIChatContext {
projectName?: string;
elementCount?: number;
layerCount?: number;
elementTypeSummary?: Record<string, number>;
}
export async function aiChat(
token: string,
messages: AIChatMessage[],
context?: AIChatContext
): Promise<{ content: string; suggestions?: string[] }> {
const res = await fetch(`${API_BASE}/api/ai/chat`, {
method: 'POST',
headers: authHeaders(token),
body: JSON.stringify({ messages, context }),
});
if (!res.ok) {
const err = await res.json().catch(() => ({ error: 'AI request failed' }));
throw new Error(err.error || 'AI request failed');
}
return res.json();
}