— Seite löschen * POST /api/pages.php?id= — Body { note: "..." } — Notiz setzen * Body { rotation: 90 } — rotieren * POST /api/pages.php?action=signature&bericht_id= * multipart: file= — Unterschrift als neue Seite am Bericht anhängen */ require_once __DIR__.'/_inc.php'; api_authenticate(); global $db, $user; if (!$user->hasRight('bericht', 'write')) api_fail('Permission denied', 403); $method = $_SERVER['REQUEST_METHOD']; $action = $_GET['action'] ?? ''; /* ---------- Unterschrift als neue Seite anhängen ---------- */ if ($method === 'POST' && $action === 'signature') { $bericht_id = (int) ($_GET['bericht_id'] ?? 0); if (!$bericht_id) api_fail('bericht_id fehlt'); if (empty($_FILES['file']['tmp_name'])) api_fail('file fehlt'); $bericht = new Bericht($db); if ($bericht->fetch($bericht_id) <= 0) api_fail('Bericht nicht gefunden', 404); // Parent-Objekt (Kunde) holen — wir brauchen den Kundennamen für die Metadaten $parent = bericht_fetch_parent($db, $bericht->element_type, $bericht->fk_element); $kundename = $parent && !empty($parent->thirdparty) ? ($parent->thirdparty->name ?? '') : ''; $parent_ref = $parent ? $parent->ref : ''; // Zusatz-Felder vom Client $signer_name = trim((string) ($_POST['signer_name'] ?? '')); $gps_lat = isset($_POST['gps_lat']) ? (float) $_POST['gps_lat'] : null; $gps_lon = isset($_POST['gps_lon']) ? (float) $_POST['gps_lon'] : null; require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php'; $workdir = DOL_DATA_ROOT.'/bericht/work/'.$bericht_id; if (!is_dir($workdir)) dol_mkdir($workdir); // Hash-Verkettung: SHA256 über alle vorherigen Seiten-Quelldateien $prev_pages = BerichtPage::fetchAllForBericht($db, $bericht_id); $hash_ctx = hash_init('sha256'); hash_update($hash_ctx, 'bericht:'.$bericht_id.'|ref:'.$bericht->ref); foreach ($prev_pages as $pp) { $full_pp = bericht_resolve_data_path($pp->source_path); if ($full_pp && file_exists($full_pp)) { hash_update_file($hash_ctx, $full_pp); } hash_update($hash_ctx, '|order:'.$pp->page_order.'|note:'.($pp->note ?? '')); } $chain_hash = hash_final($hash_ctx); // Rohe Unterschrift vom Upload $raw_sig_png = file_get_contents($_FILES['file']['tmp_name']); if (!$raw_sig_png) api_fail('Upload fehlgeschlagen', 500); $sig_img = @imagecreatefromstring($raw_sig_png); if (!$sig_img) api_fail('PNG ungültig'); $sig_w = imagesx($sig_img); $sig_h = imagesy($sig_img); // Composite-Canvas: Metadaten oben, Unterschrift in der Mitte, Footer mit Hash unten $canvas_w = max($sig_w, 1200); $header_h = 180; $footer_h = 120; $sig_area_h = 500; $canvas_h = $header_h + $sig_area_h + $footer_h; $canvas = imagecreatetruecolor($canvas_w, $canvas_h); $white = imagecolorallocate($canvas, 255, 255, 255); $black = imagecolorallocate($canvas, 0, 0, 0); $grey = imagecolorallocate($canvas, 120, 120, 120); $line_grey = imagecolorallocate($canvas, 200, 200, 200); $dark_blue = imagecolorallocate($canvas, 51, 122, 183); imagefill($canvas, 0, 0, $white); // Header-Box imagefilledrectangle($canvas, 0, 0, $canvas_w, $header_h, imagecolorallocate($canvas, 245, 248, 252)); imagerectangle($canvas, 0, 0, $canvas_w - 1, $header_h, $line_grey); // Überschrift $title = 'ABNAHMEUNTERSCHRIFT'; imagestring($canvas, 5, 20, 15, $title, $dark_blue); // Metadaten Zeile 1 $line1 = 'Bericht: '.$bericht->ref.' · '.trim($bericht->element_type.' '.$parent_ref); imagestring($canvas, 4, 20, 50, $line1, $black); // Zeile 2: Kunde $line2 = 'Kunde: '.$kundename; imagestring($canvas, 4, 20, 75, $line2, $black); // Zeile 3: Datum + Zeit (Server) + Unterzeichner $datetime = date('Y-m-d H:i:s'); $line3 = 'Zeit (Server): '.$datetime.' · Unterzeichner: '.($signer_name ?: '—'); imagestring($canvas, 4, 20, 100, $line3, $black); // Zeile 4: GPS if ($gps_lat !== null && $gps_lon !== null) { $line4 = sprintf('GPS: %.6f, %.6f', $gps_lat, $gps_lon); imagestring($canvas, 3, 20, 125, $line4, $black); } // Bestätigungstext $bestaetigung = 'Ich bestaetige hiermit die ordnungsgemaesse Ausfuehrung der dokumentierten Arbeiten.'; imagestring($canvas, 3, 20, 150, $bestaetigung, $black); // Unterschrift-Bereich mittig einsetzen, auf Breite skalieren $max_sig_w = $canvas_w - 80; $max_sig_h = $sig_area_h - 60; $ratio = min($max_sig_w / $sig_w, $max_sig_h / $sig_h); $dw = (int) ($sig_w * $ratio); $dh = (int) ($sig_h * $ratio); $dx = (int) (($canvas_w - $dw) / 2); $dy = $header_h + 30; imagecopyresampled($canvas, $sig_img, $dx, $dy, 0, 0, $dw, $dh, $sig_w, $sig_h); imagedestroy($sig_img); // Trennlinie unter der Unterschrift imageline($canvas, 40, $header_h + $sig_area_h - 20, $canvas_w - 40, $header_h + $sig_area_h - 20, $grey); imagestring($canvas, 3, (int) ($canvas_w / 2 - 50), $header_h + $sig_area_h - 15, 'Unterschrift Kunde', $grey); // Footer-Box mit Hash $fy = $header_h + $sig_area_h; imagefilledrectangle($canvas, 0, $fy, $canvas_w, $canvas_h, imagecolorallocate($canvas, 248, 248, 248)); imagerectangle($canvas, 0, $fy, $canvas_w - 1, $canvas_h - 1, $line_grey); imagestring($canvas, 3, 20, $fy + 15, 'Integritaetspruefung (SHA-256 ueber alle vorherigen Seiten):', $grey); imagestring($canvas, 2, 20, $fy + 40, $chain_hash, $black); imagestring($canvas, 2, 20, $fy + 70, 'Erfasst: '.$datetime.' · Server: '.($_SERVER['SERVER_NAME'] ?? ''), $grey); // Schreiben $filename = 'signature_'.dol_print_date(dol_now(), '%Y%m%d_%H%M%S').'.png'; $target = $workdir.'/'.$filename; if (!imagepng($canvas, $target, 6)) api_fail('PNG schreiben fehlgeschlagen', 500); imagedestroy($canvas); $relpath = str_replace(DOL_DATA_ROOT.'/', '', $target); // Metadaten als JSON neben der Datei speichern (für spätere Verifikation) $meta = array( 'bericht_id' => $bericht_id, 'bericht_ref' => $bericht->ref, 'parent_type' => $bericht->element_type, 'parent_ref' => $parent_ref, 'kunde' => $kundename, 'signer_name' => $signer_name, 'signed_at' => $datetime, 'signed_at_unix' => time(), 'user_id' => $user->id, 'user_login' => $user->login, 'gps_lat' => $gps_lat, 'gps_lon' => $gps_lon, 'chain_hash_sha256' => $chain_hash, 'page_count_at_signing' => count($prev_pages), 'remote_ip' => $_SERVER['REMOTE_ADDR'] ?? '', ); @file_put_contents($target.'.meta.json', json_encode($meta, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); $resm = $db->query("SELECT COALESCE(MAX(page_order),0) AS m FROM ".$db->prefix()."bericht_page WHERE fk_bericht = ".$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; $page->note = 'Unterschrift '.($signer_name ?: 'Kunde').' — '.$datetime; if ($page->create() <= 0) api_fail('Page-Insert fehlgeschlagen', 500); api_ok(array( 'page_id' => $page->id, 'filename' => $filename, 'chain_hash' => $chain_hash, 'signed_at' => $datetime, )); } $page_id = (int) ($_GET['id'] ?? 0); if (!$page_id) api_fail('id fehlt'); // Page laden + prüfen dass sie zum User-scope gehört (einfach: Admin oder Bericht-schreiben) $pres = $db->query("SELECT rowid, fk_bericht, source_path, source_type FROM ".$db->prefix()."bericht_page WHERE rowid = ".$page_id); if (!$pres || !($prow = $db->fetch_object($pres))) api_fail('Seite nicht gefunden', 404); /* ---------- DELETE page ---------- */ if ($method === 'DELETE' || ($method === 'POST' && ($_GET['delete'] ?? '') === '1')) { if (!$user->hasRight('bericht', 'delete')) api_fail('Permission denied', 403); // Quell-Datei löschen nur wenn sie im bericht/work/ liegt (nicht bei Anhängen von Auftrag/Rechnung) if ($prow->source_path && strpos($prow->source_path, 'bericht/work/') === 0) { $full = bericht_resolve_data_path($prow->source_path); if ($full && file_exists($full)) @unlink($full); } // Multi-Image-Slot-Einträge mit löschen $db->query("DELETE FROM ".$db->prefix()."bericht_page_image WHERE fk_page = ".$page_id); $db->query("DELETE FROM ".$db->prefix()."bericht_page WHERE rowid = ".$page_id); api_ok(); } /* ---------- UPDATE page (note, rotation) ---------- */ if ($method === 'POST') { $in = api_input(); $sets = array(); if (isset($in['note'])) { $note = (string) $in['note']; $sets[] = "note = ".($note !== '' ? "'".$db->escape($note)."'" : "NULL"); } if (isset($in['rotation'])) { $rot = (int) $in['rotation']; $rot = (($rot % 360) + 360) % 360; $sets[] = "rotation = ".$rot; } if (empty($sets)) api_fail('Nichts zu aktualisieren'); $sql = "UPDATE ".$db->prefix()."bericht_page SET ".implode(',', $sets)." WHERE rowid = ".$page_id; if (!$db->query($sql)) api_fail($db->lasterror(), 500); api_ok(); } api_fail('Methode nicht unterstützt', 405);