false, 'message' => 'Daten für die Rechnungserstellung fehlen.']); exit; } function generateInvoiceNumber() : int { return random_int(10000000, 99999999); } $orderType = 'Startgebühr'; function createInvoice(mysqli $conn, $db_tabelle_verbuchte_startgebueren, $orderType, $userId, $jsonIds, $used_verein_konten, $order_status): int { $maxRetries = 25; for ($i = 0; $i < $maxRetries; $i++) { $invoiceNumber = generateInvoiceNumber(); $stmt = $conn->prepare( "INSERT INTO `$db_tabelle_verbuchte_startgebueren` (order_id, order_type, user_id, item_ids, used_verein_konten, order_status) VALUES (?, ?, ?, ?, ?, ?)" ); $stmt->bind_param( "isissi", $invoiceNumber, $orderType, $userId, $jsonIds, $used_verein_konten, $order_status ); if ($stmt->execute()) { return $invoiceNumber; } // Duplicate key error → retry if ($conn->errno !== 1062) { throw new RuntimeException( "Database error ({$conn->errno}): {$conn->error}" ); } } throw new RuntimeException('Failed to generate unique invoice number'); } $userId = $_SESSION['user_id_trainer']; $type = 'tr'; $data = include $baseDir . '/../scripts/db/db-verbindung-script.php'; if ($data['success'] === false){ echo json_encode(['success' => false, 'message' => $data['message']]); exit; } require $baseDir . '/../scripts/db/db-tables.php'; require $baseDir . '/../scripts/db/db-functions.php'; require $baseDir . '/../composer/vendor/autoload.php'; $sql = "SELECT bi.id AS basket_id, bi.item_id, p.programm AS programm_name, p.preis, bi.id, t.betrag_bezahlt FROM $db_tabelle_warenkorb_startgebueren bi LEFT JOIN $db_tabelle_teilnehmende t ON bi.item_id = t.id LEFT JOIN $db_tabelle_kategorien p ON p.programm = t.programm WHERE bi.user_id = ?"; $stmt = $mysqli->prepare($sql); $stmt->bind_param("i", $userId); $stmt->execute(); $result = $stmt->get_result(); $rows = $result->fetch_all(MYSQLI_ASSOC); if (!$rows || count($rows) < 1) { http_response_code(422); exit; } $used_konten_json = $_POST['usedKonten'] ?? '{}'; $used_konten = json_decode($used_konten_json, true) ?? []; $konten_save = []; if (count($used_konten) > 0) { $db_konten = db_select($mysqli, $db_tabelle_vereine, '`verein`, `konto`'); $indexed_konten = array_column($db_konten, 'konto', 'verein'); $db_user_permissions = db_get_var($mysqli, "SELECT `freigabe` FROM $db_tabelle_intern_benutzende WHERE `id` = ?", [$userId]); $array_permissions = json_decode($db_user_permissions ?? '', true) ?? []; $trainer_permissions = $array_permissions['freigabenTrainer'] ?? []; foreach ($used_konten as $verein => $betrag) { if (!isset($indexed_konten[$verein])) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'Invalider Verein ' . htmlspecialchars($verein)]); exit; } elseif (!isset($trainer_permissions[$verein]) && !in_array('admin', $trainer_permissions)) { http_response_code(403); echo json_encode(['success' => false, 'message' => 'Keine Berechtigung auf den Zugriff des Kontos des Vereins ' . htmlspecialchars($verein)]); exit; } elseif ((float) $indexed_konten[$verein] < (float) $betrag) { http_response_code(403); echo json_encode(['success' => false, 'message' => 'Der Abzug des Vereins ' . htmlspecialchars($verein) . ' übersteigt das Guthaben dieses Vereins.']); exit; } $konten_save[$verein] = (float) $betrag; } } $used_verein_konten = json_encode($konten_save); $preis = 0; $ids = []; foreach ($rows as $r) { $r_preis = max(floatval($r['preis']) - floatval($r['betrag_bezahlt']), 0); $preis += $r_preis; $ids[$r['item_id']] = $r_preis; } $preis = min($preis - array_sum($konten_save), 0); $jsonIds = json_encode($ids); $all_teilnehmende_ids = array_keys($ids); if (json_last_error() !== JSON_ERROR_NONE) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'Invalid JSON']); exit; } // --- Check for existing open order --- $order_id = db_get_var($mysqli, "SELECT order_id FROM `$db_tabelle_verbuchte_startgebueren` WHERE user_id = ? AND order_status = 0 LIMIT 1", [$userId]); $orderId = null; if ($order_id === null) { // --- INSERT new order --- $stmt->close(); $order_status = 0; $new_id = createInvoice($mysqli, $db_tabelle_verbuchte_startgebueren, $orderType, $userId, $jsonIds, $used_verein_konten, $order_status); $orderId = $new_id; } else { // --- UPDATE existing order --- $stmt->close(); $orderId = $order_id; $sql = " UPDATE `$db_tabelle_verbuchte_startgebueren` SET item_ids = ?, used_verein_konten = ? WHERE order_id = ? "; $stmt = $mysqli->prepare($sql); $stmt->bind_param("ssi", $jsonIds, $used_verein_konten, $order_id); $stmt->execute(); } $keys = [ 'wkName', 'rechnungenName', 'rechnungenVorname', 'rechnungenStrasse', 'rechnungenHausnummer', 'rechnungenPostleitzahl', 'rechnungenOrt', 'rechnungenIBAN', 'linkWebseite', 'rechnungenPostversand', 'rechnungenPostversandKosten' ]; $placeholders = implode(',', array_fill(0, count($keys), '?')); $stmt = $mysqli->prepare("SELECT `name`, `value` FROM $db_tabelle_var WHERE `name` IN ($placeholders)"); $stmt->bind_param(str_repeat('s', count($keys)), ...$keys); $stmt->execute(); $variables = array_column($stmt->get_result()->fetch_all(MYSQLI_ASSOC), 'value', 'name'); $kats = db_select($mysqli, $db_tabelle_kategorien, '`programm`, `preis`'); $indexed_kats_preis = array_column($kats, 'preis', 'programm'); $current_wk_id = (int) db_get_variable($mysqli, $db_tabelle_var, ['current_wk_id']) ?: 0; $current_wk_name = db_get_var($mysqli, "SELECT `name` FROM $db_tabelle_wettkaempfe WHERE `id` = ? LIMIT 1", [$current_wk_id]) ?: ($wkName . ' ' . $current_year); class MYPDF extends TCPDF { public $columns; public $headerBottomY = 0; public $firstPageDone = false; // track first page public $abzuegeHeader = false; // track first page public $printfooter; public function Header() { // Logo always $image_file = $_SERVER['DOCUMENT_ROOT'] . '/intern/img/logo-normal.png'; $this->Image($image_file, 180, 15, 15); // Table header only for subsequent pages if ($this->abzuegeHeader) { // Use same top margin as table $this->SetY(35); // or another fixed Y $this->SetX(15); $this->SetFont('GoogleSansFlex-Regular', '', 11); $this->setCellPaddings(2, 0, 2, 0); $this->Cell($this->columns['name']['max_width'] + $this->columns['programm']['max_width'] + $this->columns['verein']['max_width'], 10, 'Beschreibung', 0, 0, 'L'); $this->Cell($this->columns['preis']['max_width'], 10, 'Abzug', 0, 0, 'C'); $this->Ln(); $this->headerBottomY = $this->GetY(); $this->SetLineWidth(0.6); $this->Line(15, $this->GetY(), 210 - 15, $this->GetY()); $this->SetLineWidth(0.2); } elseif ($this->firstPageDone && !empty($this->columns)) { // Use same top margin as table $this->SetY(35); // or another fixed Y $this->SetX(15); $this->SetFont('GoogleSansFlex-Regular', '', 11); $this->setCellPaddings(2, 0, 2, 0); foreach ($this->columns as $c) { $this->Cell($c['max_width'], 10, $c['header'], 0, 0, 'L'); } $this->Ln(); $this->headerBottomY = $this->GetY(); $this->SetLineWidth(0.6); $this->Line(15, $this->GetY(), 210 - 15, $this->GetY()); $this->SetLineWidth(0.2); } } public function Footer() { } } // --- Create PDF --- $pdf = new MYPDF('P', 'mm', 'A4', true, 'UTF-8', false); $pdf->current_year = (date('n') > 6) ? date('Y') + 1 : date('Y'); $pdf->wkp_name = trim(($variables['rechnungenVorname'] ?? '') . ' ' . ($variables['rechnungenName'] ?? '')); $pdf->wkp_adresse = trim(($variables['rechnungenStrasse'] ?? '') . ' ' . ($variables['rechnungenHausnummer'] ?? '')); $pdf->wkp_plz_ort = trim(($variables['rechnungenPostleitzahl'] ?? '') . ' ' . ($variables['rechnungenOrt'] ?? '')); $pdf->wkp_url = $variables['linkWebseite'] ?? ''; $pdf->wk_name = $variables['wkName'] ?? ''; $pdf->pers_name = trim(($_POST['vorname'] ?? '') . ' ' . ($_POST['name'] ?? '')); $pdf->pers_adresse = trim(($_POST['strasse'] ?? '') . ' ' . ($_POST['hausnummer'] ?? '')); $pdf->pers_plz_ort = trim(($_POST['plz'] ?? '') . ' ' . ($_POST['ort'] ?? '')); $pdf->printfooter = true; $pdf->current_wk_name = $current_wk_name; // Mark first page done so Header() prints table headers on subsequent pages $pdf->firstPageDone = false; $pdf->setCellPaddings(2, 0, 2, 0); $pdf->SetMargins(15, 35, 15); // Fonts $pdf->AddFont('GoogleSansFlex9pt-Bold', '', $_SERVER['DOCUMENT_ROOT'] . '/../private-files/tcpdf-fonts/googlesansflex_9ptb.php'); $pdf->AddFont('GoogleSansFlex-Regular', '', $_SERVER['DOCUMENT_ROOT'] . '/../private-files/tcpdf-fonts/gsf.php'); $pdf->SetCreator(PDF_CREATOR); $pdf->SetAuthor('WKVS'); $pdf->SetAutoPageBreak(TRUE, 10); $pdf->SetFont('GoogleSansFlex-Regular', '', 11); // --- Add a page --- $pdf->AddPage(); // --- Sender block (left) --- $pdf->SetX(15); $pdf->Cell(0, 0, $pdf->wkp_name, 0, 1); $pdf->Cell(0, 0, $pdf->wkp_adresse, 0, 1); $pdf->Cell(0, 0, $pdf->wkp_plz_ort, 0, 1); $pdf->Ln(7); $pdf->Cell(0, 0, $pdf->wkp_url, 0, 1); // --- Recipient block (right / window) --- $x = 110; // X coordinate for right window $y = 50; // Y coordinate $w = 80; // Width of recipient block $address = implode("\n", [ $pdf->pers_name, $pdf->pers_adresse, $pdf->pers_plz_ort ]); $pdf->MultiCell($w, 5, $address, 0, 'L', false, 1, $x, $y, true); date_default_timezone_set('UTC'); // --- Invoice title --- $pdf->Ln(20); // space below recipient $pdf->SetFont('GoogleSansFlex9pt-Bold', '', 15); $pdf->Cell(0, 10, 'Startgebührenrechnung ' . $pdf->current_wk_name, 0, 1, 'L'); $pdf->SetFont('GoogleSansFlex-Regular', '', 10); $pdf->Cell(0, 9, "Rechnungsnummer: " . $orderId, 0, 1, 'L'); $pdf->SetFont('GoogleSansFlex-Regular', '', 7); $pdf->Cell(0, 0, "Ausstellungsdatum: " . date("d.m.y"), 0, 1, 'L'); $pdf->Ln(10); // space below title $pdf->SetFont('GoogleSansFlex9pt-Bold', '', 13); //$turnerinnnenIds = []; $placeholders = implode(', ', array_fill(0, count($all_teilnehmende_ids), '?')); $all_teilnehmende = db_select($mysqli, $db_tabelle_teilnehmende, '`id`, name, vorname, programm, verein, `betrag_bezahlt`', "id IN ($placeholders)", $all_teilnehmende_ids); $all_teilnehmende_indexed = array_column($all_teilnehmende, null, 'id'); $columns = ['name' => ['header' => 'Name'], 'programm' => ['header' => 'Programm'], 'verein' => ['header' => 'Verein'], 'preis' => ['header' => 'Startgebühr']]; foreach ($columns as $key => $column){ $columns[$key]['max_width'] = $pdf->GetStringWidth($column['header']); } $totalPreis = 0.00; $dbdata = []; foreach ($ids as $singleid => $s_preis){ if (!isset($all_teilnehmende_indexed[$singleid])) continue; $row = &$all_teilnehmende_indexed[$singleid]; if (count($row) !== 6) continue; $float_preis = (float) $s_preis; $formated_preis = number_format($float_preis, 2, '.', "'"); $totalPreis += $float_preis; $row['formated_preis'] = $formated_preis; $programm = $row['programm'] ?? ''; $kosten_kat_normal = (float) ($indexed_kats_preis[$programm] ?? 0.00); $preis_text = $float_preis !== $kosten_kat_normal ? 'Die Startgebühren dieser Person wurden reduziert, da diese Person bereits CHF ' . number_format($row['betrag_bezahlt'], 2, '.', "'") . ' an Startgebühren zahlte' : ''; $row['preis_text'] = $preis_text; $pdf->SetFont('', '', 10); $text = 'Startgebühr '. ($row['name'] ?? '') .' '. ($row['vorname'] ?? ''); $text_width = $pdf->GetStringWidth($text); $kat_width = $pdf->GetStringWidth($row['programm'] ?? ''); $verein_width = $pdf->GetStringWidth($row['verein'] ?? ''); $preis_width = $pdf->GetStringWidth('CHF '. $formated_preis); $columns['name']['max_width'] = max($columns['name']['max_width'] ?? 0, $text_width); $columns['programm']['max_width'] = max($columns['programm']['max_width'] ?? 0, $kat_width); $columns['verein']['max_width'] = max($columns['verein']['max_width'] ?? 0, $verein_width); $columns['preis']['max_width'] = max($columns['preis']['max_width'] ?? 0, $preis_width); } unset($row); foreach ($columns as $key => $column){ $columns[$key]['max_width'] += 2; // Add some padding } $maxWidth = 210 - 30; // A4 width minus margins $totalColumnWidth = 0; foreach ($columns as $column){ $totalColumnWidth += $column['max_width']; } if ($totalColumnWidth < $maxWidth){ $scalingFactor = $maxWidth / $totalColumnWidth; foreach ($columns as $key => $column){ $columns[$key]['max_width'] = $column['max_width'] * $scalingFactor; } } foreach ($columns as $c) { $pdf->Cell($c['max_width'], 10, $c['header'], 0, 0, 'L'); } $pdf->Ln(); $pdf->SetLineWidth(0.6); $pdf->Line(15, $pdf->GetY(), 210 - 15, $pdf->GetY()); $pdf->SetLineWidth(0.2); $pdf->headerBottomY = $pdf->GetY(); // --- Set top margin for table below title --- // Mark first page done so Header() prints table headers on subsequent pages $pdf->firstPageDone = true; $pdf->SetFont('GoogleSansFlex-Regular', '', 11); $pdf->columns = $columns; $margin_top = $pdf->headerBottomY; $pdf->SetMargins(15, 45, 15); // +5 mm padding below header $pdf->SetY($margin_top); // Move cursor below header manually foreach ($all_teilnehmende_indexed as $s_id => $row){ $pdf->SetFont('', '', 10); $text = 'Startgebühr '. ($row['name'] ?? '') .' '. ($row['vorname'] ?? ''); $pdf->Cell($columns['name']['max_width'], 10, $text, 0, 0, 'L'); $pdf->Cell($columns['programm']['max_width'], 10, $row['programm'] ?? '', 0, 0, 'L'); $pdf->Cell($columns['verein']['max_width'], 10, $row['verein'] ?? '', 0, 0, 'L'); $pdf->SetFillColor(100, 100, 100); $formated_preis = $row['formated_preis'] ?? ''; $pdf->Cell($columns['preis']['max_width'], 10, 'CHF ' . $formated_preis, 0, 1, 'C'); if ($row['preis_text'] !== '') { $pdf->SetY($pdf->getY() - 3); $pdf->SetFont('', '', 6); $pdf->Cell(195, 6, $row['preis_text'], 0, 1, 'L'); $pdf->SetFont('', '', 10); } $pdf->SetDrawColor(100, 100, 100); $pdf->Line(15, $pdf->getY(), 210 - 15, $pdf->getY()); $pdf->SetDrawColor(0, 0, 0); } if ($variables['rechnungenPostversand'] && isset($_POST['postversand'])) { $pdf->SetFont('', '', 10); $text = 'Postversand der Rechnung'; $pdf->Cell($columns['name']['max_width'], 10, $text, 0, 0, 'L'); $pdf->Cell($columns['programm']['max_width'], 10, '', 0, 0, 'L'); $pdf->Cell($columns['verein']['max_width'], 10, '', 0, 0, 'L'); $pdf->SetFillColor(100, 100, 100); $pdf->Cell($columns['preis']['max_width'], 10, 'CHF '. number_format(floatval($variables['rechnungenPostversandKosten'] ?? 0), 2, ".", "'"), 0, 1, 'C'); $pdf->SetDrawColor(100, 100, 100); $pdf->Line(15, $pdf->getY(), 210 - 15, $pdf->getY()); $pdf->SetDrawColor(0, 0, 0); $totalPreis += floatval($variables['rechnungenPostversandKosten'] ?? 0); } if (count($konten_save) > 0) { $pdf->Ln(8); $pdf->SetFont('GoogleSansFlex9pt-Bold', '', 10); $pdf->Cell(0, 10, 'Abzüge (Vereinsguthaben)', 0, 1, 'L'); $pdf->SetFont('GoogleSansFlex-Regular', '', 10); $pdf->SetFont('GoogleSansFlex9pt-Bold', '', 11); $pdf->Cell($columns['name']['max_width'] + $columns['programm']['max_width'] + $columns['verein']['max_width'], 10, 'Beschreibung', 0, 0, 'L'); $pdf->Cell($columns['preis']['max_width'], 10, 'Abzug', 0, 0, 'C'); $pdf->Ln(); $pdf->SetLineWidth(0.6); $pdf->Line(15, $pdf->GetY(), 210 - 15, $pdf->GetY()); $pdf->SetLineWidth(0.2); $pdf->abzuegeHeader = true; foreach ($konten_save as $verein => $betrag) { $pdf->SetFont('', '', 10); $text = 'Guthaben '. $verein; $pdf->Cell($columns['name']['max_width'] + $columns['programm']['max_width'] + $columns['verein']['max_width'], 10, $text, 0, 0, 'L'); $pdf->SetFillColor(100, 100, 100); $formated_preis = number_format(floatval($betrag ?? 0), 2, ".", "'"); $pdf->Cell($columns['preis']['max_width'], 10, '- CHF ' . $formated_preis, 0, 1, 'C'); $pdf->SetDrawColor(100, 100, 100); $pdf->Line(15, $pdf->getY(), 210 - 15, $pdf->getY()); $pdf->SetDrawColor(0, 0, 0); $totalPreis -= (float) $betrag; } $pdf->abzuegeHeader = false; } $pdf->Ln(3); $pdf->SetFont('GoogleSansFlex9pt-Bold', '', 10); $pdf->Cell(100, 10, "Gesamt:", 0, 0, 'L'); $pdf->SetX($columns['name']['max_width'] + $columns['programm']['max_width'] + $columns['verein']['max_width'] + 15); $pdf->Cell($columns['preis']['max_width'], 10, 'CHF ' . number_format($totalPreis, 2, ".", "'"), 0, 1, 'C'); $pdf->SetFont('GoogleSansFlex-Regular', '', 10); $cent_total_preis = (int) round($totalPreis * 100); $rechnung_is_null = $cent_total_preis === 0; if ($rechnung_is_null) { $pdf->Ln(10); $pdf->SetTextColor(90, 103, 39); $pdf->MultiCell(0, 8, 'Diese Rechnung wurde als bezahlt eingetragen, da der Betrag CHF 0.00 beträgt', 0, 'L'); $pdf->SetTextColor(0, 0, 0); } else { $qrBill = QrBill\QrBill::create(); $qrBill->setCreditor( QrBill\DataGroup\Element\StructuredAddress::createWithStreet( $pdf->wkp_name, trim($variables['rechnungenStrasse'] ?? ''), trim($variables['rechnungenHausnummer'] ?? ''), trim($variables['rechnungenPostleitzahl'] ?? ''), trim($variables['rechnungenOrt'] ?? ''), 'CH' ) ); $iban = strtoupper(str_replace(' ', '', (string)($variables['rechnungenIBAN'] ?? ''))); $qrBill->setCreditorInformation( QrBill\DataGroup\Element\CreditorInformation::create($iban) ); // Add debtor information // Who has to pay the invoice? This part is optional. $qrBill->setUltimateDebtor( QrBill\DataGroup\Element\StructuredAddress::createWithStreet( $_POST['vorname'] . ' ' . $_POST['name'], $_POST['strasse'], $_POST['hausnummer'], $_POST['plz'], $_POST['ort'], 'CH' ) ); // Add payment amount information // What amount is to be paid? $qrBill->setPaymentAmountInformation( QrBill\DataGroup\Element\PaymentAmountInformation::create( 'CHF', $totalPreis ) ); // Add payment reference // This is what you will need to identify incoming payments. $qrBill->setPaymentReference( QrBill\DataGroup\Element\PaymentReference::create( QrBill\DataGroup\Element\PaymentReference::TYPE_SCOR, QrBill\Reference\RfCreditorReferenceGenerator::generate($orderId) ) ); $referenz = "Startgebührenrechnung ". $pdf->wkp_name; // Optionally, add some human-readable information about what the bill is for. $qrBill->setAdditionalInformation( QrBill\DataGroup\Element\AdditionalInformation::create( $referenz ) ); // 3. Create a full payment part for TcPDF $output = new TcPdfOutput($qrBill, 'de', $pdf); // 4. Optional, set layout options if (class_exists(DisplayOptions::class)) { $displayOptions = new DisplayOptions(); $displayOptions ->setPrintable(false) // true to remove lines for printing on a perforated stationery ->setDisplayTextDownArrows(false) // true to show arrows next to separation text, if shown ->setDisplayScissors(false) // true to show scissors instead of separation text ->setPositionScissorsAtBottom(false) // true to place scissors at the bottom, if shown ; // 5. Generate the output, applying display options when supported if (method_exists($output, 'setDisplayOptions')) { $output->setDisplayOptions($displayOptions); } } if ($pdf->getY() > 297 - 120) { $pdf->firstPageDone = false; $pdf->printfooter = false; $pdf->addPage(); } $output->getPaymentPart(); } $filename = 'Rechnung Startgebuehren '.$current_wk_name.' '.date('YmdHis').'.pdf'; $pdf->SetTitle($filename); $savePath = $baseDir . '/../private-files/rechnungen/' . $orderId . '.pdf'; // Save PDF to disk $pdf->Output($savePath, 'F'); $orderStatus = $rechnung_is_null ? 2 : 1 ; $sql = "UPDATE $db_tabelle_verbuchte_startgebueren SET order_status = ?, `preis` = ? WHERE order_id = ?"; $stmt = $mysqli->prepare($sql); $stmt->bind_param("idi", $orderStatus, $totalPreis, $orderId); $stmt->execute(); $stmt->close(); require $baseDir . '/../scripts/rechnungen/rechnungs-functions.php'; update_teilnehmende($ids, 0, $orderStatus, true); update_konten_vereine($konten_save, 0, $orderStatus, true); // 2. DELETE basket items db_delete($mysqli, $db_tabelle_warenkorb_startgebueren, ['user_id' => intval($userId)]); $mysqli->close(); // Send headers manually header('Content-Type: application/pdf'); header('Content-Disposition: attachment; filename="' . $dirFileName . '"'); header('Content-Length: ' . filesize($savePath)); // Send file contents readfile($savePath); exit;