452 lines
10 KiB
PHP
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);
|
|
}
|