feat(collab): Karte + Journal parallel bearbeitbar
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled

- Editor-Lock aufgeteilt: canEditMap (Lock-geschützt) vs. canEditJournal (nur Rolle)
- Eine Person kann zeichnen, während eine andere parallel Journal führt
  (Führungsstab-Szenario: Zeichner + Journalführer)
- Journal-Live-Sync existierte bereits (Socket journal-updated -> journal-refresh)
- Banner-Texte präzisiert ("bearbeitet die Karte", "Journal bleibt bearbeitbar")
- Version 1.4.8

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Pepe Ziberi
2026-07-19 00:39:31 +02:00
parent a081b3ebe4
commit bae1cfe9a0
3 changed files with 15 additions and 9 deletions

View File

@@ -47,8 +47,10 @@ Legende Aufwand: 🟢 klein · 🟡 mittel · 🔴 gross
## Phase 4 Echte Kollaboration ## Phase 4 Echte Kollaboration
- [ ] **4.1 Karte & Journal getrennt live-editierbar** 🔴 - [x] **4.1 Karte & Journal parallel bearbeitbar** 🟡 ✅
- Single-Editor-Lock aufbrechen; Socket.io-Sync erweitern. - Editor-Lock gilt nur noch für die Karte; Journal/Cockpit unabhängig editierbar.
- Live-Sync für Journal existierte bereits (Socket → journal-refresh).
- _Offen (Kür): echtes gleichzeitiges Zeichnen (Co-Drawing) — bewusst nicht gebaut._
## Phase 5 Kür ## Phase 5 Kür

View File

@@ -1,6 +1,6 @@
{ {
"name": "lageplan", "name": "lageplan",
"version": "1.4.7", "version": "1.4.8",
"description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation", "description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation",
"private": true, "private": true,
"scripts": { "scripts": {

View File

@@ -377,8 +377,12 @@ export default function AppPage() {
}, [authLoading, user, router]) }, [authLoading, user, router])
const roleCanEdit = user ? (user.role === 'SERVER_ADMIN' || user.role === 'TENANT_ADMIN' || user.role === 'OPERATOR') : false const roleCanEdit = user ? (user.role === 'SERVER_ADMIN' || user.role === 'TENANT_ADMIN' || user.role === 'OPERATOR') : false
// User can only edit if they have the role AND they hold the editing lock (or no one is editing) // Karte: nur mit Editing-Lock bearbeitbar (verhindert Zeichen-Konflikte).
const canEdit = !presentationLocked && roleCanEdit && (isEditingByMe || !editingBy) // Journal/Cockpit: unabhängig vom Karten-Lock — so kann eine Person zeichnen,
// während eine andere parallel das Journal führt (Führungsstab-Szenario).
const canEditMap = !presentationLocked && roleCanEdit && (isEditingByMe || !editingBy)
const canEditJournal = !presentationLocked && roleCanEdit
const canEdit = canEditMap // Abwärtskompatibel für Karten-bezogene Stellen
const isReadOnly = !!editingBy && !isEditingByMe const isReadOnly = !!editingBy && !isEditingByMe
// Auto-save: localStorage persistence + debounced API save + beacon on unload // Auto-save: localStorage persistence + debounced API save + beacon on unload
@@ -870,12 +874,12 @@ export default function AppPage() {
{isReadOnly ? ( {isReadOnly ? (
<div className="flex items-center gap-2 text-orange-600"> <div className="flex items-center gap-2 text-orange-600">
<Eye className="w-4 h-4" /> <Eye className="w-4 h-4" />
<span><strong>{editingBy?.name}</strong> bearbeitet gerade Live-Ansicht (alle 5s aktualisiert)</span> <span><strong>{editingBy?.name}</strong> bearbeitet die Karte Live-Ansicht{roleCanEdit ? ' · Journal bleibt für Sie bearbeitbar' : ''}</span>
</div> </div>
) : isEditingByMe ? ( ) : isEditingByMe ? (
<div className="flex items-center gap-2 text-green-600"> <div className="flex items-center gap-2 text-green-600">
<Lock className="w-4 h-4" /> <Lock className="w-4 h-4" />
<span>Sie bearbeiten andere sehen Ihre Änderungen in Echtzeit</span> <span>Sie bearbeiten die Karte andere sehen Ihre Änderungen in Echtzeit</span>
</div> </div>
) : ( ) : (
<div className="flex items-center gap-2 text-muted-foreground"> <div className="flex items-center gap-2 text-muted-foreground">
@@ -941,7 +945,7 @@ export default function AppPage() {
<ExerciseCockpit <ExerciseCockpit
projectId={currentProject?.id || null} projectId={currentProject?.id || null}
projectTitle={currentProject?.title || ''} projectTitle={currentProject?.title || ''}
canEdit={canEdit} canEdit={canEditJournal}
initialEvaluation={currentProject?.exerciseEvaluation || ''} initialEvaluation={currentProject?.exerciseEvaluation || ''}
onEvaluationSaved={(value) => onEvaluationSaved={(value) =>
setCurrentProject((prev) => (prev ? { ...prev, exerciseEvaluation: value } : prev)) setCurrentProject((prev) => (prev ? { ...prev, exerciseEvaluation: value } : prev))
@@ -956,7 +960,7 @@ export default function AppPage() {
einsatzleiter={currentProject?.einsatzleiter || ''} einsatzleiter={currentProject?.einsatzleiter || ''}
journalfuehrer={currentProject?.journalfuehrer || ''} journalfuehrer={currentProject?.journalfuehrer || ''}
einsatzNr={(currentProject as any)?.einsatzNr || ''} einsatzNr={(currentProject as any)?.einsatzNr || ''}
canEdit={canEdit} canEdit={canEditJournal}
tenantId={tenant?.id || null} tenantId={tenant?.id || null}
tenantName={tenant?.name || ''} tenantName={tenant?.name || ''}
tenantLogoUrl={tenant?.id ? `/api/admin/tenants/${tenant.id}/logo/serve` : null} tenantLogoUrl={tenant?.id ? `/api/admin/tenants/${tenant.id}/logo/serve` : null}