Files
leocrm/frontend/src/components/settings/WorkspaceManager.tsx
T

276 lines
13 KiB
TypeScript
Raw Normal View History

import { useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useWorkspaces, useCreateWorkspace, useUpdateWorkspace, useDeleteWorkspace, useSetWorkspaceModules, useAssignWorkspaceUser, useRemoveWorkspaceUser, type Workspace, type WorkspaceModule } from '@/api/hooks/workspaces';
import { usePluginStore } from '@/store/pluginStore';
import { LayoutGrid, Plus, Trash2, Edit, Users, Save, X, Check } from 'lucide-react';
// Static core menu items — same as Sidebar.tsx
const CORE_MENU_ITEMS = [
{ key: 'dashboard', label: 'Dashboard' },
{ key: 'contacts', label: 'Kontakte' },
];
export function WorkspaceManager() {
const { t } = useTranslation();
const { data: wsData } = useWorkspaces();
const createWs = useCreateWorkspace();
const updateWs = useUpdateWorkspace();
const deleteWs = useDeleteWorkspace();
const setModules = useSetWorkspaceModules();
const manifests = usePluginStore(s => s.manifests);
// Dynamically build available modules from core menu items + plugin manifests
const availableModules = useMemo(() => {
const pluginItems = manifests
.flatMap(m => m.menu_items || [])
.map(item => ({
key: item.path.replace(/^\//, '').split('/')[0],
label: item.label || item.label_key,
}));
// Merge core + plugin, deduplicate by key
const seen = new Set<string>();
return [...CORE_MENU_ITEMS, ...pluginItems].filter(m => {
if (seen.has(m.key)) return false;
seen.add(m.key);
return true;
});
}, [manifests]);
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 [configEditingKey, setConfigEditingKey] = useState<string | null>(null);
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(availableModules.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 = availableModules.find(a => a.key === m.module_key);
return (
<div key={m.module_key} className={`p-2 border rounded-md ${m.is_visible ? 'border-blue-300 dark:border-blue-700 bg-blue-50 dark:bg-blue-900/10' : 'border-gray-200 dark:border-gray-700'}`}>
<label className="flex items-center gap-2 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded p-1">
<input
type="checkbox"
checked={m.is_visible}
onChange={() => toggleModule(m.module_key)}
/>
<span className="text-sm flex-1">{mod?.label || m.module_key}</span>
{m.is_visible && (
<button
onClick={(e) => { e.preventDefault(); setModuleConfig(prev => prev.map(x => x.module_key === m.module_key ? { ...x, config: x.config } : x)); setConfigEditingKey(configEditingKey === m.module_key ? null : m.module_key); }}
className="text-xs px-1.5 py-0.5 border rounded hover:bg-gray-100 dark:hover:bg-gray-700"
title="Konfiguration bearbeiten"
>
</button>
)}
</label>
{m.is_visible && configEditingKey === m.module_key && (
<div className="mt-2 space-y-1">
<textarea
className="w-full text-xs font-mono p-1.5 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-900 h-20"
placeholder='{"visible_folder_ids": []}'
value={JSON.stringify(m.config || {}, null, 2)}
onChange={(e) => {
try {
const parsed = JSON.parse(e.target.value);
setModuleConfig(prev => prev.map(x => x.module_key === m.module_key ? { ...x, config: parsed } : x));
} catch {
// Invalid JSON — keep raw text for editing
}
}}
/>
<p className="text-xs text-gray-400">JSON-Konfiguration für dieses Modul (z.B. sichtbare Ordner-IDs)</p>
</div>
)}
</div>
);
})}
</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>
);
}