revert(frontend): Frontend auf letzten funktionierenden Stand 5680179 zurückgesetzt — i18n-Massen-Batch brach Dashboard-Shell in Produktion; Render-Loop-Fix und DSGVO-Antrags-UI liegen sicher in Historie für kontrollierten Wiedereinspiel
This commit is contained in:
@@ -39,7 +39,6 @@ function ProviderTab() {
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
const { t } = useTranslation();
|
||||
if (!confirm(t('aiSettings.confirmDeleteProvider'))) return;
|
||||
try { await deleteProvider(id); await load(); } catch (e: unknown) { const errObj = asError(e); setError(errObj?.message || null); }
|
||||
};
|
||||
@@ -61,7 +60,7 @@ function ProviderTab() {
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="anthropic">Anthropic</option>
|
||||
<option value="ollama">Ollama</option>
|
||||
<option value="azure">{t('aISettings.azureopenai')}</option>
|
||||
<option value="azure">Azure OpenAI</option>
|
||||
<option value="huggingface">HuggingFace</option>
|
||||
<option value="custom">Custom</option>
|
||||
</select>
|
||||
@@ -135,24 +134,24 @@ function PresetTab() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-lg font-semibold">{t('aISettings.modellepresets')}</h2>
|
||||
<h2 className="text-lg font-semibold">Modelle & Presets</h2>
|
||||
<button onClick={() => { setShowForm(!showForm); setEditId(null); setForm({ name: '', model_id: '', provider_id: '', temperature: 0.7, max_tokens: 2048, top_p: 1.0, system_prompt: '' }); }} className="px-3 py-1.5 text-sm bg-primary-600 text-white rounded-lg">{t('aiSettings.add')} </button>
|
||||
</div>
|
||||
{error && <div className="text-sm text-red-600">{typeof error === "string" ? error : (error as any)?.message || "t('common.errorOccurred')"}</div>}
|
||||
{showForm && (
|
||||
<div className="border border-secondary-200 rounded-lg p-4 space-y-3 bg-secondary-50">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<input placeholder={t('aISettings.presetname')} value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} className="border rounded-lg px-3 py-2 text-sm" />
|
||||
<input placeholder={t('aISettings.modellidzbgpt4o')} value={form.model_id} onChange={(e) => setForm({ ...form, model_id: e.target.value })} className="border rounded-lg px-3 py-2 text-sm" />
|
||||
<input placeholder="Preset Name" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} className="border rounded-lg px-3 py-2 text-sm" />
|
||||
<input placeholder="Modell ID (z.B. gpt-4o-mini)" value={form.model_id} onChange={(e) => setForm({ ...form, model_id: e.target.value })} className="border rounded-lg px-3 py-2 text-sm" />
|
||||
<select value={form.provider_id} onChange={(e) => setForm({ ...form, provider_id: e.target.value })} className="border rounded-lg px-3 py-2 text-sm">
|
||||
<option value="">{t('aISettings.anbieterwählen')}</option>
|
||||
<option value="">Anbieter wählen...</option>
|
||||
{providers.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
|
||||
</select>
|
||||
<input type="number" step="0.1" min="0" max="2" placeholder={t('aISettings.temperature')} value={form.temperature} onChange={(e) => setForm({ ...form, temperature: parseFloat(e.target.value) })} className="border rounded-lg px-3 py-2 text-sm" />
|
||||
<input type="number" placeholder={t('aISettings.maxtokens')} value={form.max_tokens} onChange={(e) => setForm({ ...form, max_tokens: parseInt(e.target.value) })} className="border rounded-lg px-3 py-2 text-sm" />
|
||||
<input type="number" step="0.1" min="0" max="1" placeholder={t('aISettings.topp')} value={form.top_p} onChange={(e) => setForm({ ...form, top_p: parseFloat(e.target.value) })} className="border rounded-lg px-3 py-2 text-sm" />
|
||||
<input type="number" step="0.1" min="0" max="2" placeholder="Temperature" value={form.temperature} onChange={(e) => setForm({ ...form, temperature: parseFloat(e.target.value) })} className="border rounded-lg px-3 py-2 text-sm" />
|
||||
<input type="number" placeholder="Max Tokens" value={form.max_tokens} onChange={(e) => setForm({ ...form, max_tokens: parseInt(e.target.value) })} className="border rounded-lg px-3 py-2 text-sm" />
|
||||
<input type="number" step="0.1" min="0" max="1" placeholder="Top P" value={form.top_p} onChange={(e) => setForm({ ...form, top_p: parseFloat(e.target.value) })} className="border rounded-lg px-3 py-2 text-sm" />
|
||||
</div>
|
||||
<textarea placeholder={t('aISettings.systempromptoptional')} value={form.system_prompt} onChange={(e) => setForm({ ...form, system_prompt: e.target.value })} rows={3} className="w-full border rounded-lg px-3 py-2 text-sm" />
|
||||
<textarea placeholder="System Prompt (optional)" value={form.system_prompt} onChange={(e) => setForm({ ...form, system_prompt: e.target.value })} rows={3} className="w-full border rounded-lg px-3 py-2 text-sm" />
|
||||
<div className="flex gap-2">
|
||||
<button onClick={handleSave} className="px-3 py-1.5 text-sm bg-primary-600 text-white rounded-lg">{t('aiSettings.save')} </button>
|
||||
<button onClick={() => setShowForm(false)} className="px-3 py-1.5 text-sm border rounded-lg">{t('aiSettings.cancel')} </button>
|
||||
@@ -164,7 +163,7 @@ function PresetTab() {
|
||||
<div key={p.id} className="border border-secondary-200 rounded-lg p-3 flex items-center justify-between">
|
||||
<div>
|
||||
<div className="font-medium text-sm">{p.name}</div>
|
||||
<div className="text-xs text-secondary-500">{p.model_id} {t('aISettings.temp')}{p.temperature} {t('aISettings.maxtokens2')}{p.max_tokens}</div>
|
||||
<div className="text-xs text-secondary-500">{p.model_id} · temp={p.temperature} · max_tokens={p.max_tokens}</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => handleEdit(p)} className="text-sm text-primary-600 hover:underline">{t('aiSettings.edit')} </button>
|
||||
@@ -214,7 +213,6 @@ function AgentTab() {
|
||||
};
|
||||
|
||||
const toggleTool = (toolName: string) => {
|
||||
const { t } = useTranslation();
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
tool_ids: prev.tool_ids.includes(toolName)
|
||||
@@ -236,13 +234,13 @@ function AgentTab() {
|
||||
<div className="border border-secondary-200 rounded-lg p-4 space-y-3 bg-secondary-50">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<input placeholder={t('aiSettings.name')} value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} className="border rounded-lg px-3 py-2 text-sm" />
|
||||
<input placeholder={t('aISettings.beschreibung')} value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} className="border rounded-lg px-3 py-2 text-sm" />
|
||||
<input placeholder="Beschreibung" value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} className="border rounded-lg px-3 py-2 text-sm" />
|
||||
</div>
|
||||
<select value={form.preset_id} onChange={(e) => setForm({ ...form, preset_id: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm">
|
||||
<option value="">{t('aISettings.presetwählen')}</option>
|
||||
<option value="">Preset wählen...</option>
|
||||
{presets.map((p) => <option key={p.id} value={p.id}>{p.name} ({p.model_id})</option>)}
|
||||
</select>
|
||||
<textarea placeholder={t('aISettings.systemprompt')} value={form.system_prompt} onChange={(e) => setForm({ ...form, system_prompt: e.target.value })} rows={4} className="w-full border rounded-lg px-3 py-2 text-sm" />
|
||||
<textarea placeholder="System Prompt" value={form.system_prompt} onChange={(e) => setForm({ ...form, system_prompt: e.target.value })} rows={4} className="w-full border rounded-lg px-3 py-2 text-sm" />
|
||||
{tools.length > 0 && (
|
||||
<div>
|
||||
<div className="text-sm font-medium mb-2">Tools:</div>
|
||||
@@ -296,11 +294,11 @@ function ToolsTab() {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-lg font-semibold">{t('aISettings.verfügbaretools')}</h2>
|
||||
<p className="text-sm text-secondary-500">{t('aISettings.diesetoolswerdenvonpluginsbereitgestellt')}</p>
|
||||
<h2 className="text-lg font-semibold">Verfügbare Tools</h2>
|
||||
<p className="text-sm text-secondary-500">Diese Tools werden von Plugins bereitgestellt und können Agenten zugewiesen werden.</p>
|
||||
{error && <div className="text-sm text-red-600">{typeof error === "string" ? error : (error as any)?.message || "t('common.errorOccurred')"}</div>}
|
||||
{tools.length === 0 ? (
|
||||
<div className="text-sm text-secondary-400 py-8 text-center">{t('aISettings.keinetoolsverfügbarpluginskönnentools')}</div>
|
||||
<div className="text-sm text-secondary-400 py-8 text-center">Keine Tools verfügbar. Plugins können Tools registrieren.</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{tools.map((tool) => (
|
||||
@@ -324,7 +322,6 @@ function ToolsTab() {
|
||||
|
||||
// ─── Main Settings Page ───
|
||||
export function AISettingsPage() {
|
||||
const { t } = useTranslation();
|
||||
const tabs = [
|
||||
{ key: 'providers', label: 'Anbieter', content: <ProviderTab /> },
|
||||
{ key: 'presets', label: 'Modelle & Presets', content: <PresetTab /> },
|
||||
@@ -334,7 +331,7 @@ export function AISettingsPage() {
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl">
|
||||
<h1 className="text-2xl font-bold text-secondary-900 mb-6">{t('aISettings.kiassistenteinstellungen')}</h1>
|
||||
<h1 className="text-2xl font-bold text-secondary-900 mb-6">KI Assistent Einstellungen</h1>
|
||||
<Tabs tabs={tabs} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -184,7 +184,7 @@ function AgentForm({
|
||||
onChange={(e) => updateField('name', e.target.value)}
|
||||
required
|
||||
className="w-full rounded-lg border border-secondary-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
placeholder={t('agentDashboard.myagent')}
|
||||
placeholder="My Agent"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -196,7 +196,7 @@ function AgentForm({
|
||||
onChange={(e) => updateField('description', e.target.value)}
|
||||
rows={2}
|
||||
className="w-full rounded-lg border border-secondary-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
placeholder={t('agentDashboard.optionaldescription')}
|
||||
placeholder="Optional description"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -213,7 +213,7 @@ function AgentForm({
|
||||
onChange={(e) => updateField('model', e.target.value)}
|
||||
list="model-suggestions"
|
||||
className="flex-1 rounded-lg border border-secondary-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
placeholder={t('agentDashboard.gpt4')}
|
||||
placeholder="gpt-4"
|
||||
/>
|
||||
<datalist id="model-suggestions">
|
||||
{commonModels.map((m) => (
|
||||
@@ -233,7 +233,7 @@ function AgentForm({
|
||||
onChange={(e) => updateField('system_prompt', e.target.value)}
|
||||
rows={4}
|
||||
className="w-full rounded-lg border border-secondary-300 px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
placeholder={t('agentDashboard.youareahelpfulassistant')}
|
||||
placeholder="You are a helpful assistant..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, { useState } from 'react';
|
||||
import { NavLink, Outlet } from 'react-router-dom';
|
||||
import { useUIStore } from '@/store/uiStore';
|
||||
import { ChevronRight, ChevronDown, Bot, Zap, Brain, Cpu, Settings2, Activity, MessageSquare, ArrowLeft } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface AgentNode {
|
||||
title: string;
|
||||
@@ -24,7 +23,6 @@ const AGENT_TREE: AgentNode[] = [
|
||||
];
|
||||
|
||||
function TreeItem({ node, depth }: { node: AgentNode; depth: number }) {
|
||||
const { t } = useTranslation();
|
||||
const [expanded, setExpanded] = useState(depth < 1);
|
||||
const hasChildren = node.children && node.children.length > 0;
|
||||
const paddingLeft = depth * 16 + 12;
|
||||
@@ -72,7 +70,6 @@ function TreeItem({ node, depth }: { node: AgentNode; depth: number }) {
|
||||
}
|
||||
|
||||
export function AgentsPage() {
|
||||
const { t } = useTranslation();
|
||||
const sidebarOpen = useUIStore((state) => state.sidebarOpen);
|
||||
|
||||
return (
|
||||
@@ -83,14 +80,14 @@ export function AgentsPage() {
|
||||
<button
|
||||
onClick={() => window.location.href = '/start'}
|
||||
className="p-1.5 rounded-md text-secondary-400 hover:bg-secondary-100 hover:text-secondary-700 flex items-center justify-center focus:outline-none"
|
||||
aria-label={t('agents.zurückzurstartseite')}
|
||||
title={t('agents.zurückzurstartseite')}
|
||||
aria-label="Zurück zur Startseite"
|
||||
title="Zurück zur Startseite"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<h1 className="text-xl font-bold text-secondary-900">Agenten</h1>
|
||||
</div>
|
||||
<nav className="space-y-0.5 overflow-y-auto" role="navigation" aria-label={t('agents.agentennavigation')}>
|
||||
<nav className="space-y-0.5 overflow-y-auto" role="navigation" aria-label="Agenten Navigation">
|
||||
{AGENT_TREE.map((node) => (
|
||||
<TreeItem key={node.title} node={node} depth={0} />
|
||||
))}
|
||||
|
||||
@@ -115,19 +115,19 @@ export function AuditLogPage() {
|
||||
label={t('auditLog.user')}
|
||||
value={filterUser}
|
||||
onChange={(e) => setFilterUser(e.target.value)}
|
||||
placeholder={t('auditLog.annaschmidt')}
|
||||
placeholder="anna.schmidt"
|
||||
/>
|
||||
<Input
|
||||
label={t('auditLog.action')}
|
||||
value={filterAction}
|
||||
onChange={(e) => setFilterAction(e.target.value)}
|
||||
placeholder={t('auditLog.createupdatedelete')}
|
||||
placeholder="create, update, delete"
|
||||
/>
|
||||
<Input
|
||||
label={t('auditLog.entity')}
|
||||
value={filterEntity}
|
||||
onChange={(e) => setFilterEntity(e.target.value)}
|
||||
placeholder={t('auditLog.companycontact')}
|
||||
placeholder="company, contact"
|
||||
/>
|
||||
<Input
|
||||
label={t('auditLog.dateFrom')}
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, { useState } from 'react';
|
||||
import { NavLink, Outlet } from 'react-router-dom';
|
||||
import { useUIStore } from '@/store/uiStore';
|
||||
import { ChevronRight, ChevronDown, Zap, Play, History, GitBranch, Settings2, Activity, Clock, ArrowLeft } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface AutomationNode {
|
||||
title: string;
|
||||
@@ -33,7 +32,6 @@ const AUTOMATION_TREE: AutomationNode[] = [
|
||||
];
|
||||
|
||||
function TreeItem({ node, depth }: { node: AutomationNode; depth: number }) {
|
||||
const { t } = useTranslation();
|
||||
const [expanded, setExpanded] = useState(depth < 1);
|
||||
const hasChildren = node.children && node.children.length > 0;
|
||||
const paddingLeft = depth * 16 + 12;
|
||||
@@ -81,7 +79,6 @@ function TreeItem({ node, depth }: { node: AutomationNode; depth: number }) {
|
||||
}
|
||||
|
||||
export function AutomationPage() {
|
||||
const { t } = useTranslation();
|
||||
const sidebarOpen = useUIStore((state) => state.sidebarOpen);
|
||||
|
||||
return (
|
||||
@@ -92,14 +89,14 @@ export function AutomationPage() {
|
||||
<button
|
||||
onClick={() => window.location.href = '/start'}
|
||||
className="p-1.5 rounded-md text-secondary-400 hover:bg-secondary-100 hover:text-secondary-700 flex items-center justify-center focus:outline-none"
|
||||
aria-label={t('automation.zurückzurstartseite')}
|
||||
title={t('automation.zurückzurstartseite')}
|
||||
aria-label="Zurück zur Startseite"
|
||||
title="Zurück zur Startseite"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<h1 className="text-xl font-bold text-secondary-900">Automation</h1>
|
||||
</div>
|
||||
<nav className="space-y-0.5 overflow-y-auto" role="navigation" aria-label={t('automation.automationnavigation')}>
|
||||
<nav className="space-y-0.5 overflow-y-auto" role="navigation" aria-label="Automation Navigation">
|
||||
{AUTOMATION_TREE.map((node) => (
|
||||
<TreeItem key={node.title} node={node} depth={0} />
|
||||
))}
|
||||
|
||||
@@ -215,7 +215,7 @@ function AutomationForm({
|
||||
onChange={(e) => updateField('name', e.target.value)}
|
||||
required
|
||||
className="w-full rounded-lg border border-secondary-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
placeholder={t('automationDashboard.myautomation')}
|
||||
placeholder="My Automation"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -227,7 +227,7 @@ function AutomationForm({
|
||||
onChange={(e) => updateField('description', e.target.value)}
|
||||
rows={2}
|
||||
className="w-full rounded-lg border border-secondary-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
placeholder={t('automationDashboard.optionaldescription')}
|
||||
placeholder="Optional description"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -255,7 +255,7 @@ function AutomationForm({
|
||||
value={form.trigger_config.event_name || ''}
|
||||
onChange={(e) => updateField('trigger_config', { ...form.trigger_config, event_name: e.target.value })}
|
||||
className="w-full rounded-lg border border-secondary-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
placeholder={t('automationDashboard.contactcreated')}
|
||||
placeholder="contact.created"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -271,7 +271,7 @@ function AutomationForm({
|
||||
className="w-full rounded-lg border border-secondary-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
placeholder="0 9 * * *"
|
||||
/>
|
||||
<p className="text-xs text-secondary-400 mt-1">{t('automationDashboard.cronexpressioneg09')}</p>
|
||||
<p className="text-xs text-secondary-400 mt-1">Cron expression (e.g. 0 9 * * * for daily at 9am)</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -294,7 +294,7 @@ function AutomationForm({
|
||||
type="text"
|
||||
value={cond.field}
|
||||
onChange={(e) => updateCondition(i, 'field', e.target.value)}
|
||||
placeholder={t('automationDashboard.field')}
|
||||
placeholder="Field"
|
||||
className="flex-1 rounded-lg border border-secondary-300 px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
/>
|
||||
<Select
|
||||
@@ -307,14 +307,14 @@ function AutomationForm({
|
||||
type="text"
|
||||
value={cond.value}
|
||||
onChange={(e) => updateCondition(i, 'value', e.target.value)}
|
||||
placeholder={t('automationDashboard.value')}
|
||||
placeholder="Value"
|
||||
className="flex-1 rounded-lg border border-secondary-300 px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeCondition(i)}
|
||||
className="text-danger-500 hover:text-danger-700 p-1"
|
||||
aria-label={t('automationDashboard.removecondition')}
|
||||
aria-label="Remove condition"
|
||||
>
|
||||
<XCircle className="h-4 w-4" />
|
||||
</button>
|
||||
@@ -353,14 +353,14 @@ function AutomationForm({
|
||||
// ignore invalid JSON while typing
|
||||
}
|
||||
}}
|
||||
placeholder={t('automationDashboard.url')}
|
||||
placeholder='{"url": "..."}'
|
||||
className="flex-1 rounded-lg border border-secondary-300 px-3 py-1.5 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeAction(i)}
|
||||
className="text-danger-500 hover:text-danger-700 p-1"
|
||||
aria-label={t('automationDashboard.removeaction')}
|
||||
aria-label="Remove action"
|
||||
>
|
||||
<XCircle className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
@@ -148,7 +148,7 @@ export function AutomationSettingsPage() {
|
||||
value={form.default_llm_model}
|
||||
onChange={(e) => updateField('default_llm_model', e.target.value)}
|
||||
className="w-full rounded-lg border border-secondary-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
placeholder={t('automationSettings.gpt4')}
|
||||
placeholder="gpt-4"
|
||||
/>
|
||||
<p className="text-xs text-secondary-400 mt-1">{t('automation.defaultLlmModelHint')}</p>
|
||||
</div>
|
||||
@@ -272,7 +272,7 @@ export function AutomationSettingsPage() {
|
||||
value={miniAppForm.app_id}
|
||||
onChange={(e) => setMiniAppForm({ ...miniAppForm, app_id: e.target.value })}
|
||||
className="w-full rounded-lg border border-secondary-300 px-3 py-2 text-sm"
|
||||
placeholder={t('automationSettings.mycustomapp')}
|
||||
placeholder="my_custom_app"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -282,7 +282,7 @@ export function AutomationSettingsPage() {
|
||||
value={miniAppForm.name}
|
||||
onChange={(e) => setMiniAppForm({ ...miniAppForm, name: e.target.value })}
|
||||
className="w-full rounded-lg border border-secondary-300 px-3 py-2 text-sm"
|
||||
placeholder={t('automationSettings.mycustomapp2')}
|
||||
placeholder="My Custom App"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -292,7 +292,7 @@ export function AutomationSettingsPage() {
|
||||
value={miniAppForm.icon}
|
||||
onChange={(e) => setMiniAppForm({ ...miniAppForm, icon: e.target.value })}
|
||||
className="w-full rounded-lg border border-secondary-300 px-3 py-2 text-sm"
|
||||
placeholder={t('automationSettings.appwindow')}
|
||||
placeholder="AppWindow"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -302,7 +302,7 @@ export function AutomationSettingsPage() {
|
||||
onChange={(e) => setMiniAppForm({ ...miniAppForm, description: e.target.value })}
|
||||
rows={2}
|
||||
className="w-full rounded-lg border border-secondary-300 px-3 py-2 text-sm"
|
||||
placeholder={t('automationSettings.optionaldescription')}
|
||||
placeholder="Optional description"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -312,7 +312,7 @@ export function AutomationSettingsPage() {
|
||||
onChange={(e) => setMiniAppForm({ ...miniAppForm, render_schema: e.target.value })}
|
||||
rows={4}
|
||||
className="w-full rounded-lg border border-secondary-300 px-3 py-2 text-sm font-mono"
|
||||
placeholder={t('automationSettings.typeformfields')}
|
||||
placeholder='{"type": "form", "fields": []}'
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3 pt-4 border-t border-secondary-200">
|
||||
|
||||
@@ -12,7 +12,6 @@ import { ResizablePanel } from '@/components/ui/ResizablePanel';
|
||||
import { usePluginToolbarStore } from '@/store/pluginToolbarStore';
|
||||
import { apiClient } from '@/api/client';
|
||||
import { streamChat, fetchAgents, fetchMessages as fetchAiMessages, type AIAgent } from '@/api/ai';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// ─── Types ───
|
||||
|
||||
@@ -217,7 +216,6 @@ interface ConversationTreeProps {
|
||||
}
|
||||
|
||||
function ConversationTree({ conversations, activeConvId, onSelect, onNewChat, loading }: ConversationTreeProps) {
|
||||
const { t } = useTranslation();
|
||||
const [expanded, setExpanded] = useState<Record<ConversationCategory, boolean>>({
|
||||
system: true,
|
||||
ai: true,
|
||||
@@ -295,7 +293,7 @@ function ConversationTree({ conversations, activeConvId, onSelect, onNewChat, lo
|
||||
</button>
|
||||
))}
|
||||
{expanded[section.category] && section.conversations.length === 0 && (
|
||||
<div className="px-6 py-2 text-xs text-secondary-400">{t('communication.keinekonversationen')}</div>
|
||||
<div className="px-6 py-2 text-xs text-secondary-400">Keine Konversationen</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
@@ -313,7 +311,6 @@ interface ChatWindowProps {
|
||||
}
|
||||
|
||||
function ChatWindow({ convId, category, onBack }: ChatWindowProps) {
|
||||
const { t } = useTranslation();
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [input, setInput] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -502,11 +499,11 @@ function ChatWindow({ convId, category, onBack }: ChatWindowProps) {
|
||||
<button
|
||||
onClick={() => setShowMiniApps(!showMiniApps)}
|
||||
className="p-1 hover:bg-secondary-100 rounded"
|
||||
title={t('communication.miniapps')}
|
||||
title="Mini-Apps"
|
||||
>
|
||||
<Sparkles className="w-4 h-4 text-secondary-400" />
|
||||
</button>
|
||||
<button className="p-1 hover:bg-secondary-100 rounded" title={t('communication.inneuemfenster')}>
|
||||
<button className="p-1 hover:bg-secondary-100 rounded" title="In neuem Fenster">
|
||||
<ExternalLink className="w-4 h-4 text-secondary-400" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -540,7 +537,7 @@ function ChatWindow({ convId, category, onBack }: ChatWindowProps) {
|
||||
{!loading && messages.length === 0 && !aiStreaming && (
|
||||
<div className="text-center text-secondary-400 py-8">
|
||||
<MessageSquare className="w-10 h-10 mx-auto mb-2 opacity-50" />
|
||||
<p className="text-sm">{t('communication.keinenachrichtenschreibedieerste')}</p>
|
||||
<p className="text-sm">Keine Nachrichten. Schreibe die erste!</p>
|
||||
</div>
|
||||
)}
|
||||
{messages.map(msg => (
|
||||
@@ -552,7 +549,7 @@ function ChatWindow({ convId, category, onBack }: ChatWindowProps) {
|
||||
<Bot className="w-4 h-4 text-primary-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-xs text-secondary-500 mb-1">{t('communication.kiantwortet')}</div>
|
||||
<div className="text-xs text-secondary-500 mb-1">KI antwortet...</div>
|
||||
<div className="ai-markdown max-w-none break-words">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{streamingContent || '...'}</ReactMarkdown>
|
||||
</div>
|
||||
@@ -564,10 +561,10 @@ function ChatWindow({ convId, category, onBack }: ChatWindowProps) {
|
||||
{/* Input */}
|
||||
<div className="border-t border-secondary-200 p-3">
|
||||
<div className="flex items-end gap-2">
|
||||
<button className="p-2 hover:bg-secondary-100 rounded-lg" title={t('communication.dateianhängen')}>
|
||||
<button className="p-2 hover:bg-secondary-100 rounded-lg" title="Datei anhängen">
|
||||
<Paperclip className="w-4 h-4 text-secondary-400" />
|
||||
</button>
|
||||
<button className="p-2 hover:bg-secondary-100 rounded-lg" title={t('communication.emoji')}>
|
||||
<button className="p-2 hover:bg-secondary-100 rounded-lg" title="Emoji">
|
||||
<Smile className="w-4 h-4 text-secondary-400" />
|
||||
</button>
|
||||
<textarea
|
||||
@@ -670,7 +667,6 @@ interface NewChatDialogProps {
|
||||
}
|
||||
|
||||
function NewChatDialog({ category, onClose, onCreate }: NewChatDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [title, setTitle] = useState('');
|
||||
const [users, setUsers] = useState<{id: string; name: string; email: string}[]>([]);
|
||||
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
|
||||
@@ -711,7 +707,7 @@ function NewChatDialog({ category, onClose, onCreate }: NewChatDialogProps) {
|
||||
/>
|
||||
{category === 'colleague' && (
|
||||
<div>
|
||||
<label className="text-sm font-medium text-secondary-700 mb-1 block">{t('communication.teilnehmerauswählen')}</label>
|
||||
<label className="text-sm font-medium text-secondary-700 mb-1 block">Teilnehmer auswählen</label>
|
||||
{loading ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
@@ -749,14 +745,12 @@ function NewChatDialog({ category, onClose, onCreate }: NewChatDialogProps) {
|
||||
// ─── Main Page ───
|
||||
|
||||
export function CommunicationPage() {
|
||||
const { t } = useTranslation();
|
||||
const [conversations, setConversations] = useState<Conversation[]>([]);
|
||||
const [activeConvId, setActiveConvId] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showNewChat, setShowNewChat] = useState<ConversationCategory | null>(null);
|
||||
const [mobileView, setMobileView] = useState<'list' | 'chat'>('list');
|
||||
const registerItems = usePluginToolbarStore((s) => s.registerItems);
|
||||
const unregisterPlugin = usePluginToolbarStore((s) => s.unregisterPlugin);
|
||||
const { registerItems, unregisterPlugin } = usePluginToolbarStore();
|
||||
|
||||
const loadConversations = useCallback(async () => {
|
||||
try {
|
||||
@@ -857,7 +851,7 @@ export function CommunicationPage() {
|
||||
<div className="flex items-center justify-center h-full text-secondary-400">
|
||||
<div className="text-center">
|
||||
<MessageSquare className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||
<p>{t('communication.wähleeinekonversationaus')}</p>
|
||||
<p>Wähle eine Konversation aus</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -13,14 +13,9 @@ import {
|
||||
type ComplianceIncident,
|
||||
type RetentionPolicyEntry,
|
||||
type IncidentCreate,
|
||||
submitDsarRequest,
|
||||
downloadDsgvoExport,
|
||||
type DsarRequestResponse,
|
||||
type DsarType,
|
||||
} from '../api/compliance';
|
||||
import { useUsers } from '../api/users';
|
||||
|
||||
type SubTab = 'registry' | 'incidents' | 'retention' | 'dsar';
|
||||
type SubTab = 'registry' | 'incidents' | 'retention';
|
||||
|
||||
export function ComplianceTab() {
|
||||
const { t } = useTranslation();
|
||||
@@ -30,7 +25,6 @@ export function ComplianceTab() {
|
||||
{ key: 'registry', label: t('compliance.aiRegistry', 'AI-Register') },
|
||||
{ key: 'incidents', label: t('compliance.incidents', 'Vorfälle') },
|
||||
{ key: 'retention', label: t('compliance.retention', 'Aufbewahrung') },
|
||||
{ key: 'dsar', label: t('compliance.dsar.tab', 'DSGVO-Anfragen') },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -59,7 +53,6 @@ export function ComplianceTab() {
|
||||
{subTab === 'registry' && <AIRegistryPanel />}
|
||||
{subTab === 'incidents' && <IncidentsPanel />}
|
||||
{subTab === 'retention' && <RetentionPanel />}
|
||||
{subTab === 'dsar' && <DsarPanel />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -468,135 +461,3 @@ function RetentionPanel() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── DSAR Panel (GDPR Art. 15/17/20) ───
|
||||
|
||||
const DSAR_TYPES: { value: DsarType; labelKey: string; fallback: string }[] = [
|
||||
{ value: 'access', labelKey: 'compliance.dsar.typeAccess', fallback: 'Auskunft (Art. 15)' },
|
||||
{ value: 'deletion', labelKey: 'compliance.dsar.typeDeletion', fallback: 'Löschung (Art. 17)' },
|
||||
{ value: 'rectification', labelKey: 'compliance.dsar.typeRectification', fallback: 'Berichtigung (Art. 16)' },
|
||||
];
|
||||
|
||||
function DsarPanel() {
|
||||
const { t } = useTranslation();
|
||||
const [userId, setUserId] = useState('');
|
||||
const [dsarType, setDsarType] = useState<DsarType>('access');
|
||||
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||
const [lastJob, setLastJob] = useState<DsarRequestResponse | null>(null);
|
||||
const [exportError, setExportError] = useState(false);
|
||||
|
||||
const { data: usersData, isLoading: usersLoading } = useUsers(1, 200);
|
||||
|
||||
const dsarMutation = useMutation({
|
||||
mutationFn: () => submitDsarRequest(userId, dsarType),
|
||||
onSuccess: (resp) => {
|
||||
setLastJob(resp);
|
||||
setConfirmDelete(false);
|
||||
},
|
||||
});
|
||||
|
||||
const selectedUser = usersData?.items.find((u) => u.id === userId);
|
||||
|
||||
const handleExportClick = async () => {
|
||||
setExportError(false);
|
||||
try {
|
||||
await downloadDsgvoExport(userId, selectedUser?.name ?? undefined);
|
||||
} catch {
|
||||
setExportError(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!userId) return;
|
||||
if (dsarType === 'deletion' && !confirmDelete) {
|
||||
setConfirmDelete(true);
|
||||
return;
|
||||
}
|
||||
dsarMutation.mutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4 max-w-2xl">
|
||||
<p className="text-sm text-secondary-600">{t('compliance.dsar.description', 'DSGVO-Anfrage für eine Person auslösen: Auskunft (Art. 15), Löschung (Art. 17) oder Berichtigung (Art. 16). Der Vorgang wird als Hintergrund-Job durch den Worker ausgeführt.')}</p>
|
||||
|
||||
<div>
|
||||
<label htmlFor="dsar-user-select" className="block text-xs font-medium text-secondary-500 uppercase mb-1">{t('compliance.dsar.person', 'Person')}</label>
|
||||
<select
|
||||
id="dsar-user-select"
|
||||
value={userId}
|
||||
onChange={(e) => { setUserId(e.target.value); setConfirmDelete(false); }}
|
||||
className="w-full max-w-md px-3 py-3 border border-secondary-300 rounded bg-white text-sm"
|
||||
aria-label={t('compliance.dsar.person', 'Person')}
|
||||
>
|
||||
<option value="">{usersLoading ? t('common.loading', 'Laden...') : t('compliance.dsar.selectPerson', '-- Bitte wählen --')}</option>
|
||||
{(usersData?.items ?? []).map((u) => (
|
||||
<option key={u.id} value={u.id}>{u.name || u.email}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
<legend className="block text-xs font-medium text-secondary-500 uppercase mb-1">{t('compliance.dsar.requestType', 'Antragsart')}</legend>
|
||||
<div className="space-y-1" role="radiogroup" aria-label={t('compliance.dsar.requestType', 'Antragsart')}>
|
||||
{DSAR_TYPES.map(({ value, labelKey, fallback }) => (
|
||||
<label
|
||||
key={value}
|
||||
className={`flex items-center gap-2 px-3 py-3 min-h-[44px] rounded cursor-pointer border ${
|
||||
dsarType === value ? 'border-primary-500 bg-primary-50' : 'border-secondary-200 hover:bg-secondary-50'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="dsar-type"
|
||||
checked={dsarType === value}
|
||||
onChange={() => { setDsarType(value); setConfirmDelete(false); }}
|
||||
aria-label={t(labelKey, fallback)}
|
||||
/>
|
||||
<span className="text-sm">{t(labelKey, fallback)}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
{dsarType === 'access' && (
|
||||
<button
|
||||
onClick={handleExportClick}
|
||||
disabled={!userId}
|
||||
className="px-4 py-3 min-h-[44px] text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 rounded disabled:opacity-50"
|
||||
aria-label={t('compliance.dsar.downloadExport', 'Datenexport herunterladen')}
|
||||
>
|
||||
{t('compliance.dsar.downloadExport', 'Datenexport herunterladen')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={!userId || dsarMutation.isPending}
|
||||
className={`px-4 py-3 min-h-[44px] text-sm font-medium text-white rounded disabled:opacity-50 ${
|
||||
dsarType === 'deletion' ? 'bg-red-600 hover:bg-red-700' : 'bg-primary-600 hover:bg-primary-700'
|
||||
}`}
|
||||
aria-label={t('compliance.dsar.submit', 'Anfrage stellen')}
|
||||
>
|
||||
{dsarType === 'deletion' && confirmDelete
|
||||
? t('compliance.dsar.confirmDeletion', 'Wirklich löschen? Unwiderruflich!')
|
||||
: t('compliance.dsar.submit', 'Anfrage stellen')}
|
||||
</button>
|
||||
{!userId && (
|
||||
<span className="text-xs text-secondary-400">{t('compliance.dsar.chooseFirst', 'Bitte zuerst eine Person wählen.')}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{dsarMutation.isPending && <p className="text-sm text-secondary-500" aria-live="polite">{t('common.saving', 'Wird gesendet...')}</p>}
|
||||
{dsarMutation.isError && <p className="text-sm text-red-600" aria-live="polite">{t('common.errorOccurred', 'Fehler beim Senden')}</p>}
|
||||
{exportError && <p className="text-sm text-red-600" aria-live="polite">{t('common.errorOccurred', 'Fehler beim Erstellen des Exports')}</p>}
|
||||
|
||||
{lastJob && (
|
||||
<div className="rounded border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-800" role="status">
|
||||
{t('compliance.dsar.queued', 'Anfrage eingereicht — Job')}{' '}
|
||||
<code className="font-mono">{lastJob.job_id}</code>{' '}
|
||||
{t('compliance.dsar.statusQueued', '(in Warteschlange). Die Bearbeitung erfolgt im Hintergrund.')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -537,13 +537,13 @@ export function ContactsListPage() {
|
||||
{/* Placeholder for saved custom views */}
|
||||
{savedViews.length === 0 ? (
|
||||
<div className="px-2 py-3 text-center">
|
||||
<p className="text-[11px] text-secondary-400 mb-2">{t('contactsList.keinebenutzerdefiniertenansichten')}</p>
|
||||
<p className="text-[11px] text-secondary-400 mb-2">Keine benutzerdefinierten Ansichten</p>
|
||||
<button
|
||||
onClick={handleSaveView}
|
||||
className="inline-flex items-center gap-1 px-2 py-1 text-[11px] text-primary-600 hover:bg-primary-50 rounded transition-colors"
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
{t('contactsList.aktuelleansichtspeichern')}
|
||||
Aktuelle Ansicht speichern
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -572,7 +572,7 @@ export function ContactsListPage() {
|
||||
className="w-full flex items-center gap-1 px-2 py-1.5 rounded text-xs text-primary-600 hover:bg-primary-50 transition-colors"
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
{t('contactsList.aktuelleansichtspeichern')}
|
||||
Aktuelle Ansicht speichern
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
@@ -756,7 +756,7 @@ export function ContactsListPage() {
|
||||
|
||||
{/* Saved Filters Modal */}
|
||||
{savedFiltersOpen && (
|
||||
<Modal open={savedFiltersOpen} onClose={() => setSavedFiltersOpen(false)} title={t('contactsList.gespeichertefilter')}>
|
||||
<Modal open={savedFiltersOpen} onClose={() => setSavedFiltersOpen(false)} title="Gespeicherte Filter">
|
||||
<SavedFilters
|
||||
entityType="contacts"
|
||||
currentCriteria={{ search: debouncedSearch, type: contactType, sortBy, sortOrder, folderId }}
|
||||
|
||||
@@ -394,7 +394,7 @@ export function CustomFieldsPage() {
|
||||
required
|
||||
value={form.label}
|
||||
onChange={(e) => handleLabelChange(e.target.value)}
|
||||
placeholder={t('customFields.zbbrancheabteilunggeburtsdatum')}
|
||||
placeholder="z.B. Branche, Abteilung, Geburtsdatum"
|
||||
/>
|
||||
|
||||
{/* Name (auto-generated) */}
|
||||
@@ -404,7 +404,7 @@ export function CustomFieldsPage() {
|
||||
value={form.name}
|
||||
onChange={(e) => handleNameChange(e.target.value)}
|
||||
helperText="Wird automatisch aus der Bezeichnung generiert"
|
||||
placeholder={t('customFields.zbbrancheabteilunggeburtsdatum2')}
|
||||
placeholder="z.B. branche, abteilung, geburtsdatum"
|
||||
/>
|
||||
|
||||
{/* Field type */}
|
||||
@@ -423,7 +423,7 @@ export function CustomFieldsPage() {
|
||||
required
|
||||
value={form.optionsStr}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, optionsStr: e.target.value }))}
|
||||
placeholder={t('customFields.option1option2option3')}
|
||||
placeholder="Option 1, Option 2, Option 3"
|
||||
helperText="Trennen Sie die Optionen mit Kommas"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -121,21 +121,21 @@ export function DashboardPage() {
|
||||
<div className="border border-secondary-200 rounded-lg p-4 bg-white">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<DollarSign className="w-4 h-4 text-warning-600" />
|
||||
<span className="text-sm font-medium text-secondary-900">{t('dashboard.llmcost24h')}</span>
|
||||
<span className="text-sm font-medium text-secondary-900">LLM Cost (24h)</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-secondary-900">${systemData.llm.last_24h_cost?.toFixed(2) ?? '0.00'}</p>
|
||||
</div>
|
||||
<div className="border border-secondary-200 rounded-lg p-4 bg-white">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<TrendingUp className="w-4 h-4 text-primary-600" />
|
||||
<span className="text-sm font-medium text-secondary-900">{t('dashboard.llmtokens24h')}</span>
|
||||
<span className="text-sm font-medium text-secondary-900">LLM Tokens (24h)</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-secondary-900">{systemData.llm.last_24h_tokens?.toLocaleString() ?? '0'}</p>
|
||||
</div>
|
||||
<div className="border border-secondary-200 rounded-lg p-4 bg-white">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Activity className="w-4 h-4 text-primary-600" />
|
||||
<span className="text-sm font-medium text-secondary-900">{t('dashboard.activeplugins')}</span>
|
||||
<span className="text-sm font-medium text-secondary-900">Active Plugins</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-secondary-900">{systemData.plugins?.active_plugins?.length ?? '—'}</p>
|
||||
</div>
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
// Guest contacts page now redirects to normal contacts page
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function GuestContactsPage() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -15,7 +13,7 @@ export function GuestContactsPage() {
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<p className="text-gray-500">{t('guestContacts.weiterleitungzukontakten')}</p>
|
||||
<p className="text-gray-500">Weiterleitung zu Kontakten...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
// Guest login page now redirects to normal login
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function GuestLoginPage() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -15,7 +13,7 @@ export function GuestLoginPage() {
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<p className="text-gray-500">{t('guestLogin.weiterleitungzumlogin')}</p>
|
||||
<p className="text-gray-500">Weiterleitung zum Login...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -142,14 +142,14 @@ export function HelpPage() {
|
||||
<button
|
||||
onClick={() => window.location.href = '/start'}
|
||||
className="p-1.5 rounded-md text-secondary-400 hover:bg-secondary-100 hover:text-secondary-700 flex items-center justify-center focus:outline-none"
|
||||
aria-label={t('help.zurückzurstartseite')}
|
||||
title={t('help.zurückzurstartseite')}
|
||||
aria-label="Zurück zur Startseite"
|
||||
title="Zurück zur Startseite"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<h1 className="text-xl font-bold text-secondary-900">Hilfe</h1>
|
||||
</div>
|
||||
<nav className="space-y-0.5 overflow-y-auto" role="navigation" aria-label={t('help.hilfenavigation')}>
|
||||
<nav className="space-y-0.5 overflow-y-auto" role="navigation" aria-label="Hilfe Navigation">
|
||||
{HELP_TREE.map((node) => (
|
||||
<TreeItem key={node.title} node={node} depth={0} />
|
||||
))}
|
||||
|
||||
@@ -38,7 +38,7 @@ export function ImportExportPage() {
|
||||
|
||||
{/* Tab navigation */}
|
||||
<div className="border-b border-secondary-200">
|
||||
<nav className="flex gap-1" aria-label={t('importExport.tabs')}>
|
||||
<nav className="flex gap-1" aria-label="Tabs">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, { useState } from 'react';
|
||||
import { NavLink, Outlet } from 'react-router-dom';
|
||||
import { useUIStore } from '@/store/uiStore';
|
||||
import { ChevronRight, ChevronDown, ScrollText, FileText, AlertTriangle, ArrowLeft } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface LogNode {
|
||||
title: string;
|
||||
@@ -41,7 +40,6 @@ const LOG_TREE: LogNode[] = [
|
||||
];
|
||||
|
||||
function TreeItem({ node, depth }: { node: LogNode; depth: number }) {
|
||||
const { t } = useTranslation();
|
||||
const [expanded, setExpanded] = useState(depth < 1);
|
||||
const hasChildren = node.children && node.children.length > 0;
|
||||
const paddingLeft = depth * 16 + 12;
|
||||
@@ -89,7 +87,6 @@ function TreeItem({ node, depth }: { node: LogNode; depth: number }) {
|
||||
}
|
||||
|
||||
export function LogsPage() {
|
||||
const { t } = useTranslation();
|
||||
const sidebarOpen = useUIStore((state) => state.sidebarOpen);
|
||||
|
||||
return (
|
||||
@@ -100,14 +97,14 @@ export function LogsPage() {
|
||||
<button
|
||||
onClick={() => window.location.href = '/start'}
|
||||
className="p-1.5 rounded-md text-secondary-400 hover:bg-secondary-100 hover:text-secondary-700 flex items-center justify-center focus:outline-none"
|
||||
aria-label={t('logs.zurückzurstartseite')}
|
||||
title={t('logs.zurückzurstartseite')}
|
||||
aria-label="Zurück zur Startseite"
|
||||
title="Zurück zur Startseite"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<h1 className="text-xl font-bold text-secondary-900">Logs</h1>
|
||||
</div>
|
||||
<nav className="space-y-0.5 overflow-y-auto" role="navigation" aria-label={t('logs.logsnavigation')}>
|
||||
<nav className="space-y-0.5 overflow-y-auto" role="navigation" aria-label="Logs Navigation">
|
||||
{LOG_TREE.map((node) => (
|
||||
<TreeItem key={node.title} node={node} depth={0} />
|
||||
))}
|
||||
|
||||
@@ -970,7 +970,7 @@ export function MailPage() {
|
||||
<button
|
||||
onClick={() => setActiveView('folders')}
|
||||
className="inline-flex items-center gap-1 px-2 py-1 rounded text-sm text-secondary-700 hover:bg-secondary-100 min-h-touch"
|
||||
aria-label={t('mail.zurückzuordnern')}
|
||||
aria-label="Zurück zu Ordnern"
|
||||
data-testid="mobile-back-to-folders"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
@@ -1003,7 +1003,7 @@ export function MailPage() {
|
||||
<button
|
||||
onClick={() => setActiveView('list')}
|
||||
className="inline-flex items-center gap-1 px-2 py-1 rounded text-sm text-secondary-700 hover:bg-secondary-100 min-h-touch"
|
||||
aria-label={t('mail.zurückzurliste')}
|
||||
aria-label="Zurück zur Liste"
|
||||
data-testid="mobile-back-to-list"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
|
||||
@@ -221,25 +221,25 @@ export function MailSettingsPage() {
|
||||
label={t('mail.email')}
|
||||
{...registerAccount('email')}
|
||||
error={accountErrors.email?.message === 'required' ? t('validation.required') : accountErrors.email?.message === 'invalidEmail' ? t('validation.email') : undefined}
|
||||
placeholder={t('mailSettings.userexamplecom')}
|
||||
placeholder="user@example.com"
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
label={t('mail.displayName')}
|
||||
{...registerAccount('display_name')}
|
||||
placeholder={t('mailSettings.johndoe')}
|
||||
placeholder="John Doe"
|
||||
/>
|
||||
<Input
|
||||
label="Benutzername (IMAP/SMTP)"
|
||||
{...registerAccount('username')}
|
||||
placeholder={t('mailSettings.leerlassenfüremailadresse')}
|
||||
placeholder="Leer lassen für E-Mail-Adresse"
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Input
|
||||
label={t('mail.imapHost')}
|
||||
{...registerAccount('imap_host')}
|
||||
error={accountErrors.imap_host?.message === 'required' ? t('validation.required') : undefined}
|
||||
placeholder={t('mailSettings.imapexamplecom')}
|
||||
placeholder="imap.example.com"
|
||||
/>
|
||||
<Input
|
||||
label={t('mail.imapPort')}
|
||||
@@ -253,7 +253,7 @@ export function MailSettingsPage() {
|
||||
label={t('mail.smtpHost')}
|
||||
{...registerAccount('smtp_host')}
|
||||
error={accountErrors.smtp_host?.message === 'required' ? t('validation.required') : undefined}
|
||||
placeholder={t('mailSettings.smtpexamplecom')}
|
||||
placeholder="smtp.example.com"
|
||||
/>
|
||||
<Input
|
||||
label={t('mail.smtpPort')}
|
||||
@@ -275,7 +275,7 @@ export function MailSettingsPage() {
|
||||
{...registerAccount('is_shared')}
|
||||
className="rounded"
|
||||
/>
|
||||
{t('mailSettings.geteiltespostfachfüralletenantbenutzer')}
|
||||
Geteiltes Postfach (für alle Tenant-Benutzer sichtbar)
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<Button type="submit" isLoading={accountSubmitting} size="sm">{t('common.save')}</Button>
|
||||
@@ -392,7 +392,7 @@ export function MailSettingsPage() {
|
||||
className="text-xs text-secondary-400 hover:text-secondary-600"
|
||||
data-testid={`folder-mapping-btn-${acc.id}`}
|
||||
>
|
||||
{t('mailSettings.ordnerzuordnungbearbeiten')}
|
||||
Ordner-Zuordnung bearbeiten
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ShieldX } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function NoAccessPage() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-secondary-50 p-4">
|
||||
<ShieldX className="w-16 h-16 text-secondary-400 mb-4" strokeWidth={1.5} />
|
||||
<h1 className="text-2xl font-bold text-secondary-700 mb-2">{t('noAccessPage.keinzugriff')}</h1>
|
||||
<h1 className="text-2xl font-bold text-secondary-700 mb-2">Kein Zugriff</h1>
|
||||
<p className="text-secondary-500 mb-6 text-center max-w-md">
|
||||
{t('noAccessPage.siehabenkeineberechtigungaufdiese')}
|
||||
Sie haben keine Berechtigung, auf diese Seite zuzugreifen.
|
||||
Bitte wenden Sie sich an einen Administrator, falls Sie Zugriff benötigen.
|
||||
</p>
|
||||
<Link
|
||||
to="/dashboard"
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 transition-colors"
|
||||
>
|
||||
{t('noAccessPage.zumdashboard')}
|
||||
Zum Dashboard
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -60,8 +60,7 @@ interface DownloadEntry {
|
||||
export function ReportsPage() {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const registerItems = usePluginToolbarStore((s) => s.registerItems);
|
||||
const unregisterPlugin = usePluginToolbarStore((s) => s.unregisterPlugin);
|
||||
const { registerItems, unregisterPlugin } = usePluginToolbarStore();
|
||||
|
||||
// Data hooks
|
||||
const { data: templates = [], isLoading: templatesLoading } = useReportTemplates();
|
||||
@@ -398,7 +397,7 @@ export function ReportsPage() {
|
||||
<textarea
|
||||
value={jsonData}
|
||||
onChange={(e) => setJsonData(e.target.value)}
|
||||
placeholder={t('reports.keyvalue')}
|
||||
placeholder='{"key": "value"}'
|
||||
className="w-full text-xs font-mono border border-secondary-300 rounded px-2 py-1.5 h-40 resize-none"
|
||||
spellCheck={false}
|
||||
data-testid="textarea-json-data"
|
||||
|
||||
@@ -74,8 +74,8 @@ export function SettingsPage() {
|
||||
<button
|
||||
onClick={() => window.location.href = '/start'}
|
||||
className="p-1.5 rounded-md text-secondary-400 hover:bg-secondary-100 hover:text-secondary-700 flex items-center justify-center focus:outline-none"
|
||||
aria-label={t('settings.zurückzurstartseite')}
|
||||
title={t('settings.zurückzurstartseite')}
|
||||
aria-label="Zurück zur Startseite"
|
||||
title="Zurück zur Startseite"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
@@ -167,7 +167,7 @@ function RestoreModal({ open, backup, onConfirm, onCancel, isRestoring }: Restor
|
||||
type="text"
|
||||
value={confirmText}
|
||||
onChange={(e) => setConfirmText(e.target.value)}
|
||||
placeholder={t('settingsBackup.restore')}
|
||||
placeholder="RESTORE"
|
||||
className={clsx(
|
||||
'block w-full rounded-md border border-secondary-300 px-3 py-2 text-base min-h-touch',
|
||||
'focus:outline-none focus:ring-2 focus:ring-danger-500 focus:border-danger-500',
|
||||
|
||||
@@ -135,7 +135,7 @@ export function SettingsMcpPage() {
|
||||
onChange={(e) => setExecuteToolName(e.target.value)}
|
||||
className="border border-secondary-300 rounded px-3 py-1.5 text-sm"
|
||||
>
|
||||
<option value="">{t('settingsMcp.select')}</option>
|
||||
<option value="">-- Select --</option>
|
||||
{toolsData?.tools.map((tool) => (
|
||||
<option key={tool.name} value={tool.name}>{tool.name}</option>
|
||||
))}
|
||||
|
||||
@@ -149,7 +149,7 @@ function InstallPluginSection() {
|
||||
type="text"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
placeholder={t('settingsPlugins.httpsexamplecompluginzip')}
|
||||
placeholder="https://example.com/plugin.zip"
|
||||
className="flex-1 rounded-lg border border-secondary-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
data-testid="plugin-url-input"
|
||||
/>
|
||||
|
||||
@@ -198,7 +198,7 @@ function FreigabenTab() {
|
||||
<button
|
||||
onClick={() => setConfirmDelete(row)}
|
||||
className="p-1 rounded hover:bg-danger-50 text-danger-600"
|
||||
aria-label={t('settingsRechte.löschen')}
|
||||
aria-label="Löschen"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
@@ -217,7 +217,7 @@ function FreigabenTab() {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Card title={t('settingsRechte.freigabenübersicht')}>
|
||||
<Card title="Freigaben Übersicht">
|
||||
<div className="flex gap-4 mb-4">
|
||||
<div className="w-64">
|
||||
<Select
|
||||
@@ -233,7 +233,7 @@ function FreigabenTab() {
|
||||
<div className="w-64">
|
||||
<Input
|
||||
label="Nach Principal suchen"
|
||||
placeholder={t('settingsRechte.nameoderid')}
|
||||
placeholder="Name oder ID..."
|
||||
value={filterPrincipal}
|
||||
onChange={(e) => setFilterPrincipal(e.target.value)}
|
||||
/>
|
||||
@@ -253,7 +253,7 @@ function FreigabenTab() {
|
||||
open={!!confirmDelete}
|
||||
onCancel={() => setConfirmDelete(null)}
|
||||
onConfirm={handleDelete}
|
||||
title={t('settingsRechte.berechtigunglöschen')}
|
||||
title="Berechtigung löschen"
|
||||
message={`Soll die Berechtigung für ${entityTypeMap.get(confirmDelete.entity_type) || confirmDelete.entity_type} wirklich gelöscht werden?`}
|
||||
confirmLabel={deleting ? 'Wird gelöscht...' : 'Löschen'}
|
||||
variant="danger"
|
||||
@@ -325,7 +325,7 @@ function AuditTab() {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Card title={t('settingsRechte.auditlogfürberechtigungen')}>
|
||||
<Card title="Audit-Log für Berechtigungen">
|
||||
<Table
|
||||
columns={columns}
|
||||
data={data?.items || []}
|
||||
@@ -384,7 +384,7 @@ export function SettingsRechtePage() {
|
||||
<h1 className="text-2xl font-bold text-secondary-900">Rechteverwaltung</h1>
|
||||
|
||||
<div className="border-b border-secondary-200">
|
||||
<nav className="flex gap-4" role="tablist" aria-label={t('settingsRechte.rechteverwaltungtabs')}>
|
||||
<nav className="flex gap-4" role="tablist" aria-label="Rechteverwaltung Tabs">
|
||||
{tabs.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
return (
|
||||
|
||||
@@ -127,7 +127,7 @@ export function SettingsSequencesPage() {
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-secondary-700">{t('sequences.prefix')}</label>
|
||||
<input type="text" {...register('prefix')} placeholder={t('settingsSequences.re')} className="mt-1 block w-full rounded-md border border-secondary-300 px-3 py-1.5 text-sm" />
|
||||
<input type="text" {...register('prefix')} placeholder="RE-" className="mt-1 block w-full rounded-md border border-secondary-300 px-3 py-1.5 text-sm" />
|
||||
{errors.prefix && <p className="text-xs text-danger-600 mt-1">{errorMsg(errors.prefix.message)}</p>}
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -167,10 +167,10 @@ function AdressenTab() {
|
||||
{
|
||||
key: 'actions', header: '', render: (row) => (
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => handleEdit(row)} className="p-1 rounded hover:bg-secondary-100" aria-label={t('settingsStammdaten.bearbeiten')}>
|
||||
<button onClick={() => handleEdit(row)} className="p-1 rounded hover:bg-secondary-100" aria-label="Bearbeiten">
|
||||
<Pencil className="w-4 h-4" />
|
||||
</button>
|
||||
<button onClick={() => handleDelete(row.id)} className="p-1 rounded hover:bg-danger-50 text-danger-600" aria-label={t('settingsStammdaten.löschen')}>
|
||||
<button onClick={() => handleDelete(row.id)} className="p-1 rounded hover:bg-danger-50 text-danger-600" aria-label="Löschen">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -180,14 +180,14 @@ function AdressenTab() {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Card title={t('settingsStammdaten.adressverwaltung')} actions={
|
||||
<Card title="Adressverwaltung" actions={
|
||||
<Button size="sm" onClick={handleAdd}>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
{t('common.add', 'Hinzufügen')}
|
||||
</Button>
|
||||
}>
|
||||
{loading ? (
|
||||
<div className="text-center py-4 text-secondary-500">{t('settingsStammdaten.ladeadressen')}</div>
|
||||
<div className="text-center py-4 text-secondary-500">Lade Adressen...</div>
|
||||
) : (
|
||||
<Table columns={columns} data={addresses} rowKey={(row) => row.id} emptyMessage="Noch keine Adressen angelegt" />
|
||||
)}
|
||||
@@ -352,10 +352,10 @@ function KontenTab() {
|
||||
{
|
||||
key: 'actions', header: '', render: (row) => (
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => handleEdit(row)} className="p-1 rounded hover:bg-secondary-100" aria-label={t('settingsStammdaten.bearbeiten')}>
|
||||
<button onClick={() => handleEdit(row)} className="p-1 rounded hover:bg-secondary-100" aria-label="Bearbeiten">
|
||||
<Pencil className="w-4 h-4" />
|
||||
</button>
|
||||
<button onClick={() => handleDelete(row.id)} className="p-1 rounded hover:bg-danger-50 text-danger-600" aria-label={t('settingsStammdaten.löschen')}>
|
||||
<button onClick={() => handleDelete(row.id)} className="p-1 rounded hover:bg-danger-50 text-danger-600" aria-label="Löschen">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -365,14 +365,14 @@ function KontenTab() {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Card title={t('settingsStammdaten.bankkontenverwaltung')} actions={
|
||||
<Card title="Bankkontenverwaltung" actions={
|
||||
<Button size="sm" onClick={handleAdd}>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
{t('common.add', 'Hinzufügen')}
|
||||
</Button>
|
||||
}>
|
||||
{loading ? (
|
||||
<div className="text-center py-4 text-secondary-500">{t('settingsStammdaten.ladekonten')}</div>
|
||||
<div className="text-center py-4 text-secondary-500">Lade Konten...</div>
|
||||
) : (
|
||||
<Table columns={columns} data={accounts} rowKey={(row) => row.id} emptyMessage="Noch keine Konten angelegt" />
|
||||
)}
|
||||
@@ -447,7 +447,7 @@ export function SettingsStammdatenPage() {
|
||||
<h1 className="text-2xl font-bold text-secondary-900">Stammdaten</h1>
|
||||
|
||||
<div className="border-b border-secondary-200">
|
||||
<nav className="flex gap-4" role="tablist" aria-label={t('settingsStammdaten.stammdatentabs')}>
|
||||
<nav className="flex gap-4" role="tablist" aria-label="Stammdaten Tabs">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
|
||||
@@ -22,7 +22,7 @@ export function SettingsSystemPage() {
|
||||
<h1 className="text-2xl font-bold text-secondary-900">System</h1>
|
||||
|
||||
<div className="border-b border-secondary-200">
|
||||
<nav className="flex gap-4" role="tablist" aria-label={t('settingsSystem.systemtabs')}>
|
||||
<nav className="flex gap-4" role="tablist" aria-label="System Tabs">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
|
||||
@@ -135,7 +135,7 @@ export function SettingsTaxesPage() {
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-secondary-700">{t('taxes.country')}</label>
|
||||
<input type="text" {...register('country')} maxLength={2} placeholder={t('settingsTaxes.de')} className="mt-1 block w-full rounded-md border border-secondary-300 px-3 py-1.5 text-sm" />
|
||||
<input type="text" {...register('country')} maxLength={2} placeholder="DE" className="mt-1 block w-full rounded-md border border-secondary-300 px-3 py-1.5 text-sm" />
|
||||
{errors.country && <p className="text-xs text-danger-600 mt-1">{errorMsg(errors.country.message)}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -217,7 +217,7 @@ export function SettingsThemePage() {
|
||||
value={primaryColor}
|
||||
onChange={(e) => handlePrimaryChange(e.target.value)}
|
||||
className="flex-1"
|
||||
placeholder={t('settingsTheme.2563eb')}
|
||||
placeholder="#2563eb"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -239,7 +239,7 @@ export function SettingsThemePage() {
|
||||
value={accentColor}
|
||||
onChange={(e) => handleAccentChange(e.target.value)}
|
||||
className="flex-1"
|
||||
placeholder={t('settingsTheme.d946ef')}
|
||||
placeholder="#d946ef"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -302,10 +302,10 @@ export function SettingsThemePage() {
|
||||
<div className="space-y-4">
|
||||
{/* Buttons */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant="primary" size="sm">{t('settingsTheme.primarybutton')}</Button>
|
||||
<Button variant="secondary" size="sm">{t('settingsTheme.secondarybutton')}</Button>
|
||||
<Button variant="danger" size="sm">{t('settingsTheme.dangerbutton')}</Button>
|
||||
<Button variant="ghost" size="sm">{t('settingsTheme.ghostbutton')}</Button>
|
||||
<Button variant="primary" size="sm">Primary Button</Button>
|
||||
<Button variant="secondary" size="sm">Secondary Button</Button>
|
||||
<Button variant="danger" size="sm">Danger Button</Button>
|
||||
<Button variant="ghost" size="sm">Ghost Button</Button>
|
||||
</div>
|
||||
{/* Badges */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -317,11 +317,11 @@ export function SettingsThemePage() {
|
||||
</div>
|
||||
{/* Input preview */}
|
||||
<div className="max-w-xs">
|
||||
<Input label="Beispiel-Input" placeholder={t('settingsTheme.texteingeben')} />
|
||||
<Input label="Beispiel-Input" placeholder="Text eingeben..." />
|
||||
</div>
|
||||
{/* Card preview */}
|
||||
<div className="bg-white rounded-lg border border-secondary-200 p-4 shadow-sm">
|
||||
<p className="text-sm text-secondary-700">{t('settingsTheme.diesisteinebeispielkartemit')}</p>
|
||||
<p className="text-sm text-secondary-700">Dies ist eine Beispiel-Karte mit dem aktuellen Theme.</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -19,7 +19,7 @@ export function SettingsUserManagementPage() {
|
||||
<h1 className="text-2xl font-bold text-secondary-900">Nutzerverwaltung</h1>
|
||||
|
||||
<div className="border-b border-secondary-200">
|
||||
<nav className="flex gap-4" role="tablist" aria-label={t('settingsUserManagement.nutzerverwaltungtabs')}>
|
||||
<nav className="flex gap-4" role="tablist" aria-label="Nutzerverwaltung Tabs">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
|
||||
@@ -260,7 +260,7 @@ export function SettingsUsersPage() {
|
||||
type="email"
|
||||
{...register('email')}
|
||||
error={errorMsg(errors.email?.message)}
|
||||
placeholder={t('settingsUsers.neumitarbeiterfirmade')}
|
||||
placeholder="neu.mitarbeiter@firma.de"
|
||||
data-testid="invite-email"
|
||||
/>
|
||||
<Input
|
||||
|
||||
@@ -64,8 +64,8 @@ export function StartPage() {
|
||||
<button
|
||||
onClick={() => navigate('/start')}
|
||||
className="p-1.5 rounded-md text-secondary-400 hover:bg-secondary-100 hover:text-secondary-700 flex items-center justify-center focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
||||
aria-label={t('startPage.zurückzurstartseite')}
|
||||
title={t('startPage.zurückzurstartseite')}
|
||||
aria-label="Zurück zur Startseite"
|
||||
title="Zurück zur Startseite"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</button>
|
||||
@@ -101,7 +101,7 @@ export function StartPage() {
|
||||
<div className="max-w-5xl mx-auto p-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold text-secondary-900">Willkommen{user?.first_name ? `, ${user.first_name}` : ''}!</h1>
|
||||
<p className="text-sm text-secondary-500 mt-1">{t('startPage.wähleeinenworkspaceaus')}</p>
|
||||
<p className="text-sm text-secondary-500 mt-1">Wähle einen Workspace aus</p>
|
||||
</div>
|
||||
|
||||
{/* Dashboard stats */}
|
||||
@@ -171,7 +171,7 @@ export function StartPage() {
|
||||
>
|
||||
<Plus className="w-8 h-8 text-secondary-300 group-hover:text-primary-400 transition-colors" />
|
||||
<span className="text-sm text-secondary-400 group-hover:text-primary-600 mt-2 transition-colors">
|
||||
{t('startPage.workspacehinzufügen')}
|
||||
Workspace hinzufügen
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -116,18 +116,18 @@ function TaskTree({ tasks, selectedTaskId, onSelect, filter, setFilter }: TaskTr
|
||||
<button
|
||||
onClick={() => setTreeMode('status')}
|
||||
className={`px-2 py-1 text-xs rounded ${treeMode === 'status' ? 'bg-primary-100 text-primary-700' : 'text-secondary-500 hover:bg-secondary-100'}`}
|
||||
>{t('tasks.nachstatus')}</button>
|
||||
>Nach Status</button>
|
||||
<button
|
||||
onClick={() => setTreeMode('priority')}
|
||||
className={`px-2 py-1 text-xs rounded ${treeMode === 'priority' ? 'bg-primary-100 text-primary-700' : 'text-secondary-500 hover:bg-secondary-100'}`}
|
||||
>{t('tasks.nachpriorität')}</button>
|
||||
>Nach Priorität</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-2">
|
||||
<button
|
||||
onClick={() => setFilter({})}
|
||||
className={`w-full text-left px-3 py-2 rounded-md text-sm font-medium min-h-touch hover:bg-secondary-100 ${!filter.status && !filter.priority ? 'bg-primary-50 text-primary-700' : 'text-secondary-700'}`}
|
||||
>{t('tasks.alletasks')}{tasks.length})</button>
|
||||
>Alle Tasks ({tasks.length})</button>
|
||||
{grouped.map(group => (
|
||||
<div key={group.key} className="mt-1">
|
||||
<div
|
||||
@@ -201,7 +201,7 @@ function TaskDetail({ task, onEdit, onDelete, onStatusChange }: TaskDetailProps)
|
||||
<div><span className="font-medium text-secondary-500">Status:</span>
|
||||
<select value={task.status} onChange={(e) => onStatusChange(e.target.value as TaskStatus)} className="ml-2 text-sm border border-secondary-200 rounded px-2 py-1">
|
||||
<option value="open">Offen</option>
|
||||
<option value="in_progress">{t('tasks.inbearbeitung')}</option>
|
||||
<option value="in_progress">In Bearbeitung</option>
|
||||
<option value="review">Review</option>
|
||||
<option value="done">Erledigt</option>
|
||||
<option value="cancelled">Abgebrochen</option>
|
||||
@@ -220,8 +220,7 @@ function TaskDetail({ task, onEdit, onDelete, onStatusChange }: TaskDetailProps)
|
||||
export function TasksPage() {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const registerItems = usePluginToolbarStore((s) => s.registerItems);
|
||||
const unregisterPlugin = usePluginToolbarStore((s) => s.unregisterPlugin);
|
||||
const { registerItems, unregisterPlugin } = usePluginToolbarStore();
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
const [filter, setFilter] = useState<TaskFilter>({});
|
||||
@@ -371,7 +370,7 @@ export function TasksPage() {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{col.items.length === 0 && <p className="text-xs text-secondary-400 text-center py-4">{t('tasks.keinetasks')}</p>}
|
||||
{col.items.length === 0 && <p className="text-xs text-secondary-400 text-center py-4">Keine Tasks</p>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -92,7 +92,7 @@ export function WorkflowsPage() {
|
||||
{t('workflows.title', 'Workflows')}
|
||||
</h1>
|
||||
<p className="text-sm text-secondary-500 mt-1">
|
||||
{t('workflows.definierenundverwaltensieautomatisiertew')}
|
||||
Definieren und verwalten Sie automatisierte Workflows
|
||||
</p>
|
||||
</div>
|
||||
{activeTab === 'definitions' && (
|
||||
@@ -147,9 +147,9 @@ export function WorkflowsPage() {
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-3 text-danger-600">
|
||||
<AlertCircle className="h-5 w-5" />
|
||||
<span>{t('workflows.fehlerbeimladenderworkflows')}</span>
|
||||
<span>Fehler beim Laden der Workflows</span>
|
||||
<Button size="sm" variant="secondary" onClick={() => refetch()}>
|
||||
{t('workflows.erneutversuchen')}
|
||||
Erneut versuchen
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -157,12 +157,12 @@ export function WorkflowsPage() {
|
||||
|
||||
{!isLoading && !isError && workflows.length === 0 && (
|
||||
<EmptyState
|
||||
title={t('workflows.keineworkflows')}
|
||||
title="Keine Workflows"
|
||||
description="Erstellen Sie Ihren ersten Workflow, um automatisierte Prozesse zu definieren."
|
||||
icon={<WorkflowIcon className="h-8 w-8" />}
|
||||
action={
|
||||
<Button onClick={openCreate} icon={<Plus className="h-4 w-4" />}>
|
||||
{t('workflows.workflowerstellen')}
|
||||
Workflow erstellen
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
@@ -216,7 +216,7 @@ export function WorkflowsPage() {
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => openEdit(wf)}
|
||||
title={t('workflows.bearbeiten')}
|
||||
title="Bearbeiten"
|
||||
>
|
||||
<Settings2 className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -224,7 +224,7 @@ export function WorkflowsPage() {
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => setConfirmDelete(wf)}
|
||||
title={t('workflows.loeschen')}
|
||||
title="Loeschen"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-danger-500" />
|
||||
</Button>
|
||||
@@ -269,7 +269,7 @@ export function WorkflowsPage() {
|
||||
open={!!confirmDelete}
|
||||
onCancel={() => setConfirmDelete(null)}
|
||||
onConfirm={handleDelete}
|
||||
title={t('workflows.workflowloeschen')}
|
||||
title="Workflow loeschen"
|
||||
message={`Moechten Sie den Workflow "${confirmDelete?.name}" wirklich loeschen?`}
|
||||
confirmLabel="Loeschen"
|
||||
variant="danger"
|
||||
|
||||
@@ -40,11 +40,7 @@ vi.mock('react-i18next', () => ({
|
||||
// Mock toast
|
||||
vi.mock('@/components/ui/Toast', () => ({
|
||||
useToast: () => ({
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
info: vi.fn(),
|
||||
remove: vi.fn(),
|
||||
toast: { success: vi.fn(), error: vi.fn() },
|
||||
}),
|
||||
}));
|
||||
|
||||
|
||||
@@ -38,11 +38,7 @@ vi.mock('react-i18next', () => ({
|
||||
// Mock toast
|
||||
vi.mock('@/components/ui/Toast', () => ({
|
||||
useToast: () => ({
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
info: vi.fn(),
|
||||
remove: vi.fn(),
|
||||
toast: { success: vi.fn(), error: vi.fn() },
|
||||
}),
|
||||
}));
|
||||
|
||||
|
||||
@@ -20,9 +20,9 @@ export function AgentsOverviewPage() {
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h1 className="text-2xl font-bold text-secondary-900 mb-4">{t('agentsOverview.agentenübersicht')}</h1>
|
||||
<h1 className="text-2xl font-bold text-secondary-900 mb-4">Agenten Übersicht</h1>
|
||||
<p className="text-secondary-600 mb-6">
|
||||
{t('agentsOverview.verwaltensiekiagentenführensie')}
|
||||
Verwalten Sie KI-Agenten, führen Sie diese aus und überwachen Sie deren Ausführungen.
|
||||
</p>
|
||||
|
||||
{/* Loading */}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function AgentsPlaceholderPage() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h1 className="text-2xl font-bold text-secondary-900 mb-4">Agenten</h1>
|
||||
<p className="text-secondary-600">
|
||||
{t('agentsPlaceholder.dieseseitewirdgeradeerstelltwählen')}
|
||||
Diese Seite wird gerade erstellt. Wählen Sie ein Thema aus dem Menü links.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function AutomationOverviewPage() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h1 className="text-2xl font-bold text-secondary-900 mb-4">{t('automationOverview.automationübersicht')}</h1>
|
||||
<h1 className="text-2xl font-bold text-secondary-900 mb-4">Automation Übersicht</h1>
|
||||
<p className="text-secondary-600 mb-6">
|
||||
{t('automationOverview.erstellenundverwaltensieautomatisiertewo')}
|
||||
Erstellen und verwalten Sie automatisierte Workflows. Definieren Sie Trigger, Bedingungen und Aktionen.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="p-6 bg-white rounded-xl border border-secondary-200">
|
||||
@@ -15,14 +13,14 @@ export function AutomationOverviewPage() {
|
||||
<svg className="w-6 h-6 text-warning-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
||||
</div>
|
||||
<h3 className="font-semibold text-secondary-900">Automations</h3>
|
||||
<p className="text-sm text-secondary-500 mt-1">{t('automationOverview.workflowserstellenundverwalten')}</p>
|
||||
<p className="text-sm text-secondary-500 mt-1">Workflows erstellen und verwalten</p>
|
||||
</div>
|
||||
<div className="p-6 bg-white rounded-xl border border-secondary-200">
|
||||
<div className="w-12 h-12 bg-primary-100 rounded-lg flex items-center justify-center mb-3">
|
||||
<svg className="w-6 h-6 text-primary-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
|
||||
</div>
|
||||
<h3 className="font-semibold text-secondary-900">Einstellungen</h3>
|
||||
<p className="text-sm text-secondary-500 mt-1">{t('automationOverview.triggeraktionenundbedingungenkonfigurier')}</p>
|
||||
<p className="text-sm text-secondary-500 mt-1">Trigger, Aktionen und Bedingungen konfigurieren</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function AutomationPlaceholderPage() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h1 className="text-2xl font-bold text-secondary-900 mb-4">Automation</h1>
|
||||
<p className="text-secondary-600">
|
||||
{t('automationPlaceholder.dieseseitewirdgeradeerstelltwählen')}
|
||||
Diese Seite wird gerade erstellt. Wählen Sie ein Thema aus dem Menü links.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,33 +1,31 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function HelpApiDocsPage() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto prose prose-secondary">
|
||||
<h1 className="text-2xl font-bold text-secondary-900 mb-4">{t('helpApiDocs.apidokumentation')}</h1>
|
||||
<h1 className="text-2xl font-bold text-secondary-900 mb-4">API Dokumentation</h1>
|
||||
<p className="text-secondary-600 mb-4">
|
||||
{t('helpApiDocs.dievollständigeapidokumentationfindensie')} <a href="/docs" target="_blank" rel="noopener noreferrer" className="text-primary-600 hover:underline">/docs</a> {t('helpApiDocs.swaggerui')}
|
||||
Die vollständige API-Dokumentation finden Sie unter <a href="/docs" target="_blank" rel="noopener noreferrer" className="text-primary-600 hover:underline">/docs</a> (Swagger UI).
|
||||
</p>
|
||||
<h2 className="text-lg font-semibold text-secondary-800 mt-6 mb-2">Übersicht</h2>
|
||||
<p className="text-secondary-600 mb-4">
|
||||
{t('helpApiDocs.leocrmbieteteinerestapimit')}
|
||||
LeoCRM bietet eine REST-API mit über 224 Endpoints. Die API verwendet JSON für Request- und Response-Bodies.
|
||||
</p>
|
||||
<h2 className="text-lg font-semibold text-secondary-800 mt-6 mb-2">Authentifizierung</h2>
|
||||
<p className="text-secondary-600 mb-4">
|
||||
{t('helpApiDocs.dieapiverwendetsessionbasierteauthentifi')} <code className="bg-secondary-100 px-1.5 py-0.5 rounded text-sm">{t('helpApiDocs.postapiv1authlogin')}</code> {t('helpApiDocs.wirdeinsessioncookiegesetzt')}
|
||||
Die API verwendet Session-basierte Authentifizierung mit HttpOnly-Cookies. Nach dem Login über <code className="bg-secondary-100 px-1.5 py-0.5 rounded text-sm">POST /api/v1/auth/login</code> wird ein Session-Cookie gesetzt.
|
||||
</p>
|
||||
<h2 className="text-lg font-semibold text-secondary-800 mt-6 mb-2">{t('helpApiDocs.wichtigeendpoints')}</h2>
|
||||
<h2 className="text-lg font-semibold text-secondary-800 mt-6 mb-2">Wichtige Endpoints</h2>
|
||||
<ul className="list-disc list-inside text-secondary-600 space-y-1">
|
||||
<li><code className="bg-secondary-100 px-1.5 py-0.5 rounded text-sm">{t('helpApiDocs.getapiv1contacts')}</code> {t('helpApiDocs.kontakteabrufen')}</li>
|
||||
<li><code className="bg-secondary-100 px-1.5 py-0.5 rounded text-sm">{t('helpApiDocs.postapiv1contacts')}</code> {t('helpApiDocs.kontakterstellen')}</li>
|
||||
<li><code className="bg-secondary-100 px-1.5 py-0.5 rounded text-sm">{t('helpApiDocs.getapiv1calendarentries')}</code> {t('helpApiDocs.kalendereinträge')}</li>
|
||||
<li><code className="bg-secondary-100 px-1.5 py-0.5 rounded text-sm">{t('helpApiDocs.getapiv1mailaccounts')}</code> {t('helpApiDocs.mailkonten')}</li>
|
||||
<li><code className="bg-secondary-100 px-1.5 py-0.5 rounded text-sm">{t('helpApiDocs.getapiv1pluginsactivemanifests')}</code> {t('helpApiDocs.pluginmanifeste')}</li>
|
||||
<li><code className="bg-secondary-100 px-1.5 py-0.5 rounded text-sm">GET /api/v1/contacts</code> — Kontakte abrufen</li>
|
||||
<li><code className="bg-secondary-100 px-1.5 py-0.5 rounded text-sm">POST /api/v1/contacts</code> — Kontakt erstellen</li>
|
||||
<li><code className="bg-secondary-100 px-1.5 py-0.5 rounded text-sm">GET /api/v1/calendar/entries</code> — Kalendereinträge</li>
|
||||
<li><code className="bg-secondary-100 px-1.5 py-0.5 rounded text-sm">GET /api/v1/mail/accounts</code> — Mail-Konten</li>
|
||||
<li><code className="bg-secondary-100 px-1.5 py-0.5 rounded text-sm">GET /api/v1/plugins/active-manifests</code> — Plugin-Manifeste</li>
|
||||
</ul>
|
||||
<div className="mt-6 p-4 bg-primary-50 rounded-lg border border-primary-200">
|
||||
<p className="text-sm text-primary-700">
|
||||
📖 <strong>{t('helpApiDocs.vollständigedoku')}</strong> <a href="/docs" target="_blank" rel="noopener noreferrer" className="underline">{t('helpApiDocs.swaggeruiöffnen')}</a>
|
||||
📖 <strong>Vollständige Doku:</strong> <a href="/docs" target="_blank" rel="noopener noreferrer" className="underline">Swagger UI öffnen</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function HelpContactsPage() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto prose prose-secondary">
|
||||
<h1 className="text-2xl font-bold text-secondary-900 mb-4">{t('helpContacts.kontakteverwalten')}</h1>
|
||||
<h2 className="text-lg font-semibold text-secondary-800 mt-6 mb-2">{t('helpContacts.kontakteerstellen')}</h2>
|
||||
<h1 className="text-2xl font-bold text-secondary-900 mb-4">Kontakte verwalten</h1>
|
||||
<h2 className="text-lg font-semibold text-secondary-800 mt-6 mb-2">Kontakte erstellen</h2>
|
||||
<p className="text-secondary-600 mb-4">
|
||||
{t('helpContacts.gehensiezukontakteundklicken')}
|
||||
Gehen Sie zu Kontakte und klicken Sie auf "Neuer Kontakt". Füllen Sie die Felder aus und speichern Sie. Sie können Firmen und Personen anlegen.
|
||||
</p>
|
||||
<h2 className="text-lg font-semibold text-secondary-800 mt-6 mb-2">Kontaktpersonen</h2>
|
||||
<p className="text-secondary-600 mb-4">
|
||||
{t('helpContacts.jederfirmakönnenmehrerekontaktpersonenzu')}
|
||||
Jeder Firma können mehrere Kontaktpersonen zugeordnet werden. Öffnen Sie eine Firma und fügen Sie Personen hinzu.
|
||||
</p>
|
||||
<h2 className="text-lg font-semibold text-secondary-800 mt-6 mb-2">Tags</h2>
|
||||
<p className="text-secondary-600 mb-4">
|
||||
{t('helpContacts.verwendensietagsumkontaktezu')}
|
||||
Verwenden Sie Tags um Kontakte zu kategorisieren. Tags können frei vergeben werden und helfen bei der Filterung.
|
||||
</p>
|
||||
<h2 className="text-lg font-semibold text-secondary-800 mt-6 mb-2">Ordner</h2>
|
||||
<p className="text-secondary-600 mb-4">
|
||||
{t('helpContacts.organisierensiekontakteinordnernordner')}
|
||||
Organisieren Sie Kontakte in Ordnern. Ordner können verschachtelt werden und eigene Berechtigungen haben.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function HelpLoginPage() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto prose prose-secondary">
|
||||
<h1 className="text-2xl font-bold text-secondary-900 mb-4">{t('helpLogin.loginanmeldung')}</h1>
|
||||
<h1 className="text-2xl font-bold text-secondary-900 mb-4">Login & Anmeldung</h1>
|
||||
<h2 className="text-lg font-semibold text-secondary-800 mt-6 mb-2">Anmeldung</h2>
|
||||
<p className="text-secondary-600 mb-4">
|
||||
{t('helpLogin.rufensiedieleocrmurlauf')}
|
||||
Rufen Sie die LeoCRM-URL auf (z.B. https://crm.media-on.de) und melden Sie sich mit Ihrer E-Mail-Adresse und Ihrem Passwort an.
|
||||
</p>
|
||||
<h2 className="text-lg font-semibold text-secondary-800 mt-6 mb-2">{t('helpLogin.passwortvergessen')}</h2>
|
||||
<h2 className="text-lg font-semibold text-secondary-800 mt-6 mb-2">Passwort vergessen?</h2>
|
||||
<p className="text-secondary-600 mb-4">
|
||||
{t('helpLogin.klickensieaufderloginseite')}
|
||||
Klicken Sie auf der Login-Seite auf "Passwort vergessen". Sie erhalten eine E-Mail mit einem Link zum Zurücksetzen Ihres Passworts.
|
||||
</p>
|
||||
<h2 className="text-lg font-semibold text-secondary-800 mt-6 mb-2">Sicherheit</h2>
|
||||
<ul className="list-disc list-inside text-secondary-600 space-y-1">
|
||||
<li>{t('helpLogin.ihresitzungwirdübereinsicheres')}</li>
|
||||
<li>{t('helpLogin.nachinaktivitätwirddiesitzungautomatisch')}</li>
|
||||
<li>{t('helpLogin.passwörterwerdenmitbcryptcost12')}</li>
|
||||
<li>Ihre Sitzung wird über ein sicheres HttpOnly-Cookie verwaltet</li>
|
||||
<li>Nach Inaktivität wird die Sitzung automatisch beendet</li>
|
||||
<li>Passwörter werden mit bcrypt (cost=12) verschlüsselt gespeichert</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function HelpMailSetupPage() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto prose prose-secondary">
|
||||
<h1 className="text-2xl font-bold text-secondary-900 mb-4">{t('helpMailSetup.postfacheinrichten')}</h1>
|
||||
<h2 className="text-lg font-semibold text-secondary-800 mt-6 mb-2">{t('helpMailSetup.imapkontohinzufügen')}</h2>
|
||||
<h1 className="text-2xl font-bold text-secondary-900 mb-4">Postfach einrichten</h1>
|
||||
<h2 className="text-lg font-semibold text-secondary-800 mt-6 mb-2">IMAP-Konto hinzufügen</h2>
|
||||
<p className="text-secondary-600 mb-4">
|
||||
{t('helpMailSetup.gehensiezueinstellungenemailund')}
|
||||
Gehen Sie zu Einstellungen → Email und klicken Sie auf "Konto hinzufügen". Geben Sie Ihre IMAP- und SMTP-Serverdaten ein.
|
||||
</p>
|
||||
<h2 className="text-lg font-semibold text-secondary-800 mt-6 mb-2">{t('helpMailSetup.benötigtedaten')}</h2>
|
||||
<h2 className="text-lg font-semibold text-secondary-800 mt-6 mb-2">Benötigte Daten</h2>
|
||||
<ul className="list-disc list-inside text-secondary-600 space-y-1">
|
||||
<li>{t('helpMailSetup.imapserverzbimapexample')}</li>
|
||||
<li>{t('helpMailSetup.imapportmeist993fürssl')}</li>
|
||||
<li>{t('helpMailSetup.smtpserverzbsmtpexample')}</li>
|
||||
<li>{t('helpMailSetup.smtpportmeist587fürtls')}</li>
|
||||
<li>{t('helpMailSetup.emailadresseundpasswort')}</li>
|
||||
<li>IMAP-Server (z.B. imap.example.com)</li>
|
||||
<li>IMAP-Port (meist 993 für SSL)</li>
|
||||
<li>SMTP-Server (z.B. smtp.example.com)</li>
|
||||
<li>SMTP-Port (meist 587 für TLS)</li>
|
||||
<li>E-Mail-Adresse und Passwort</li>
|
||||
</ul>
|
||||
<h2 className="text-lg font-semibold text-secondary-800 mt-6 mb-2">Synchronisation</h2>
|
||||
<p className="text-secondary-600 mb-4">
|
||||
{t('helpMailSetup.nachdemeinrichtenwirdihrpostfach')}
|
||||
Nach dem Einrichten wird Ihr Postfach automatisch synchronisiert. Neue E-Mails werden im Hintergrund abgerufen.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,30 +1,28 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function HelpNavigationPage() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto prose prose-secondary">
|
||||
<h1 className="text-2xl font-bold text-secondary-900 mb-4">Navigation</h1>
|
||||
<h2 className="text-lg font-semibold text-secondary-800 mt-6 mb-2">Startseite</h2>
|
||||
<p className="text-secondary-600 mb-4">
|
||||
{t('helpNavigation.nachdemlogingelangensiezur')}
|
||||
Nach dem Login gelangen Sie zur Startseite. Hier können Sie einen Workspace auswählen oder zu den Einstellungen und der Hilfe navigieren.
|
||||
</p>
|
||||
<h2 className="text-lg font-semibold text-secondary-800 mt-6 mb-2">Workspace</h2>
|
||||
<p className="text-secondary-600 mb-4">
|
||||
{t('helpNavigation.einworkspaceistihrarbeitsbereichmit')}
|
||||
Ein Workspace ist Ihr Arbeitsbereich mit Sidebar-Navigation. Hier finden Sie Kontakte, Kalender, E-Mail und alle anderen Module.
|
||||
</p>
|
||||
<h2 className="text-lg font-semibold text-secondary-800 mt-6 mb-2">Sidebar</h2>
|
||||
<p className="text-secondary-600 mb-4">
|
||||
{t('helpNavigation.dashamburgermenüobenlinksblendet')}
|
||||
Das Hamburger-Menü oben links blendet die Seitenleiste ein und aus. Die Sidebar zeigt alle verfügbaren Module.
|
||||
</p>
|
||||
<h2 className="text-lg font-semibold text-secondary-800 mt-6 mb-2">Zurück-Pfeil</h2>
|
||||
<p className="text-secondary-600 mb-4">
|
||||
{t('helpNavigation.rechtsnebendemhamburgermenüfinden')}
|
||||
Rechts neben dem Hamburger-Menü finden Sie einen Zurück-Pfeil, der Sie zurück zur Startseite bringt.
|
||||
</p>
|
||||
<h2 className="text-lg font-semibold text-secondary-800 mt-6 mb-2">{t('helpNavigation.globalesuche')}</h2>
|
||||
<h2 className="text-lg font-semibold text-secondary-800 mt-6 mb-2">Globale Suche</h2>
|
||||
<p className="text-secondary-600 mb-4">
|
||||
{t('helpNavigation.verwendensiedielupeobenoder')}
|
||||
Verwenden Sie die Lupe oben oder Strg+K um das Kommando-Palette zu öffnen und schnell nach Kontakten, Mails oder Dateien zu suchen.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function HelpPlaceholderPage() {
|
||||
const { t } = useTranslation();
|
||||
const params = useParams();
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto prose prose-secondary">
|
||||
<h1 className="text-2xl font-bold text-secondary-900 mb-4">Hilfe</h1>
|
||||
<p className="text-secondary-600">
|
||||
{t('helpPlaceholder.diesehilfeseite')}{params['*'] || 'unbekannt'}{t('helpPlaceholder.wirdgeradeerstelltschauensiespäter')}
|
||||
Diese Hilfeseite ({params['*'] || 'unbekannt'}) wird gerade erstellt. Schauen Sie später wieder vorbei.
|
||||
</p>
|
||||
<p className="text-secondary-400 text-sm mt-4">
|
||||
{t('helpPlaceholder.wählensieeinthemaausdem')}
|
||||
Wählen Sie ein Thema aus dem Menü links um weitere Hilfe-Artikel zu lesen.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,33 +1,31 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function HelpWelcomePage() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto prose prose-secondary">
|
||||
<h1 className="text-2xl font-bold text-secondary-900 mb-4">{t('helpWelcome.willkommenbeileocrm')}</h1>
|
||||
<h1 className="text-2xl font-bold text-secondary-900 mb-4">Willkommen bei LeoCRM</h1>
|
||||
<p className="text-secondary-600 mb-4">
|
||||
{t('helpWelcome.leocrmisteinselbstgehostetescrm')}
|
||||
LeoCRM ist ein selbst-gehostetes CRM-System für kleine Vertriebsteams. Es bietet Kontakte, Kalender, E-Mail, Dateiverwaltung und mehr — alles in einer Anwendung.
|
||||
</p>
|
||||
<h2 className="text-lg font-semibold text-secondary-800 mt-6 mb-2">Hauptfunktionen</h2>
|
||||
<ul className="list-disc list-inside text-secondary-600 space-y-1">
|
||||
<li><strong>{t('helpWelcome.kontakteverwalten')}</strong> {t('helpWelcome.firmenpersonenkontaktdatenzentralspeiche')}</li>
|
||||
<li><strong>Kalender</strong> {t('helpWelcome.termineaufgabenunderinnerungen')}</li>
|
||||
<li><strong>E-Mail</strong> {t('helpWelcome.imappostfächersynchronisierendirektantwo')}</li>
|
||||
<li><strong>{t('helpWelcome.dateiendms')}</strong> {t('helpWelcome.dokumentehochladenteilenundverwalten')}</li>
|
||||
<li><strong>KI-Assistent</strong> {t('helpWelcome.intelligentehilfebeiderarbeit')}</li>
|
||||
<li><strong>Workflows</strong> {t('helpWelcome.automatisierteprozesse')}</li>
|
||||
<li><strong>Kontakte verwalten</strong> — Firmen, Personen, Kontaktdaten zentral speichern</li>
|
||||
<li><strong>Kalender</strong> — Termine, Aufgaben und Erinnerungen</li>
|
||||
<li><strong>E-Mail</strong> — IMAP-Postfächer synchronisieren, direkt antworten</li>
|
||||
<li><strong>Dateien (DMS)</strong> — Dokumente hochladen, teilen und verwalten</li>
|
||||
<li><strong>KI-Assistent</strong> — Intelligente Hilfe bei der Arbeit</li>
|
||||
<li><strong>Workflows</strong> — Automatisierte Prozesse</li>
|
||||
</ul>
|
||||
<h2 className="text-lg font-semibold text-secondary-800 mt-6 mb-2">{t('helpWelcome.ersteschritte')}</h2>
|
||||
<h2 className="text-lg font-semibold text-secondary-800 mt-6 mb-2">Erste Schritte</h2>
|
||||
<ol className="list-decimal list-inside text-secondary-600 space-y-1">
|
||||
<li>{t('helpWelcome.meldensiesichmitihrenzugangsdaten')}</li>
|
||||
<li>{t('helpWelcome.wählensieeinenworkspaceaufder')}</li>
|
||||
<li>{t('helpWelcome.beginnensiemitdemanlegenvon')}</li>
|
||||
<li>{t('helpWelcome.richtensieihremailpostfach')}</li>
|
||||
<li>Melden Sie sich mit Ihren Zugangsdaten an</li>
|
||||
<li>Wählen Sie einen Workspace auf der Startseite</li>
|
||||
<li>Beginnen Sie mit dem Anlegen von Kontakten</li>
|
||||
<li>Richten Sie Ihr E-Mail-Postfach unter Einstellungen → Email ein</li>
|
||||
</ol>
|
||||
<div className="mt-6 p-4 bg-primary-50 rounded-lg border border-primary-200">
|
||||
<p className="text-sm text-primary-700">
|
||||
💡 <strong>Tipp:</strong> {t('helpWelcome.nutzensiedieglobalesuchelupe')}
|
||||
💡 <strong>Tipp:</strong> Nutzen Sie die globale Suche (Lupe oben) oder das Kommando-Palette (Strg+K) um schnell zu finden was Sie brauchen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function LogsOverviewPage() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h1 className="text-2xl font-bold text-secondary-900 mb-4">{t('logsOverview.logsübersicht')}</h1>
|
||||
<h1 className="text-2xl font-bold text-secondary-900 mb-4">Logs Übersicht</h1>
|
||||
<p className="text-secondary-600 mb-6">
|
||||
{t('logsOverview.systemundauditlogseinsehenfiltern')}
|
||||
System- und Audit-Logs einsehen, filtern und exportieren.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="p-6 bg-white rounded-xl border border-secondary-200">
|
||||
@@ -15,21 +13,21 @@ export function LogsOverviewPage() {
|
||||
<svg className="w-6 h-6 text-primary-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>
|
||||
</div>
|
||||
<h3 className="font-semibold text-secondary-900">Audit-Log</h3>
|
||||
<p className="text-sm text-secondary-500 mt-1">{t('logsOverview.alleänderungennachverfolgen')}</p>
|
||||
<p className="text-sm text-secondary-500 mt-1">Alle Änderungen nachverfolgen</p>
|
||||
</div>
|
||||
<div className="p-6 bg-white rounded-xl border border-secondary-200">
|
||||
<div className="w-12 h-12 bg-warning-100 rounded-lg flex items-center justify-center mb-3">
|
||||
<svg className="w-6 h-6 text-warning-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
|
||||
</div>
|
||||
<h3 className="font-semibold text-secondary-900">System-Logs</h3>
|
||||
<p className="text-sm text-secondary-500 mt-1">{t('logsOverview.containerworkerdatenbank')}</p>
|
||||
<p className="text-sm text-secondary-500 mt-1">Container, Worker, Datenbank</p>
|
||||
</div>
|
||||
<div className="p-6 bg-white rounded-xl border border-secondary-200">
|
||||
<div className="w-12 h-12 bg-danger-100 rounded-lg flex items-center justify-center mb-3">
|
||||
<svg className="w-6 h-6 text-danger-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
</div>
|
||||
<h3 className="font-semibold text-secondary-900">Fehler</h3>
|
||||
<p className="text-sm text-secondary-500 mt-1">{t('logsOverview.apiundpluginfehler')}</p>
|
||||
<p className="text-sm text-secondary-500 mt-1">API- und Plugin-Fehler</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function LogsPlaceholderPage() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h1 className="text-2xl font-bold text-secondary-900 mb-4">Logs</h1>
|
||||
<p className="text-secondary-600">
|
||||
{t('logsPlaceholder.dieseseitewirdgeradeerstelltwählen')}
|
||||
Diese Seite wird gerade erstellt. Wählen Sie ein Thema aus dem Menü links.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user