API 1.5.1: Thumbnails, Aufnahmezeit, Seitenfelder, Textnotizen

Die API-Seite zur Baustelle-PWA holt nach, was der Editor mit 1.5.0 bekommen hat.

photo.php kann jetzt size=thumb&w=<px> und rendert ueber bericht_attachment_thumb()
(GD, Cache, EXIF-Rotation) statt die Originaldatei auszuliefern. size=small konnte nur
ein von Dolibarr vorgefertigtes thumbs/<name>_small.<ext> liefern - fuer Uploads ueber
orders.php?action=upload_photo gibt es das nicht, dort laeuft kein vignette(). Die PWA
lud dadurch fuer jede Kachel das komplette Foto: gemessen 9,8 KB statt 235 KB je Kachel.
Antwort mit ETag aus der mtime, Folgeaufrufe enden mit 304.

orders.php?action=photos liefert taken_at (EXIF-Aufnahmezeit ueber bericht_file_taken_at,
sonst Dateidatum) und sortiert danach statt nach filemtime.

reports.php gibt je Seite title und composite_path aus. title konnte ueber pages.php
gesetzt werden, kam aber nie zurueck. composite_path ist das im Editor gebaute
Seitenbild - ohne das zeigen Clients nur das Rohbild ohne Anmerkungen, bei
Raster-Layouts eines von bis zu sechs Bildern und bei title_only gar keins.

Neu: api/note.php - Textnotizen zum Auftrag, aufgebaut wie die Sprachnotiz. Die Notiz
liegt als notiz_<betreff>_<datum>.txt im Auftragsverzeichnis und ist damit auch im
Dolibarr-Auftrag und in der Anhaenge-Spalte des Editors sichtbar. Liste, Lesen, Anlegen,
Aendern, Loeschen. Der file-Parameter wird gegen ^notiz_[A-Za-z0-9_.-]*\.txt$ geprueft;
gegen Pfad-Ausbrueche getestet.

ROADMAP.md wiederhergestellt - sie war mit eb37a4b geloescht worden, weil alle Punkte
abgehakt waren. Sie bleibt ab jetzt liegen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-08-22 18:11:42 +02:00
parent a61ce6c918
commit a8d8291c3a
9 changed files with 555 additions and 23 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-08-22 (Version 1.5.1)
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.
@ -154,16 +154,24 @@ PWA-API-Layer + Usability-Features:
- ✅ 4.b Touch-Unterschrift (POST /api/pages.php?action=signature) - ✅ 4.b Touch-Unterschrift (POST /api/pages.php?action=signature)
- ✅ Datei-Typ-Unterscheidung: Bilder (Modal), Audio (Player), PDFs (Canvas), Dokumente (Download) - ✅ Datei-Typ-Unterscheidung: Bilder (Modal), Audio (Player), PDFs (Canvas), Dokumente (Download)
**Auth-Hinweis (Stand 1.5.1):** Der Block unten stammt aus der JWT-Zeit. Seit der
SSO-Migration laeuft alles ueber das HttpOnly-Cookie `awl_sso` (Modul `awlauth`) —
kein Bearer-Token, kein `?jwt=`-Query-Parameter mehr. `api/_jwt.php` existiert nicht mehr.
Endpoints mit `awlauth_require` brauchen zusaetzlich den Header `X-Requested-With`
(CSRF); reine GET-Reads (`photo.php`, `pdf.php`) nutzen `awlauth_verify` ohne diesen.
API-Endpoints (neue): API-Endpoints (neue):
- POST /api/auth.php — JWT-Login (username, password) - POST /api/auth.php — Login (username, password) -> setzt awl_sso-Cookie
- GET /api/orders.php — Aufträge des Users (filter: q, open) - GET /api/orders.php — Aufträge des Users (filter: q, open)
- GET /api/orders.php?id=<id> — Order-Detail - GET /api/orders.php?id=<id> — Order-Detail
- GET /api/orders.php?id=<id>&action=photos — Order-Dateien (Bilder/Audio/PDFs/Dokumente) - GET /api/orders.php?id=<id>&action=photos — Order-Dateien (Bilder/Audio/PDFs/Dokumente)
- POST /api/orders.php?action=create — Neuer Order (socid, title, ref_client, date) - POST /api/orders.php?action=create — Neuer Order (socid, title, ref_client, date)
- GET /api/photo.php?relpath=<path> — Datei-Serving (JWT via Header oder Query-Param, Whitelist: facture|commande|propal|bericht) - GET /api/photo.php?relpath=<path> — Datei-Serving (Whitelist: facture|commande|propal|bericht)
- GET /api/photo.php?relpath=<path>&size=thumb&w=<px> — Thumbnail (1.5.1, GD + Cache + ETag)
- POST /api/pages.php?action=signature&bericht_id=<id> — Touch-Unterschrift - POST /api/pages.php?action=signature&bericht_id=<id> — Touch-Unterschrift
- DELETE /api/pages.php?id=<id> — Seite löschen - DELETE /api/pages.php?id=<id> — Seite löschen
- POST /api/pages.php?id=<id> — Seite-Notiz / Rotation - POST /api/pages.php?id=<id> — Seite-Notiz / Titel / Rotation / Layout
- GET/POST /api/note.php?order_id=<id> — Textnotizen zum Auftrag (1.5.1)
PWA neue Komponenten (app.js): PWA neue Komponenten (app.js):
- openFileViewer() — Generischer Dateibetrachter (Bilder/PDFs/Audio/Download) - openFileViewer() — Generischer Dateibetrachter (Bilder/PDFs/Audio/Download)
@ -215,9 +223,29 @@ Vollständiger Workflow für Kunden-Unterschrift auf dem Handy. Source-Repo: `da
### Wichtige Fallen ### Wichtige Fallen
- `EXPEDITION_ADDON_PDF_ODT_PATH` enthält wörtlich den String `"DOL_DATA_ROOT/..."` → muss mit `preg_replace('/DOL_DATA_ROOT/', DOL_DATA_ROOT, $d)` aufgelöst werden - `EXPEDITION_ADDON_PDF_ODT_PATH` enthält wörtlich den String `"DOL_DATA_ROOT/..."` → muss mit `preg_replace('/DOL_DATA_ROOT/', DOL_DATA_ROOT, $d)` aufgelöst werden
- `llx_element_element`-Richtung: **commande = source, shipping = target** (NICHT umgekehrt) - `llx_element_element`-Richtung: **commande = source, shipping = target** (NICHT umgekehrt)
- JWT für `<img>`/`<object>`-Tags: Query-Param `?jwt=<token>` als Fallback in `api/_jwt.php`, da diese Tags keine Authorization-Header senden - ~~JWT für `<img>`/`<object>`-Tags per `?jwt=`~~ — mit der SSO-Migration hinfällig: das
`awl_sso`-Cookie geht same-origin automatisch mit, auch bei `<img>`/`<audio>`. Deshalb
nutzen `photo.php` und `pdf.php` `awlauth_verify` (ohne CSRF-Header-Pflicht) — sonst
könnten sie nicht per `window.location` oder als `src` geladen werden.
- ODT-Templates für Expedition liegen unter `DOL_DATA_ROOT/doctemplates/shipments/*.odt` - ODT-Templates für Expedition liegen unter `DOL_DATA_ROOT/doctemplates/shipments/*.odt`
## API-Fallen (2026-08-22 verifiziert)
- **`photo.php?size=small` ist KEIN Thumbnail-Generator.** Es liefert nur eine Datei aus, die
Dolibarr selbst unter `thumbs/<name>_small.<ext>` erzeugt hat. Fuer Uploads ueber
`orders.php?action=upload_photo` gibt es die nicht — dort laeuft kein `vignette()`. Ergebnis
war: die PWA lud fuer jede Kachel das Originalfoto. Der richtige Weg ist seit 1.5.1
`size=thumb&w=<px>` (nutzt `bericht_attachment_thumb()`).
- **`reports.php` muss `composite_path` mitliefern**, sonst zeigen Clients das Rohbild statt
der bearbeiteten Seite — und bei Raster-Layouts nur eines von bis zu sechs Bildern
(`title_only` hat gar kein `source_path`).
- **Ein `<img loading="lazy">`, das noch nicht im DOM haengt, laedt der Browser nicht.** Es hat
kein Layout und gilt damit nie als sichtbar. Erst einhaengen, dann `src` setzen — sonst
bleibt die Kachel ewig beim Platzhalter (genau das ist beim Umbau der PWA passiert).
- **Dateiname-Parameter immer gegen ein Muster pruefen** (`note.php`: `^notiz_[\w.\-]*\.txt$`),
nicht nur `basename()`. Sonst laesst sich ueber `../` jede Datei unter `DOL_DATA_ROOT`
ueberschreiben.
## Phase 5 — PWA Erweiterungen (geplant) ## Phase 5 — PWA Erweiterungen (geplant)
- Sprachnotizen-Transkription via Whisper (POST /api/transcribe.php) - Sprachnotizen-Transkription via Whisper (POST /api/transcribe.php)
- PIN-Schutz / WebAuthn - PIN-Schutz / WebAuthn

View file

@ -1,5 +1,52 @@
# Changelog # Changelog
## 1.5.1 — 2026-08-22
Die API-Seite zur Baustelle-PWA holt nach, was der Editor mit 1.5.0 bekommen hat.
### Thumbnails für die PWA
- **`api/photo.php` kann jetzt `size=thumb&w=<px>`** und rendert damit über
`bericht_attachment_thumb()` — dieselbe Funktion, die auch die Anhänge-Kacheln im Editor
füllt (GD, Cache unter `bericht/thumbs/`, EXIF-Ausrichtung).
- Bisher kannte der Endpoint nur `size=small|mini`, und das lieferte ausschließlich ein
Thumbnail, das **Dolibarr selbst** unter `thumbs/<name>_small.<ext>` abgelegt hatte. Fotos aus
der PWA kommen über `orders.php?action=upload_photo` herein, und dort läuft **kein
`vignette()`** — es gab also nie eines, und die Fotoliste lud für jede Kachel die
Originaldatei. Gemessen an einer Testdatei: **9,8 KB statt 235 KB je Kachel**.
- Antwort mit `ETag` aus der mtime und `Cache-Control: max-age=30 Tage`; ein wiederholter
Aufruf endet mit `304`. Ein ersetztes Original bekommt automatisch einen neuen ETag.
- Nicht-Bilder (PDF, Video) fallen unverändert auf das Original zurück.
### Aufnahmezeit in der Anhängeliste
- **`api/orders.php?action=photos` liefert `taken_at`** — die EXIF-Aufnahmezeit über
`bericht_file_taken_at()`, sonst das Dateidatum. Sortiert wird danach statt nach `filemtime`.
Bei Uploads aus der Galerie war die Upload-Reihenfolge bisher die falsche Reihenfolge.
### Bericht-Seiten: Titel und fertiges Seitenbild
- **`api/reports.php` gibt je Seite `title` und `composite_path` mit aus.**
- `title` konnte über `api/pages.php` gesetzt werden und wurde in der PWA auch gerendert, kam
aber nie zurück — der Titel wirkte, als würde er nicht gespeichert.
- `composite_path` ist das im Editor gebaute Seitenbild. Ohne dieses Feld zeigt ein Client nur
`source_path`, also das Rohbild ohne Anmerkungen — und bei einem Raster-Layout eines von bis
zu sechs Bildern. Bei `title_only` ist `source_path` leer, die PWA zeigte dort eine
Fehler-Kachel.
### Neu: Textnotizen zum Auftrag (`api/note.php`)
- Getippte Notiz zum Auftrag, aufgebaut wie die Sprachnotiz: die Notiz liegt als
`notiz_<betreff>_<datum>.txt` im Auftrags-Verzeichnis und ist damit im Dolibarr-Auftrag und
in der Anhänge-Spalte des Editors sichtbar.
- `GET` (Liste mit Betreff, Vorschau und Zeitstempel), `GET &file=` (eine Notiz),
`POST` (anlegen), `POST &file=` (ändern), `POST &file=&delete=1` (löschen).
- Dateikopf bis zur ersten Leerzeile trägt `Betreff:`, `Erfasst:` und `Geändert:` (jeweils mit
Login), darunter der freie Text. Bewusst als Klartext lesbar, auch wenn jemand die `.txt`
direkt im ECM öffnet.
- Der `file`-Parameter wird gegen `^notiz_[A-Za-z0-9_.\-]*\.txt$` geprüft — ohne das ließe sich
über `../` jede Datei unter `DOL_DATA_ROOT` überschreiben. Gegen Pfad-Ausbrüche getestet.
## 1.5.0 — 2026-08-21 ## 1.5.0 — 2026-08-21
### Anhänge-Spalte: Miniaturen statt Dateinamen ### Anhänge-Spalte: Miniaturen statt Dateinamen

View file

@ -85,16 +85,24 @@ Die **Baustelle-PWA** (`https://awl.data-it-solution.de/baustelle/`) nutzt die f
``` ```
POST /custom/bericht/api/auth.php POST /custom/bericht/api/auth.php
Body: { login: string, password: string } Body: { login: string, password: string }
Response: { ok: true, token: JWT, user: {...} } Response: { ok: true, user: {...} } + HttpOnly-Cookie awl_sso
``` ```
JWT-Token hat 7 Tage Gültigkeit und enthält: `sub` (user id), `login`, `name`, `perms` (read/write/delete/admin). Seit der SSO-Migration läuft die Anmeldung über das Modul **awlauth**: `auth.php` setzt das
HttpOnly-Cookie `awl_sso` (Single-Sign-on/-out über alle AWL-Apps), Folge-Requests
authentisieren sich same-origin über dieses Cookie. **Kein Bearer-Token und kein `?jwt=`
mehr** — auch `<img>`- und `<audio>`-Tags kommen ohne Query-Parameter aus.
Endpoints mit `awlauth_require` erwarten zusätzlich den Header `X-Requested-With:
XMLHttpRequest` (CSRF-Schutz); reine GET-Reads wie `photo.php` und `pdf.php` nutzen
`awlauth_verify` ohne diese Anforderung, damit sie auch per `window.location` gehen.
### Order-APIs ### Order-APIs
``` ```
GET /custom/bericht/api/orders.php GET /custom/bericht/api/orders.php
GET /custom/bericht/api/orders.php?id=<id> GET /custom/bericht/api/orders.php?id=<id>
GET /custom/bericht/api/orders.php?id=<id>&action=photos GET /custom/bericht/api/orders.php?id=<id>&action=photos
Je Datei: filename, size, mime, date (mtime), taken_at (EXIF-Aufnahmezeit, sonst
Dateidatum), relpath. Sortiert nach taken_at, neuste zuerst.
POST /custom/bericht/api/orders.php?action=create POST /custom/bericht/api/orders.php?action=create
Body: { socid, ref_client, title?, note_private?, date?, date_livraison?, validate? } Body: { socid, ref_client, title?, note_private?, date?, date_livraison?, validate? }
ref_client ("Ihr Zeichen") ist PFLICHT. validate=true gibt den Auftrag direkt frei — ref_client ("Ihr Zeichen") ist PFLICHT. validate=true gibt den Auftrag direkt frei —
@ -104,9 +112,19 @@ POST /custom/bericht/api/orders.php?action=create
### Dateien ### Dateien
``` ```
GET /custom/bericht/api/photo.php?relpath=<path>&jwt=<token> GET /custom/bericht/api/photo.php?relpath=<path>
Liefert Dateien aus DOL_DATA_ROOT (Whitelist: facture/, commande/, propal/, bericht/) Liefert Dateien aus DOL_DATA_ROOT (Whitelist: facture/, commande/, propal/, bericht/)
Optional: ?size=small|mini (Thumbnails), ?download=1 (Attachment-Header) Auth über das awl_sso-Cookie (same-origin), auch für <img>-Tags.
?size=thumb&w=<px> Serverseitig erzeugtes Thumbnail (GD, gecacht unter
bericht/thumbs/, mit ETag aus der mtime → Folgeaufrufe 304).
w: 40800, Default 320. Das ist der Weg für Kachel-Ansichten.
Ist die Datei kein Bild, kommt das Original.
?size=small|mini Altes Verhalten: nutzt ein von Dolibarr vorgefertigtes
thumbs/<name>_small.<ext>. Für Dateien, die über die API
hochgeladen wurden, existiert so eines NICHT (kein vignette()) —
dort kam bis 1.5.1 immer das Original zurück.
?download=1 Attachment-Header
``` ```
### PDF-Ansicht ### PDF-Ansicht
@ -118,11 +136,32 @@ GET /custom/bericht/api/pdf.php?id=<bericht_id>&jwt=<token>
### Seiten-Verwaltung (PWA) ### Seiten-Verwaltung (PWA)
``` ```
DELETE /custom/bericht/api/pages.php?id=<page_id> DELETE /custom/bericht/api/pages.php?id=<page_id>
POST /custom/bericht/api/pages.php?id=<page_id> Body: { note: string } POST /custom/bericht/api/pages.php?id=<page_id> Body: { note, title, rotation, layout }
POST /custom/bericht/api/pages.php?action=signature&bericht_id=<id> POST /custom/bericht/api/pages.php?action=signature&bericht_id=<id>
Body: FormData mit file=<PNG-Blob>, signer_name, gps_lat, gps_lon Body: FormData mit file=<PNG-Blob>, signer_name, gps_lat, gps_lon
``` ```
`reports.php?id=<id>` gibt je Seite `composite_path` und `title` mit aus. **Clients sollen
`composite_path` anzeigen, wenn es gesetzt ist** — das ist das im Editor gebaute Seitenbild
mit Anmerkungen. `source_path` ist nur die Quelldatei: bei Raster-Layouts eines von bis zu
sechs Bildern, bei `title_only` leer.
### Textnotizen zum Auftrag (PWA)
```
GET /custom/bericht/api/note.php?order_id=<id> Liste
GET /custom/bericht/api/note.php?order_id=<id>&file=<name> eine Notiz mit Text
POST /custom/bericht/api/note.php?order_id=<id> anlegen { subject, text }
POST /custom/bericht/api/note.php?order_id=<id>&file=<name> ändern { subject, text }
POST /custom/bericht/api/note.php?order_id=<id>&file=<name>&delete=1
```
Notizen liegen als `notiz_<betreff>_<datum>.txt` im Auftrags-Verzeichnis — dasselbe Muster
wie die Sprachnotiz (`voice.php`). Sie sind damit auch im Dolibarr-Auftrag und in der
Anhänge-Spalte des Editors zu sehen. Der Dateikopf (bis zur ersten Leerzeile) trägt
`Betreff:`, `Erfasst:` und `Geändert:`, darunter steht der freie Text. `file` wird gegen
`^notiz_[A-Za-z0-9_.\-]*\.txt$` geprüft — ohne das ließe sich über `../` jede Datei unter
`DOL_DATA_ROOT` überschreiben.
### Lieferungen + Unterschrift (PWA) ### Lieferungen + Unterschrift (PWA)
``` ```
GET /custom/bericht/api/shipments.php?order_id=<id> GET /custom/bericht/api/shipments.php?order_id=<id>
@ -215,19 +254,19 @@ bericht/
│ ├── set_slot_image.php Bild eines Rasterplatzes setzen │ ├── set_slot_image.php Bild eines Rasterplatzes setzen
│ ├── upload_extra.php Direkter Upload │ ├── upload_extra.php Direkter Upload
│ └── verify_signature.php Unterschrift prüfen │ └── verify_signature.php Unterschrift prüfen
├── api/ REST-API (JWT-Auth) ├── api/ REST-API (Auth über awl_sso-Cookie, Modul awlauth)
│ ├── _inc.php JWT-Authentifizierung + Dolibarr-Init │ ├── _inc.php Dolibarr-Init + awlauth_require + JSON-Helper
│ ├── _jwt.php JWT encoding/decoding (mit ?jwt= Query-Fallback)
│ ├── auth.php Login-Endpoint │ ├── auth.php Login-Endpoint
│ ├── orders.php Order-Liste, Detail, Fotos, Create │ ├── orders.php Order-Liste, Detail, Fotos, Create
│ ├── shipments.php Lieferungen-Liste, PDF-Stream, Unterschrift-Confirm │ ├── shipments.php Lieferungen-Liste, PDF-Stream, Unterschrift-Confirm
│ ├── photo.php Datei-Serving mit Whitelist │ ├── photo.php Datei-Serving mit Whitelist + Thumbnails (size=thumb)
│ ├── pdf.php Finalized Bericht-PDF │ ├── pdf.php Finalized Bericht-PDF
│ ├── pages.php Seiten-Verwaltung (Note, Rotation, Signature) │ ├── pages.php Seiten-Verwaltung (Note, Rotation, Signature)
│ ├── reports.php Bericht-CRUD │ ├── reports.php Bericht-CRUD
│ ├── templates.php ODT-Templates │ ├── templates.php ODT-Templates
│ ├── materials.php Materiallisten │ ├── materials.php Materiallisten
│ ├── voice.php Sprachnotizen (Upload + Transkription) │ ├── voice.php Sprachnotizen (Upload + Transkription)
│ ├── note.php Textnotizen zum Auftrag (Liste/Lesen/Anlegen/Ändern)
│ └── 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, Anhänge-Spalte,

193
ROADMAP.md Normal file
View file

@ -0,0 +1,193 @@
# ROADMAP — Modul Bericht
Stand: 2026-08-22 · Version im Repo: 1.5.1
> Diese Datei war mit `eb37a4b` gelöscht worden, weil alle Punkte abgehakt waren. Sie bleibt
> ab jetzt liegen — erledigte Punkte werden abgehakt, nicht entfernt.
---
## 0. API-Seite für die Baustelle-PWA ✅ 1.5.1
Die PWA (`data/baustelle-pwa`) spricht ausschließlich `api/`. Mit 1.5.0 war Infrastruktur
entstanden, die dort nicht ankam — mit 1.5.1 nachgezogen. Gegenstück: `ROADMAP.md` im PWA-Repo, Abschnitt 1.
- [x] **`api/photo.php`: `size=thumb&w=<px>`** ✅ 1.5.1 — bis dahin lieferte der Endpoint nur ein Thumbnail,
wenn Dolibarr schon eines unter `thumbs/<name>_small.<ext>` erzeugt hat. Fotos aus der PWA
kommen über `orders.php?action=upload_photo` herein, und dort läuft **kein `vignette()`**
es gibt also nie eins, und die PWA lädt für jede Kachel das volle Foto.
`bericht_attachment_thumb()` (1.5.0, GD + Cache + EXIF-Rotation) macht genau das Richtige
und ist über die bereits eingebundene `lib/bericht.lib.php` erreichbar.
Dazu ETag aus der mtime, damit wiederholte Aufrufe mit 304 enden.
- [x] **`api/orders.php?action=photos`: `taken_at`** ✅ 1.5.1 — sortiert wurde nach `filemtime`.
Der Editor nutzt seit 1.5.0 die EXIF-Aufnahmezeit (`bericht_file_taken_at()`, eigener
Parser weil im Prod-Container kein `exif`-Modul). Bei Uploads aus der Galerie ist die
Upload-Zeit die falsche Reihenfolge.
- [x] **`api/reports.php`: `title` im `pages`-Array** ✅ 1.5.1 — `api/pages.php` konnte den Seitentitel
setzen und die PWA rendert ihn, aber das Detail gab ihn nicht aus. Der Titel wirkte in der
PWA deshalb, als würde er nicht gespeichert.
- [x] **`api/reports.php`: `composite_path` im `pages`-Array** ✅ 1.5.1 — die PWA zeigte `source_path`,
also das Rohbild ohne Anmerkungen. Bei Raster-Layouts (`grid_2` … `grid_6`,
`before_after`) ist das nur eines von bis zu sechs Bildern, bei `title_only` gar keins
(die PWA zeigte dann eine ❌-Kachel).
- [x] **Neu: `api/note.php`** ✅ 1.5.1 — Textnotizen zum Auftrag (Liste, Lesen, Anlegen, Ändern,
Löschen). Ablage wie bei der Sprachnotiz: `notiz_<betreff>_<datum>.txt` im
Auftrags-Verzeichnis, damit die Notiz auch im Dolibarr-Auftrag und in der Anhänge-Spalte
des Editors auftaucht. Der `file`-Parameter ist gegen ein Muster geprüft (Pfad-Ausbruch).
Gegenstück in der PWA: siehe `ROADMAP.md` im Repo `baustelle-pwa`, Abschnitte 1 und 3 —
dort ebenfalls erledigt.
---
## 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.1 — 2026-08-22
API-Seite zur PWA nachgezogen (Abschnitt 0): Thumbnail-Endpoint `size=thumb` statt Ausliefern
der Originaldateien (9,8 KB statt 235 KB je Kachel, mit ETag/304), EXIF-Aufnahmezeit in der
Anhängeliste, `title` und `composite_path` in der Bericht-Detailantwort, neuer Endpoint
`api/note.php` für Textnotizen.
### 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

179
api/note.php Normal file
View file

@ -0,0 +1,179 @@
<?php
/* Textnotizen zu einem Auftrag getippt statt diktiert.
*
* GET /api/note.php?order_id=<id> Liste aller Notizen
* GET /api/note.php?order_id=<id>&file=<name> eine Notiz mit Text
* POST /api/note.php?order_id=<id> anlegen { subject, text }
* POST /api/note.php?order_id=<id>&file=<name> ändern { subject, text }
* POST /api/note.php?order_id=<id>&file=<name>&delete=1 löschen
*
* Gegenstück zu voice.php: die Notiz liegt als Datei im Auftrags-Verzeichnis und taucht
* damit in der Dokumentliste der PWA und in der Anhänge-Spalte des Bericht-Editors auf.
*
* Dateiformat (bewusst als Klartext lesbar, auch wenn jemand die .txt direkt öffnet):
*
* Betreff: Zählernummern Keller
* Erfasst: 22.08.2026 17:51 · data
* Geändert: 22.08.2026 18:20 · data
*
* <freier Text>
*
* Der Kopf geht bis zur ersten Leerzeile. Der Betreff steht zusätzlich im Dateinamen,
* damit man die Notiz in der Dokumentliste wiederfindet.
*/
require_once __DIR__.'/_inc.php';
api_authenticate();
global $db, $user, $conf;
require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
$order_id = (int) ($_GET['order_id'] ?? 0);
if (!$order_id) api_fail('order_id fehlt');
$cmd = new Commande($db);
if ($cmd->fetch($order_id) <= 0) api_fail('Auftrag nicht gefunden', 404);
$dir = $conf->commande->multidir_output[$cmd->entity].'/'.dol_sanitizeFileName($cmd->ref);
/**
* Prüft den übergebenen Dateinamen. Nur Notiz-Dateien im Auftragsordner sind erlaubt
* ohne das könnte man über ../ jede Datei unter DOL_DATA_ROOT überschreiben.
*/
function note_safe_path($dir, $name)
{
$base = basename((string) $name);
if ($base === '' || $base !== $name) return null;
if (!preg_match('/^notiz_[A-Za-z0-9_.\-]*\.txt$/', $base)) return null;
return $dir.'/'.$base;
}
/** Kopf + Text einer Notizdatei auseinandernehmen. */
function note_parse($raw)
{
$meta = array('subject' => '', 'created' => '', 'modified' => '');
$lines = preg_split('/\r\n|\r|\n/', (string) $raw);
$i = 0;
for (; $i < count($lines); $i++) {
$line = $lines[$i];
if (trim($line) === '') { $i++; break; }
if (preg_match('/^Betreff:\s*(.*)$/u', $line, $m)) { $meta['subject'] = trim($m[1]); continue; }
if (preg_match('/^Erfasst:\s*(.*)$/u', $line, $m)) { $meta['created'] = trim($m[1]); continue; }
if (preg_match('/^Geändert:\s*(.*)$/u', $line, $m)) { $meta['modified'] = trim($m[1]); continue; }
// Keine Kopfzeile — die Datei hat keinen Kopf, alles ist Text
$i = 0;
break;
}
$meta['text'] = implode("\n", array_slice($lines, $i));
return $meta;
}
/** Kopf + Text wieder zu einer Datei zusammensetzen. */
function note_build($subject, $text, $created, $modified)
{
$head = 'Betreff: '.$subject."\n".'Erfasst: '.$created."\n";
if ($modified !== '') $head .= 'Geändert: '.$modified."\n";
return $head."\n".rtrim((string) $text)."\n";
}
/* ---------- Liste ---------- */
if ($_SERVER['REQUEST_METHOD'] === 'GET' && empty($_GET['file'])) {
$out = array();
foreach (glob($dir.'/notiz_*.txt', GLOB_NOSORT) ?: array() as $f) {
if (!is_file($f)) continue;
$meta = note_parse(@file_get_contents($f));
$text = trim($meta['text']);
$out[] = array(
'file' => basename($f),
'subject' => $meta['subject'],
'created' => $meta['created'],
'modified' => $meta['modified'],
// Kurzfassung für die Liste, damit die App nicht jede Datei einzeln holen muss
'preview' => dol_trunc(preg_replace('/\s+/u', ' ', $text), 140, 'right', 'UTF-8', 1),
'date' => (int) @filemtime($f),
'size' => (int) @filesize($f),
'relpath' => str_replace(DOL_DATA_ROOT.'/', '', $f),
);
}
usort($out, function ($a, $b) { return $b['date'] <=> $a['date']; });
api_ok(array('notes' => $out, 'count' => count($out)));
}
/* ---------- Einzelne Notiz lesen ---------- */
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$path = note_safe_path($dir, $_GET['file'] ?? '');
if (!$path || !is_file($path)) api_fail('Notiz nicht gefunden', 404);
$meta = note_parse(@file_get_contents($path));
api_ok(array('note' => array(
'file' => basename($path),
'subject' => $meta['subject'],
'text' => $meta['text'],
'created' => $meta['created'],
'modified' => $meta['modified'],
'relpath' => str_replace(DOL_DATA_ROOT.'/', '', $path),
)));
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') api_fail('Methode nicht unterstützt', 405);
if (!$user->hasRight('bericht', 'write')) api_fail('Permission denied', 403);
/* ---------- Löschen ---------- */
if (!empty($_GET['delete'])) {
if (!$user->hasRight('bericht', 'delete')) api_fail('Permission denied', 403);
$path = note_safe_path($dir, $_GET['file'] ?? '');
if (!$path || !is_file($path)) api_fail('Notiz nicht gefunden', 404);
if (!@unlink($path)) api_fail('Löschen fehlgeschlagen', 500);
api_ok();
}
$in = api_input();
$subject = trim((string) ($in['subject'] ?? ''));
$text = (string) ($in['text'] ?? '');
if ($subject === '' && trim($text) === '') api_fail('Notiz ist leer');
if (strlen($text) > 200000) api_fail('Notiz ist zu lang');
$stamp = dol_print_date(dol_now(), '%d.%m.%Y %H:%M').' · '.$user->login;
/* ---------- Ändern ---------- */
if (!empty($_GET['file'])) {
$path = note_safe_path($dir, $_GET['file']);
if (!$path || !is_file($path)) api_fail('Notiz nicht gefunden', 404);
$old = note_parse(@file_get_contents($path));
$created = $old['created'] !== '' ? $old['created'] : $stamp;
if (@file_put_contents($path, note_build($subject, $text, $created, $stamp)) === false) {
api_fail('Speichern fehlgeschlagen', 500);
}
api_ok(array(
'file' => basename($path),
'subject' => $subject,
'relpath' => str_replace(DOL_DATA_ROOT.'/', '', $path),
));
}
/* ---------- Neu anlegen ---------- */
if (!is_dir($dir)) dol_mkdir($dir);
// Betreff in einen dateinamenstauglichen Teil überführen — der Name ist das Einzige,
// was in der Dokumentliste zu sehen ist.
$subject_part = '';
if ($subject !== '') {
$s = dol_string_nospecial(dol_sanitizeFileName($subject), '_');
$s = trim(preg_replace('/_+/', '_', $s), '_');
if ($s !== '') $subject_part = '_'.dol_trunc($s, 60, 'right', 'UTF-8', 1);
}
$filename = 'notiz'.$subject_part.'_'.dol_print_date(dol_now(), '%Y%m%d_%H%M%S').'.txt';
$target = $dir.'/'.$filename;
if (@file_put_contents($target, note_build($subject, $text, $stamp, '')) === false) {
api_fail('Speichern fehlgeschlagen', 500);
}
@chmod($target, 0664);
api_ok(array(
'file' => $filename,
'subject' => $subject,
'relpath' => str_replace(DOL_DATA_ROOT.'/', '', $target),
'size' => (int) filesize($target),
));

View file

@ -207,17 +207,23 @@ if ($action === 'photos') {
// Ignoriere Meta-Dateien, Previews und Hidden Files // Ignoriere Meta-Dateien, Previews und Hidden Files
if (strpos($name, '.meta') !== false || strpos($name, '_preview') !== false || $name[0] === '.') continue; if (strpos($name, '.meta') !== false || strpos($name, '_preview') !== false || $name[0] === '.') continue;
$mtime = (int) @filemtime($filepath);
$out[] = array( $out[] = array(
'filename' => $name, 'filename' => $name,
'size' => (int) @filesize($filepath), 'size' => (int) @filesize($filepath),
'mime' => dol_mimetype($name), 'mime' => dol_mimetype($name),
'date' => (int) @filemtime($filepath), 'date' => $mtime,
// Aufnahmezeit aus EXIF, sonst das Dateidatum. Bei Uploads aus der Galerie
// ist die Upload-Zeit die falsche Reihenfolge — auf der Baustelle zählt,
// wann das Foto entstanden ist. Eigener Parser, weil der Prod-Container
// kein exif-Modul geladen hat (siehe lib/bericht.lib.php).
'taken_at' => (int) bericht_file_taken_at($filepath, $mtime),
'relpath' => str_replace(DOL_DATA_ROOT.'/', '', $filepath), 'relpath' => str_replace(DOL_DATA_ROOT.'/', '', $filepath),
); );
} }
} }
// Sortiere nach Datum (neuste zuerst) // Sortiere nach Aufnahmezeit (neuste zuerst)
usort($out, function($a, $b) { return $b['date'] <=> $a['date']; }); usort($out, function($a, $b) { return $b['taken_at'] <=> $a['taken_at']; });
api_ok(array('photos' => $out, 'count' => count($out))); api_ok(array('photos' => $out, 'count' => count($out)));
} }

View file

@ -3,12 +3,14 @@
* Liefert eine Datei aus DOL_DATA_ROOT aus, authentifiziert per JWT. * Liefert eine Datei aus DOL_DATA_ROOT aus, authentifiziert per JWT.
* Whitelist: nur facture/, commande/, propal/, bericht/ * Whitelist: nur facture/, commande/, propal/, bericht/
* *
* Für den Thumb-Request der PWA werden auch Thumbnails ausgeliefert
* (Dolibarr legt <name>_small.png unter thumbs/ ab).
*
* Query: * Query:
* relpath relativer Pfad unter DOL_DATA_ROOT * relpath relativer Pfad unter DOL_DATA_ROOT
* size=small|mini (optional, nutzt automatisch das Thumb) * size=thumb&w=<px> serverseitig erzeugtes Thumbnail (GD, gecacht, mit ETag).
* Das ist der Weg für Kachel-Ansichten. w: 40800, Default 320.
* size=small|mini altes Verhalten: nutzt ein von Dolibarr vorgefertigtes
* thumbs/<name>_small.<ext>, falls vorhanden. Für Dateien aus
* der PWA existiert so eines nicht (kein vignette() beim Upload).
* download=1 als Anhang ausliefern statt inline
*/ */
// Dieser Endpoint liefert Binärdaten aus — KEIN JSON Content-Type! // Dieser Endpoint liefert Binärdaten aus — KEIN JSON Content-Type!
// Deshalb nicht _inc.php nutzen, sondern awlauth + Dolibarr manuell laden. // Deshalb nicht _inc.php nutzen, sondern awlauth + Dolibarr manuell laden.
@ -74,7 +76,40 @@ if (!$full || !file_exists($full)) {
exit; exit;
} }
// Thumb-Variante // Serverseitig erzeugtes Thumbnail (GD, gecacht) — der Weg für die Kachel-Ansicht der PWA.
//
// Die alte Variante size=small/mini konnte nur ein Thumbnail ausliefern, das Dolibarr selbst
// unter thumbs/<name>_small.<ext> abgelegt hatte. Fotos aus der PWA kommen aber über
// orders.php?action=upload_photo herein, und dort läuft kein vignette() — es gab also nie
// eines, und jede Kachel lud das komplette Handyfoto. bericht_attachment_thumb() (seit 1.5.0,
// auch vom Editor genutzt) rendert bei Bedarf und legt das Ergebnis unter bericht/thumbs/ ab.
if ($size === 'thumb') {
$w = (int) ($_GET['w'] ?? 320);
if ($w < 40) $w = 40;
if ($w > 800) $w = 800;
// ETag über mtime: ein ersetztes Original liefert einen neuen Wert, sonst antwortet
// der Server mit 304 und überträgt gar nichts.
$etag = '"'.sha1($relpath.'|'.@filemtime($full).'|'.$w).'"';
header('ETag: '.$etag);
header('Cache-Control: private, max-age=2592000');
if (trim((string) ($_SERVER['HTTP_IF_NONE_MATCH'] ?? '')) === $etag) {
http_response_code(304);
exit;
}
$thumb_data = bericht_attachment_thumb($full, $w);
if ($thumb_data !== null) {
header('Content-Type: image/jpeg');
header('Content-Length: '.strlen($thumb_data));
echo $thumb_data;
exit;
}
// Kein unterstütztes Bild (PDF/Video) oder zu groß für den Speicher:
// unten regulär das Original ausliefern.
}
// Alt-Variante: von Dolibarr vorgefertigtes Thumbnail, falls vorhanden
if ($size === 'small' || $size === 'mini') { if ($size === 'small' || $size === 'mini') {
$dir = dirname($full); $dir = dirname($full);
$base = pathinfo($full, PATHINFO_FILENAME); $base = pathinfo($full, PATHINFO_FILENAME);

View file

@ -212,8 +212,13 @@ foreach ($pages as $p) {
'page_order' => (int) $p->page_order, 'page_order' => (int) $p->page_order,
'source_type'=> $p->source_type, 'source_type'=> $p->source_type,
'source_path'=> $p->source_path, 'source_path'=> $p->source_path,
// Vom Editor gebautes Seitenbild (seit 1.5.0). Wer nur source_path anzeigt, sieht
// das Rohbild ohne Anmerkungen — und bei Raster-Layouts nur eines von bis zu sechs
// Bildern, bei title_only gar keins. Clients zeigen composite_path, wenn gesetzt.
'composite_path' => $p->composite_path,
'rotation' => (int) $p->rotation, 'rotation' => (int) $p->rotation,
'note' => $p->note, 'note' => $p->note,
'title' => $p->title,
'layout' => $p->layout, 'layout' => $p->layout,
); );
} }

View file

@ -27,7 +27,7 @@ class modBericht extends DolibarrModules
$this->editor_name = 'Alles Watt läuft'; $this->editor_name = 'Alles Watt läuft';
$this->editor_url = ''; $this->editor_url = '';
$this->version = '1.5.0'; $this->version = '1.5.1';
$this->const_name = 'MAIN_MODULE_'.strtoupper($this->name); $this->const_name = 'MAIN_MODULE_'.strtoupper($this->name);
$this->picto = 'fa-file-pdf'; $this->picto = 'fa-file-pdf';