2026-06-26 10:50:24 +02:00
|
|
|
|
/**
|
2026-07-03 17:21:42 +02:00
|
|
|
|
* Dashboard Page - 3-column layout with project folder tree, project cards, and info panel
|
2026-06-26 10:50:24 +02:00
|
|
|
|
*/
|
2026-07-03 17:21:42 +02:00
|
|
|
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
2026-06-26 10:50:24 +02:00
|
|
|
|
import { useAuth } from '../contexts/AuthContext';
|
2026-06-28 13:52:54 +02:00
|
|
|
|
import { NotificationPanel } from '../components/NotificationPanel';
|
|
|
|
|
|
import { ShareDialog } from '../components/ShareDialog';
|
2026-07-03 17:21:42 +02:00
|
|
|
|
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';
|
2026-06-26 10:50:24 +02:00
|
|
|
|
|
2026-06-26 17:48:24 +02:00
|
|
|
|
const API_BASE = import.meta.env.VITE_API_BASE || '';
|
2026-07-03 17:21:42 +02:00
|
|
|
|
const ROOT_FOLDER_ID = '__root__';
|
2026-06-26 10:50:24 +02:00
|
|
|
|
|
|
|
|
|
|
interface DashboardProps {
|
|
|
|
|
|
onOpenProject: (projectId: string) => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function Dashboard({ onOpenProject }: DashboardProps) {
|
|
|
|
|
|
const { user, logout, token } = useAuth();
|
|
|
|
|
|
const [projects, setProjects] = useState<Project[]>([]);
|
2026-07-03 17:21:42 +02:00
|
|
|
|
const [allProjects, setAllProjects] = useState<Project[]>([]);
|
|
|
|
|
|
const [folders, setFolders] = useState<ProjectFolder[]>([]);
|
2026-06-26 10:50:24 +02:00
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
const [showCreate, setShowCreate] = useState(false);
|
|
|
|
|
|
const [newName, setNewName] = useState('');
|
|
|
|
|
|
const [newDesc, setNewDesc] = useState('');
|
2026-06-28 13:52:54 +02:00
|
|
|
|
const [shareProjectId, setShareProjectId] = useState<string | null>(null);
|
2026-07-03 17:21:42 +02:00
|
|
|
|
const [selectedFolderId, setSelectedFolderId] = useState<string | null>(ROOT_FOLDER_ID);
|
|
|
|
|
|
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null);
|
|
|
|
|
|
const [shares, setShares] = useState<ProjectShare[]>([]);
|
|
|
|
|
|
const [sharesLoading, setSharesLoading] = useState(false);
|
|
|
|
|
|
const [draggedProjectId, setDraggedProjectId] = useState<string | null>(null);
|
|
|
|
|
|
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; folderId: string | null } | null>(null);
|
|
|
|
|
|
const [renamingFolderId, setRenamingFolderId] = useState<string | null>(null);
|
|
|
|
|
|
const [renameValue, setRenameValue] = useState('');
|
|
|
|
|
|
const [showCreateFolder, setShowCreateFolder] = useState(false);
|
|
|
|
|
|
const [newFolderName, setNewFolderName] = useState('');
|
|
|
|
|
|
const [newFolderParent, setNewFolderParent] = useState<string | null>(null);
|
2026-06-26 10:50:24 +02:00
|
|
|
|
|
2026-07-03 17:21:42 +02:00
|
|
|
|
const fetchAll = useCallback(async () => {
|
|
|
|
|
|
if (!token) return;
|
2026-06-26 10:50:24 +02:00
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
2026-07-03 17:21:42 +02:00
|
|
|
|
const [projs, fldrs] = await Promise.all([
|
|
|
|
|
|
getProjects(token),
|
|
|
|
|
|
getProjectFolders(token),
|
|
|
|
|
|
]);
|
|
|
|
|
|
setAllProjects(projs);
|
|
|
|
|
|
setFolders(fldrs);
|
2026-06-26 10:50:24 +02:00
|
|
|
|
} catch {
|
|
|
|
|
|
// ignore
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [token]);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-07-03 17:21:42 +02:00
|
|
|
|
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<string, string>();
|
|
|
|
|
|
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]);
|
2026-06-26 10:50:24 +02:00
|
|
|
|
|
|
|
|
|
|
const handleCreate = async () => {
|
2026-07-03 17:21:42 +02:00
|
|
|
|
if (!newName.trim() || !token) return;
|
2026-06-26 10:50:24 +02:00
|
|
|
|
try {
|
2026-07-03 17:21:42 +02:00
|
|
|
|
const folderId = selectedFolderId === ROOT_FOLDER_ID || selectedFolderId === '__unfiled__'
|
|
|
|
|
|
? null : selectedFolderId;
|
2026-06-26 10:50:24 +02:00
|
|
|
|
const res = await fetch(`${API_BASE}/api/projects`, {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
2026-07-03 17:21:42 +02:00
|
|
|
|
body: JSON.stringify({ name: newName, description: newDesc || null, folder_id: folderId }),
|
2026-06-26 10:50:24 +02:00
|
|
|
|
});
|
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
|
setNewName('');
|
|
|
|
|
|
setNewDesc('');
|
|
|
|
|
|
setShowCreate(false);
|
2026-07-03 17:21:42 +02:00
|
|
|
|
fetchAll();
|
2026-06-26 10:50:24 +02:00
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// ignore
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleDelete = async (id: string, e: React.MouseEvent) => {
|
|
|
|
|
|
e.stopPropagation();
|
2026-07-03 17:21:42 +02:00
|
|
|
|
if (!confirm('Projekt wirklich loeschen?')) return;
|
|
|
|
|
|
if (!token) return;
|
2026-06-26 10:50:24 +02:00
|
|
|
|
try {
|
2026-07-03 17:21:42 +02:00
|
|
|
|
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();
|
2026-06-26 10:50:24 +02:00
|
|
|
|
} catch {
|
|
|
|
|
|
// ignore
|
2026-07-03 17:21:42 +02:00
|
|
|
|
} 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);
|
2026-06-26 10:50:24 +02:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-03 17:21:42 +02:00
|
|
|
|
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 (
|
|
|
|
|
|
<span className="folder-action-buttons">
|
|
|
|
|
|
<button
|
|
|
|
|
|
className="folder-action-btn"
|
|
|
|
|
|
title="Umbenennen"
|
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
setRenamingFolderId(node.id);
|
|
|
|
|
|
const folder = folders.find(f => f.id === node.id);
|
|
|
|
|
|
if (folder) setRenameValue(folder.name);
|
|
|
|
|
|
}}
|
|
|
|
|
|
>✎</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
className="folder-action-btn delete"
|
|
|
|
|
|
title="Loeschen"
|
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
handleDeleteFolder(node.id);
|
|
|
|
|
|
}}
|
|
|
|
|
|
>✕</button>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-26 10:50:24 +02:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="dashboard-page">
|
|
|
|
|
|
<header className="dashboard-header">
|
|
|
|
|
|
<div className="dashboard-brand">
|
|
|
|
|
|
<h1>Web CAD</h1>
|
|
|
|
|
|
<span className="dashboard-user">{user?.name} ({user?.role})</span>
|
|
|
|
|
|
</div>
|
2026-06-28 13:52:54 +02:00
|
|
|
|
<div className="dashboard-header-actions">
|
2026-06-28 14:16:50 +02:00
|
|
|
|
{token && <NotificationPanel token={token} />}
|
2026-06-28 13:52:54 +02:00
|
|
|
|
<button className="dashboard-logout" onClick={logout}>Abmelden</button>
|
|
|
|
|
|
</div>
|
2026-06-26 10:50:24 +02:00
|
|
|
|
</header>
|
|
|
|
|
|
|
2026-07-03 17:21:42 +02:00
|
|
|
|
<div className="dashboard-3col">
|
|
|
|
|
|
<div className="dashboard-treeview" onContextMenu={(e) => handleContextMenu(e, null)}>
|
|
|
|
|
|
<div className="treeview-header">
|
|
|
|
|
|
<h3>Ordner</h3>
|
|
|
|
|
|
<button
|
|
|
|
|
|
className="treeview-add-folder-btn"
|
|
|
|
|
|
title="Neuer Ordner"
|
|
|
|
|
|
onClick={() => { setNewFolderParent(null); setShowCreateFolder(true); }}
|
|
|
|
|
|
>+</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{showCreateFolder && (
|
|
|
|
|
|
<div className="folder-create-form">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
placeholder="Ordnername"
|
|
|
|
|
|
value={newFolderName}
|
|
|
|
|
|
onChange={e => setNewFolderName(e.target.value)}
|
|
|
|
|
|
onKeyDown={e => { if (e.key === 'Enter') handleCreateFolder(); if (e.key === 'Escape') setShowCreateFolder(false); }}
|
|
|
|
|
|
autoFocus
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div className="folder-create-actions">
|
|
|
|
|
|
<button onClick={handleCreateFolder}>OK</button>
|
|
|
|
|
|
<button onClick={() => { setShowCreateFolder(false); setNewFolderName(''); }}>×</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<TreeView
|
|
|
|
|
|
nodes={treeNodes}
|
|
|
|
|
|
selectedId={selectedFolderId ?? undefined}
|
|
|
|
|
|
onSelect={handleFolderSelect}
|
|
|
|
|
|
renderActions={renderFolderActions}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{renamingFolderId && (
|
|
|
|
|
|
<div className="folder-rename-form">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={renameValue}
|
|
|
|
|
|
onChange={e => 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
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<div className="treeview-drop-overlay">
|
|
|
|
|
|
{folders.map(folder => (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={folder.id}
|
|
|
|
|
|
className="treeview-drop-zone"
|
|
|
|
|
|
onDragOver={handleFolderDragOver}
|
|
|
|
|
|
onDragLeave={handleFolderDragLeave}
|
|
|
|
|
|
onDrop={(e) => handleFolderDrop(e, folder.id)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
2026-06-26 10:50:24 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-03 17:21:42 +02:00
|
|
|
|
<div className="dashboard-content">
|
|
|
|
|
|
<div className="dashboard-section-header">
|
|
|
|
|
|
<h2>{folderNameById.get(selectedFolderId ?? ROOT_FOLDER_ID) ?? 'Projekte'}</h2>
|
|
|
|
|
|
<button className="dashboard-create-btn" onClick={() => setShowCreate(!showCreate)}>
|
|
|
|
|
|
+ Neues Projekt
|
|
|
|
|
|
</button>
|
2026-06-26 10:50:24 +02:00
|
|
|
|
</div>
|
2026-07-03 17:21:42 +02:00
|
|
|
|
|
|
|
|
|
|
{showCreate && (
|
|
|
|
|
|
<div className="dashboard-create-form">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
placeholder="Projektname"
|
|
|
|
|
|
value={newName}
|
|
|
|
|
|
onChange={e => setNewName(e.target.value)}
|
|
|
|
|
|
onKeyDown={e => { if (e.key === 'Enter') handleCreate(); }}
|
|
|
|
|
|
autoFocus
|
|
|
|
|
|
/>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
placeholder="Beschreibung (optional)"
|
|
|
|
|
|
value={newDesc}
|
|
|
|
|
|
onChange={e => setNewDesc(e.target.value)}
|
|
|
|
|
|
onKeyDown={e => { if (e.key === 'Enter') handleCreate(); }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<button onClick={handleCreate}>Erstellen</button>
|
|
|
|
|
|
<button onClick={() => setShowCreate(false)}>Abbrechen</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{loading ? (
|
|
|
|
|
|
<p className="dashboard-loading">Lade Projekte...</p>
|
|
|
|
|
|
) : projects.length === 0 ? (
|
|
|
|
|
|
<p className="dashboard-empty">Keine Projekte in dieser Ansicht. Erstellen Sie ein neues Projekt.</p>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="dashboard-project-grid">
|
|
|
|
|
|
{projects.map(project => (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={project.id}
|
|
|
|
|
|
className={`dashboard-project-card${selectedProjectId === project.id ? ' selected' : ''}`}
|
|
|
|
|
|
onClick={() => handleProjectCardClick(project.id)}
|
|
|
|
|
|
draggable
|
|
|
|
|
|
onDragStart={(e) => handleProjectDragStart(e, project.id)}
|
|
|
|
|
|
onDragEnd={() => setDraggedProjectId(null)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<h3>{project.name}</h3>
|
|
|
|
|
|
{project.description && <p>{project.description}</p>}
|
|
|
|
|
|
<div className="dashboard-project-meta">
|
|
|
|
|
|
<span>{new Date(project.created_at).toLocaleDateString('de-DE')}</span>
|
|
|
|
|
|
<div className="dashboard-project-actions">
|
|
|
|
|
|
<button
|
|
|
|
|
|
className="dashboard-open-btn"
|
|
|
|
|
|
onClick={(e) => { e.stopPropagation(); onOpenProject(project.id); }}
|
|
|
|
|
|
>
|
|
|
|
|
|
Oeffnen
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
className="dashboard-share-btn"
|
|
|
|
|
|
onClick={(e) => { e.stopPropagation(); setShareProjectId(project.id); }}
|
|
|
|
|
|
>
|
|
|
|
|
|
Teilen
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
className="dashboard-delete-btn"
|
|
|
|
|
|
onClick={(e) => handleDelete(project.id, e)}
|
|
|
|
|
|
>
|
|
|
|
|
|
Loeschen
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-06-28 13:52:54 +02:00
|
|
|
|
</div>
|
2026-06-26 10:50:24 +02:00
|
|
|
|
</div>
|
2026-07-03 17:21:42 +02:00
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="dashboard-info-panel">
|
|
|
|
|
|
{selectedProject ? (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<h3 className="info-panel-title">{selectedProject.name}</h3>
|
|
|
|
|
|
{selectedProject.description && (
|
|
|
|
|
|
<p className="info-panel-description">{selectedProject.description}</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<div className="info-panel-section">
|
|
|
|
|
|
<h4>Metadaten</h4>
|
|
|
|
|
|
<dl className="info-panel-meta">
|
|
|
|
|
|
<dt>Erstellt</dt>
|
|
|
|
|
|
<dd>{formatDate(selectedProject.created_at)}</dd>
|
|
|
|
|
|
<dt>Aktualisiert</dt>
|
|
|
|
|
|
<dd>{formatDate(selectedProject.updated_at)}</dd>
|
|
|
|
|
|
<dt>Owner</dt>
|
|
|
|
|
|
<dd>{selectedProject.owner_id}</dd>
|
|
|
|
|
|
<dt>Ordner</dt>
|
|
|
|
|
|
<dd>{selectedProject.folder_id ? (folderNameById.get(selectedProject.folder_id) ?? 'Unbekannt') : 'Ohne Ordner'}</dd>
|
|
|
|
|
|
</dl>
|
2026-06-26 10:50:24 +02:00
|
|
|
|
</div>
|
2026-07-03 17:21:42 +02:00
|
|
|
|
<div className="info-panel-section">
|
|
|
|
|
|
<h4>Sichtbarkeit / Freigaben</h4>
|
|
|
|
|
|
{sharesLoading ? (
|
|
|
|
|
|
<p className="info-panel-loading">Lade Freigaben...</p>
|
|
|
|
|
|
) : shares.length === 0 ? (
|
|
|
|
|
|
<p className="info-panel-empty">Keine Freigaben</p>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<ul className="info-panel-shares">
|
|
|
|
|
|
{shares.map(share => (
|
|
|
|
|
|
<li key={share.id} className="info-panel-share-item">
|
|
|
|
|
|
<span className="info-panel-share-email">{share.shared_with_email}</span>
|
|
|
|
|
|
<span className="info-panel-share-perm">{share.permission}</span>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button
|
|
|
|
|
|
className="info-panel-open-btn"
|
|
|
|
|
|
onClick={() => onOpenProject(selectedProject.id)}
|
|
|
|
|
|
>
|
|
|
|
|
|
Projekt oeffnen
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="info-panel-placeholder">
|
|
|
|
|
|
<p>Waehlen Sie ein Projekt aus, um Details zu sehen.</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2026-06-26 10:50:24 +02:00
|
|
|
|
</div>
|
2026-06-28 13:52:54 +02:00
|
|
|
|
|
2026-07-03 17:21:42 +02:00
|
|
|
|
{contextMenu && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div className="context-menu-overlay" onClick={() => setContextMenu(null)} />
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="context-menu"
|
|
|
|
|
|
style={{ left: contextMenu.x, top: contextMenu.y }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<button
|
|
|
|
|
|
className="context-menu-item"
|
|
|
|
|
|
onClick={() => handleContextAction('new', contextMenu.folderId)}
|
|
|
|
|
|
>Neuer Ordner</button>
|
|
|
|
|
|
{contextMenu.folderId && contextMenu.folderId !== ROOT_FOLDER_ID && contextMenu.folderId !== '__unfiled__' && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<button
|
|
|
|
|
|
className="context-menu-item"
|
|
|
|
|
|
onClick={() => handleContextAction('rename', contextMenu.folderId)}
|
|
|
|
|
|
>Umbenennen</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
className="context-menu-item danger"
|
|
|
|
|
|
onClick={() => handleContextAction('delete', contextMenu.folderId)}
|
|
|
|
|
|
>Loeschen</button>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-06-28 14:16:50 +02:00
|
|
|
|
{token && (
|
|
|
|
|
|
<ShareDialog
|
|
|
|
|
|
token={token}
|
|
|
|
|
|
projectId={shareProjectId ?? ''}
|
|
|
|
|
|
open={!!shareProjectId}
|
|
|
|
|
|
onClose={() => setShareProjectId(null)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2026-06-26 10:50:24 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|