Compare commits
No commits in common. "d3edc6ebfc55ac14d4064cd39cb18ec2db761522" and "a61ce6c918f1b618d8623e3e1c2d64315cef591f" have entirely different histories.
d3edc6ebfc
...
a61ce6c918
9 changed files with 23 additions and 555 deletions
38
CLAUDE.md
38
CLAUDE.md
|
|
@ -1,6 +1,6 @@
|
|||
# Bericht-Modul — Projekt-Status & Architektur
|
||||
|
||||
## Stand 2026-08-22 (Version 1.5.1)
|
||||
## Stand 2026-08-21 (Version 1.5.0)
|
||||
|
||||
Dolibarr-Custom-Modul für Arbeitsberichte mit Browser-PDF-Editor + PWA-API-Layer + Lieferschein-Unterschrift.
|
||||
|
||||
|
|
@ -154,24 +154,16 @@ PWA-API-Layer + Usability-Features:
|
|||
- ✅ 4.b Touch-Unterschrift (POST /api/pages.php?action=signature)
|
||||
- ✅ 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):
|
||||
- POST /api/auth.php — Login (username, password) -> setzt awl_sso-Cookie
|
||||
- POST /api/auth.php — JWT-Login (username, password)
|
||||
- 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>&action=photos — Order-Dateien (Bilder/Audio/PDFs/Dokumente)
|
||||
- POST /api/orders.php?action=create — Neuer Order (socid, title, ref_client, date)
|
||||
- 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)
|
||||
- GET /api/photo.php?relpath=<path> — Datei-Serving (JWT via Header oder Query-Param, Whitelist: facture|commande|propal|bericht)
|
||||
- POST /api/pages.php?action=signature&bericht_id=<id> — Touch-Unterschrift
|
||||
- DELETE /api/pages.php?id=<id> — Seite löschen
|
||||
- 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)
|
||||
- POST /api/pages.php?id=<id> — Seite-Notiz / Rotation
|
||||
|
||||
PWA neue Komponenten (app.js):
|
||||
- openFileViewer() — Generischer Dateibetrachter (Bilder/PDFs/Audio/Download)
|
||||
|
|
@ -223,29 +215,9 @@ Vollständiger Workflow für Kunden-Unterschrift auf dem Handy. Source-Repo: `da
|
|||
### 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
|
||||
- `llx_element_element`-Richtung: **commande = source, shipping = target** (NICHT umgekehrt)
|
||||
- ~~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.
|
||||
- JWT für `<img>`/`<object>`-Tags: Query-Param `?jwt=<token>` als Fallback in `api/_jwt.php`, da diese Tags keine Authorization-Header senden
|
||||
- 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)
|
||||
- Sprachnotizen-Transkription via Whisper (POST /api/transcribe.php)
|
||||
- PIN-Schutz / WebAuthn
|
||||
|
|
|
|||
47
ChangeLog.md
47
ChangeLog.md
|
|
@ -1,52 +1,5 @@
|
|||
# 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
|
||||
|
||||
### Anhänge-Spalte: Miniaturen statt Dateinamen
|
||||
|
|
|
|||
57
README.md
57
README.md
|
|
@ -85,24 +85,16 @@ Die **Baustelle-PWA** (`https://awl.data-it-solution.de/baustelle/`) nutzt die f
|
|||
```
|
||||
POST /custom/bericht/api/auth.php
|
||||
Body: { login: string, password: string }
|
||||
Response: { ok: true, user: {...} } + HttpOnly-Cookie awl_sso
|
||||
Response: { ok: true, token: JWT, user: {...} }
|
||||
```
|
||||
|
||||
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.
|
||||
JWT-Token hat 7 Tage Gültigkeit und enthält: `sub` (user id), `login`, `name`, `perms` (read/write/delete/admin).
|
||||
|
||||
### Order-APIs
|
||||
```
|
||||
GET /custom/bericht/api/orders.php
|
||||
GET /custom/bericht/api/orders.php?id=<id>
|
||||
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
|
||||
Body: { socid, ref_client, title?, note_private?, date?, date_livraison?, validate? }
|
||||
ref_client ("Ihr Zeichen") ist PFLICHT. validate=true gibt den Auftrag direkt frei —
|
||||
|
|
@ -112,19 +104,9 @@ POST /custom/bericht/api/orders.php?action=create
|
|||
|
||||
### Dateien
|
||||
```
|
||||
GET /custom/bericht/api/photo.php?relpath=<path>
|
||||
GET /custom/bericht/api/photo.php?relpath=<path>&jwt=<token>
|
||||
Liefert Dateien aus DOL_DATA_ROOT (Whitelist: facture/, commande/, propal/, bericht/)
|
||||
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: 40–800, 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
|
||||
Optional: ?size=small|mini (Thumbnails), ?download=1 (Attachment-Header)
|
||||
```
|
||||
|
||||
### PDF-Ansicht
|
||||
|
|
@ -136,32 +118,11 @@ GET /custom/bericht/api/pdf.php?id=<bericht_id>&jwt=<token>
|
|||
### Seiten-Verwaltung (PWA)
|
||||
```
|
||||
DELETE /custom/bericht/api/pages.php?id=<page_id>
|
||||
POST /custom/bericht/api/pages.php?id=<page_id> Body: { note, title, rotation, layout }
|
||||
POST /custom/bericht/api/pages.php?id=<page_id> Body: { note: string }
|
||||
POST /custom/bericht/api/pages.php?action=signature&bericht_id=<id>
|
||||
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)
|
||||
```
|
||||
GET /custom/bericht/api/shipments.php?order_id=<id>
|
||||
|
|
@ -254,19 +215,19 @@ bericht/
|
|||
│ ├── set_slot_image.php Bild eines Rasterplatzes setzen
|
||||
│ ├── upload_extra.php Direkter Upload
|
||||
│ └── verify_signature.php Unterschrift prüfen
|
||||
├── api/ REST-API (Auth über awl_sso-Cookie, Modul awlauth)
|
||||
│ ├── _inc.php Dolibarr-Init + awlauth_require + JSON-Helper
|
||||
├── api/ REST-API (JWT-Auth)
|
||||
│ ├── _inc.php JWT-Authentifizierung + Dolibarr-Init
|
||||
│ ├── _jwt.php JWT encoding/decoding (mit ?jwt= Query-Fallback)
|
||||
│ ├── auth.php Login-Endpoint
|
||||
│ ├── orders.php Order-Liste, Detail, Fotos, Create
|
||||
│ ├── shipments.php Lieferungen-Liste, PDF-Stream, Unterschrift-Confirm
|
||||
│ ├── photo.php Datei-Serving mit Whitelist + Thumbnails (size=thumb)
|
||||
│ ├── photo.php Datei-Serving mit Whitelist
|
||||
│ ├── pdf.php Finalized Bericht-PDF
|
||||
│ ├── pages.php Seiten-Verwaltung (Note, Rotation, Signature)
|
||||
│ ├── reports.php Bericht-CRUD
|
||||
│ ├── templates.php ODT-Templates
|
||||
│ ├── materials.php Materiallisten
|
||||
│ ├── voice.php Sprachnotizen (Upload + Transkription)
|
||||
│ ├── note.php Textnotizen zum Auftrag (Liste/Lesen/Anlegen/Ändern)
|
||||
│ └── transcribe.php Whisper-Transkription
|
||||
├── js/
|
||||
│ ├── editor.js PDF.js + Fabric.js Integration, Anhänge-Spalte,
|
||||
|
|
|
|||
193
ROADMAP.md
193
ROADMAP.md
|
|
@ -1,193 +0,0 @@
|
|||
# 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 20–40 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 (150–620 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.1–1.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
179
api/note.php
|
|
@ -1,179 +0,0 @@
|
|||
<?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),
|
||||
));
|
||||
|
|
@ -207,23 +207,17 @@ if ($action === 'photos') {
|
|||
// Ignoriere Meta-Dateien, Previews und Hidden Files
|
||||
if (strpos($name, '.meta') !== false || strpos($name, '_preview') !== false || $name[0] === '.') continue;
|
||||
|
||||
$mtime = (int) @filemtime($filepath);
|
||||
$out[] = array(
|
||||
'filename' => $name,
|
||||
'size' => (int) @filesize($filepath),
|
||||
'mime' => dol_mimetype($name),
|
||||
'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),
|
||||
'date' => (int) @filemtime($filepath),
|
||||
'relpath' => str_replace(DOL_DATA_ROOT.'/', '', $filepath),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Sortiere nach Aufnahmezeit (neuste zuerst)
|
||||
usort($out, function($a, $b) { return $b['taken_at'] <=> $a['taken_at']; });
|
||||
// Sortiere nach Datum (neuste zuerst)
|
||||
usort($out, function($a, $b) { return $b['date'] <=> $a['date']; });
|
||||
api_ok(array('photos' => $out, 'count' => count($out)));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,14 +3,12 @@
|
|||
* Liefert eine Datei aus DOL_DATA_ROOT aus, authentifiziert per JWT.
|
||||
* 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:
|
||||
* relpath — relativer Pfad unter DOL_DATA_ROOT
|
||||
* size=thumb&w=<px> — serverseitig erzeugtes Thumbnail (GD, gecacht, mit ETag).
|
||||
* Das ist der Weg für Kachel-Ansichten. w: 40–800, 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
|
||||
* size=small|mini (optional, nutzt automatisch das Thumb)
|
||||
*/
|
||||
// Dieser Endpoint liefert Binärdaten aus — KEIN JSON Content-Type!
|
||||
// Deshalb nicht _inc.php nutzen, sondern awlauth + Dolibarr manuell laden.
|
||||
|
|
@ -76,40 +74,7 @@ if (!$full || !file_exists($full)) {
|
|||
exit;
|
||||
}
|
||||
|
||||
// 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
|
||||
// Thumb-Variante
|
||||
if ($size === 'small' || $size === 'mini') {
|
||||
$dir = dirname($full);
|
||||
$base = pathinfo($full, PATHINFO_FILENAME);
|
||||
|
|
|
|||
|
|
@ -212,13 +212,8 @@ foreach ($pages as $p) {
|
|||
'page_order' => (int) $p->page_order,
|
||||
'source_type'=> $p->source_type,
|
||||
'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,
|
||||
'note' => $p->note,
|
||||
'title' => $p->title,
|
||||
'layout' => $p->layout,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class modBericht extends DolibarrModules
|
|||
|
||||
$this->editor_name = 'Alles Watt läuft';
|
||||
$this->editor_url = '';
|
||||
$this->version = '1.5.1';
|
||||
$this->version = '1.5.0';
|
||||
$this->const_name = 'MAIN_MODULE_'.strtoupper($this->name);
|
||||
$this->picto = 'fa-file-pdf';
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue