/** * 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 { 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 { 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 { 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 { 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 { await fetch(`${API_BASE}/api/projects/${id}`, { method: 'DELETE', headers: authHeaders(token), }); } // ─── Drawings ─────────────────────────────────────────── export async function getDrawings(token: string, projectId: string): Promise { 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 { 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 { 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 { 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): Promise { 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 { await fetch(`${API_BASE}/api/elements/${elementId}`, { method: 'DELETE', headers: authHeaders(token), }); } // ─── Layers ───────────────────────────────────────────── export async function getLayers(token: string, drawingId: string): Promise { 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 { 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): Promise { 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 { await fetch(`${API_BASE}/api/layers/${layerId}`, { method: 'DELETE', headers: authHeaders(token), }); } // ─── Blocks ───────────────────────────────────────────── export async function getBlocks(token: string, drawingId: string): Promise { 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 { 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): Promise { 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 { 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 { 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 { const raw = await getElements(token, drawingId); return raw.map(dbElementToFrontend); } export async function createElementTyped(token: string, drawingId: string, el: CADElement): Promise { const raw = await createElement(token, drawingId, frontendElementToDb(el, drawingId)); return dbElementToFrontend(raw); } export async function getLayersTyped(token: string, drawingId: string): Promise { const raw = await getLayers(token, drawingId); return raw.map(dbLayerToFrontend); } export async function createLayerTyped(token: string, drawingId: string, layer: CADLayer): Promise { const raw = await createLayer(token, drawingId, frontendLayerToDb(layer, drawingId)); return dbLayerToFrontend(raw); } export async function getBlocksTyped(token: string, drawingId: string): Promise { const raw = await getBlocks(token, drawingId); return raw.map(dbBlockToFrontend); } export async function createBlockTyped(token: string, drawingId: string, block: BlockDefinition): Promise { const raw = await createBlock(token, drawingId, frontendBlockToDb(block, drawingId)); return dbBlockToFrontend(raw); } const projectLoadCache = new Map>(); export async function loadProjectDataTyped(token: string, projectId: string): Promise { // 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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; } 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(); }