diff --git a/ajax/create_upload_token.php b/ajax/create_upload_token.php index 1291a6a..7f014f3 100644 --- a/ajax/create_upload_token.php +++ b/ajax/create_upload_token.php @@ -1,22 +1,42 @@ hasRight('bericht', 'write')) bericht_ajax_fail('Permission denied', 403); -$berichtid = (int) ($_POST['berichtid'] ?? 0); -if (!$berichtid) bericht_ajax_fail('berichtid fehlt'); +$element_id = (int) ($_POST['element_id'] ?? 0); +$element_type = (string) ($_POST['element_type'] ?? 'order'); -$bericht = new Bericht($db); -if ($bericht->fetch($berichtid) <= 0) bericht_ajax_fail('Bericht nicht gefunden', 404); +// Abwärtskompatibilität: berichtid akzeptieren, daraus element_id ableiten +if (!$element_id && !empty($_POST['berichtid'])) { + require_once __DIR__.'/../class/bericht.class.php'; + $b = new Bericht($db); + if ($b->fetch((int) $_POST['berichtid']) > 0) { + $element_id = $b->fk_element; + $element_type = $b->element_type; + } +} + +if (!$element_id) bericht_ajax_fail('element_id fehlt'); + +// Parent-Objekt validieren +$valid_types = array('order', 'invoice', 'propal'); +if (!in_array($element_type, $valid_types)) bericht_ajax_fail('element_type ungültig'); + +// Prüfen ob Objekt existiert +$tok_check = new BerichtUploadToken($db); +$tok_check->fk_element = $element_id; +$tok_check->element_type = $element_type; +$parent = $tok_check->fetchParentObject(); +if (!$parent) bericht_ajax_fail('Objekt nicht gefunden', 404); $tok = new BerichtUploadToken($db); -$hex = $tok->create($berichtid, $user->id); +$hex = $tok->create($element_id, $element_type, $user->id); if (!$hex) bericht_ajax_fail('Token-Erstellung fehlgeschlagen'); $base = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https://' : 'http://').$_SERVER['HTTP_HOST']; diff --git a/api/orders.php b/api/orders.php index 80618fb..370c862 100644 --- a/api/orders.php +++ b/api/orders.php @@ -102,59 +102,25 @@ if ($action === 'upload_photo' && $_SERVER['REQUEST_METHOD'] === 'POST') { if (!$user->hasRight('bericht', 'write')) api_fail('Schreibrechte fehlen', 403); if (empty($_FILES['file']['tmp_name'])) api_fail('file fehlt'); - // Bericht zum Auftrag suchen: neuester ENTWURF wird erweitert. - // Wenn nur finalisierte Berichte existieren, wird ein neuer Entwurf angelegt. - // Mit ?bericht_id=X kann die PWA einen spezifischen Bericht addressieren. - $wanted_id = (int) ($_GET['bericht_id'] ?? 0); - $bericht = null; - if ($wanted_id > 0) { - $b = new Bericht($db); - if ($b->fetch($wanted_id) > 0 && $b->fk_element == $cmd->id && $b->element_type === 'order') { - $bericht = $b; - } - } - if (!$bericht) { - $list = Bericht::fetchAllForElement($db, 'order', $cmd->id); - foreach ($list as $b) { - if ((int) $b->status === Bericht::STATUS_DRAFT) { $bericht = $b; break; } - } - } - if (!$bericht) { - $bericht = new Bericht($db); - $bericht->element_type = 'order'; - $bericht->fk_element = $cmd->id; - $bericht->titel = 'Bericht '.$cmd->ref; - $bericht->auftragsnummer = $cmd->ref; - $bericht->template_odt = getDolGlobalString('BERICHT_DEFAULT_TEMPLATE', ''); - if ($bericht->create($user) <= 0) api_fail('Bericht-Anlage fehlgeschlagen', 500); - } - - // Datei speichern + // Datei validieren $orig = dol_sanitizeFileName($_FILES['file']['name']); $ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION)); if (!in_array($ext, array('jpg', 'jpeg', 'png'))) api_fail('Dateityp nicht unterstützt'); - $workdir = DOL_DATA_ROOT.'/bericht/work/'.$bericht->id; - if (!is_dir($workdir)) dol_mkdir($workdir); - $target = $workdir.'/api_'.dol_print_date(dol_now(), '%Y%m%d_%H%M%S').'_'.uniqid().'.'.$ext; + // Foto direkt in den Dolibarr-Standard-Auftragsordner speichern (kein Bericht nötig) + $upload_dir = $conf->commande->multidir_output[$cmd->entity].'/'.dol_sanitizeFileName($cmd->ref); + if (!is_dir($upload_dir)) dol_mkdir($upload_dir); + + $filename = 'foto_'.dol_print_date(dol_now(), '%Y%m%d_%H%M%S').'_'.uniqid().'.'.$ext; + $target = $upload_dir.'/'.$filename; if (!move_uploaded_file($_FILES['file']['tmp_name'], $target)) api_fail('Upload fehlgeschlagen', 500); $relpath = str_replace(DOL_DATA_ROOT.'/', '', $target); - // Als Page anlegen - $resm = $db->query("SELECT COALESCE(MAX(page_order),0) AS m FROM ".$db->prefix()."bericht_page WHERE fk_bericht = ".((int) $bericht->id)); - $next = ($resm && ($o = $db->fetch_object($resm))) ? ((int) $o->m) + 1 : 1; - $page = new BerichtPage($db); - $page->fk_bericht = $bericht->id; - $page->page_order = $next; - $page->source_type = 'upload'; - $page->source_path = $relpath; - if ($page->create() <= 0) api_fail('Page-Insert fehlgeschlagen', 500); - api_ok(array( - 'bericht_id' => (int) $bericht->id, - 'page_id' => (int) $page->id, - 'filename' => basename($target), + 'filename' => $filename, + 'relpath' => $relpath, + 'size' => filesize($target), )); } diff --git a/bericht_card.php b/bericht_card.php index 41974a3..2139bec 100644 --- a/bericht_card.php +++ b/bericht_card.php @@ -278,8 +278,10 @@ if (!$bericht) { // Daten für JS bereitstellen $editor_config = array( - 'berichtid' => (int) $bericht->id, - 'token' => newToken(), + 'berichtid' => (int) $bericht->id, + 'element_id' => (int) $parent->id, + 'element_type' => $element, + 'token' => newToken(), 'urls' => array( 'save_annotations' => dol_buildpath('/bericht/ajax/save_annotations.php', 1), 'upload_extra' => dol_buildpath('/bericht/ajax/upload_extra.php', 1), diff --git a/class/upload_token.class.php b/class/upload_token.class.php index 8bb2c2e..0724dcf 100644 --- a/class/upload_token.class.php +++ b/class/upload_token.class.php @@ -1,6 +1,7 @@ db->query("DELETE FROM ".$this->db->prefix()."bericht_upload_token" ." WHERE expires_at < '".$this->db->idate(dol_now())."'", 1); - $this->token = bin2hex(random_bytes(32)); - $this->fk_bericht = (int) $fk_bericht; + $this->token = bin2hex(random_bytes(32)); + $this->fk_element = (int) $fk_element; + $this->element_type = $element_type; $this->fk_user_creat = (int) $fk_user; - $this->datec = dol_now(); - $this->expires_at = $this->datec + ($lifetime ?: self::DEFAULT_LIFETIME); - $this->max_uploads = $max_uploads ?: self::DEFAULT_MAX_UPLOADS; + $this->datec = dol_now(); + $this->expires_at = $this->datec + ($lifetime ?: self::DEFAULT_LIFETIME); + $this->max_uploads = $max_uploads ?: self::DEFAULT_MAX_UPLOADS; $this->uploads_count = 0; $sql = "INSERT INTO ".$this->db->prefix()."bericht_upload_token " - ."(token, fk_bericht, fk_user_creat, expires_at, uploads_count, max_uploads, datec) VALUES (" + ."(token, fk_element, element_type, fk_user_creat, expires_at, uploads_count, max_uploads, datec) VALUES (" ."'".$this->db->escape($this->token)."'," - .$this->fk_bericht."," + .$this->fk_element."," + ."'".$this->db->escape($this->element_type)."'," .$this->fk_user_creat."," ."'".$this->db->idate($this->expires_at)."'," ."0," @@ -64,7 +73,7 @@ class BerichtUploadToken public static function fetchValid(DoliDB $db, $token) { if (!preg_match('/^[a-f0-9]{64}$/', $token)) return null; - $sql = "SELECT rowid, token, fk_bericht, fk_user_creat, expires_at, uploads_count, max_uploads, datec" + $sql = "SELECT rowid, token, fk_element, element_type, fk_user_creat, expires_at, uploads_count, max_uploads, datec" ." FROM ".$db->prefix()."bericht_upload_token" ." WHERE token = '".$db->escape($token)."'" ." AND expires_at > '".$db->idate(dol_now())."'" @@ -73,17 +82,67 @@ class BerichtUploadToken if (!$res || $db->num_rows($res) === 0) return null; $obj = $db->fetch_object($res); $t = new self($db); - $t->id = (int) $obj->rowid; - $t->token = $obj->token; - $t->fk_bericht = (int) $obj->fk_bericht; - $t->fk_user_creat = (int) $obj->fk_user_creat; - $t->expires_at = $db->jdate($obj->expires_at); - $t->uploads_count = (int) $obj->uploads_count; - $t->max_uploads = (int) $obj->max_uploads; - $t->datec = $db->jdate($obj->datec); + $t->id = (int) $obj->rowid; + $t->token = $obj->token; + $t->fk_element = (int) $obj->fk_element; + $t->element_type = $obj->element_type; + $t->fk_user_creat = (int) $obj->fk_user_creat; + $t->expires_at = $db->jdate($obj->expires_at); + $t->uploads_count = (int) $obj->uploads_count; + $t->max_uploads = (int) $obj->max_uploads; + $t->datec = $db->jdate($obj->datec); return $t; } + /** + * Ermittelt den Upload-Ordner für dieses Token basierend auf element_type. + * @return string|false Absoluter Pfad zum Upload-Ordner, false bei Fehler + */ + public function getUploadDir() + { + global $conf; + $parent = $this->fetchParentObject(); + if (!$parent) return false; + + $ref = dol_sanitizeFileName($parent->ref); + switch ($this->element_type) { + case 'order': + return $conf->commande->multidir_output[$parent->entity].'/'.$ref; + case 'invoice': + return $conf->facture->multidir_output[$parent->entity].'/'.$ref; + case 'propal': + return $conf->propal->multidir_output[$parent->entity].'/'.$ref; + default: + return false; + } + } + + /** + * Lädt das Dolibarr-Parent-Objekt (Auftrag/Rechnung/Angebot). + * @return CommonObject|false + */ + public function fetchParentObject() + { + switch ($this->element_type) { + case 'order': + require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php'; + $obj = new Commande($this->db); + break; + case 'invoice': + require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php'; + $obj = new Facture($this->db); + break; + case 'propal': + require_once DOL_DOCUMENT_ROOT.'/comm/propal/class/propal.class.php'; + $obj = new Propal($this->db); + break; + default: + return false; + } + if ($obj->fetch($this->fk_element) <= 0) return false; + return $obj; + } + public function incrementCount() { $this->uploads_count++; diff --git a/core/modules/modBericht.class.php b/core/modules/modBericht.class.php index 85f170d..898898c 100644 --- a/core/modules/modBericht.class.php +++ b/core/modules/modBericht.class.php @@ -165,6 +165,10 @@ class modBericht extends DolibarrModules // Der Editor rendert sein Fabric-Canvas bei jedem Save zu einem PNG und // lädt es hoch — damit ist PDF-Output identisch mit Editor-Anzeige. "ALTER TABLE ".$this->db->prefix()."bericht_page ADD COLUMN composite_path VARCHAR(512) DEFAULT NULL", + // Foto-Upload entkoppeln: Token an Dolibarr-Objekt statt an Bericht binden + "ALTER TABLE ".$this->db->prefix()."bericht_upload_token ADD COLUMN fk_element INTEGER NOT NULL DEFAULT 0", + "ALTER TABLE ".$this->db->prefix()."bericht_upload_token ADD COLUMN element_type VARCHAR(32) NOT NULL DEFAULT 'order'", + "UPDATE ".$this->db->prefix()."bericht_upload_token SET fk_element = fk_bericht, element_type = 'order' WHERE fk_element = 0 AND fk_bericht > 0", // Phase 5.9: Materialliste pro Auftrag "CREATE TABLE IF NOT EXISTS ".$this->db->prefix()."bericht_material (" ."rowid INT AUTO_INCREMENT PRIMARY KEY," diff --git a/js/editor.js b/js/editor.js index 4736eca..aa06e84 100644 --- a/js/editor.js +++ b/js/editor.js @@ -1019,7 +1019,8 @@ async function openQrModal() { const fd = new FormData(); fd.append('token', cfg.token); - fd.append('berichtid', cfg.berichtid); + fd.append('element_id', cfg.element_id); + fd.append('element_type', cfg.element_type); const r = await fetch(cfg.urls.create_upload_token, { method: 'POST', body: fd }); const data = await r.json(); if (!data.success) { @@ -1048,16 +1049,19 @@ document.getElementById('bericht-qr-modal').style.display = 'block'; - // Polling alle 5 Sek nach neuen Pages - qrLastPageCount = document.querySelectorAll('.page-thumb').length; + // Polling alle 5 Sek: Fotos landen jetzt im Auftragsordner, nicht als Pages. + // Prüfe ob sich die Anhänge-Anzahl ändert → Seite neu laden für Anhänge-Browser-Refresh. + qrLastPageCount = document.querySelectorAll('.attachment-item').length; if (qrPollInterval) clearInterval(qrPollInterval); qrPollInterval = setInterval(async () => { try { const r = await fetch(cfg.urls.list_pages + '?berichtid=' + cfg.berichtid); const d = await r.json(); - if (d.success && d.count !== qrLastPageCount) { + // Fallback: auch auf Pages prüfen (bestehende Flows), primär aber Anhänge-Änderung + const currentAttachments = document.querySelectorAll('.attachment-item').length; + if (currentAttachments !== qrLastPageCount || (d.success && d.count !== document.querySelectorAll('.page-thumb').length)) { document.querySelector('.qr-status').textContent = - '✓ ' + (d.count - qrLastPageCount) + ' neue(s) Foto(s) hochgeladen — Seite wird neu geladen…'; + '✓ Neue Fotos hochgeladen — Seite wird neu geladen…'; setTimeout(() => location.reload(), 1500); clearInterval(qrPollInterval); } diff --git a/mobile_upload.php b/mobile_upload.php index dcd2dfb..e0a96a6 100644 --- a/mobile_upload.php +++ b/mobile_upload.php @@ -1,6 +1,7 @@ fetch($tok->fk_bericht) <= 0) { + // Upload-Ziel über Token ermitteln (Dolibarr-Standard-Ordner) + $upload_dir = $tok->getUploadDir(); + if (!$upload_dir) { http_response_code(404); - echo json_encode(array('success' => false, 'error' => 'Bericht nicht gefunden')); + echo json_encode(array('success' => false, 'error' => 'Objekt nicht gefunden')); exit; } @@ -54,32 +55,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_FILES['file']['tmp_name']) exit; } - $workdir = DOL_DATA_ROOT.'/bericht/work/'.$tok->fk_bericht; - if (!is_dir($workdir)) dol_mkdir($workdir); + if (!is_dir($upload_dir)) dol_mkdir($upload_dir); - $target = $workdir.'/mobile_'.dol_print_date(dol_now(), '%Y%m%d_%H%M%S').'_'.uniqid().'.'.$ext; + $filename = 'foto_'.dol_print_date(dol_now(), '%Y%m%d_%H%M%S').'_'.uniqid().'.'.$ext; + $target = $upload_dir.'/'.$filename; if (!move_uploaded_file($_FILES['file']['tmp_name'], $target)) { echo json_encode(array('success' => false, 'error' => 'Upload fehlgeschlagen')); exit; } - $relpath = str_replace(DOL_DATA_ROOT.'/', '', $target); - - // Als neue Bericht-Page einfügen - $res = $db->query("SELECT COALESCE(MAX(page_order),0) AS m FROM ".$db->prefix()."bericht_page WHERE fk_bericht = ".((int) $tok->fk_bericht)); - $next_order = ($res && ($o = $db->fetch_object($res))) ? ((int) $o->m) + 1 : 1; - - $page = new BerichtPage($db); - $page->fk_bericht = $tok->fk_bericht; - $page->page_order = $next_order; - $page->source_type = 'upload'; - $page->source_path = $relpath; - if ($page->create() <= 0) { - echo json_encode(array('success' => false, 'error' => 'DB-Insert fehlgeschlagen')); - exit; - } $tok->incrementCount(); - echo json_encode(array('success' => true, 'pageid' => $page->id, 'filename' => basename($target))); + echo json_encode(array('success' => true, 'filename' => $filename)); exit; } @@ -88,24 +74,23 @@ if (!$tok) { http_response_code(403); ?>
-Dieser Upload-Link ist abgelaufen oder ungültig.
-Bitte im Bericht-Editor einen neuen QR-Code generieren.
+Bitte im Editor einen neuen QR-Code generieren.
fetch($tok->fk_bericht); -$auftragsnr = $bericht->auftragsnummer ?: $bericht->ref; +// Parent-Objekt laden für Anzeige +$parent = $tok->fetchParentObject(); +$parent_ref = $parent ? $parent->ref : '???'; $valid_min = max(1, round(($tok->expires_at - dol_now()) / 60)); ?> @@ -115,7 +100,7 @@ $valid_min = max(1, round(($tok->expires_at - dol_now()) / 60)); -