phase5: workspace management UI in Settings → Rechte → Workspaces
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -17,7 +17,8 @@ import { EmptyState } from '@/components/ui/EmptyState';
|
||||
import { useToast } from '@/components/ui/Toast';
|
||||
import { ConfirmDialog } from '@/components/ui/ConfirmDialog';
|
||||
import { apiDelete } from '@/api/client';
|
||||
import { Search, Trash2, Shield, Users, Share2, History } from 'lucide-react';
|
||||
import { Search, Trash2, Shield, Users, Share2, History, LayoutGrid } from 'lucide-react';
|
||||
import { WorkspaceManager } from '@/components/settings/WorkspaceManager';
|
||||
import clsx from 'clsx';
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||
@@ -372,6 +373,7 @@ export function SettingsRechtePage() {
|
||||
{ key: 'rollen', label: 'Rollen', icon: Shield, permission: 'roles:read' },
|
||||
{ key: 'gruppen', label: 'Gruppen', icon: Users, permission: 'groups:read' },
|
||||
{ key: 'freigaben', label: 'Freigaben', icon: Share2, permission: 'settings:read' },
|
||||
{ key: 'workspaces', label: 'Workspaces', icon: LayoutGrid, permission: 'workspaces:read' },
|
||||
{ key: 'audit', label: 'Audit', icon: History, permission: 'audit:read' },
|
||||
].filter((tab) => hasPermission(tab.permission));
|
||||
|
||||
@@ -408,6 +410,7 @@ export function SettingsRechtePage() {
|
||||
{activeTab === 'rollen' && <SettingsRolesPage />}
|
||||
{activeTab === 'gruppen' && <SettingsGroupsPage />}
|
||||
{activeTab === 'freigaben' && <FreigabenTab />}
|
||||
{activeTab === 'workspaces' && <WorkspaceManager />}
|
||||
{activeTab === 'audit' && <AuditTab />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user