fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
625
website/app/admin/workflow/page.tsx
Normal file
625
website/app/admin/workflow/page.tsx
Normal file
@@ -0,0 +1,625 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* BPMN Workflow Editor
|
||||
*
|
||||
* Admin interface for:
|
||||
* - Creating and editing BPMN 2.0 process diagrams
|
||||
* - Deploying processes to Camunda 7
|
||||
* - Managing deployed process definitions
|
||||
* - Viewing and completing pending tasks
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import AdminLayout from '@/components/admin/AdminLayout'
|
||||
import SystemInfoSection, { SYSTEM_INFO_CONFIGS } from '@/components/admin/SystemInfoSection'
|
||||
|
||||
// Types
|
||||
interface ProcessDefinition {
|
||||
id: string
|
||||
key: string
|
||||
name: string
|
||||
version: number
|
||||
deploymentId: string
|
||||
}
|
||||
|
||||
interface Task {
|
||||
id: string
|
||||
name: string
|
||||
processDefinitionId: string
|
||||
processInstanceId: string
|
||||
assignee?: string
|
||||
created: string
|
||||
}
|
||||
|
||||
interface CamundaHealth {
|
||||
connected: boolean
|
||||
engines?: Array<{ name: string }>
|
||||
error?: string
|
||||
}
|
||||
|
||||
// Default empty BPMN diagram
|
||||
const EMPTY_BPMN = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
|
||||
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
|
||||
xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
|
||||
xmlns:camunda="http://camunda.org/schema/1.0/bpmn"
|
||||
id="Definitions_1"
|
||||
targetNamespace="http://bpmn.io/schema/bpmn">
|
||||
<bpmn:process id="Process_1" name="Neuer Prozess" isExecutable="true">
|
||||
<bpmn:startEvent id="StartEvent_1" name="Start" />
|
||||
</bpmn:process>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
|
||||
<bpmndi:BPMNShape id="StartEvent_1_di" bpmnElement="StartEvent_1">
|
||||
<dc:Bounds x="180" y="160" width="36" height="36" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="186" y="203" width="24" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
</bpmn:definitions>`
|
||||
|
||||
export default function WorkflowPage() {
|
||||
// Refs
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const modelerRef = useRef<any>(null)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
// State
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [camundaHealth, setCamundaHealth] = useState<CamundaHealth>({ connected: false })
|
||||
const [processes, setProcesses] = useState<ProcessDefinition[]>([])
|
||||
const [tasks, setTasks] = useState<Task[]>([])
|
||||
const [showProcessPanel, setShowProcessPanel] = useState(false)
|
||||
const [showTaskPanel, setShowTaskPanel] = useState(false)
|
||||
const [elementCount, setElementCount] = useState(0)
|
||||
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null)
|
||||
|
||||
// Show toast notification
|
||||
const showToast = useCallback((message: string, type: 'success' | 'error') => {
|
||||
setToast({ message, type })
|
||||
setTimeout(() => setToast(null), 3000)
|
||||
}, [])
|
||||
|
||||
// Check Camunda health
|
||||
const checkCamundaHealth = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch('/api/bpmn/health')
|
||||
const data = await response.json()
|
||||
setCamundaHealth(data)
|
||||
} catch {
|
||||
setCamundaHealth({ connected: false, error: 'Connection failed' })
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Initialize BPMN modeler
|
||||
useEffect(() => {
|
||||
let mounted = true
|
||||
|
||||
const initModeler = async () => {
|
||||
if (!containerRef.current) return
|
||||
|
||||
try {
|
||||
// Dynamic import of bpmn-js (client-side only)
|
||||
const BpmnModeler = (await import('bpmn-js/lib/Modeler')).default
|
||||
|
||||
if (!mounted) return
|
||||
|
||||
const modeler = new BpmnModeler({
|
||||
container: containerRef.current,
|
||||
keyboard: {
|
||||
bindTo: document
|
||||
}
|
||||
})
|
||||
|
||||
modelerRef.current = modeler
|
||||
|
||||
// Load empty diagram
|
||||
await modeler.importXML(EMPTY_BPMN)
|
||||
|
||||
// Fit viewport
|
||||
const canvas = modeler.get('canvas') as any
|
||||
canvas.zoom('fit-viewport')
|
||||
|
||||
// Update element count on changes
|
||||
modeler.on('elements.changed', () => {
|
||||
if (modelerRef.current) {
|
||||
const elementRegistry = modelerRef.current.get('elementRegistry') as any
|
||||
setElementCount(elementRegistry.getAll().length)
|
||||
}
|
||||
})
|
||||
|
||||
// Initial element count
|
||||
const elementRegistry = modeler.get('elementRegistry') as any
|
||||
setElementCount(elementRegistry.getAll().length)
|
||||
|
||||
setIsLoading(false)
|
||||
} catch (err) {
|
||||
console.error('Error initializing BPMN modeler:', err)
|
||||
showToast('Fehler beim Initialisieren des Editors', 'error')
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
initModeler()
|
||||
checkCamundaHealth()
|
||||
|
||||
return () => {
|
||||
mounted = false
|
||||
if (modelerRef.current) {
|
||||
modelerRef.current.destroy()
|
||||
}
|
||||
}
|
||||
}, [checkCamundaHealth, showToast])
|
||||
|
||||
// Load deployed processes
|
||||
const loadProcesses = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/bpmn/process-definition')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setProcesses(data)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading processes:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Load pending tasks
|
||||
const loadTasks = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/bpmn/tasks/pending')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setTasks(data)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading tasks:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create new diagram
|
||||
const handleNew = async () => {
|
||||
if (!modelerRef.current) return
|
||||
|
||||
try {
|
||||
await modelerRef.current.importXML(EMPTY_BPMN)
|
||||
;(modelerRef.current.get('canvas') as any).zoom('fit-viewport')
|
||||
showToast('Neues Diagramm erstellt', 'success')
|
||||
} catch (err) {
|
||||
showToast('Fehler beim Erstellen', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
// Open file
|
||||
const handleOpen = () => {
|
||||
fileInputRef.current?.click()
|
||||
}
|
||||
|
||||
// Load file
|
||||
const handleFileLoad = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file || !modelerRef.current) return
|
||||
|
||||
try {
|
||||
const xml = await file.text()
|
||||
await modelerRef.current.importXML(xml)
|
||||
;(modelerRef.current.get('canvas') as any).zoom('fit-viewport')
|
||||
showToast(`Datei geladen: ${file.name}`, 'success')
|
||||
} catch (err) {
|
||||
showToast('Fehler beim Laden der Datei', 'error')
|
||||
}
|
||||
|
||||
// Reset input
|
||||
event.target.value = ''
|
||||
}
|
||||
|
||||
// Save as XML
|
||||
const handleSaveXML = async () => {
|
||||
if (!modelerRef.current) return
|
||||
|
||||
try {
|
||||
const { xml } = await modelerRef.current.saveXML({ format: true })
|
||||
downloadFile(xml, 'process.bpmn', 'application/xml')
|
||||
showToast('XML exportiert', 'success')
|
||||
} catch (err) {
|
||||
showToast('Fehler beim Speichern', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
// Save as SVG
|
||||
const handleSaveSVG = async () => {
|
||||
if (!modelerRef.current) return
|
||||
|
||||
try {
|
||||
const { svg } = await modelerRef.current.saveSVG()
|
||||
downloadFile(svg, 'process.svg', 'image/svg+xml')
|
||||
showToast('SVG exportiert', 'success')
|
||||
} catch (err) {
|
||||
showToast('Fehler beim Speichern', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
// Deploy to Camunda
|
||||
const handleDeploy = async () => {
|
||||
if (!modelerRef.current) return
|
||||
|
||||
if (!camundaHealth.connected) {
|
||||
showToast('Camunda nicht verbunden', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const { xml } = await modelerRef.current.saveXML({ format: true })
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('deployment-name', `BreakPilot-Process-${Date.now()}`)
|
||||
formData.append('data', new Blob([xml], { type: 'application/octet-stream' }), 'process.bpmn')
|
||||
|
||||
const response = await fetch('/api/bpmn/deployment/create', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json()
|
||||
showToast(`Deployment erfolgreich: ${result.name}`, 'success')
|
||||
loadProcesses()
|
||||
} else {
|
||||
throw new Error('Deployment failed')
|
||||
}
|
||||
} catch (err) {
|
||||
showToast('Deployment fehlgeschlagen', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
// Load process definition XML
|
||||
const handleLoadProcess = async (definitionId: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/bpmn/process-definition/${definitionId}/xml`)
|
||||
const data = await response.json()
|
||||
|
||||
if (data.bpmn20Xml && modelerRef.current) {
|
||||
await modelerRef.current.importXML(data.bpmn20Xml)
|
||||
;(modelerRef.current.get('canvas') as any).zoom('fit-viewport')
|
||||
setShowProcessPanel(false)
|
||||
showToast('Prozess geladen', 'success')
|
||||
}
|
||||
} catch (err) {
|
||||
showToast('Fehler beim Laden des Prozesses', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
// Complete task
|
||||
const handleCompleteTask = async (taskId: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/bpmn/task/${taskId}/complete`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({})
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
showToast('Task abgeschlossen', 'success')
|
||||
loadTasks()
|
||||
} else {
|
||||
throw new Error('Failed to complete task')
|
||||
}
|
||||
} catch (err) {
|
||||
showToast('Fehler beim Abschliessen', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
// Zoom controls
|
||||
const handleZoomIn = () => {
|
||||
if (!modelerRef.current) return
|
||||
const canvas = modelerRef.current.get('canvas') as any
|
||||
canvas.zoom(canvas.zoom() * 1.2)
|
||||
}
|
||||
|
||||
const handleZoomOut = () => {
|
||||
if (!modelerRef.current) return
|
||||
const canvas = modelerRef.current.get('canvas') as any
|
||||
canvas.zoom(canvas.zoom() / 1.2)
|
||||
}
|
||||
|
||||
const handleZoomFit = () => {
|
||||
if (!modelerRef.current) return
|
||||
;(modelerRef.current.get('canvas') as any).zoom('fit-viewport')
|
||||
}
|
||||
|
||||
// Download helper
|
||||
const downloadFile = (content: string, filename: string, contentType: string) => {
|
||||
const blob = new Blob([content], { type: contentType })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = filename
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminLayout title="BPMN Workflow Editor" description="Geschaeftsprozesse modellieren und automatisieren">
|
||||
{/* Hidden file input */}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".bpmn,.xml"
|
||||
onChange={handleFileLoad}
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
{/* Toolbar */}
|
||||
<div className="flex flex-wrap items-center gap-3 mb-4 p-3 bg-white rounded-xl border border-slate-200">
|
||||
{/* File operations */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleNew}
|
||||
className="px-3 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 text-sm font-medium flex items-center gap-2"
|
||||
>
|
||||
<span>+</span> Neu
|
||||
</button>
|
||||
<button
|
||||
onClick={handleOpen}
|
||||
className="px-3 py-2 border border-slate-300 rounded-lg hover:bg-slate-50 text-sm font-medium"
|
||||
>
|
||||
Oeffnen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="w-px h-6 bg-slate-200" />
|
||||
|
||||
{/* Export */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleSaveXML}
|
||||
className="px-3 py-2 border border-slate-300 rounded-lg hover:bg-slate-50 text-sm font-medium"
|
||||
>
|
||||
XML
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSaveSVG}
|
||||
className="px-3 py-2 border border-slate-300 rounded-lg hover:bg-slate-50 text-sm font-medium"
|
||||
>
|
||||
SVG
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="w-px h-6 bg-slate-200" />
|
||||
|
||||
{/* Camunda */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleDeploy}
|
||||
disabled={!camundaHealth.connected}
|
||||
className="px-3 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed text-sm font-medium flex items-center gap-2"
|
||||
>
|
||||
Deployen
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
loadProcesses()
|
||||
setShowProcessPanel(true)
|
||||
}}
|
||||
className="px-3 py-2 border border-slate-300 rounded-lg hover:bg-slate-50 text-sm font-medium"
|
||||
>
|
||||
Prozesse
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
loadTasks()
|
||||
setShowTaskPanel(!showTaskPanel)
|
||||
}}
|
||||
className="px-3 py-2 border border-slate-300 rounded-lg hover:bg-slate-50 text-sm font-medium"
|
||||
>
|
||||
Tasks
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="w-px h-6 bg-slate-200" />
|
||||
|
||||
{/* Zoom */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleZoomIn}
|
||||
className="px-3 py-2 border border-slate-300 rounded-lg hover:bg-slate-50 text-sm"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<button
|
||||
onClick={handleZoomOut}
|
||||
className="px-3 py-2 border border-slate-300 rounded-lg hover:bg-slate-50 text-sm"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<button
|
||||
onClick={handleZoomFit}
|
||||
className="px-3 py-2 border border-slate-300 rounded-lg hover:bg-slate-50 text-sm"
|
||||
>
|
||||
Fit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Canvas */}
|
||||
<div className="relative bg-white rounded-xl border border-slate-200 overflow-hidden" style={{ height: 'calc(100vh - 340px)' }}>
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-white z-10">
|
||||
<div className="text-slate-500">Lade Editor...</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={containerRef} className="w-full h-full" />
|
||||
|
||||
{/* Status Bar */}
|
||||
<div className="absolute bottom-0 left-0 right-0 flex justify-between items-center px-4 py-2 bg-slate-50 border-t border-slate-200 text-xs text-slate-500">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`w-2 h-2 rounded-full ${camundaHealth.connected ? 'bg-green-500' : 'bg-red-500'}`}
|
||||
/>
|
||||
<span>Camunda: {camundaHealth.connected ? 'Verbunden' : 'Nicht verbunden'}</span>
|
||||
</div>
|
||||
<div>Elemente: {elementCount}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Task Panel */}
|
||||
{showTaskPanel && (
|
||||
<div className="mt-4 bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="font-semibold text-slate-900">Offene Tasks</h3>
|
||||
<span className="px-2 py-1 bg-primary-100 text-primary-700 rounded-full text-xs font-medium">
|
||||
{tasks.length}
|
||||
</span>
|
||||
</div>
|
||||
{tasks.length === 0 ? (
|
||||
<div className="text-center py-6 text-slate-500">Keine offenen Tasks</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{tasks.map((task) => (
|
||||
<div
|
||||
key={task.id}
|
||||
className="flex justify-between items-center p-3 bg-slate-50 rounded-lg"
|
||||
>
|
||||
<div>
|
||||
<div className="font-medium text-slate-900">{task.name}</div>
|
||||
<div className="text-xs text-slate-500">{task.processDefinitionId}</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleCompleteTask(task.id)}
|
||||
className="px-3 py-1.5 bg-primary-600 text-white rounded text-sm hover:bg-primary-700"
|
||||
>
|
||||
Erledigen
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Process Panel (Slide-in) */}
|
||||
{showProcessPanel && (
|
||||
<div className="fixed inset-0 z-50 flex justify-end">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/50"
|
||||
onClick={() => setShowProcessPanel(false)}
|
||||
/>
|
||||
<div className="relative w-80 bg-white h-full shadow-xl overflow-y-auto">
|
||||
<div className="sticky top-0 bg-white border-b border-slate-200 p-4 flex justify-between items-center">
|
||||
<h3 className="font-semibold text-slate-900">Deployed Processes</h3>
|
||||
<button
|
||||
onClick={() => setShowProcessPanel(false)}
|
||||
className="text-slate-400 hover:text-slate-600"
|
||||
>
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
{processes.length === 0 ? (
|
||||
<div className="text-center py-8 text-slate-500">Keine Prozesse deployed</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{processes.map((process) => (
|
||||
<div
|
||||
key={process.id}
|
||||
onClick={() => handleLoadProcess(process.id)}
|
||||
className="p-3 border border-slate-200 rounded-lg hover:border-primary-300 cursor-pointer"
|
||||
>
|
||||
<div className="font-medium text-slate-900">{process.name || process.key}</div>
|
||||
<div className="text-xs text-slate-500">
|
||||
Version {process.version} | {process.key}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Toast Notification */}
|
||||
{toast && (
|
||||
<div
|
||||
className={`fixed bottom-6 right-6 px-4 py-3 rounded-lg shadow-lg z-50 ${
|
||||
toast.type === 'success'
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-red-600 text-white'
|
||||
}`}
|
||||
>
|
||||
{toast.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* System Info Section */}
|
||||
<div className="mt-8 border-t border-slate-200 pt-8">
|
||||
<SystemInfoSection config={SYSTEM_INFO_CONFIGS.workflow || {
|
||||
title: 'BPMN Workflow Engine',
|
||||
description: 'Camunda 7 basierte Prozessautomatisierung',
|
||||
version: '7.21.0',
|
||||
architecture: {
|
||||
layers: [
|
||||
{
|
||||
name: 'Frontend',
|
||||
color: '#3b82f6',
|
||||
components: ['bpmn-js Editor', 'React Components']
|
||||
},
|
||||
{
|
||||
name: 'Backend',
|
||||
color: '#8b5cf6',
|
||||
components: ['FastAPI Proxy', 'REST API']
|
||||
},
|
||||
{
|
||||
name: 'Engine',
|
||||
color: '#10b981',
|
||||
components: ['Camunda 7', 'Process Executor']
|
||||
},
|
||||
{
|
||||
name: 'Datenbank',
|
||||
color: '#f59e0b',
|
||||
components: ['PostgreSQL', 'Camunda Schema']
|
||||
}
|
||||
]
|
||||
},
|
||||
features: [
|
||||
{ name: 'BPMN 2.0 Modellierung', status: 'active' as const, description: 'Visueller Editor mit bpmn-js' },
|
||||
{ name: 'Process Deployment', status: 'active' as const, description: 'Deploy zu Camunda Engine' },
|
||||
{ name: 'Task Management', status: 'active' as const, description: 'User Tasks bearbeiten' },
|
||||
{ name: 'Process History', status: 'planned' as const, description: 'Historische Prozessdaten' }
|
||||
],
|
||||
roadmap: [
|
||||
{
|
||||
title: 'Phase 1: Editor Integration',
|
||||
priority: 'high' as const,
|
||||
items: ['bpmn-js Editor', 'Camunda Deployment', 'Task Inbox']
|
||||
},
|
||||
{
|
||||
title: 'Phase 2: Consent Workflow',
|
||||
priority: 'high' as const,
|
||||
items: ['Document Workflow', 'DSB Approval', 'Auto-Publishing']
|
||||
},
|
||||
{
|
||||
title: 'Phase 3: Weitere Prozesse',
|
||||
priority: 'medium' as const,
|
||||
items: ['GDPR DSR Workflow', 'Exam Correction', 'Onboarding']
|
||||
}
|
||||
],
|
||||
technicalDetails: [
|
||||
{ key: 'Engine', value: 'Camunda 7.21.0' },
|
||||
{ key: 'Editor', value: 'bpmn-js 17.11.1' },
|
||||
{ key: 'Lizenz', value: 'Apache 2.0' },
|
||||
{ key: 'Port', value: '8089' }
|
||||
],
|
||||
privacyNotes: [
|
||||
'Prozessdaten werden lokal in PostgreSQL gespeichert',
|
||||
'Keine Daten werden an externe Dienste gesendet',
|
||||
'Apache 2.0 Lizenz - kommerziell nutzbar'
|
||||
]
|
||||
}} />
|
||||
</div>
|
||||
</AdminLayout>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user