b311ab7aa1
Check Cross-Plugin Imports / check (push) Has been cancelled
- 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
272 lines
18 KiB
TypeScript
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} />;
|
|
}
|