Files
leocrm/frontend/src/routes/index.tsx
T
Agent Zero b311ab7aa1
Check Cross-Plugin Imports / check (push) Has been cancelled
feat(L1-L3): Dokumente-Generator — Briefpapier+Block-System+Drag&Drop-Editor+Renderer
- Briefpapier (letterheads): Seiten-Setup (A4/A5/Letter, Ränder), Header/Footer-Blöcke, Wasserzeichen, Logo-Upload (DocumentAsset, data:-URI-only)
- Druckvorlagen (print_templates): Block-Komposition mit Briefpapier-Ref + entity_type
- Block-Registry (document_blocks.py): text/image/shape(line/rect/circle)/table/spacer/divider/placeholder/pagebreak + Modul-Beiträge via document_blocks()-Contract
- Renderer (document_renderer.py): Blocks→HTML→PDF via WeasyPrint (SSRF-Sandbox data:-URI-only), @page-Frame mit running header/footer, Placeholder-Beispiel-Defaults gegen StrictUndefined
- Contract-Beitrag contacts: document_placeholders/document_data (#359-Muster wie importexport_entities)
- 13 neue Endpoints in documents.py: Letterhead-CRUD, Template-CRUD, Assets, document-blocks, document-placeholders, preview (HTML), render (PDF)
- Migration: Plugin-SQL 0003 (idempotent) + Alembic 0143 (Dual-Path, RLS fail-closed crm_api)
- Frontend: api/documents.ts, Settings→Dokumente (settings_pages), BlockEditor (@dnd-kit Palette/Canvas/Config/Live-Preview-iframe), LetterheadEditor, PrintTemplateEditor, DocumentGenerationDialog (global, ContactDetailPage-Integration)
- i18n de/en, api-documentation.md, plugin-development-guide.md, PROGRESS.md

Verifikation: 32/32 neue Tests + 9/9 Regressionen, tsc exit 0, Build OK 2.79s, Alembic-Fresh-DB 0143 mit RLS bewiesen, ruff clean
2026-08-29 09:49:16 +02:00

272 lines
18 KiB
TypeScript

// TODO: P2-F1 — Replace hardcoded plugin routes with dynamic PluginRouteRenderer
import React, { Suspense } from 'react';
import { createBrowserRouter, RouterProvider, Navigate } from 'react-router-dom';
import { AppShell } from '@/components/layout/AppShell';
import { StartLayout } from '@/components/layout/StartLayout';
import { ProtectedRoute } from './ProtectedRoute';
import { ProtectedRoute as PermissionRoute } from '@/components/common/ProtectedRoute';
import { LoginPage } from '@/pages/Login';
import { GuestLoginPage } from '@/pages/GuestLogin';
import { GuestContactsPage } from '@/pages/GuestContacts';
import { PasswordResetRequestPage } from '@/pages/PasswordResetRequest';
import { PasswordResetConfirmPage } from '@/pages/PasswordResetConfirm';
import { Loader2 } from 'lucide-react';
import { PluginRouteRenderer } from '@/components/plugins/PluginRouteRenderer';
import { ErrorBoundary } from '@/components/common/ErrorBoundary';
// Lazy-loaded pages (code-splitting)
const DashboardPage = React.lazy(() => import('@/pages/Dashboard').then(m => ({ default: m.DashboardPage })));
const AuditLogPage = React.lazy(() => import('@/pages/AuditLog').then(m => ({ default: m.AuditLogPage })));
const GlobalSearchResultsPage = React.lazy(() => import('@/pages/GlobalSearchResults').then(m => ({ default: m.GlobalSearchResultsPage })));
const SettingsPage = React.lazy(() => import('@/pages/Settings').then(m => ({ default: m.SettingsPage })));
const SettingsProfilePage = React.lazy(() => import('@/pages/SettingsProfile').then(m => ({ default: m.SettingsProfilePage })));
const SettingsRolesPage = React.lazy(() => import('@/pages/SettingsRoles').then(m => ({ default: m.SettingsRolesPage })));
const SettingsUsersPage = React.lazy(() => import('@/pages/SettingsUsers').then(m => ({ default: m.SettingsUsersPage })));
const SettingsGroupsPage = React.lazy(() => import('@/pages/SettingsGroups').then(m => ({ default: m.SettingsGroupsPage })));
const SettingsPluginsPage = React.lazy(() => import('@/pages/SettingsPlugins').then(m => ({ default: m.SettingsPluginsPage })));
const SettingsSystemPage = React.lazy(() => import('@/pages/SettingsSystem').then(m => ({ default: m.SettingsSystemPage })));
const SettingsCurrenciesPage = React.lazy(() => import('@/pages/SettingsCurrencies').then(m => ({ default: m.SettingsCurrenciesPage })));
const SettingsTaxesPage = React.lazy(() => import('@/pages/SettingsTaxes').then(m => ({ default: m.SettingsTaxesPage })));
const SettingsSequencesPage = React.lazy(() => import('@/pages/SettingsSequences').then(m => ({ default: m.SettingsSequencesPage })));
const CalendarPage = React.lazy(() => import('@/pages/Calendar').then(m => ({ default: m.CalendarPage })));
const CalendarKanbanPage = React.lazy(() => import('@/pages/CalendarKanban').then(m => ({ default: m.CalendarKanbanPage })));
const DmsPage = React.lazy(() => import('@/pages/Dms').then(m => ({ default: m.DmsPage })));
const DmsTrashPage = React.lazy(() => import('@/pages/DmsTrash').then(m => ({ default: m.DmsTrashPage })));
const TrashPage = React.lazy(() => import('@/pages/Trash').then(m => ({ default: m.TrashPage })));
const MailPage = React.lazy(() => import('@/pages/Mail').then(m => ({ default: m.MailPage })));
const MailSettingsPage = React.lazy(() => import('@/pages/MailSettings').then(m => ({ default: m.MailSettingsPage })));
const SettingsNotificationsPage = React.lazy(() => import('@/pages/SettingsNotifications').then(m => ({ default: m.SettingsNotificationsPage })));
const AISettingsPage = React.lazy(() => import('@/pages/AISettings').then(m => ({ default: m.AISettingsPage })));
const ProactiveAISettings = React.lazy(() => import('@/pages/ProactiveAISettings').then(m => ({ default: m.ProactiveAISettings })));
const SettingsThemePage = React.lazy(() => import('@/pages/SettingsTheme').then(m => ({ default: m.SettingsThemePage })));
const SettingsMcpPage = React.lazy(() => import('@/pages/SettingsMcp').then(m => ({ default: m.SettingsMcpPage })));
const DmsStandalonePage = React.lazy(() => import('@/pages/DmsStandalone').then(m => ({ default: m.DmsStandalonePage })));
const CalendarStandalonePage = React.lazy(() => import('@/pages/CalendarStandalone').then(m => ({ default: m.CalendarStandalonePage })));
const MailStandalonePage = React.lazy(() => import('@/pages/MailStandalone').then(m => ({ default: m.MailStandalonePage })));
const ContactsStandalonePage = React.lazy(() => import('@/pages/ContactsStandalone').then(m => ({ default: m.ContactsStandalonePage })));
const SettingsAIPage = React.lazy(() => import('@/pages/SettingsAI').then(m => ({ default: m.SettingsAIPage })));
const SettingsMenuOrderPage = React.lazy(() => import('@/pages/SettingsMenuOrder').then(m => ({ default: m.SettingsMenuOrderPage })));
const SettingsStammdatenPage = React.lazy(() => import('@/pages/SettingsStammdaten').then(m => ({ default: m.SettingsStammdatenPage })));
const SettingsUserManagementPage = React.lazy(() => import('@/pages/SettingsUserManagement').then(m => ({ default: m.SettingsUserManagementPage })));
const AutomationDashboardPage = React.lazy(() => import('@/pages/AutomationDashboard').then(m => ({ default: m.AutomationDashboardPage })));
const AgentDashboardPage = React.lazy(() => import('@/pages/AgentDashboard').then(m => ({ default: m.AgentDashboardPage })));
const AutomationSettingsPage = React.lazy(() => import('@/pages/AutomationSettings').then(m => ({ default: m.AutomationSettingsPage })));
const ReportsPage = React.lazy(() => import('@/pages/Reports').then(m => ({ default: m.ReportsPage })));
const DocumentSettingsPage = React.lazy(() => import('@/pages/DocumentSettings').then(m => ({ default: m.DocumentSettingsPage })));
const TasksPage = React.lazy(() => import('@/pages/Tasks').then(m => ({ default: m.TasksPage })));
const CommunicationPage = React.lazy(() => import('@/pages/Communication').then(m => ({ default: m.CommunicationPage })));
const WorkflowsPage = React.lazy(() => import('@/pages/Workflows').then(m => ({ default: m.WorkflowsPage })));
const ImportExportPage = React.lazy(() => import('@/pages/ImportExport').then(m => ({ default: m.ImportExportPage })));
const TagsPage = React.lazy(() => import('@/pages/Tags').then(m => ({ default: m.TagsPage })));
const CustomFieldsPage = React.lazy(() => import('@/pages/CustomFields').then(m => ({ default: m.CustomFieldsPage })));
const ActivityTimelinePage = React.lazy(() => import('@/pages/ActivityTimeline').then(m => ({ default: m.ActivityTimelinePage })));
const SettingsWebhooksPage = React.lazy(() => import('@/pages/SettingsWebhooks').then(m => ({ default: m.SettingsWebhooksPage })));
const SettingsBackupPage = React.lazy(() => import('@/pages/SettingsBackup').then(m => ({ default: m.SettingsBackupPage })));
const WorkspaceManagerPage = React.lazy(() => import('@/pages/SettingsWorkspaces').then(m => ({ default: m.WorkspaceManagerPage })));
const SettingsRechtePage = React.lazy(() => import('@/pages/SettingsRechte').then(m => ({ default: m.SettingsRechtePage })));
const NoAccessPage = React.lazy(() => import('@/pages/NoAccessPage').then(m => ({ default: m.NoAccessPage })));
const StartPage = React.lazy(() => import('@/pages/StartPage').then(m => ({ default: m.StartPage })));
const HelpPage = React.lazy(() => import('@/pages/Help').then(m => ({ default: m.HelpPage })));
const HelpWelcomePage = React.lazy(() => import('@/pages/help/HelpWelcome').then(m => ({ default: m.HelpWelcomePage })));
const HelpLoginPage = React.lazy(() => import('@/pages/help/HelpLogin').then(m => ({ default: m.HelpLoginPage })));
const HelpNavigationPage = React.lazy(() => import('@/pages/help/HelpNavigation').then(m => ({ default: m.HelpNavigationPage })));
const HelpContactsPage = React.lazy(() => import('@/pages/help/HelpContacts').then(m => ({ default: m.HelpContactsPage })));
const HelpMailSetupPage = React.lazy(() => import('@/pages/help/HelpMailSetup').then(m => ({ default: m.HelpMailSetupPage })));
const HelpPlaceholderPage = React.lazy(() => import('@/pages/help/HelpPlaceholder').then(m => ({ default: m.HelpPlaceholderPage })));
const AgentsPage = React.lazy(() => import('@/pages/Agents').then(m => ({ default: m.AgentsPage })));
const AgentsOverviewPage = React.lazy(() => import('@/pages/agents/AgentsOverview').then(m => ({ default: m.AgentsOverviewPage })));
const AgentsPlaceholderPage = React.lazy(() => import('@/pages/agents/AgentsPlaceholder').then(m => ({ default: m.AgentsPlaceholderPage })));
const AutomationPage = React.lazy(() => import('@/pages/Automation').then(m => ({ default: m.AutomationPage })));
const AutomationOverviewPage = React.lazy(() => import('@/pages/automation/AutomationOverview').then(m => ({ default: m.AutomationOverviewPage })));
const AutomationPlaceholderPage = React.lazy(() => import('@/pages/automation/AutomationPlaceholder').then(m => ({ default: m.AutomationPlaceholderPage })));
const LogsPage = React.lazy(() => import('@/pages/Logs').then(m => ({ default: m.LogsPage })));
const LogsOverviewPage = React.lazy(() => import('@/pages/logs/LogsOverview').then(m => ({ default: m.LogsOverviewPage })));
const LogsPlaceholderPage = React.lazy(() => import('@/pages/logs/LogsPlaceholder').then(m => ({ default: m.LogsPlaceholderPage })));
const HelpApiDocsPage = React.lazy(() => import('@/pages/help/HelpApiDocs').then(m => ({ default: m.HelpApiDocsPage })));
const ApiDocsPage = React.lazy(() => import('@/pages/ApiDocs').then(m => ({ default: m.ApiDocsPage })));
const WikiPage = React.lazy(() => import('@/pages/Wiki').then(m => ({ default: m.WikiPage })));
const SystemDashboardPage = React.lazy(() => import('@/pages/SystemDashboard').then(m => ({ default: m.SystemDashboardPage })));
/** Centered spinner fallback for lazy-loaded routes */
function PageLoader() {
return (
<div className="flex items-center justify-center min-h-[50vh]" role="status" aria-label="Seite wird geladen">
<Loader2 className="animate-spin h-8 w-8 text-primary-500" aria-hidden="true" />
</div>
);
}
/** Wrap a lazy element in Suspense with PageLoader fallback */
function withSuspense(element: React.ReactElement) {
return <Suspense fallback={<PageLoader />}>{element}</Suspense>;
}
const router = createBrowserRouter([
{
path: '/login',
element: <LoginPage />,
},
{
},
{
path: '/dms-standalone',
element: <ErrorBoundary>{withSuspense(<DmsStandalonePage />)}</ErrorBoundary>,
},
{
path: '/calendar-standalone',
element: <ErrorBoundary>{withSuspense(<CalendarStandalonePage />)}</ErrorBoundary>,
},
{
path: '/mail-standalone',
element: <ErrorBoundary>{withSuspense(<MailStandalonePage />)}</ErrorBoundary>,
},
{
path: '/contacts-standalone',
element: <ErrorBoundary>{withSuspense(<ContactsStandalonePage />)}</ErrorBoundary>,
},
{
path: '/password-reset',
element: <PasswordResetRequestPage />,
},
{
path: '/password-reset/confirm',
element: <PasswordResetConfirmPage />,
},
{
path: '/guest/login',
element: <GuestLoginPage />,
},
{
path: '/guest/contacts',
element: <GuestContactsPage />,
},
{
path: '/kein-zugriff',
element: <ErrorBoundary>{withSuspense(<NoAccessPage />)}</ErrorBoundary>,
},
{
element: (
<ProtectedRoute>
<StartLayout />
</ProtectedRoute>
),
children: [
{ path: '/start', element: withSuspense(<StartPage />) },
{
path: '/help',
element: withSuspense(<HelpPage />),
children: [
{ path: 'welcome', element: withSuspense(<HelpWelcomePage />) },
{ path: 'login', element: withSuspense(<HelpLoginPage />) },
{ path: 'navigation', element: withSuspense(<HelpNavigationPage />) },
{ path: 'contacts', element: withSuspense(<HelpContactsPage />) },
{ path: 'mail-setup', element: withSuspense(<HelpMailSetupPage />) },
{ path: 'api-docs', element: withSuspense(<HelpApiDocsPage />) },
{ path: '*', element: withSuspense(<HelpPlaceholderPage />) },
],
},
{
path: '/agents',
element: withSuspense(<AgentsPage />),
children: [
{ path: 'overview', element: withSuspense(<AgentsOverviewPage />) },
{ path: 'automation', element: withSuspense(<AutomationDashboardPage />) },
{ path: 'automation-settings', element: withSuspense(<AutomationSettingsPage />) },
{ path: 'ai-settings', element: withSuspense(<AISettingsPage />) },
{ path: 'ai-proactive', element: withSuspense(<ProactiveAISettings />) },
{ path: '*', element: withSuspense(<AgentsPlaceholderPage />) },
],
},
{
path: '/automation',
element: withSuspense(<AutomationPage />),
children: [
{ path: 'overview', element: withSuspense(<AutomationOverviewPage />) },
{ path: 'runs', element: withSuspense(<AutomationDashboardPage />) },
{ path: 'versions', element: withSuspense(<AgentsPlaceholderPage />) },
{ path: 'triggers', element: withSuspense(<AgentsPlaceholderPage />) },
{ path: 'actions', element: withSuspense(<AgentsPlaceholderPage />) },
{ path: 'conditions', element: withSuspense(<AgentsPlaceholderPage />) },
{ path: '*', element: withSuspense(<AutomationPlaceholderPage />) },
],
},
{
path: '/logs',
element: withSuspense(<LogsPage />),
children: [
{ path: 'overview', element: withSuspense(<LogsOverviewPage />) },
{ path: '*', element: withSuspense(<LogsPlaceholderPage />) },
],
},
{
path: '/settings',
element: <PermissionRoute permission="settings:read">{withSuspense(<SettingsPage />)}</PermissionRoute>,
children: [
{ path: 'stammdaten', element: withSuspense(<SettingsStammdatenPage />) },
{ path: 'user-management', element: withSuspense(<SettingsUserManagementPage />) },
{ path: 'roles', element: withSuspense(<SettingsRolesPage />) },
{ path: 'users', element: withSuspense(<SettingsUsersPage />) },
{ path: 'groups', element: withSuspense(<SettingsGroupsPage />) },
{ path: 'plugins', element: withSuspense(<SettingsPluginsPage />) },
{ path: 'system', element: withSuspense(<SettingsSystemPage />) },
{ path: 'currencies', element: withSuspense(<SettingsCurrenciesPage />) },
{ path: 'taxes', element: withSuspense(<SettingsTaxesPage />) },
{ path: 'sequences', element: withSuspense(<SettingsSequencesPage />) },
{ path: 'mail', element: withSuspense(<MailSettingsPage />) },
{ path: 'notifications', element: withSuspense(<SettingsNotificationsPage />) },
{ path: 'ai', element: withSuspense(<AISettingsPage />) },
{ path: 'ai-proactive', element: withSuspense(<ProactiveAISettings />) },
{ path: 'theme', element: withSuspense(<SettingsThemePage />) },
{ path: 'automation', element: withSuspense(<AutomationSettingsPage />) },
{ path: 'mcp', element: withSuspense(<SettingsMcpPage />) },
{ path: 'ai-settings', element: withSuspense(<SettingsAIPage />) },
{ path: 'menu', element: withSuspense(<SettingsMenuOrderPage />) },
{ path: 'custom-fields', element: withSuspense(<CustomFieldsPage />) },
{ path: 'webhooks', element: withSuspense(<SettingsWebhooksPage />) },
{ path: 'workspaces', element: withSuspense(<WorkspaceManagerPage />) },
{ path: 'documents', element: withSuspense(<DocumentSettingsPage />) },
{ path: 'backup', element: withSuspense(<SettingsBackupPage />) },
{ path: 'rechte', element: <PermissionRoute permission="settings:read">{withSuspense(<SettingsRechtePage />)}</PermissionRoute> },
{ path: '*', element: <ErrorBoundary>{<PluginRouteRenderer />}</ErrorBoundary> },
],
},
],
},
{
element: (
<ProtectedRoute>
<AppShell />
</ProtectedRoute>
),
children: [
{ path: '/', element: <Navigate to="/start" replace /> },
{ path: '/dashboard', element: withSuspense(<DashboardPage />) },
{ path: '/audit-log', element: <PermissionRoute permission="audit:read">{withSuspense(<AuditLogPage />)}</PermissionRoute> },
{ path: '/search', element: withSuspense(<GlobalSearchResultsPage />) },
{ path: '/calendar', element: <PermissionRoute permission="calendar:read">{withSuspense(<CalendarPage />)}</PermissionRoute> },
{ path: '/calendar/kanban', element: <PermissionRoute permission="calendar:read">{withSuspense(<CalendarKanbanPage />)}</PermissionRoute> },
{ path: '/dms', element: <PermissionRoute permission="dms:read">{withSuspense(<DmsPage />)}</PermissionRoute> },
{ path: '/dms/trash', element: <PermissionRoute permission="dms:read">{withSuspense(<DmsTrashPage />)}</PermissionRoute> },
{ path: '/trash', element: <PermissionRoute permission="contacts:read">{withSuspense(<TrashPage />)}</PermissionRoute> },
{ path: '/mail', element: <PermissionRoute permission="mail:read">{withSuspense(<MailPage />)}</PermissionRoute> },
{ path: '/mail/settings', element: <PermissionRoute permission="mail:config">{withSuspense(<MailSettingsPage />)}</PermissionRoute> },
{ path: '/reports', element: <PermissionRoute permission="reports:read">{withSuspense(<ReportsPage />)}</PermissionRoute> },
{ path: '/tasks', element: <PermissionRoute permission="tasks:read">{withSuspense(<TasksPage />)}</PermissionRoute> },
{ path: '/communication', element: <PermissionRoute permission="comm:read">{withSuspense(<CommunicationPage />)}</PermissionRoute> },
{ path: '/workflows', element: <PermissionRoute permission="workflows:read">{withSuspense(<WorkflowsPage />)}</PermissionRoute> },
{ path: '/import-export', element: <PermissionRoute permission="import_export:read">{withSuspense(<ImportExportPage />)}</PermissionRoute> },
{ path: 'tags', element: <PermissionRoute permission="tags:read">{withSuspense(<TagsPage />)}</PermissionRoute> },
{ path: '/api-docs', element: <PermissionRoute permission="settings:read">{withSuspense(<ApiDocsPage />)}</PermissionRoute> },
{ path: '/activity', element: <PermissionRoute permission="audit:read">{withSuspense(<ActivityTimelinePage />)}</PermissionRoute> },
{ path: '/wiki', element: <PermissionRoute permission="wiki:read">{withSuspense(<WikiPage />)}</PermissionRoute> },
{ path: '/system-dashboard', element: withSuspense(<SystemDashboardPage />) },
{ path: '/profile', element: withSuspense(<SettingsProfilePage />) },
{ path: '*', element: <ErrorBoundary>{<PluginRouteRenderer />}</ErrorBoundary> },
],
},
]);
export function AppRouter() {
return <RouterProvider router={router} />;
}