Compare commits

..

2 commits

Author SHA1 Message Date
Eduard Wisch
a61ce6c918 Trigger: Deploy Doku + nohook-Fix [deploy]
All checks were successful
Deploy bericht / deploy (push) Successful in 13s
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 00:46:08 +02:00
Eduard Wisch
eb37a4b632 Doku auf Stand 1.5.0: README + CLAUDE.md, dol_dir_list mit nohook=1
README: Anhaenge-Spalte, Arbeitstisch, Autosave, Tastenkuerzel, Bildtausch,
Sammelaktionen und Drag & Drop beschrieben; Architektur-Baum vervollstaendigt
(15 fehlende ajax-Endpoints, imageviewer, Root-Seiten, SQL-Dateien); GD als
Voraussetzung ergaenzt; llx_bericht_upload_token ist nicht mehr "geplant".

CLAUDE.md: Abschnitt zur Seitengeometrie als zentralem Begriff, LocalStorage-Keys,
Phase 1 abgeschlossen. Korrigiert die falsche Notiz, --inputbackgroundcolor gebe es
im awl-dark nicht — die Variable existiert (KB #512), deshalb waren die Eingabefelder
im Editor anders gestylt als Dolibarrs eigene.

Fix: dol_dir_list() zusaetzlich mit $nohook = 1 (KB #1060) — sonst laeuft der
getDirList-Hook fremder Module mit, der global $object auswertet; bericht_collect_attachments()
laeuft seit 1.5.0 auch im AJAX-Kontext (ajax/fragments.php), wo es das nicht gibt.

ROADMAP.md entfernt: alle Punkte abgearbeitet, Inhalt steht im ChangeLog.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 00:45:21 +02:00
4 changed files with 129 additions and 194 deletions

View file

@ -1,6 +1,6 @@
# Bericht-Modul — Projekt-Status & Architektur # Bericht-Modul — Projekt-Status & Architektur
## Stand 2026-05-27 ## Stand 2026-08-21 (Version 1.5.0)
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,19 +34,42 @@ 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`, `--colortextlink`, `--colorbackhmenu1`, `--colortextbackhmenu`, `--colorbackvmenu1` Verwendete: `--colorbacktitle1`, `--colortext`, `--colorbackbody`, `--colorboxbordertitle1`,
`--colortextlink`, `--colorbackhmenu1`, `--colortextbackhmenu`, `--colorbackvmenu1`,
`--colorbacklinepair1`, `--colorbacklinebreak`, `--colorbacklinepairchecked`,
`--colortexttitlenotab`, `--colortexttitlenotab2`, `--inputbackgroundcolor`, `--inputbordercolor`,
`--btncolorborderhover`
NICHT vorhanden im awl-dark: `--inputbackgroundcolor`, `--inputtextcolor` — stattdessen Fallbacks nutzen. **Korrektur 2026-08-21:** Hier stand, `--inputbackgroundcolor` und `--inputtextcolor` gebe es im
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 - pdf.worker.min.js — **Pfad NIE hartkodieren**, sonst fehlt `/custom` und PDF.js fällt auf den
„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) - Sortable.min.js (SortableJS 1.15) — mit `group: {put: false}`, sonst kollidiert das Sortieren
mit dem eigenen Ablegen von Anhang-Kacheln
- qrcode.min.js (QR-Code für den Mobile-Upload)
### LocalStorage Settings ### LocalStorage Settings
Key: `bericht.editor.settings.v1` | Key | Inhalt |
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)
@ -56,7 +79,7 @@ Speichert: color, stroke, fontFamily, fontSize, bold, italic, zoom
--- ---
## Phase 1 — Bericht-Modul-Erweiterungen (in Arbeit) ## Phase 1 — Bericht-Modul-Erweiterungen ✅ (abgeschlossen mit 1.5.0)
### ✅ 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)
@ -75,16 +98,40 @@ Speichert: color, stroke, fontFamily, fontSize, bold, italic, zoom
- [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)
- [/] **1.4 Mehrere Bilder pro Seite** — DB + Klassen-Skelett da, Grid-Rendering im PDF + Editor noch offen (siehe TODO unten) - [x] **1.4 Mehrere Bilder pro Seite** ✅ 1.5.0 — Raster im PDF und im Editor; Plätze direkt auf
- [/] **1.5 Bildgröße pro Seite** — image_scale/image_align Spalten da, UI noch offen der Seite anklickbar, einzeln tauschbar
- [ ] **1.7 Kunden-Tab** — Tab "Berichte" auf thirdparty - [x] **1.5 Bildgröße pro Seite** ✅ — image_scale/image_align mit Auswahl in der Werkzeugleiste
- [x] **1.7 Kunden-Tab** ✅ — `bericht_thirdparty.php`, Konstante `BERICHT_TAB_ON_THIRDPARTY`
### TODO Phase 1.4 + 1.5 (Folge-Commit) ### Seitengeometrie — der wichtigste Begriff im Modul (seit 1.5.0)
- BerichtPage::getImages() liest llx_bericht_page_image
- Editor: Layout-Dropdown pro Seite (1/2/4/6 Slots) `bericht_page_geometry($format, $orientation, $has_note)` in `lib/bericht.lib.php` ist die
- Drag&Drop von Anhängen in Slots oder "Als Grid hinzufügen"-Button **einzige** Quelle für Seitenmaße und Ränder. Editor **und** PDF-Erzeugung rechnen damit:
- generate_pdf/preview_pdf: Grid-Rendering (calculate slot rects)
- Bei `single`-Layout: image_scale (1.0/0.7/0.5) + image_align (fit/center/topleft/topright) - Ränder 10 mm, Kopfbereich **30 mm** (dort druckt TCPDF Logo/Titel, Trennlinie bei y=27),
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,10 +6,16 @@ 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-Browser** zeigt alle Dateien des aktuellen Dokuments **und** der direkt verknüpften Objekte (z. B. der Auftrag zur Rechnung) - **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.
- **Auswahl per Checkbox** — markierte Dateien werden als Seiten in den Bericht übernommen - **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.
- **Browser-Editor** mit PDF.js + Fabric.js: Pfeile, Kreise, Rechtecke, Freihand, Text, Farbe, Strichstärke, Undo/Redo - **Auswahl per Kachel** — die Reihenfolge der Auswahl (① ② ③) bestimmt auch die Verteilung auf die Plätze im Raster-Layout. Bereits verwendete Dateien sind markiert.
- **Seiten-Verwaltung** per Drag&Drop (SortableJS): umordnen, löschen, drehen, neue Seiten hochladen - **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, ↑/↓)
- **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)
@ -27,8 +33,13 @@ 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
@ -42,9 +53,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 ankreuzen → **Auswahl in Bericht übernehmen** 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.
5. Im mittleren Editor mit den Werkzeugen Pfeile, Texte etc. zeichnen 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.
6. Seiten rechts per Drag&Drop sortieren, einzelne Seiten löschen, neue Dateien hochladen 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.
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
@ -152,8 +163,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` | Multi-Image-Layout pro Seite (Phase 1.4) | | `llx_bericht_page_image` | Bilder der einzelnen Plätze bei Raster-Layouts (grid_2 … grid_6, before_after) |
| `llx_bericht_upload_token` | Mobile-Upload-Tokens (geplant) | | `llx_bericht_upload_token` | Zeitlich begrenzte Tokens für den QR-Mobile-Upload |
| `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
@ -162,26 +173,48 @@ 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 │ ├── bericht.class.php Bericht + BerichtPage CRUD, Slot-Geometrie
│ ├── 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, Auftragsnr., Templates, Shipment-PDF, ├── lib/bericht.lib.php Helper: Anhänge sammeln, Vorschaubilder, EXIF-Parser,
│ Signature-Box, FPDI-Stempel-Fallback) │ Seitengeometrie (bericht_page_geometry), PDF-Rendering,
│ 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
├── ajax/ Legacy Endpoints (Token-geschützt) ├── bericht_batch.php Sammel-Erstellung von Berichten
│ ├── _inc.php Gemeinsamer Header ├── bericht_thirdparty.php Read-only Übersicht auf der Kundenkarte
├── 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
│ ├── upload_extra.php Direkter Upload │ ├── attachment_thumb.php Vorschaubild eines Anhangs (GD, gecacht, ETag)
│ ├── save_annotations.php Fabric-JSON speichern │ ├── create_grid_page.php Raster-Seite aus mehreren Bildern anlegen
│ ├── save_signature_box.php UPSERT der Signatur-Box pro Template │ ├── 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 │ ├── page_meta.php Annotationen + Notiz laden
│ ├── page_image.php Seitenbild/PDF ausliefern │ ├── preview_pdf.php PDF-Vorschau ohne zu finalisieren
│ ├── delete_page.php │ ├── process_document.php Import hochgeladener Dokumente
│ ├── reorder_pages.php │ ├── reorder_pages.php Reihenfolge speichern
│ └── generate_pdf.php Finalisierung: TCPDF + FPDI + ODT-Deckblatt │ ├── 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
│ └── verify_signature.php Unterschrift prüfen
├── 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)
@ -197,12 +230,18 @@ 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 │ ├── editor.js PDF.js + Fabric.js Integration, Anhänge-Spalte,
│ └── lib/ PDF.js, Fabric.js, SortableJS (lokal) │ │ Seitengeometrie, Drag&Drop, Autosave, Tastenkürzel
├── css/bericht.css │ ├── imageviewer.js Großansicht (Zoom, Wischen, PDF-Modus)
│ └── 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
``` ```

View file

@ -1,151 +0,0 @@
# 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); $files = dol_dir_list($upload_dir, 'files', 1, '', '(\.meta|_preview.*\.png|thumbs)$', 'name', SORT_ASC, 1, 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); $files = dol_dir_list($linked_dir, 'files', 1, '', '(\.meta|_preview.*\.png|thumbs)$', 'name', SORT_ASC, 1, 1);
foreach ($files as $f) { foreach ($files as $f) {
$result[] = array( $result[] = array(
'source' => $linked_module_type, 'source' => $linked_module_type,