Compare commits

..

No commits in common. "a61ce6c918f1b618d8623e3e1c2d64315cef591f" and "4102ebfe5bd494b706cd03988d3fe92be0efbb15" have entirely different histories.

4 changed files with 194 additions and 129 deletions

View file

@ -1,6 +1,6 @@
# Bericht-Modul — Projekt-Status & Architektur # Bericht-Modul — Projekt-Status & Architektur
## Stand 2026-08-21 (Version 1.5.0) ## Stand 2026-05-27
Dolibarr-Custom-Modul für Arbeitsberichte mit Browser-PDF-Editor + PWA-API-Layer + Lieferschein-Unterschrift. Dolibarr-Custom-Modul für Arbeitsberichte mit Browser-PDF-Editor + PWA-API-Layer + Lieferschein-Unterschrift.
@ -34,42 +34,19 @@ Berichte gehören 1:1 zu einem Parent (`element_type` + `fk_element`). Auf einer
**500033** — kollidiert NICHT mit BankImport (500021). Permission-IDs sind 500033 + n. **500033** — kollidiert NICHT mit BankImport (500021). Permission-IDs sind 500033 + n.
### CSS-Variablen (Dolibarr awl-dark Theme) ### CSS-Variablen (Dolibarr awl-dark Theme)
Verwendete: `--colorbacktitle1`, `--colortext`, `--colorbackbody`, `--colorboxbordertitle1`, Verwendete: `--colorbacktitle1`, `--colortext`, `--colorbackbody`, `--colorboxbordertitle1`, `--colortextlink`, `--colorbackhmenu1`, `--colortextbackhmenu`, `--colorbackvmenu1`
`--colortextlink`, `--colorbackhmenu1`, `--colortextbackhmenu`, `--colorbackvmenu1`,
`--colorbacklinepair1`, `--colorbacklinebreak`, `--colorbacklinepairchecked`,
`--colortexttitlenotab`, `--colortexttitlenotab2`, `--inputbackgroundcolor`, `--inputbordercolor`,
`--btncolorborderhover`
**Korrektur 2026-08-21:** Hier stand, `--inputbackgroundcolor` und `--inputtextcolor` gebe es im NICHT vorhanden im awl-dark: `--inputbackgroundcolor`, `--inputtextcolor` — stattdessen Fallbacks nutzen.
awl-dark nicht. Das stimmt für `--inputbackgroundcolor` **nicht** — die Variable ist gesetzt
(`rgb(20,22,24)`), ebenso `--inputbordercolor` (`rgb(54,57,62)`). Wegen der falschen Notiz waren
die Eingabefelder im Editor mit `--colorbackbody` gestylt und sahen anders aus als Dolibarrs
eigene Felder. Vollständige, per DevTools geprüfte Liste: **KB #512**.
`--colorbackhmenu1` (`rgb(39,44,49)`) taugt **nicht** für Aktiv-Zustände — praktisch derselbe Ton
wie der Toolbar-Hintergrund. Dafür `--colorbacklinepairchecked` (`rgb(30,87,116)`) nehmen.
### JS-Libraries (lokal in js/lib/) ### JS-Libraries (lokal in js/lib/)
- pdf.min.js (PDF.js 3.11) - pdf.min.js (PDF.js 3.11)
- pdf.worker.min.js — **Pfad NIE hartkodieren**, sonst fehlt `/custom` und PDF.js fällt auf den - pdf.worker.min.js
„fake worker" zurück (rendert dann im Hauptthread). Kommt über
`cfg.urls.pdf_worker` aus `dol_buildpath()`.
- fabric.min.js (Fabric.js 5.3) - fabric.min.js (Fabric.js 5.3)
- Sortable.min.js (SortableJS 1.15) — mit `group: {put: false}`, sonst kollidiert das Sortieren - Sortable.min.js (SortableJS 1.15)
mit dem eigenen Ablegen von Anhang-Kacheln
- qrcode.min.js (QR-Code für den Mobile-Upload)
### LocalStorage Settings ### LocalStorage Settings
| Key | Inhalt | Key: `bericht.editor.settings.v1`
|---|---| Speichert: color, stroke, fontFamily, fontSize, bold, italic, zoom
| `bericht.editor.settings.v1` | color, stroke, fontFamily, fontSize, bold, italic, zoom |
| `bericht.attachments.view.v1` | Kachel- oder Listenansicht der Anhänge |
| `bericht.attachments.sort.v1` | Sortierung (taken / taken_desc / name / size_desc) |
| `bericht.attachments.filter.v1` | Filter (all / image / pdf / unused) |
| `bericht.attachments.tilesize.v1` | Kachelgröße (s / m / l) |
| `bericht.attachments.width.v1` | Breite der Anhänge-Spalte in px |
| `bericht.attachments.collapsed.v1` | Spalte eingeklappt (1/0) |
| `bericht.editor.margins.v1` | Seitenränder im Arbeitstisch anzeigen (1/0) |
### Forgejo ### Forgejo
- Repo: `data/bericht` (NICHT `data-it/bericht` — Token hat keine org-write rechte) - Repo: `data/bericht` (NICHT `data-it/bericht` — Token hat keine org-write rechte)
@ -79,7 +56,7 @@ wie der Toolbar-Hintergrund. Dafür `--colorbacklinepairchecked` (`rgb(30,87,116
--- ---
## Phase 1 — Bericht-Modul-Erweiterungen ✅ (abgeschlossen mit 1.5.0) ## Phase 1 — Bericht-Modul-Erweiterungen (in Arbeit)
### ✅ Erledigt vor Phase 1 ### ✅ Erledigt vor Phase 1
- Modul-Scaffold (modBericht, SQL, Lang, Rechte korrekt nach Stundenzettel-Format) - Modul-Scaffold (modBericht, SQL, Lang, Rechte korrekt nach Stundenzettel-Format)
@ -98,40 +75,16 @@ wie der Toolbar-Hintergrund. Dafür `--colorbacklinepairchecked` (`rgb(30,87,116
- [x] **1.1 Live-PDF-Vorschau** - [x] **1.1 Live-PDF-Vorschau**
- [x] **1.2 Anhänge löschen** - [x] **1.2 Anhänge löschen**
- [x] **1.3 Seitengröße A4/A3/Letter + Hoch/Quer** ✅ — global pro Bericht (nicht pro Seite override) - [x] **1.3 Seitengröße A4/A3/Letter + Hoch/Quer** ✅ — global pro Bericht (nicht pro Seite override)
- [x] **1.4 Mehrere Bilder pro Seite** ✅ 1.5.0 — Raster im PDF und im Editor; Plätze direkt auf - [/] **1.4 Mehrere Bilder pro Seite** — DB + Klassen-Skelett da, Grid-Rendering im PDF + Editor noch offen (siehe TODO unten)
der Seite anklickbar, einzeln tauschbar - [/] **1.5 Bildgröße pro Seite** — image_scale/image_align Spalten da, UI noch offen
- [x] **1.5 Bildgröße pro Seite** ✅ — image_scale/image_align mit Auswahl in der Werkzeugleiste - [ ] **1.7 Kunden-Tab** — Tab "Berichte" auf thirdparty
- [x] **1.7 Kunden-Tab** ✅ — `bericht_thirdparty.php`, Konstante `BERICHT_TAB_ON_THIRDPARTY`
### Seitengeometrie — der wichtigste Begriff im Modul (seit 1.5.0) ### TODO Phase 1.4 + 1.5 (Folge-Commit)
- BerichtPage::getImages() liest llx_bericht_page_image
`bericht_page_geometry($format, $orientation, $has_note)` in `lib/bericht.lib.php` ist die - Editor: Layout-Dropdown pro Seite (1/2/4/6 Slots)
**einzige** Quelle für Seitenmaße und Ränder. Editor **und** PDF-Erzeugung rechnen damit: - Drag&Drop von Anhängen in Slots oder "Als Grid hinzufügen"-Button
- generate_pdf/preview_pdf: Grid-Rendering (calculate slot rects)
- Ränder 10 mm, Kopfbereich **30 mm** (dort druckt TCPDF Logo/Titel, Trennlinie bei y=27), - Bei `single`-Layout: image_scale (1.0/0.7/0.5) + image_align (fit/center/topleft/topright)
Fußbereich 16 mm, Notizzone 25 % der Seitenhöhe (nur wenn die Seite eine Notiz hat)
- Der Editor-Canvas ist der **Inhaltsbereich**, nicht das Blatt. Das PDF setzt das gespeicherte
Composite-PNG 1:1 dorthin.
- **Nicht** erneut einpassen — genau das war vor 1.5.0 der Fehler (Inhalt wurde zweimal
verkleinert, breiter Rand, Vorschau stimmte nicht).
- Composites von vor 1.5.0 haben Blatt-Proportion; sie werden an der Proportion erkannt und
proportional eingepasst statt verzerrt. Diese Weiche nicht entfernen, solange Altberichte
neu erzeugt werden könnten.
- Raster-Plätze über `BerichtPage::slotRectsInBox()` (PHP) bzw. `slotRectsPercent()` (JS) —
beide teilen denselben Inhaltsbereich auf und müssen zusammenpassen.
### Weitere Fallen (2026-08-21 verifiziert)
- `dol_dir_list()` braucht `$mode = 1` für `size`/`date` **und** `$nohook = 1` — sonst läuft der
`getDirList`-Hook fremder Module mit, der `global $object` auswertet (im AJAX-Kontext nicht da).
Siehe KB #1060.
- `document.php` braucht `&attachment=0`, sonst wird das PDF heruntergeladen statt angezeigt.
- **Prod-Container hat kein `exif` und kein Imagick** — EXIF liest das Modul selbst
(`bericht_jpeg_taken_at()`), PDF-Vorschauen laufen über PDF.js im Browser.
- Seitenwechsel sind serialisiert (Kette + Token in `loadPage()`). Beim schnellen Durchklicken
überlappten sich sonst zwei Ladevorgänge und die Notiz landete auf der falschen Seite.
- Fabric-Ereignisse (`object:added` …) feuern auch beim **Aufbauen** einer Seite — deshalb
`buildingPage`-Flag, sonst meldet das Autosave sofort „nicht gespeichert".
--- ---

View file

@ -6,16 +6,10 @@ Bilder und PDFs lassen sich im Browser annotieren (Pfeile, Kreise, Rechtecke, Te
## Funktionen ## Funktionen
- **Reiter „Bericht"** auf Rechnungen, Aufträgen, Angeboten und Lieferungen (jeweils per Konstante deaktivierbar) - **Reiter „Bericht"** auf Rechnungen, Aufträgen, Angeboten und Lieferungen (jeweils per Konstante deaktivierbar)
- **Anhänge-Spalte als Kachelraster** mit Vorschaubildern — zeigt alle Dateien des aktuellen Dokuments **und** der direkt verknüpften Objekte (z. B. der Auftrag zur Rechnung). Bild-Miniaturen entstehen serverseitig (GD, gecacht), PDF-Anhänge zeigen Seite 1 (PDF.js). Klick öffnet die Großansicht — Bilder mit Zoom und Wischen, PDFs im PDF-Viewer des Browsers. - **Anhänge-Browser** zeigt alle Dateien des aktuellen Dokuments **und** der direkt verknüpften Objekte (z. B. der Auftrag zur Rechnung)
- **Sortieren und Filtern** der Anhänge nach Aufnahmezeit (EXIF), Name oder Größe · nur Bilder / nur PDFs / noch nicht verwendete. Spaltenbreite ziehbar, Spalte einklappbar, Kachelgröße in drei Stufen. - **Auswahl per Checkbox** — markierte Dateien werden als Seiten in den Bericht übernommen
- **Auswahl per Kachel** — die Reihenfolge der Auswahl (① ② ③) bestimmt auch die Verteilung auf die Plätze im Raster-Layout. Bereits verwendete Dateien sind markiert. - **Browser-Editor** mit PDF.js + Fabric.js: Pfeile, Kreise, Rechtecke, Freihand, Text, Farbe, Strichstärke, Undo/Redo
- **Browser-Editor** mit PDF.js + Fabric.js: Pfeile, Kreise, Rechtecke, Freihand, Text, Farbe, Strichstärke, Undo/Redo — mit Tastenkürzeln (V/P/R/K/A/T, Entf, Strg+Z/Y/S, ↑/↓) - **Seiten-Verwaltung** per Drag&Drop (SortableJS): umordnen, löschen, drehen, neue Seiten hochladen
- **Arbeitstisch zeigt die echte Seite**: Kopfbereich (Logo/Titel), Seitenränder und Fußbereich sind sichtbar; die Arbeitsfläche entspricht genau dem Bereich, der gedruckt wird — das PDF übernimmt sie 1:1
- **Automatisches Speichern** kurz nach der letzten Änderung, beim Seitenwechsel und beim Wegschalten des Tabs; beim Verlassen mit offenen Änderungen warnt der Browser
- **Seiten-Verwaltung** per Drag&Drop (SortableJS): umordnen, löschen, drehen, neue Seiten hochladen. Mehrere Seiten markieren (Klick auf die Seitennummer, Umschalt für einen Bereich) und gesammelt löschen oder verdoppeln.
- **Bild einer Seite austauschen** ohne sie zu löschen — Reihenfolge, Notiz und Anmerkungen bleiben erhalten. Bei Raster-Layouts lässt sich jeder Platz einzeln tauschen, direkt auf der Seite anklickbar.
- **Ziehen und Ablegen**: Kachel auf die Seitenliste legt neue Seiten an, Kachel auf die Arbeitsfläche ersetzt das Bild der offenen Seite, Kachel auf einen Raster-Platz setzt genau diesen. Dateien vom Rechner auf die Anhänge-Spalte werden hochgeladen.
- **Mobiler Upload per QR-Code** — Handy scannt, lädt Fotos direkt in den offenen Bericht (zeitlich begrenzte Token)
- **Notizen pro Seite** — werden im finalen PDF unten auf der Seite gedruckt - **Notizen pro Seite** — werden im finalen PDF unten auf der Seite gedruckt
- **Deckblatt aus ODT-Vorlage** mit Platzhaltern (`{auftragsnummer}`, `{kunde_name}`, `{datum}`, …) - **Deckblatt aus ODT-Vorlage** mit Platzhaltern (`{auftragsnummer}`, `{kunde_name}`, `{datum}`, …)
- **ODT-Templates** im Admin-Bereich verwaltbar (mehrere Vorlagen, Standard wählbar) - **ODT-Templates** im Admin-Bereich verwaltbar (mehrere Vorlagen, Standard wählbar)
@ -33,13 +27,8 @@ Bilder und PDFs lassen sich im Browser annotieren (Pfeile, Kreise, Rechtecke, Te
- TCPDF (in Dolibarr enthalten) - TCPDF (in Dolibarr enthalten)
- **FPDI** (für PDF-Anhänge in den Bericht zu mergen) — empfohlen, optional - **FPDI** (für PDF-Anhänge in den Bericht zu mergen) — empfohlen, optional
- **LibreOffice headless** (für ODT→PDF Konvertierung der Deckblätter) - **LibreOffice headless** (für ODT→PDF Konvertierung der Deckblätter)
- **GD** (PHP-Extension) für die Vorschaubilder der Anhänge und die Raster-Vorschau
- Optional: `pdfinfo` oder `imagick` für PDF-Seitenanzahl-Erkennung - Optional: `pdfinfo` oder `imagick` für PDF-Seitenanzahl-Erkennung
> Imagick und die PHP-Extension `exif` sind im Produktions-Container **nicht** installiert.
> PDF-Vorschauen laufen deshalb über PDF.js im Browser, EXIF-Daten liest das Modul mit einem
> eigenen Parser (`bericht_jpeg_taken_at()` / `bericht_jpeg_orientation()`).
## Installation ## Installation
1. Modul-Verzeichnis nach `dolibarr/htdocs/custom/bericht/` (oder per Symlink aus dem Module-Mount-Pfad) kopieren 1. Modul-Verzeichnis nach `dolibarr/htdocs/custom/bericht/` (oder per Symlink aus dem Module-Mount-Pfad) kopieren
@ -53,9 +42,9 @@ Bilder und PDFs lassen sich im Browser annotieren (Pfeile, Kreise, Rechtecke, Te
1. Eine Rechnung öffnen (`/compta/facture/card.php?id=…`) 1. Eine Rechnung öffnen (`/compta/facture/card.php?id=…`)
2. Reiter **Bericht** auswählen 2. Reiter **Bericht** auswählen
3. **+ Neuer Bericht** klicken — die Auftragsnummer wird automatisch übernommen 3. **+ Neuer Bericht** klicken — die Auftragsnummer wird automatisch übernommen
4. Im Editor links die gewünschten Anhänge auswählen (Kachel-Checkbox; die Reihenfolge der Auswahl zählt) → **Auswahl in Bericht übernehmen**. Alternativ eine Kachel direkt in die Seitenliste ziehen. 4. Im Editor links die gewünschten Anhänge ankreuzen → **Auswahl in Bericht übernehmen**
5. Im mittleren Editor mit den Werkzeugen Pfeile, Texte etc. zeichnen — der graue Bereich oben und unten zeigt, wo Logo, Titel und Seitenzahl gedruckt werden. Änderungen werden automatisch gespeichert. 5. Im mittleren Editor mit den Werkzeugen Pfeile, Texte etc. zeichnen
6. Seiten rechts per Drag&Drop sortieren, einzelne Seiten löschen oder über die Seitennummer mehrere markieren und gesammelt löschen/verdoppeln. Ein falsches Bild lässt sich über **🔄** auf der Seitenminiatur austauschen. 6. Seiten rechts per Drag&Drop sortieren, einzelne Seiten löschen, neue Dateien hochladen
7. **Bericht finalisieren** — PDF wird erzeugt, Deckblatt aus der ODT-Vorlage gerendert und unter den verknüpften Dokumenten der Rechnung abgelegt 7. **Bericht finalisieren** — PDF wird erzeugt, Deckblatt aus der ODT-Vorlage gerendert und unter den verknüpften Dokumenten der Rechnung abgelegt
## ODT-Template Platzhalter ## ODT-Template Platzhalter
@ -163,8 +152,8 @@ Per `admin/setup.php` oder `llx_const`:
|---|---| |---|---|
| `llx_bericht` | Bericht-Header (element_type ∈ {invoice, order, propal, shipment}, fk_element, status, …) | | `llx_bericht` | Bericht-Header (element_type ∈ {invoice, order, propal, shipment}, fk_element, status, …) |
| `llx_bericht_page` | Einzelne Seiten mit Fabric-JSON-Annotationen, Layout, Notiz | | `llx_bericht_page` | Einzelne Seiten mit Fabric-JSON-Annotationen, Layout, Notiz |
| `llx_bericht_page_image` | Bilder der einzelnen Plätze bei Raster-Layouts (grid_2 … grid_6, before_after) | | `llx_bericht_page_image` | Multi-Image-Layout pro Seite (Phase 1.4) |
| `llx_bericht_upload_token` | Zeitlich begrenzte Tokens für den QR-Mobile-Upload | | `llx_bericht_upload_token` | Mobile-Upload-Tokens (geplant) |
| `llx_bericht_signature_box` | Pro Lieferschein-Template gespeicherte Signatur-Box-Geometrie (mm) | | `llx_bericht_signature_box` | Pro Lieferschein-Template gespeicherte Signatur-Box-Geometrie (mm) |
## Architektur ## Architektur
@ -173,48 +162,26 @@ Per `admin/setup.php` oder `llx_const`:
bericht/ bericht/
├── core/modules/modBericht.class.php Modul-Descriptor, Tabs, Extrafields-Init, Konstanten ├── core/modules/modBericht.class.php Modul-Descriptor, Tabs, Extrafields-Init, Konstanten
├── class/ ├── class/
│ ├── bericht.class.php Bericht + BerichtPage CRUD, Slot-Geometrie │ ├── bericht.class.php Bericht + BerichtPage CRUD
│ ├── upload_token.class.php Tokens für den QR-Mobile-Upload
│ └── actions_bericht.class.php Hook: beforeODTSave setzt {signature} + Meta-Variablen │ └── actions_bericht.class.php Hook: beforeODTSave setzt {signature} + Meta-Variablen
├── lib/bericht.lib.php Helper: Anhänge sammeln, Vorschaubilder, EXIF-Parser, ├── lib/bericht.lib.php Helper (Anhänge, Auftragsnr., Templates, Shipment-PDF,
│ Seitengeometrie (bericht_page_geometry), PDF-Rendering, │ Signature-Box, FPDI-Stempel-Fallback)
│ Render-Funktionen für Seiten-/Anhängeliste,
│ Signature-Box, FPDI-Stempel-Fallback
├── bericht_card.php Editor-Seite (Tab-Inhalt) ├── bericht_card.php Editor-Seite (Tab-Inhalt)
├── admin/ ├── admin/
│ ├── setup.php Admin: ODT-Templates, Konstanten, Signatur-Größe │ ├── setup.php Admin: ODT-Templates, Konstanten, Signatur-Größe
│ ├── signature_box_editor.php Visueller PDF-Editor für Signatur-Box-Position │ ├── signature_box_editor.php Visueller PDF-Editor für Signatur-Box-Position
│ └── signature_box_preview.php Beispiel-PDF-Renderer für den Editor │ └── signature_box_preview.php Beispiel-PDF-Renderer für den Editor
├── bericht_batch.php Sammel-Erstellung von Berichten ├── ajax/ Legacy Endpoints (Token-geschützt)
├── bericht_thirdparty.php Read-only Übersicht auf der Kundenkarte │ ├── _inc.php Gemeinsamer Header
├── mobile_upload.php Upload-Seite für den QR-Code (Handy)
├── ajax/ Endpoints für den Editor (Token-geschützt)
│ ├── _inc.php Gemeinsamer Header (Rechte, JSON, Fatal-Handler)
│ ├── add_attachment.php Anhang als Seite hinzufügen │ ├── add_attachment.php Anhang als Seite hinzufügen
│ ├── attachment_thumb.php Vorschaubild eines Anhangs (GD, gecacht, ETag)
│ ├── create_grid_page.php Raster-Seite aus mehreren Bildern anlegen
│ ├── create_upload_token.php Token für den QR-Mobile-Upload
│ ├── delete_attachment.php Datei aus den Dokumenten des Belegs löschen
│ ├── delete_page.php Einzelne Seite löschen
│ ├── fragments.php Seiten-/Anhängeliste als HTML (Arbeiten ohne Reload)
│ ├── generate_pdf.php Finalisierung: TCPDF + FPDI + ODT-Deckblatt
│ ├── get_photo.php / list_photos.php Fotos für die PWA
│ ├── list_pages.php Seitenliste als JSON (Polling)
│ ├── page_bulk.php Mehrere Seiten löschen oder verdoppeln
│ ├── page_image.php Seitenbild/PDF ausliefern (Raster: Composite via GD)
│ ├── page_meta.php Annotationen + Notiz laden
│ ├── preview_pdf.php PDF-Vorschau ohne zu finalisieren
│ ├── process_document.php Import hochgeladener Dokumente
│ ├── reorder_pages.php Reihenfolge speichern
│ ├── replace_page_source.php Bild einer Seite / eines Rasterplatzes austauschen
│ ├── save_annotations.php Fabric-JSON + Composite-PNG speichern
│ ├── save_as_template.php Bericht als Vorlage sichern
│ ├── save_meta.php Titel, Format, Ausrichtung
│ ├── save_page_options.php Layout, Bildgröße, Position je Seite
│ ├── save_signature_box.php UPSERT der Signatur-Box pro Template
│ ├── set_slot_image.php Bild eines Rasterplatzes setzen
│ ├── upload_extra.php Direkter Upload │ ├── upload_extra.php Direkter Upload
│ └── verify_signature.php Unterschrift prüfen │ ├── save_annotations.php Fabric-JSON speichern
│ ├── save_signature_box.php UPSERT der Signatur-Box pro Template
│ ├── page_meta.php Annotationen + Notiz laden
│ ├── page_image.php Seitenbild/PDF ausliefern
│ ├── delete_page.php
│ ├── reorder_pages.php
│ └── generate_pdf.php Finalisierung: TCPDF + FPDI + ODT-Deckblatt
├── api/ REST-API (JWT-Auth) ├── api/ REST-API (JWT-Auth)
│ ├── _inc.php JWT-Authentifizierung + Dolibarr-Init │ ├── _inc.php JWT-Authentifizierung + Dolibarr-Init
│ ├── _jwt.php JWT encoding/decoding (mit ?jwt= Query-Fallback) │ ├── _jwt.php JWT encoding/decoding (mit ?jwt= Query-Fallback)
@ -230,18 +197,12 @@ bericht/
│ ├── voice.php Sprachnotizen (Upload + Transkription) │ ├── voice.php Sprachnotizen (Upload + Transkription)
│ └── transcribe.php Whisper-Transkription │ └── transcribe.php Whisper-Transkription
├── js/ ├── js/
│ ├── editor.js PDF.js + Fabric.js Integration, Anhänge-Spalte, │ ├── editor.js PDF.js + Fabric.js Integration
│ │ Seitengeometrie, Drag&Drop, Autosave, Tastenkürzel │ └── lib/ PDF.js, Fabric.js, SortableJS (lokal)
│ ├── imageviewer.js Großansicht (Zoom, Wischen, PDF-Modus) ├── css/bericht.css
│ └── lib/ PDF.js, Fabric.js, SortableJS, QRCode (lokal)
├── css/
│ ├── bericht.css Editor-Layout, Kacheln, Blatt-Darstellung
│ └── imageviewer.css Großansicht
├── sql/ ├── sql/
│ ├── llx_bericht.sql / .key.sql │ ├── llx_bericht.sql / .key.sql
│ ├── llx_bericht_page.sql / .key.sql │ ├── llx_bericht_page.sql / .key.sql
│ ├── llx_bericht_page_image.sql / .key.sql
│ ├── llx_bericht_upload_token.sql / .key.sql
│ └── llx_bericht_signature_box.sql / .key.sql │ └── llx_bericht_signature_box.sql / .key.sql
└── langs/{de_DE,en_US}/bericht.lang └── langs/{de_DE,en_US}/bericht.lang
``` ```

151
ROADMAP.md Normal file
View file

@ -0,0 +1,151 @@
# ROADMAP — Modul Bericht
Stand: 2026-08-21 · Version im Repo: 1.5.0
---
## 1. Anhänge-Spalte (links im Editor) — Hauptbaustelle
Ausgangslage (Eddy, 21.08.2026): *„die Dateinamen können in dem schmalen Fenster nicht ganz
angezeigt werden, ich habe Schwierigkeiten die richtigen Bilder auszusuchen."*
Die Spalte ist 260 px breit und zeigt pro Datei nur eine Zeile `🖼 langer_dateiname… 2,3 MB 🗑️`.
Bei 2040 Baustellenfotos mit Namen wie `IMG_20260812_141233_HDR.jpg` ist das unbrauchbar.
### 1.1 Miniatur-Vorschau statt Dateiname ✅ 1.5.0
- [x] Kachel-Raster (2 Spalten) statt Liste
- [x] Server-Thumbnails für Bilder (GD, gecacht) — Original-Handyfotos werden **nicht** mehr voll geladen
- [x] PDF-Anhänge: Seite 1 als Miniatur (PDF.js, clientseitig, lazy)
- [x] Klick auf Miniatur → Lightbox (Zoom/Pan/Swipe), `✕` bzw. Esc schließt — PDFs im Vollbild-Viewer
- [x] Dateiname zweizeilig unter der Kachel + voller Name als Tooltip
### 1.2 Auswahl bedienbar machen ✅ 1.5.0
- [x] Checkbox als Overlay auf der Kachel (großes Klickziel), Klick auf die Namenszeile wählt ebenfalls aus
- [x] Auswahl-Reihenfolge sichtbar (① ② ③) — bestimmt die Slot-Reihenfolge im Grid-Layout
(vorher: DOM-Reihenfolge, also nicht steuerbar)
- [x] „Alle / Keine" pro Herkunfts-Gruppe + Zähler „3 ausgewählt"
- [x] Bereits im Bericht verwendete Anhänge werden markiert (✓-Badge, ausgegraut)
### 1.3 Sortierung und Filter ✅ 1.5.0
- [x] Sortierung nach **Aufnahmezeit** (auf- und absteigend), Name, Größe — jeweils innerhalb der
Herkunfts-Gruppe, damit die Zuordnung zum Beleg erhalten bleibt
- [x] Filter: alle · nur Bilder · nur PDFs · noch nicht verwendet
- [x] Aufnahmezeit steht unter der Kachel (`12.08. 11:48 · 56 Kb`)
- [x] EXIF-Parser in PHP (`bericht_jpeg_taken_at`), weil das `exif`-Modul im Container
`90-Dolibarr-Prod-Custom` **nicht geladen** ist. Gegen das PHP-Modul geprüft: identische Werte.
Fallback auf das Dateidatum, wenn kein EXIF vorhanden ist.
- [x] Auswahl von Sortierung/Filter/Ansicht wird gemerkt (localStorage)
### 1.4 Spaltenbreite, Kachelgröße, Ziehen und Ablegen ✅ 1.5.0
- [x] **Spaltenbreite ziehbar** — Trennlinie zwischen Anhängen und Arbeitsfläche (150620 px),
Doppelklick setzt auf 260 px zurück; Breite wird gemerkt
- [x] **Ein-/ausklappbar** über „«" in der Kopfzeile — eingeklappt bleiben 26 px stehen
- [x] **Kachelgröße** klein / mittel / groß über „⤢" (in der kleinsten Stufe entfällt die Größenangabe)
- [x] **Kachel auf die Seitenliste ziehen** → neue Seite(n). Sind mehrere Kacheln angekreuzt und man
zieht eine davon, kommen alle mit.
- [x] **Kachel auf die Arbeitsfläche ziehen** → ersetzt das Bild der offenen Seite
- [x] **Kachel auf einen Raster-Platz ziehen** → setzt genau diesen Platz
- [x] **Dateien vom Rechner auf die Spalte ziehen** → Upload (nicht unterstützte Typen werden
benannt und übersprungen)
- [x] SortableJS bekam `group: {put: false}` — sonst streiten sich Sortierung und Ablegen um die
gleichen Ereignisse
---
## 2. Seitenansicht ("Arbeitstisch") ✅ 1.5.0
Vorher zeigte der Editor ein A4-Blatt, auf dem das Foto formatfüllend lag. Beim PDF wurde
dieses ganze Blatt **nochmal** in den Satzspiegel eingepasst — der Inhalt schrumpfte also ein
zweites Mal, und die Vorschau entsprach nicht dem Ergebnis. Seiten, die nie im Editor
gespeichert wurden, liefen zudem über den Kopfbereich (Logo/Titel) hinweg.
- [x] Eine gemeinsame Geometrie-Quelle: `bericht_page_geometry($format, $orientation, $has_note)`
in `lib/bericht.lib.php` — liefert Seitenmaß, Ränder (10 mm), Kopfbereich (30 mm),
Fußbereich (16 mm), Notizzone und den Inhaltsbereich. Editor **und** PDF rechnen damit.
- [x] Arbeitsfläche = Inhaltsbereich. Drumherum das Blatt mit schraffierter Kopf-/Fußzone
(das, was TCPDF selbst druckt) und einer Notizzone, sobald die Seite eine Notiz hat.
- [x] PDF setzt das Composite 1:1 in den Inhaltsbereich statt es erneut einzupassen
- [x] Alle Layout-Zweige der PDF-Erzeugung (Einzelbild, Raster, Vorher/Nachher, Notiz)
auf dieselbe Geometrie umgestellt — vorher rechneten sie mit `margin = 10` ab
Blattkante und schoben Bilder unter den Kopfbereich
- [x] Raster-Vorschau (`ajax/page_image.php`) rendert im Format des Inhaltsbereichs
- [x] Umschalter „📐 Rand" in der Werkzeugleiste (Zustand wird gemerkt)
- [x] Rückwärtskompatibel: Composites aus der Zeit davor haben Blatt-Proportion und werden
an der Proportion erkannt und proportional eingepasst statt verzerrt
---
## 3. Bild einer Seite austauschen ✅ 1.5.0
- [x] `ajax/replace_page_source.php` — tauscht die Quelle einer Seite; Reihenfolge, Notiz und
Anmerkungen bleiben erhalten (vorher: Seite löschen und hinten neu anhängen)
- [x] Raster-Seiten: einzelner Platz austauschbar (nutzt das vorhandene `set_slot_image.php`,
für das es bis dahin keine Bedienung gab)
- [x] Bedienung: „🔄" auf der Seitenminiatur → Statusleiste am unteren Rand → links auf das
Ersatzbild klicken. Esc bricht ab. Bei Rastern vorher den Platz (1…n) wählen.
- [x] Finalisierte Berichte werden serverseitig abgewiesen
---
## 4. Editor / Arbeitsablauf
- [x] **Kein `location.reload()` mehr** ✅ 1.5.0 — Hinzufügen, Löschen, Hochladen, Bildtausch und
der QR-Upload aktualisieren nur noch die betroffenen Listen. Neuer Endpoint
`ajax/fragments.php` liefert Seiten- und Anhängeliste als HTML; gerendert wird über
`bericht_print_page_list()` / `bericht_print_attachment_list()`, dieselben Funktionen wie beim
Seitenaufbau. Der Editor bleibt auf der bearbeiteten Seite, Zoom und Ansicht bleiben erhalten.
Einzige Ausnahme: nach dem Finalisieren wird bewusst neu geladen, weil der Bericht den Status
wechselt und die ganze Karte anders aussieht.
- [x] **Autosave** ✅ 1.5.0 — 4 Sekunden nach der letzten Änderung wird gespeichert, ebenso beim
Seitenwechsel und beim Wegschalten des Tabs. Zustand steht in der Fußleiste
(„● nicht gespeichert" / „✓ gespeichert"), beim Verlassen der Seite warnt der Browser.
Wichtig: das Aufbauen einer Seite (und Zoomen) löst Fabric-Events aus, zählt aber nicht als
Änderung — sonst stünde direkt nach dem Öffnen „nicht gespeichert" da.
- [x] **Tastenkürzel** ✅ 1.5.0 — V/P/R/K/A/T für die Werkzeuge, Entf löscht die Auswahl (das
Hintergrundbild ist ausgenommen), Strg+Z/Y, Strg+S, ↑/↓ blättert durch die Seiten.
In Eingabefeldern und bei offenem Dialog oder offener Großansicht greifen sie nicht.
Die Kürzel stehen in den Tooltips der Schaltflächen.
- [x] **Seitenliste: Mehrfachauswahl** ✅ 1.5.0 — Klick auf die Seitennummer markiert, Umschalt
erweitert bis dorthin. Sammelleiste mit Verdoppeln (⧉), Löschen (🗑️) und Aufheben (Esc).
Neuer Endpoint `ajax/page_bulk.php`; Kopien landen direkt hinter ihrem Original
(`bericht_reorder_with_copies()`), nach dem Löschen wird lückenlos neu nummeriert.
`composite_path` wird bewusst **nicht** mitkopiert — sonst zeigten beide Seiten auf dieselbe
Datei und das Bearbeiten der einen überschriebe die andere.
- [x] **Raster-Plätze direkt anklicken** ✅ 1.5.0 — über der Arbeitsfläche liegt ein Gitter mit
einer Schaltfläche je Platz (bei Vorher/Nachher entsprechend beschriftet). Klick setzt das
links ausgewählte Bild, Ablegen per Drag setzt das gezogene. Die Aufteilung entspricht
`BerichtPage::slotRectsInBox()`. Die Plätze reagieren nur im Auswahlmodus — beim Zeichnen
haben die Werkzeuge Vorrang, sonst käme man auf Raster-Seiten nicht mehr zum Zeichnen.
- [x] **Seitenwechsel serialisiert** ✅ 1.5.0 — beim schnellen Durchklicken überlappten sich zwei
Durchläufe von `loadPage()`: der zweite speicherte die noch nicht umgeschaltete Seite ein
zweites Mal, und beide rendern in denselben Canvas — so konnte die Notiz der einen Seite bei
der anderen landen. Jeder Aufruf bekommt jetzt eine Nummer und bricht ab, sobald ein neuerer
gestartet ist.
## 5. Kleinkram / Fixes
- [x] **Mehrfach-Upload**: `bindExtraUpload` verarbeitete nur `inp.files[0]` — Mehrfachauswahl im
Dateidialog war möglich, es kam aber nur eine Datei an ✅ 1.5.0
- [x] Upload-Fortschritt ✅ 1.5.0 — der Knopf zeigt beim Mehrfach-Upload „⏳ 3 / 8"
- [x] `att-delete` löscht die Datei wirklich aus den Dokumenten des Belegs ✅ 1.5.0 — der Dialog
sagt das jetzt deutlich und warnt zusätzlich, wenn die Datei im Bericht in Gebrauch ist.
---
## Erledigt
### 1.5.0 — 2026-08-21
Vollständig abgearbeitet. Anhänge-Kachelraster mit Miniaturen (1.11.4), Server-Thumbnail-Endpoint,
PDF-Lightbox, Arbeitstisch mit echter Seitengeometrie (2), Bildaustausch auf bestehenden Seiten (3),
Arbeiten ohne Neuladen, Autosave, Tastenkürzel, Sammelaktionen auf Seiten und Raster-Plätze (4).
Nebenbei gefundene und behobene Fehler:
- `dol_dir_list()` wurde ohne `$mode = 1` aufgerufen — dadurch waren `size` und `date` leer
und in der Anhängeliste stand „ Bytes" ohne Zahl
- PDF.js-Worker-Pfad war hartkodiert ohne `/custom`-Präfix → PDF.js lief im „fake worker"-Modus
auf dem Hauptthread (spürbar langsamer beim Rendern)
- `document.php` wurde ohne `attachment=0` verlinkt → PDFs wurden heruntergeladen statt angezeigt
- Größen-Auswahl der Seite blieb leer, weil der DB-Wert `1` die Option `1.0` nicht traf
- Mehrfach-Upload nahm nur die erste Datei
- `--inputbackgroundcolor` wurde im Modul als „im AwlDark nicht vorhanden" behandelt; die
Variable existiert (KB #512, per DevTools bestätigt) — Editor-Eingabefelder nutzen jetzt
dieselben Farben wie Dolibarr selbst

View file

@ -482,7 +482,7 @@ function bericht_collect_attachments(DoliDB $db, CommonObject $parent, $element_
if ($dir_key && !empty($conf->{$dir_key}->multidir_output[$parent->entity])) { if ($dir_key && !empty($conf->{$dir_key}->multidir_output[$parent->entity])) {
$upload_dir = $conf->{$dir_key}->multidir_output[$parent->entity].'/'.dol_sanitizeFileName($parent->ref); $upload_dir = $conf->{$dir_key}->multidir_output[$parent->entity].'/'.dol_sanitizeFileName($parent->ref);
if (is_dir($upload_dir)) { if (is_dir($upload_dir)) {
$files = dol_dir_list($upload_dir, 'files', 1, '', '(\.meta|_preview.*\.png|thumbs)$', 'name', SORT_ASC, 1, 1); $files = dol_dir_list($upload_dir, 'files', 1, '', '(\.meta|_preview.*\.png|thumbs)$', 'name', SORT_ASC, 1);
foreach ($files as $f) { foreach ($files as $f) {
$result[] = array( $result[] = array(
'source' => $element_type, 'source' => $element_type,
@ -509,7 +509,7 @@ function bericht_collect_attachments(DoliDB $db, CommonObject $parent, $element_
if (empty($conf->{$linked_dolibarr_type}->multidir_output[$lobj->entity])) continue; if (empty($conf->{$linked_dolibarr_type}->multidir_output[$lobj->entity])) continue;
$linked_dir = $conf->{$linked_dolibarr_type}->multidir_output[$lobj->entity].'/'.dol_sanitizeFileName($lobj->ref); $linked_dir = $conf->{$linked_dolibarr_type}->multidir_output[$lobj->entity].'/'.dol_sanitizeFileName($lobj->ref);
if (!is_dir($linked_dir)) continue; if (!is_dir($linked_dir)) continue;
$files = dol_dir_list($linked_dir, 'files', 1, '', '(\.meta|_preview.*\.png|thumbs)$', 'name', SORT_ASC, 1, 1); $files = dol_dir_list($linked_dir, 'files', 1, '', '(\.meta|_preview.*\.png|thumbs)$', 'name', SORT_ASC, 1);
foreach ($files as $f) { foreach ($files as $f) {
$result[] = array( $result[] = array(
'source' => $linked_module_type, 'source' => $linked_module_type,