421 lines
14 KiB
TypeScript
421 lines
14 KiB
TypeScript
|
|
/**
|
|||
|
|
* 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 || 'http://localhost:3001';
|
|||
|
|
|
|||
|
|
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();
|
|||
|
|
}
|