/** * Dashboard Page - 3-column layout with project folder tree, project cards, and info panel */ import { useState, useEffect, useCallback, useMemo } from 'react'; import { useAuth } from '../contexts/AuthContext'; import { NotificationPanel } from '../components/NotificationPanel'; import { ShareDialog } from '../components/ShareDialog'; import TreeView from '../components/TreeView'; import type { TreeNode } from '../types/ui.types'; import { getProjects, deleteProject, getProjectFolders, createProjectFolder, renameProjectFolder, deleteProjectFolder, moveProjectToFolder, getProjectShares, type Project, type ProjectFolder, type ProjectShare, } from '../services/api'; const API_BASE = import.meta.env.VITE_API_BASE || ''; const ROOT_FOLDER_ID = '__root__'; interface DashboardProps { onOpenProject: (projectId: string) => void; } export function Dashboard({ onOpenProject }: DashboardProps) { const { user, logout, token } = useAuth(); const [projects, setProjects] = useState([]); const [allProjects, setAllProjects] = useState([]); const [folders, setFolders] = useState([]); const [loading, setLoading] = useState(true); const [showCreate, setShowCreate] = useState(false); const [newName, setNewName] = useState(''); const [newDesc, setNewDesc] = useState(''); const [shareProjectId, setShareProjectId] = useState(null); const [selectedFolderId, setSelectedFolderId] = useState(ROOT_FOLDER_ID); const [selectedProjectId, setSelectedProjectId] = useState(null); const [shares, setShares] = useState([]); const [sharesLoading, setSharesLoading] = useState(false); const [draggedProjectId, setDraggedProjectId] = useState(null); const [contextMenu, setContextMenu] = useState<{ x: number; y: number; folderId: string | null } | null>(null); const [renamingFolderId, setRenamingFolderId] = useState(null); const [renameValue, setRenameValue] = useState(''); const [showCreateFolder, setShowCreateFolder] = useState(false); const [newFolderName, setNewFolderName] = useState(''); const [newFolderParent, setNewFolderParent] = useState(null); const fetchAll = useCallback(async () => { if (!token) return; setLoading(true); try { const [projs, fldrs] = await Promise.all([ getProjects(token), getProjectFolders(token), ]); setAllProjects(projs); setFolders(fldrs); } catch { // ignore } finally { setLoading(false); } }, [token]); useEffect(() => { fetchAll(); }, [fetchAll]); useEffect(() => { if (selectedFolderId === ROOT_FOLDER_ID) { setProjects(allProjects); } else if (selectedFolderId === null) { setProjects(allProjects.filter(p => p.folder_id === null)); } else { setProjects(allProjects.filter(p => p.folder_id === selectedFolderId)); } }, [allProjects, selectedFolderId]); useEffect(() => { if (!token || !selectedProjectId) { setShares([]); return; } setSharesLoading(true); getProjectShares(token, selectedProjectId) .then(s => setShares(s)) .catch(() => setShares([])) .finally(() => setSharesLoading(false)); }, [token, selectedProjectId]); const selectedProject = useMemo( () => allProjects.find(p => p.id === selectedProjectId) ?? null, [allProjects, selectedProjectId], ); const treeNodes = useMemo((): TreeNode[] => { const rootProjectsCount = allProjects.filter(p => p.folder_id === null).length; const root: TreeNode = { id: ROOT_FOLDER_ID, name: 'Alle Projekte', count: allProjects.length, expanded: true, children: [ { id: '__unfiled__', name: 'Ohne Ordner', count: rootProjectsCount }, ], }; const buildFolderNode = (folder: ProjectFolder): TreeNode => { const childFolders = folders.filter(f => f.parent_id === folder.id); const projCount = allProjects.filter(p => p.folder_id === folder.id).length; return { id: folder.id, name: folder.name, count: projCount, expanded: true, children: childFolders.map(buildFolderNode), }; }; const topFolders = folders.filter(f => f.parent_id === null); root.children!.push(...topFolders.map(buildFolderNode)); return [root]; }, [folders, allProjects]); const folderNameById = useMemo(() => { const map = new Map(); map.set(ROOT_FOLDER_ID, 'Alle Projekte'); map.set('__unfiled__', 'Ohne Ordner'); for (const f of folders) map.set(f.id, f.name); return map; }, [folders]); const handleCreate = async () => { if (!newName.trim() || !token) return; try { const folderId = selectedFolderId === ROOT_FOLDER_ID || selectedFolderId === '__unfiled__' ? null : selectedFolderId; const res = await fetch(`${API_BASE}/api/projects`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify({ name: newName, description: newDesc || null, folder_id: folderId }), }); if (res.ok) { setNewName(''); setNewDesc(''); setShowCreate(false); fetchAll(); } } catch { // ignore } }; const handleDelete = async (id: string, e: React.MouseEvent) => { e.stopPropagation(); if (!confirm('Projekt wirklich loeschen?')) return; if (!token) return; try { await deleteProject(token, id); if (selectedProjectId === id) setSelectedProjectId(null); fetchAll(); } catch { // ignore } }; const handleFolderSelect = (id: string) => { if (id === ROOT_FOLDER_ID) { setSelectedFolderId(ROOT_FOLDER_ID); } else if (id === '__unfiled__') { setSelectedFolderId(null); } else { setSelectedFolderId(id); } }; const handleCreateFolder = async () => { if (!newFolderName.trim() || !token) return; const parentId = newFolderParent === ROOT_FOLDER_ID || newFolderParent === '__unfiled__' ? null : newFolderParent; try { await createProjectFolder(token, newFolderName, parentId); setNewFolderName(''); setShowCreateFolder(false); setNewFolderParent(null); fetchAll(); } catch { // ignore } }; const handleRenameFolder = async (id: string) => { if (!renameValue.trim() || !token) return; try { await renameProjectFolder(token, id, renameValue); setRenamingFolderId(null); setRenameValue(''); fetchAll(); } catch { // ignore } }; const handleDeleteFolder = async (id: string) => { if (!token) return; if (!confirm('Ordner loeschen? Projekte darin werden nach "Ohne Ordner" verschoben.')) return; try { await deleteProjectFolder(token, id); if (selectedFolderId === id) setSelectedFolderId(ROOT_FOLDER_ID); fetchAll(); } catch { // ignore } }; const handleProjectCardClick = (projectId: string) => { setSelectedProjectId(projectId); }; const handleProjectDragStart = (e: React.DragEvent, projectId: string) => { setDraggedProjectId(projectId); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', `project:${projectId}`); }; const handleFolderDragOver = (e: React.DragEvent) => { if (!draggedProjectId) return; e.preventDefault(); e.dataTransfer.dropEffect = 'move'; (e.currentTarget as HTMLElement).classList.add('tree-drop-target'); }; const handleFolderDragLeave = (e: React.DragEvent) => { (e.currentTarget as HTMLElement).classList.remove('tree-drop-target'); }; const handleFolderDrop = async (e: React.DragEvent, folderId: string) => { e.preventDefault(); e.stopPropagation(); (e.currentTarget as HTMLElement).classList.remove('tree-drop-target'); if (!draggedProjectId || !token) return; const targetFolderId = folderId === ROOT_FOLDER_ID || folderId === '__unfiled__' ? null : folderId; try { await moveProjectToFolder(token, draggedProjectId, targetFolderId); fetchAll(); } catch { // ignore } finally { setDraggedProjectId(null); } }; const handleContextMenu = (e: React.MouseEvent, folderId: string | null) => { e.preventDefault(); e.stopPropagation(); setContextMenu({ x: e.clientX, y: e.clientY, folderId }); }; const handleContextAction = (action: string, folderId: string | null) => { setContextMenu(null); if (action === 'new') { setNewFolderParent(folderId); setShowCreateFolder(true); } else if (action === 'rename' && folderId) { const folder = folders.find(f => f.id === folderId); if (folder) { setRenamingFolderId(folderId); setRenameValue(folder.name); } } else if (action === 'delete' && folderId) { handleDeleteFolder(folderId); } }; const formatDate = (dateStr: string) => { return new Date(dateStr).toLocaleDateString('de-DE', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', }); }; const renderFolderActions = (node: TreeNode) => { if (node.id === ROOT_FOLDER_ID || node.id === '__unfiled__') return null; return ( ); }; return (

Web CAD

{user?.name} ({user?.role})
{token && }
handleContextMenu(e, null)}>

Ordner

{showCreateFolder && (
setNewFolderName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleCreateFolder(); if (e.key === 'Escape') setShowCreateFolder(false); }} autoFocus />
)} {renamingFolderId && (
setRenameValue(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleRenameFolder(renamingFolderId); if (e.key === 'Escape') { setRenamingFolderId(null); setRenameValue(''); } }} onBlur={() => { if (renameValue.trim()) handleRenameFolder(renamingFolderId); else { setRenamingFolderId(null); setRenameValue(''); } }} autoFocus />
)}
{folders.map(folder => (
handleFolderDrop(e, folder.id)} /> ))}

{folderNameById.get(selectedFolderId ?? ROOT_FOLDER_ID) ?? 'Projekte'}

{showCreate && (
setNewName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleCreate(); }} autoFocus /> setNewDesc(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleCreate(); }} />
)} {loading ? (

Lade Projekte...

) : projects.length === 0 ? (

Keine Projekte in dieser Ansicht. Erstellen Sie ein neues Projekt.

) : (
{projects.map(project => (
handleProjectCardClick(project.id)} draggable onDragStart={(e) => handleProjectDragStart(e, project.id)} onDragEnd={() => setDraggedProjectId(null)} >

{project.name}

{project.description &&

{project.description}

}
{new Date(project.created_at).toLocaleDateString('de-DE')}
))}
)}
{selectedProject ? ( <>

{selectedProject.name}

{selectedProject.description && (

{selectedProject.description}

)}

Metadaten

Erstellt
{formatDate(selectedProject.created_at)}
Aktualisiert
{formatDate(selectedProject.updated_at)}
Owner
{selectedProject.owner_id}
Ordner
{selectedProject.folder_id ? (folderNameById.get(selectedProject.folder_id) ?? 'Unbekannt') : 'Ohne Ordner'}

Sichtbarkeit / Freigaben

{sharesLoading ? (

Lade Freigaben...

) : shares.length === 0 ? (

Keine Freigaben

) : (
    {shares.map(share => (
  • {share.shared_with_email} {share.permission}
  • ))}
)}
) : (

Waehlen Sie ein Projekt aus, um Details zu sehen.

)}
{contextMenu && ( <>
setContextMenu(null)} />
{contextMenu.folderId && contextMenu.folderId !== ROOT_FOLDER_ID && contextMenu.folderId !== '__unfiled__' && ( <> )}
)} {token && ( setShareProjectId(null)} /> )}
); }