diff --git a/backend/src/database/DatabaseInterface.ts b/backend/src/database/DatabaseInterface.ts index 380eacb..d1798e5 100644 --- a/backend/src/database/DatabaseInterface.ts +++ b/backend/src/database/DatabaseInterface.ts @@ -112,7 +112,7 @@ export interface DatabaseInterface { listUsers(): DBUser[]; // Projects - listProjects(): DBProject[]; + listProjects(ownerId?: string): DBProject[]; getProject(id: string): DBProject | null; createProject(data: Partial): DBProject; updateProject(id: string, data: Partial): DBProject | null; diff --git a/backend/src/database/SqliteAdapter.ts b/backend/src/database/SqliteAdapter.ts index 6fc4ea4..834d0ff 100644 --- a/backend/src/database/SqliteAdapter.ts +++ b/backend/src/database/SqliteAdapter.ts @@ -73,7 +73,10 @@ export class SqliteAdapter implements DatabaseInterface { } // ─── Projects ────────────────────────────────────── - listProjects(): DBProject[] { + listProjects(ownerId?: string): DBProject[] { + if (ownerId) { + return this.db.prepare('SELECT * FROM projects WHERE owner_id = ? ORDER BY updated_at DESC').all(ownerId) as DBProject[]; + } return this.db.prepare('SELECT * FROM projects ORDER BY updated_at DESC').all() as DBProject[]; } diff --git a/backend/src/routes/projects.ts b/backend/src/routes/projects.ts index d14acc2..22cc27f 100644 --- a/backend/src/routes/projects.ts +++ b/backend/src/routes/projects.ts @@ -7,10 +7,11 @@ import { requireAuth } from '../auth/authMiddleware.js'; import type { AuthService } from '../auth/AuthService.js'; export function registerProjectRoutes(fastify: FastifyInstance, db: DatabaseInterface, authService: AuthService) { - // List all projects + // List projects owned by the authenticated user fastify.get('/api/projects', async (request, reply) => { - if (!requireAuth(request, reply, authService)) return; - return db.listProjects(); + const user = requireAuth(request, reply, authService); + if (!user) return; + return db.listProjects(user.id); }); // Get single project @@ -24,10 +25,11 @@ export function registerProjectRoutes(fastify: FastifyInstance, db: DatabaseInte // Create project fastify.post('/api/projects', async (request, reply) => { - if (!requireAuth(request, reply, authService)) return; + const user = requireAuth(request, reply, authService); + if (!user) return; const body = request.body as Partial; if (!body.name) return reply.code(400).send({ error: 'Name is required' }); - return reply.code(201).send(db.createProject(body)); + return reply.code(201).send(db.createProject({ ...body, owner_id: user.id })); }); // Update project diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f4434b1..231e08a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -534,7 +534,13 @@ const CADEditor: React.FC = ({ projectId, token, onNavigateBack const handleRenameBlock = useCallback((id: string, name: string) => { setBlocks((prev) => prev.map((b) => b.id === id ? { ...b, name } : b)); setCommandHistory((prev) => [...prev, { prefix: '·', text: `Block umbenannt: ${name}`, type: 'info' }]); - }, []); + // Persist rename to backend + if (token) { + updateBlock(token, id, { name }).catch((err) => { + console.error('Failed to save block rename:', err); + }); + } + }, [token]); const handleDuplicateBlock = useCallback((id: string) => { setBlocks((prev) => { @@ -547,10 +553,16 @@ const CADEditor: React.FC = ({ projectId, token, onNavigateBack name: `${block.name} (Kopie)`, elements: block.elements.map((el) => ({ ...el, id: `${el.id}_copy_${Date.now()}` })), }; + // Persist duplicate to backend + if (drawingId && token) { + createBlockTyped(token, drawingId, copy).catch((err) => { + console.error('Failed to save duplicated block:', err); + }); + } return [...prev, copy]; }); setCommandHistory((prev) => [...prev, { prefix: '·', text: 'Block dupliziert', type: 'info' }]); - }, []); + }, [drawingId, token]); const handleDeleteBlock = useCallback((id: string) => { setBlocks((prev) => prev.filter((b) => b.id !== id)); @@ -643,15 +655,42 @@ const CADEditor: React.FC = ({ projectId, token, onNavigateBack const result = await importFile(file); if (result.success) { setElements((prev) => [...prev, ...result.elements]); + // Push imported elements to Yjs and backend + for (const el of result.elements) { + collab.setElement(el); + } + if (drawingId && token) { + setSavedStatus('Speichert…'); + Promise.all(result.elements.map(el => createElementTyped(token, drawingId, el))) + .then(() => setSavedStatus('gespeichert')) + .catch((err) => { + console.error('Failed to save imported elements:', err); + setSavedStatus('Fehler beim Speichern'); + }); + } if (result.layers && result.layers.length > 0) { - setLayers((prev) => { - const existingIds = new Set(prev.map(l => l.id)); - const newLayers = result.layers!.filter(l => !existingIds.has(l.id)); - return [...prev, ...newLayers]; - }); + const existingIds = new Set(layers.map(l => l.id)); + const newLayers = result.layers.filter(l => !existingIds.has(l.id)); + setLayers((prev) => [...prev, ...newLayers]); + // Push imported layers to Yjs and backend + for (const layer of newLayers) { + collab.setLayer(layer); + } + if (drawingId && token) { + Promise.all(newLayers.map(l => createLayerTyped(token, drawingId, l))) + .catch((err) => console.error('Failed to save imported layers:', err)); + } } if (result.blocks && result.blocks.length > 0) { setBlocks((prev) => [...prev, ...result.blocks!]); + // Push imported blocks to Yjs and backend + for (const block of result.blocks) { + collab.setBlock(block); + } + if (drawingId && token) { + Promise.all(result.blocks.map(b => createBlockTyped(token, drawingId, b))) + .catch((err) => console.error('Failed to save imported blocks:', err)); + } } setCommandHistory((prev) => [...prev, { prefix: '·', text: `Importiert: ${result.elements.length} Elemente aus ${file.name}`, type: 'info' }]); if (result.warnings.length > 0) { @@ -660,7 +699,7 @@ const CADEditor: React.FC = ({ projectId, token, onNavigateBack } else { setCommandHistory((prev) => [...prev, { prefix: '·', text: `Import fehlgeschlagen: ${result.error || 'Unbekannter Fehler'}`, type: 'info' }]); } - }, []); + }, [drawingId, token, collab, layers]); const handleExport = useCallback(async (format: ExportFormat) => { const projectData: ProjectData = { @@ -866,8 +905,20 @@ const CADEditor: React.FC = ({ projectId, token, onNavigateBack }); }, [drawingId, token]); const handleToggleLayer = useCallback((id: string) => { - setLayers((prev) => prev.map((l) => l.id === id ? { ...l, visible: !l.visible } : l)); - }, []); + setLayers((prev) => { + const updated = prev.map((l) => l.id === id ? { ...l, visible: !l.visible } : l); + const changed = updated.find((l) => l.id === id); + if (changed) { + collab.setLayer(changed); + if (token) { + updateLayer(token, id, { visible: changed.visible }).catch((err) => { + console.error('Failed to save layer visibility:', err); + }); + } + } + return updated; + }); + }, [token, collab]); const handleDeleteLayer = useCallback((id: string) => { setLayers((prev) => prev.filter((l) => l.id !== id)); // Delete from backend @@ -878,29 +929,95 @@ const CADEditor: React.FC = ({ projectId, token, onNavigateBack } }, [token]); const handleRenameLayer = useCallback((id: string, name: string) => { - setLayers((prev) => prev.map((l) => l.id === id ? { ...l, name } : l)); - }, []); + setLayers((prev) => { + const updated = prev.map((l) => l.id === id ? { ...l, name } : l); + const changed = updated.find((l) => l.id === id); + if (changed) { + collab.setLayer(changed); + if (token) { + updateLayer(token, id, { name }).catch((err) => { + console.error('Failed to save layer rename:', err); + }); + } + } + return updated; + }); + }, [token, collab]); const handleDuplicateLayer = useCallback((id: string) => { setLayers((prev) => { const layer = prev.find((l) => l.id === id); if (!layer) return prev; const newId = `layer-${Date.now()}`; const copy: CADLayer = { ...layer, id: newId, name: `${layer.name} (Kopie)`, sortOrder: prev.length }; + collab.setLayer(copy); + if (drawingId && token) { + createLayerTyped(token, drawingId, copy).catch((err) => { + console.error('Failed to save duplicated layer:', err); + }); + } return [...prev, copy]; }); - }, []); + }, [drawingId, token, collab]); const handleToggleLock = useCallback((id: string) => { - setLayers((prev) => prev.map((l) => l.id === id ? { ...l, locked: !l.locked } : l)); - }, []); + setLayers((prev) => { + const updated = prev.map((l) => l.id === id ? { ...l, locked: !l.locked } : l); + const changed = updated.find((l) => l.id === id); + if (changed) { + collab.setLayer(changed); + if (token) { + updateLayer(token, id, { locked: changed.locked }).catch((err) => { + console.error('Failed to save layer lock state:', err); + }); + } + } + return updated; + }); + }, [token, collab]); const handleUpdateLayerColor = useCallback((id: string, color: string) => { - setLayers((prev) => prev.map((l) => l.id === id ? { ...l, color } : l)); - }, []); + setLayers((prev) => { + const updated = prev.map((l) => l.id === id ? { ...l, color } : l); + const changed = updated.find((l) => l.id === id); + if (changed) { + collab.setLayer(changed); + if (token) { + updateLayer(token, id, { color }).catch((err) => { + console.error('Failed to save layer color:', err); + }); + } + } + return updated; + }); + }, [token, collab]); const handleUpdateLayerLineType = useCallback((id: string, lineType: 'solid' | 'dashed' | 'dotted') => { - setLayers((prev) => prev.map((l) => l.id === id ? { ...l, lineType } : l)); - }, []); + setLayers((prev) => { + const updated = prev.map((l) => l.id === id ? { ...l, lineType } : l); + const changed = updated.find((l) => l.id === id); + if (changed) { + collab.setLayer(changed); + if (token) { + updateLayer(token, id, { lineType }).catch((err) => { + console.error('Failed to save layer line type:', err); + }); + } + } + return updated; + }); + }, [token, collab]); const handleUpdateLayerTransparency = useCallback((id: string, transparency: number) => { - setLayers((prev) => prev.map((l) => l.id === id ? { ...l, transparency } : l)); - }, []); + setLayers((prev) => { + const updated = prev.map((l) => l.id === id ? { ...l, transparency } : l); + const changed = updated.find((l) => l.id === id); + if (changed) { + collab.setLayer(changed); + if (token) { + updateLayer(token, id, { transparency }).catch((err) => { + console.error('Failed to save layer transparency:', err); + }); + } + } + return updated; + }); + }, [token, collab]); const handleZoomIn = useCallback(() => { setCommandHistory((prev) => [...prev, { prefix: '·', text: 'Zoom: vergrößert', type: 'info' }]);