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

421 lines
14 KiB
TypeScript
Raw Normal View History

/**
* 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;
}
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();
}