phase5: workspace management UI in Settings → Rechte → Workspaces

This commit is contained in:
Agent Zero
2026-07-29 22:22:37 +02:00
parent fca7191269
commit 0fb0ca9925
2 changed files with 239 additions and 1 deletions
@@ -0,0 +1,235 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useWorkspaces, useCreateWorkspace, useUpdateWorkspace, useDeleteWorkspace, useSetWorkspaceModules, useAssignWorkspaceUser, useRemoveWorkspaceUser, type Workspace, type WorkspaceModule } from '@/api/hooks/workspaces';
import { LayoutGrid, Plus, Trash2, Edit, Users, Save, X, Check } from 'lucide-react';
const AVAILABLE_MODULES = [
{ key: 'contacts', label: 'Kontakte' },
{ key: 'calendar', label: 'Kalender' },
{ key: 'mail', label: 'E-Mail' },
{ key: 'tasks', label: 'Aufgaben' },
{ key: 'dms', label: 'Dokumente' },
{ key: 'kommunikation', label: 'Kommunikation' },
{ key: 'reports', label: 'Reports' },
{ key: 'automation', label: 'Automation' },
{ key: 'tags', label: 'Tags' },
{ key: 'dashboard', label: 'Dashboard' },
{ key: 'settings', label: 'Einstellungen' },
{ key: 'audit', label: 'Audit-Log' },
];
export function WorkspaceManager() {
const { t } = useTranslation();
const { data: wsData } = useWorkspaces();
const createWs = useCreateWorkspace();
const updateWs = useUpdateWorkspace();
const deleteWs = useDeleteWorkspace();
const setModules = useSetWorkspaceModules();
const [showCreate, setShowCreate] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [editName, setEditName] = useState('');
const [editDesc, setEditDesc] = useState('');
const [editIcon, setEditIcon] = useState('LayoutGrid');
const [editDefault, setEditDefault] = useState(false);
const [moduleWsId, setModuleWsId] = useState<string | null>(null);
const [moduleConfig, setModuleConfig] = useState<WorkspaceModule[]>([]);
const workspaces = wsData?.items || [];
const handleCreate = async () => {
await createWs.mutateAsync({ name: editName, icon: editIcon, description: editDesc, is_default: editDefault });
setShowCreate(false);
setEditName(''); setEditDesc(''); setEditDefault(false);
};
const handleSaveEdit = async () => {
if (!editingId) return;
await updateWs.mutateAsync({ id: editingId, name: editName, description: editDesc, is_default: editDefault });
setEditingId(null);
};
const handleDelete = async (id: string) => {
if (confirm('Workspace wirklich löschen?')) {
await deleteWs.mutateAsync(id);
}
};
const openModuleEditor = (ws: Workspace) => {
setModuleWsId(ws.id);
const existing = ws.modules || [];
setModuleConfig(AVAILABLE_MODULES.map(m => {
const existingMod = existing.find(e => e.module_key === m.key);
return {
module_key: m.key,
is_visible: existingMod?.is_visible ?? false,
menu_order: existingMod?.menu_order ?? 0,
config: existingMod?.config || {},
};
}));
};
const saveModules = async () => {
if (!moduleWsId) return;
await setModules.mutateAsync({ workspaceId: moduleWsId, modules: moduleConfig });
setModuleWsId(null);
};
const toggleModule = (key: string) => {
setModuleConfig(prev => prev.map(m =>
m.module_key === key ? { ...m, is_visible: !m.is_visible } : m
));
};
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">{t('workspaces.title', 'Workspaces')}</h2>
<button
onClick={() => { setShowCreate(true); setEditName(''); setEditDesc(''); setEditDefault(false); }}
className="flex items-center gap-1.5 px-3 py-1.5 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm font-medium"
>
<Plus className="w-4 h-4" />
{t('workspaces.create', 'Neuer Workspace')}
</button>
</div>
{showCreate && (
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 space-y-3 bg-gray-50 dark:bg-gray-800/50">
<h3 className="font-medium">{t('workspaces.createNew', 'Neuen Workspace erstellen')}</h3>
<input
type="text"
placeholder="Name"
value={editName}
onChange={e => setEditName(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-900 text-sm"
/>
<input
type="text"
placeholder="Beschreibung"
value={editDesc}
onChange={e => setEditDesc(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-900 text-sm"
/>
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" checked={editDefault} onChange={e => setEditDefault(e.target.checked)} />
Als Standard-Workspace
</label>
<div className="flex gap-2">
<button onClick={handleCreate} disabled={!editName} className="px-3 py-1.5 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm font-medium disabled:opacity-50">
<Save className="w-4 h-4 inline mr-1" />
{t('common.save', 'Speichern')}
</button>
<button onClick={() => setShowCreate(false)} className="px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded-md text-sm">
{t('common.cancel', 'Abbrechen')}
</button>
</div>
</div>
)}
{moduleWsId && (
<div className="border border-blue-200 dark:border-blue-800 rounded-lg p-4 space-y-3 bg-blue-50 dark:bg-blue-900/20">
<div className="flex items-center justify-between">
<h3 className="font-medium">{t('workspaces.moduleConfig', 'Module konfigurieren')}</h3>
<div className="flex gap-2">
<button onClick={saveModules} className="px-3 py-1.5 bg-blue-600 text-white rounded-md text-sm font-medium">
<Save className="w-4 h-4 inline mr-1" />
{t('common.save', 'Speichern')}
</button>
<button onClick={() => setModuleWsId(null)} className="px-3 py-1.5 border rounded-md text-sm">
<X className="w-4 h-4 inline" />
</button>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{moduleConfig.map(m => {
const mod = AVAILABLE_MODULES.find(a => a.key === m.module_key);
return (
<label key={m.module_key} className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-md cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800">
<input
type="checkbox"
checked={m.is_visible}
onChange={() => toggleModule(m.module_key)}
/>
<span className="text-sm">{mod?.label || m.module_key}</span>
</label>
);
})}
</div>
</div>
)}
<div className="space-y-2">
{workspaces.length === 0 && (
<p className="text-sm text-gray-500 dark:text-gray-400 py-4 text-center">
{t('workspaces.empty', 'Keine Workspaces vorhanden. Erstellen Sie einen neuen Workspace.')}
</p>
)}
{workspaces.map(ws => (
<div key={ws.id} className="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
{editingId === ws.id ? (
<div className="space-y-2">
<input type="text" value={editName} onChange={e => setEditName(e.target.value)} className="w-full px-3 py-2 border rounded-md text-sm" />
<input type="text" value={editDesc} onChange={e => setEditDesc(e.target.value)} className="w-full px-3 py-2 border rounded-md text-sm" placeholder="Beschreibung" />
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" checked={editDefault} onChange={e => setEditDefault(e.target.checked)} />
Standard
</label>
<div className="flex gap-2">
<button onClick={handleSaveEdit} className="px-3 py-1.5 bg-blue-600 text-white rounded-md text-sm">
<Check className="w-4 h-4 inline mr-1" />
Speichern
</button>
<button onClick={() => setEditingId(null)} className="px-3 py-1.5 border rounded-md text-sm">
Abbrechen
</button>
</div>
</div>
) : (
<div className="flex items-start justify-between">
<div className="flex items-start gap-3">
<LayoutGrid className="w-5 h-5 text-gray-400 mt-0.5" />
<div>
<div className="flex items-center gap-2">
<span className="font-medium">{ws.name}</span>
{ws.is_default && <span className="text-xs px-1.5 py-0.5 bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded">Standard</span>}
</div>
{ws.description && <p className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">{ws.description}</p>}
<div className="flex items-center gap-3 mt-1 text-xs text-gray-400">
<span className="flex items-center gap-1"><Users className="w-3 h-3" /> {ws.user_count || 0} User</span>
<span>{ws.modules?.length || 0} Module</span>
</div>
</div>
</div>
<div className="flex items-center gap-1">
<button
onClick={() => openModuleEditor(ws)}
className="p-1.5 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md text-sm"
title="Module"
>
<LayoutGrid className="w-4 h-4" />
</button>
<button
onClick={() => { setEditingId(ws.id); setEditName(ws.name); setEditDesc(ws.description || ''); setEditDefault(ws.is_default); }}
className="p-1.5 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md text-sm"
title="Bearbeiten"
>
<Edit className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(ws.id)}
className="p-1.5 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-md text-sm text-red-600"
title="Löschen"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
)}
</div>
))}
</div>
</div>
);
}