Diagnose: bei alten Berichten ohne composite_path wurde das bgImage
beim Öffnen nicht geladen (fabric.Image.fromURL mit crossOrigin bei
Blob-URL hing sich auf). Danach hat jeder Save das Composite ohne
Bild gespeichert → PDF-Seite leer.
Fix 1 — renderImage:
- Native Image() statt fabric.Image.fromURL
- Kein crossOrigin bei Blob-URLs (überflüssig, verursachte Hänger)
- Timeout 10s mit console.warn
- onerror mit Debug-Info (mime, bufSize)
- applyTool() nach Einfügen damit Tool-Lock greift
Fix 2 — savePageAnnotations:
- Wenn KEIN Bild im Canvas ist (weder bgImage=true noch type=image),
wird der Save ABGEBROCHEN statt ein leeres Composite zu speichern.
- Dadurch kann ein alter Bericht nicht versehentlich mit einem
leeren Composite überschrieben werden wenn der Bild-Load hakt.
- Toast-Warnung für den User, console.warn für Debug.
Fix 3 — finalize-Handler:
- Native confirm() vor dem Finalisieren
- Bessere Fehler-Diagnose: lese Response als text(), parse JSON,
logge bei JSON-Parse-Fehler den Raw-Body
- toast() statt alert() für Fehler
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
Wenn ein Bericht vor Phase 6 gespeichert wurde, enthält fabric_json
nur Shapes (Pfeile, Text etc.) — das Quellbild war damals noch
pdfCanvas-Hintergrund. Beim Restore war jsonHasObjects=true und
rerenderCurrent wurde nicht aufgerufen → Canvas weiß.
Fix: Zusätzlicher Check jsonHasBgImage. Wenn kein Image im JSON
(weder bgImage=true noch type=image), wird zusätzlich zu den
geladenen Shapes rerenderCurrent() aufgerufen, das das Quellbild
als fabric.Image hinter die Shapes legt.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
Drei Probleme behoben:
1. Wenn fabric_json existierte aber leer oder ohne Objekte war, wurde
rerenderCurrent() NICHT aufgerufen und das Canvas blieb leer.
2. Canvas-Dimensionen wurden nur im fabric_json-Zweig gesetzt — bei
altem Berichten ohne JSON fehlte der Init und das Canvas war 1x1.
3. loadFromJSON-Callback hat zwar das Promise resolved, aber die
nachfolgende applyTool() für Tool-Locking fehlte, dadurch waren
die geladenen Bilder nach Reload immer ziehbar.
Fix:
- Canvas-Dimensionen IMMER zuerst auf A4 setzen
- Nur laden wenn fabric_json tatsächlich Objekte enthält
- Sonst (oder bei Parse-Fehler) Fallback auf rerenderCurrent() das
das Quell-Bild frisch als fabric.Image lädt
- applyTool() nach loadFromJSON damit Tool-Lock-Status korrekt ist
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
Anhänge-Spalte:
- Buttons und Labels waren unterschiedlich breit → Scrollbalken querte
die Box. Fix: alle Controls (.butAction, button, select) in
.bericht-attachments auf display:block + width:100% + box-sizing
border-box, einheitliches Padding, ellipsis bei Überlänge.
- overflow-x: hidden verhindert horizontale Scrollbar im Container.
- min-width: 0 damit grid-Container nicht überlaufen.
Zeichen-Tools:
- Problem: Beim Malen wurden die Bilder (fabric.Image) mitbewegt
weil sie immer selectable/evented waren.
- Fix: applyTool() setzt auf ALLEN Canvas-Objekten selectable=false
und evented=false sobald ein Zeichen-Tool aktiv ist. Nur bei
'select' werden sie wieder aktiviert.
- discardActiveObject() nach Tool-Wechsel damit keine Selektion
aktiv bleibt.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
Der Layout-Dropdown in der Toolbar bezog sich nur auf die aktuell
angezeigte Seite, nicht auf die Übernahme-Aktion. Ergebnis: User
stellten 2x2 ein, kreuzten 4 Bilder an, klickten 'Übernehmen' — und
bekamen 4 einzelne Seiten statt einer Grid-Seite.
Fix: Neben dem Übernahme-Button eigener Layout-Selector mit Optionen
Einzeln / 2 / 2v / 4 / 6 / Vorher-Nachher. Bei Grid-Layout werden
die ausgewählten Bilder automatisch in Gruppen à slotCount aufgeteilt
und entsprechend viele Seiten angelegt (z.B. 10 Bilder + grid_4 = 3
Seiten mit 4+4+2). Die einzelnen ▭▭/▦/VN-Buttons entfallen damit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
Vorher blieb 'Seite wählen oder Fotos hinzufügen' auch sichtbar wenn
eine Seite geladen wurde — die .empty-Klasse wurde nur beim Seiten-
wechsel per loadPage() entfernt, nicht beim initialen Load. Resultat:
bei einem Bericht mit bestehender Seite erschien der Text mitten im
weißen Canvas.
Fix: beim init() wird die Klasse nur gesetzt wenn überhaupt keine
Page-Thumbs existieren (Bericht wirklich leer).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
Paradigmen-Wechsel: Editor rendert bei jedem Save sein Fabric-Canvas
als PNG und lädt es hoch. PDF nutzt dieses PNG 1:1 statt die Shapes
serverseitig nachzuzeichnen.
Damit ist garantiert: was du im Editor siehst, ist EXAKT das was im PDF
landet. Alle Pfeil/Text/Shape-Rendering-Bugs zwischen Fabric-JSON und
PHP-Nachzeichnung sind Geschichte.
Kernänderungen:
1. DB: Neue Spalte bericht_page.composite_path (Migration im init())
2. ajax/save_annotations.php: nimmt multipart file 'composite' entgegen,
speichert es unter bericht/work/<fkb>/composite_<pid>.png
3. lib/bericht.lib.php: bericht_render_page_to_pdf prüft composite_path
zuerst — wenn vorhanden, wird eine Seite mit genau diesem PNG als
volles Bild gerendert, fertig. Fallback auf alte Logik bei alten
Berichten ohne Composite.
4. editor.js renderImage: Quellbild wird NICHT mehr auf pdfCanvas
gezeichnet, sondern als fabric.Image ins Fabric-Canvas geladen —
ZIEHBAR, SKALIERBAR, ROTIERBAR wie jedes andere Objekt.
Mehrere Bilder auf einer Seite kein Problem mehr.
5. editor.js savePageAnnotations: nach Shape-State wird toDataURL
mit multiplier:2 aufgerufen, PNG-Blob hochgeladen zusammen mit
fabric_json (für spätere Edits) und note.
6. editor.js loadPage: wenn fabric_json existiert, wird dieses
clientseitig wieder eingeladen (inkl. eingebettete Bilder) — das
Quell-Bild wird nicht mehr neu aus der Quelle geholt. Bei leerer
Seite läuft der alte Render-Flow.
Phase 6 Bonus — Text mit Hintergrund:
- Neuer color-picker 'BG:' in der Toolbar + 'Ø'-Button (kein BG)
- Fabric IText bekommt textBackgroundColor + padding:6
- Bei selektiertem Text-Objekt wird BG live angewendet
- Dataset-Flag 'active' toggelt zwischen ein/aus
Dark-Input-Fix:
- Textarea in .bericht-page-note nutzte --inputbackgroundcolor
(existiert in awl-dark nicht → Fallback #fff = weiße Fläche mit
schwarzer Schrift auf Dark-Theme)
- Jetzt: --colorbackbody + --colortext + --colorboxbordertitle1
- Generischer Input-Style für alle Text-Eingaben in .bericht-editor
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
Wenn noch keine Seite geladen ist, wirkte der Editor mit dem winzigen
1x1-Canvas sehr abgebastelt. Jetzt zeigt der Canvas-Wrap im leeren
Zustand eine echte A4-Hochformat-Fläche (max 900px, aspect-ratio 1:1.414)
mit Hinweistext 'Seite wählen oder Fotos hinzufügen'.
- .bericht-canvas-wrap.empty setzt aspect-ratio auf A4
- ::after Pseudo-Element als Hinweistext
- init() markiert den Wrap als 'empty'
- loadPage() entfernt die Klasse sobald ein Bild geladen wird
- min-height 400px damit auch ohne Content Platz da ist
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
Bericht-Vorlagen (Phase 5.5):
- DB: is_template, template_label in llx_bericht
- Bericht::fetchAllTemplates() und createFromTemplate()
- fetchAllForElement() blendet Vorlagen aus
- ajax/save_as_template.php erzeugt Vorlage aus aktuellem Bericht
- Desktop-Editor: '📋 Als Vorlage' Button im Action-Bereich
- Bericht-Übersicht: Vorlagen-Dropdown beim + Neu Button
- api/templates.php: GET list + POST create_from_template
Schnell-Bericht (Phase 4.a/4.i):
- api/reports.php?action=create POST-Endpoint: Titel, Format,
Orientation, ODT-Template, optional template_id
- api/odt_templates.php: Liste der Deckblatt-Vorlagen
Whisper-Transkription (Phase 5.7):
- api/transcribe.php: POST mit relpath, nutzt externen Whisper-
HTTP-Endpoint (whisper.cpp server ODER OpenAI-kompatibel)
- Konfiguration im Admin: BERICHT_WHISPER_URL/MODE/API_KEY/LANG
- Sprache default 'de'
Cron-Fix:
- BerichtUploadToken::cleanupExpired() ist jetzt Instanz-Methode
(Dolibarr ruft new Klasse($db) bei jobtype=method auf)
- Returnwert für Cron-Success/Failure
- Statische Variante als cleanupExpiredStatic() für direkte Aufrufe
- Damit läuft der tägliche Cron 'Expired Upload-Tokens bereinigen'
nicht mehr hängend
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
ajax/verify_signature.php:
- Liest Metadaten aus .meta.json neben einer Unterschrift-Seite
- Rechnet SHA256 über die zum Signaturzeitpunkt existierenden Seiten
(erste N laut meta.page_count_at_signing)
- Vergleicht mit dem gespeicherten Hash → verified true/false
- Liefert reason bei Fehlschlag (Seitenanzahl oder Inhalt geändert)
bericht_card.php:
- Seiten mit .meta.json kriegen 🔒-Badge + grünen Rand
- 🔍-Button neben dem Löschen-Button öffnet Verifikations-Modal
editor.js:
- showSignatureVerifyResult-Modal zeigt alle Metadaten,
beide Hashes, GPS als OpenStreetMap-Link, IP, User, Zeit
- Grün/Rot je nach Verifikationsergebnis
api/pages.php:
- Neuer Action reorder: POST mit {order:[ids]} schreibt neue
page_order-Werte als Transaktion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
Phase 2.1 Token-System:
- Neue Tabelle llx_bericht_upload_token (token, fk_bericht, expires_at,
uploads_count, max_uploads)
- BerichtUploadToken-Klasse mit create/fetchValid/incrementCount/cleanupExpired
- Cronjob 'Bericht: Expired Upload-Tokens bereinigen' täglich
- 64-Hex random_bytes-Tokens, 1h Lifetime, 100 Uploads max
Phase 2.2 QR-Upload Lite:
- mobile_upload.php — Mobile-optimierte Page ohne Dolibarr-Login,
Auth nur über Token in URL/Form
- 📷 Foto aufnehmen (capture=environment) und 📂 Galerie
- Clientseitiges Resize auf max 2000px (Canvas, JPEG q=0.85)
- Upload-Status mit Toast-Notifications
- Liste der hochgeladenen Bilder live in der Page
- ajax/create_upload_token.php — generiert Token für aktiven Bericht
- ajax/list_pages.php — Polling-Endpoint für Editor
- 📱 Mobil hochladen-Button im Editor → QR-Modal mit qrcodejs
- Polling alle 5s nach neuen Pages, auto-reload bei Änderung
- QR-Modal styled für Dark-Theme
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
Phase 1.3 Seitenformat A4/A3/A5/Letter + Hoch/Quer:
- Neue Spalten page_format, page_orientation in llx_bericht
- Bericht-Meta zeigt Format + Orientation Selects
- Auto-Save via neuem ajax/save_meta.php
- generate_pdf + preview_pdf nutzen die gewählten Werte
- Bilder werden dynamisch via getPageWidth/getPageHeight skaliert
(statt hardcoded 210x297 für A4)
Phase 1.4 + 1.5 Schema-Vorbereitung:
- Neue Tabelle llx_bericht_page_image für Multi-Image-Seiten
- Spalten layout, image_scale, image_align in llx_bericht_page
- DB-Migrationen im init() für bestehende Installationen
(ALTER TABLE mit Error-Suppress)
- Grid-Rendering im Editor/PDF folgt im nächsten Commit
(siehe CLAUDE.md TODO)
Phase 1.7 Tab "Berichte" auf Kundenkarte:
- Neue Konstante BERICHT_TAB_ON_THIRDPARTY (default 1)
- Tab-Definition in modBericht für 'thirdparty' Element
- Neue Datei bericht_thirdparty.php
- UNION-SQL über bericht JOIN commande/facture/propal mit fk_soc
- Read-only flache Tabelle sortiert nach Datum
- Pro Bericht: Quelle (Symbol + Ref-Link), Status, Öffnen/Zur Quelle
Version-Bump 1.0.0 → 1.1.0, ChangeLog ergänzt.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
Phase 1.6 Verknüpfte Sicht Auftrag↔Rechnung:
- Bericht::fetchLinkedForElement liest llx_element_element-Verknüpfungen
- linkToElement/unlinkFromElement n:m-API
- Bericht-Übersicht zeigt drei Sektionen: direkt zugeordnet,
zusätzlich verknüpft, aus verknüpften Aufträgen (read-only)
- 'Übernehmen'-Button erstellt llx_element_element-Eintrag
- 'Lösen'-Button entfernt Verknüpfung
- generate_pdf legt das fertige PDF auch unter den verknüpften
Elementen ab + ECM-Eintrag
Phase 1.1 Live-PDF-Vorschau:
- Neuer Endpoint ajax/preview_pdf.php — wie generate_pdf, aber:
schreibt nicht in ECM, ändert nicht den Status, streamt direkt
- 👁️ Vorschau-Button im Editor öffnet Modal mit iframe (PDF.js
Viewer des Browsers)
- bericht_burn_annotations und bericht_render_cover_internal in
lib/bericht.lib.php verschoben (gemeinsam genutzt)
- ESC-Key + Backdrop-Click schließen das Modal
Phase 1.2 Anhänge löschen:
- Neuer Endpoint ajax/delete_attachment.php mit Path-Whitelist
(nur facture/, commande/, propal/), löscht Datei + thumbs +
llx_ecm_files-Eintrag
- 🗑️-Button in jeder Anhang-Zeile, Confirm-Dialog mit
Quell-Auftrag/Rechnung im Text
- Inline-Remove ohne Page-Reload
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
- Farbe, Strichstärke, Schriftart, Schriftgröße, Bold/Italic und Zoom
werden in localStorage unter bericht.editor.settings.v1 gespeichert
- Beim nächsten Öffnen werden alle Werte wiederhergestellt
- Alle Toolbar-Buttons und Inputs haben jetzt deutsche Tooltips
(Farbe, Strichstärke, Schriftart, Größe, Fett, Kursiv, Zoom -/+/Reset,
Rotation links/rechts)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
- Jeder Thumb hat jetzt einen mini canvas, der das Bild oder die erste
PDF-Seite gerendert anzeigt (max 200px)
- Papier-Look: A4 aspect-ratio (1:1.414), weißer Hintergrund per Default
- 🌓-Button im Pages-Header schaltet zwischen paper-light und paper-dark
- Toolbar-Inputs (select/number/checkbox): heller Text auf dunklem
Hintergrund für Dark-Theme
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
- Toolbar: Schriftart (Helvetica/Arial/Times/Courier/Verdana/Georgia),
Größe (8-120), Bold/Italic Checkboxen — wirken auf neues Text-Tool
und auf selektierte Texte (Sync via selection:created/updated)
- Zoom-Buttons (-/+/Reset) mit Anzeige in %, skaliert getTargetCanvasWidth
- Pfeil-Tool: Drag-to-Draw mit echter Pfeilspitze als Group (Linie +
Triangle), drehbar/skalierbar/verschiebbar wie alle anderen Shapes
- Rect/Ellipse: ebenfalls Drag-to-Draw statt fester Größe
- ResizeObserver auf canvas-wrap re-rendert die Seite wenn sich die
Container-Breite ändert (z.B. DevTools öffnen)
- Toolbar-Inputs/Selects nutzen Dolibarr CSS-Variablen für Dark-Theme
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
Fabric.js wickelt das Canvas in einen .canvas-container-Wrapper ein.
Vorher habe ich nur das innere #fabric-canvas absolut positioniert,
während der Wrapper im normalen Flow unter dem PDF-Canvas blieb -
dadurch konnten die Tools nicht klicken (Klicks gingen ans PDF-Canvas)
und Annotationen erschienen unter dem Bild.
Jetzt wird der .canvas-container exakt über dem PDF-Canvas platziert
mit z-index:10 und pointer-events:auto.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
Auf Wide-Screens (>1200px mittlere Spalte) blieb rechts vom Bild ein
großer leerer Streifen, weil getTargetCanvasWidth auf 1200px gedeckelt
war. Jetzt nimmt der Canvas immer die volle Container-Breite ein.
Bei Browser-Resize wird die aktuelle Seite neu gerendert.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
Die ⟲/⟳-Buttons rotieren jetzt die SEITE statt Fabric-Objekte:
- Image: ctx.rotate beim drawImage, Buffer-Größe getauscht
- PDF: pdfjsLib viewport mit rotation-Param
- Rotation in llx_bericht_page.rotation persistiert
- Beim Seitenwechsel wird die gespeicherte Rotation aus page_meta geladen
- Annotationen werden bei Rotation gelöscht (rotieren VOR dem Annotieren)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
- renderImage skaliert auch hoch wenn img kleiner als Container (vorher
blieb rechts leerer Platz bei kleinen Querformat-Handyfotos)
- max-width auf pdf-canvas entfernt (drawing-buffer != display size hat
das Fabric-Overlay verrutschen lassen)
- resizeFabricToCanvas nutzt getBoundingClientRect für pixelgenaue
Positionierung über requestAnimationFrame
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
- editor.js: PDF/Bild-Render nutzt jetzt die tatsächliche Breite des
.bericht-canvas-wrap Containers (max 1200), damit die mittlere Spalte
nicht überquillt und die Seiten-Sidebar sichtbar bleibt
- CSS: grid-template-columns nutzt minmax(0, 1fr) um Überlauf zu
verhindern, #pdf-canvas hat max-width:100%
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
Dolibarr-Modul für Arbeitsberichte aus Rechnungs-Anhängen mit Browser-PDF-Editor.
- Reiter "Bericht" auf Rechnungen, Aufträgen und Angeboten
- Anhänge-Browser inkl. verknüpfter Objekte (Auftrag → Rechnung)
- PDF.js + Fabric.js Browser-Editor: Pfeile, Kreise, Rechtecke, Freihand, Text
- SortableJS Seiten-Verwaltung mit Drag&Drop
- ODT-Deckblatt mit Platzhaltern, Templates im Admin verwaltbar
- TCPDF + FPDI Finalisierung mit eingebrannten Annotationen
- ECM-Verknüpfung: PDF erscheint unter Verknüpfte Dokumente
- Auftragsnummer aus existierendem Extrafield options_auftragsnummer
- Mehrere Berichte pro Dokument
- Beim Aktivieren werden vorhandene Extrafields nicht überschrieben
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>