Files
anjuma-dashboard/api/invoice_pdf.php
T
2026-07-22 01:12:45 +02:00

452 lines
10 KiB
PHP

<?php
ini_set('display_errors', 0);
error_reporting(E_ALL);
header('Content-Type: application/json; charset=utf-8');
set_exception_handler(function(Throwable $e){
http_response_code(500);
echo json_encode([
'ok' => false,
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
], JSON_UNESCAPED_UNICODE);
exit;
});
set_error_handler(function($severity, $message, $file, $line){
throw new ErrorException($message, 0, $severity, $file, $line);
});
require_once "../config/config.php";
global $pdo;
$config = require __DIR__ . '/../config/invoice.php';
require_once __DIR__ . '/../vendor/autoload.php';
use Dompdf\Dompdf;
use Dompdf\Options;
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
function eur($n){ return number_format((int)$n, 0, ',', '.') . " €"; }
function imgToDataUri(string $path): string {
if (!file_exists($path)) {
throw new RuntimeException("Logo nicht gefunden: $path");
}
if (!is_readable($path)) {
throw new RuntimeException("Logo nicht lesbar (Rechte): $path");
}
$data = file_get_contents($path);
if ($data === false) {
throw new RuntimeException("Logo konnte nicht gelesen werden: $path");
}
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
$mime = match($ext) {
'png' => 'image/png',
'jpg', 'jpeg' => 'image/jpeg',
'svg' => 'image/svg+xml',
default => throw new RuntimeException("Logo-Format nicht unterstützt: .$ext (bitte png/jpg/svg)")
};
return "data:$mime;base64," . base64_encode($data);
}
try {
$id = (int)($_GET['id'] ?? 0);
if ($id <= 0) throw new RuntimeException('id fehlt');
// Load invoice + items
$st = $pdo->prepare("SELECT * FROM invoices WHERE id=:id");
$st->execute([':id'=>$id]);
$inv = $st->fetch(PDO::FETCH_ASSOC);
if (!$inv) throw new RuntimeException('Rechnung nicht gefunden');
$it = $pdo->prepare("SELECT * FROM invoice_items WHERE invoice_id=:id ORDER BY pos ASC, id ASC");
$it->execute([':id'=>$id]);
$items = $it->fetchAll(PDO::FETCH_ASSOC);
// Ensure pdf dir
$pdfDir = $config['pdf_dir'];
if (!is_dir($pdfDir)) {
if (!mkdir($pdfDir, 0775, true)) throw new RuntimeException('pdf_dir kann nicht erstellt werden');
}
$issuer = $config['issuer'];
$pay = $config['payment'];
$branding = $config['branding'] ?? [];
$accent = $branding['accent'] ?? '#111';
$logoData = imgToDataUri($branding['logo_path'] ?? null);
$logoHtml = '';
if ($logoData) {
$logoHtml = '<img src="'.h($logoData).'" style="height:52px;" alt="ANJUMA Logo">';
}
$html = '
<html>
<head>
<meta charset="utf-8">
<style>
@page { margin: 26px 30px; }
body {
font-family: DejaVu Sans, Arial, sans-serif;
font-size: 11px;
color: #111;
}
/* ===== HEADER ===== */
.header {
width:100%;
}
.header-left {
float:left;
width:60%;
}
.header-right {
float:right;
width:38%;
text-align:right;
}
.clear { clear:both; }
.header-name {
font-size:14px;
font-weight:700;
}
.header-address {
font-size:10px;
color:#666;
}
.line {
height:4px;
background: '.$accent.';
margin: 12px 0 18px 0;
}
.title {
font-size:28px;
font-weight:800;
margin-bottom:10px;
}
/* ===== META ===== */
.meta {
width:100%;
margin-bottom:14px;
}
.meta td {
padding:2px 0;
}
.meta .k {
width:140px;
color:#666;
}
.meta .v {
font-weight:600;
}
/* ===== SECTIONS ===== */
.section {
margin-top:18px;
}
.section-title {
font-size:10px;
letter-spacing:.06em;
text-transform:uppercase;
color:#666;
margin-bottom:6px;
}
.section-content {
border-top:2px solid #111;
padding-top:8px;
}
/* ===== ADDRESS ===== */
.addr-left {
float:left;
width:48%;
}
.addr-right {
float:right;
width:48%;
}
/* ===== ITEMS TABLE ===== */
table.items {
width:100%;
border-collapse:collapse;
margin-top:20px;
}
table.items th {
background:#f3f3f3;
padding:8px;
font-size:10px;
text-transform:uppercase;
border-top:1px solid #ccc;
border-bottom:1px solid #ccc;
}
table.items td {
padding:10px 8px;
border-bottom:1px solid #eee;
}
.num {
text-align:right;
white-space:nowrap;
}
/* ===== SUMMARY ===== */
.summary {
margin-top:16px;
}
.summary-note {
float:left;
width:58%;
font-size:10px;
color:#555;
line-height:1.4;
}
.summary-bar {
float:right;
width:40%;
background:#f3f3f3;
border-top:3px solid #111;
padding:10px 12px;
}
.summary-bar table {
width:100%;
}
.summary-bar td {
padding:4px 0;
}
.summary-bar .grand {
font-size:18px;
font-weight:800;
}
/* ===== PAYMENT ===== */
.payment {
margin-top:20px;
border-top:2px solid #111;
padding-top:8px;
}
.payment td {
padding:3px 0;
}
.muted { color:#666; }
/* ===== FOOTER ===== */
.footer {
position: fixed;
left:30px;
right:30px;
bottom:18px;
font-size:9px;
color:#666;
border-top:1px solid #eee;
padding-top:6px;
}
</style>
</head>
<body>
<!-- HEADER -->
<div class="header">
<div class="header-left">
<div class="header-name">'.h($issuer['name']).'</div>
<div class="header-address">
'.h($issuer['street']).' • '.h($issuer['zip']).' '.h($issuer['city']).'
</div>
</div>
<div class="header-right">
'.$logoHtml.'
</div>
<div class="clear"></div>
</div>
<div class="line"></div>
<div class="title">Rechnung</div>
<table class="meta">
<tr><td class="k">Rechnungsnummer</td><td class="v">'.h($inv['invoice_no']).'</td></tr>
<tr><td class="k">Rechnungsdatum</td><td class="v">'.h(date('d.m.Y', strtotime($inv['invoice_date']))).'</td></tr>
<tr><td class="k">Leistungsdatum</td><td class="v">'.h(date('d.m.Y', strtotime($inv['service_date']))).'</td></tr>
<tr><td class="k">Fällig am</td><td class="v">'.h(date('d.m.Y', strtotime($inv['due_date']))).'</td></tr>
</table>
<!-- ADDRESSES -->
<div class="section">
<div class="addr-left">
<div class="section-title">Rechnung an</div>
<div class="section-content">
<b>'.h($inv['customer_name']).'</b><br>
'.($inv['customer_street'] ? h($inv['customer_street']).'<br>' : '').'
'.h($inv['customer_zip']).' '.h($inv['customer_city']).'<br>
'.h($inv['customer_country']).'
</div>
</div>
<div class="addr-right">
<div class="section-title">Aussteller</div>
<div class="section-content">
<b>'.h($issuer['name']).'</b><br>
'.h($issuer['street']).'<br>
'.h($issuer['zip']).' '.h($issuer['city']).'<br>
'.h($issuer['country']).'<br><br>
<span class="muted">Steuernr.:</span> '.h($issuer['tax_no']).'<br>
<span class="muted">E-Mail:</span> '.h($issuer['email']).'<br>
<span class="muted">Tel.:</span> '.h($issuer['phone']).'
</div>
</div>
<div class="clear"></div>
</div>
<!-- ITEMS -->
<table class="items">
<thead>
<tr>
<th style="width:40px;">Pos</th>
<th>Beschreibung</th>
<th class="num" style="width:70px;">Menge</th>
<th class="num" style="width:110px;">Einzelpreis</th>
<th class="num" style="width:110px;">Gesamt</th>
</tr>
</thead>
<tbody>';
// --- ITEMS LOOP (wie gehabt) ---
$pos = 1;
$sum = 0;
foreach ($items as $row) {
$line = (int)$row['line_total_eur'];
$sum += $line;
$html .= '
<tr>
<td>'.h($row['pos'] ?? $pos).'</td>
<td>'.h($row['description']).'</td>
<td class="num">'.h($row['qty']).'</td>
<td class="num">'.h(eur($row['unit_price_eur'])).'</td>
<td class="num"><b>'.h(eur($line)).'</b></td>
</tr>';
$pos++;
}
// --- CLOSE TABLE + SUMMARY + PAYMENT + FOOTER ---
$html .= '
</tbody>
</table>
<!-- SUMMARY -->
<div class="summary">
<div class="summary-note">
'.h($config['kleinunternehmer_text']).'
'.(!empty($inv['notes']) ? '<br><br>'.nl2br(h($inv['notes'])) : '').'
</div>
<div class="summary-bar">
<table>
<tr>
<td class="muted">Zwischensumme</td>
<td class="num">'.h(eur($sum)).'</td>
</tr>
<tr>
<td class="muted">Umsatzsteuer</td>
<td class="num">0 €</td>
</tr>
<tr>
<td style="padding-top:8px;"><b>Gesamtbetrag</b></td>
<td class="num grand">'.h(eur($sum)).'</td>
</tr>
</table>
</div>
<div class="clear"></div>
</div>
<!-- PAYMENT -->
<div class="payment">
<div class="section-title">Zahlungsinformationen</div>
<div class="section-content" style="border-top:0; padding-top:0;">
<div style="margin-bottom:8px;">
Bitte überweisen Sie den Betrag innerhalb von '.h($pay['pay_within_days']).' Tagen.
</div>
<table>
<tr>
<td style="width:70px;" class="muted">IBAN</td>
<td><b>'.h($pay['iban']).'</b></td>
</tr>
<tr>
<td class="muted">BIC</td>
<td><b>'.h($pay['bic']).'</b></td>
</tr>
<tr>
<td class="muted">Bank</td>
<td><b>'.h($pay['bank']).'</b></td>
</tr>
</table>
</div>
</div>
<!-- FOOTER -->
<div class="footer">
'.h($issuer['name']).' • '.h($issuer['street']).', '.h($issuer['zip']).' '.h($issuer['city']).' • Steuernr.: '.h($issuer['tax_no']).'
</div>
</body>
</html>';
// Dompdf options
$options = new Options();
$options->set('isRemoteEnabled', false);
$options->set('defaultFont', 'DejaVu Sans');
$dompdf = new Dompdf($options);
$dompdf->loadHtml($html, 'UTF-8');
$dompdf->setPaper('A4', 'portrait');
$dompdf->render();
$pdfOutput = $dompdf->output();
// Save file
$filename = $inv['invoice_no'] . '.pdf';
$fullPath = rtrim($pdfDir, '/').'/'.$filename;
$relativePath = 'storage/invoices/' . $filename;
if (file_put_contents($fullPath, $pdfOutput) === false) {
throw new RuntimeException('PDF konnte nicht gespeichert werden: ' . $fullPath);
}
// Store pdf_path in DB (relative is nicer)
$pdo->prepare("UPDATE invoices SET pdf_path=:p WHERE id=:id")
->execute([':p'=>$relativePath, ':id'=>$id]);
// Output download
header('Content-Type: application/pdf');
header('Content-Disposition: inline; filename="'.basename($filename).'"');
echo $pdfOutput;
} catch (Throwable $e) {
http_response_code(400);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['ok'=>false,'error'=>$e->getMessage()], JSON_UNESCAPED_UNICODE);
}