OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

bundles_pdf.php

8 KB
<?php
/**
 * Profilübersicht als PDF (Dompdf, Landscape). Optik wie der Vertrags-Export:
 * je Profil eine Gruppe mit Positionen und einer Zwei-Preis-Zwischensumme
 * (einmalig / laufend pro Jahr), am Ende die Gesamtsumme über alle Profile.
 * Druck-/PDF-Kontext → Hex-Farben laut STYLEGUIDE.md zulässig.
 */
$disp  = ($display ?? 'net') === 'gross' ? 'gross' : 'net';
$other = $disp === 'gross' ? 'net' : 'gross';
$money = fn($v) => $v === null ? '–' : $this->moneyHtml($v);
$billingLabels = $billing ?? ['once' => 'einmalig', 'monthly' => 'pro Monat', 'yearly' => 'pro Jahr'];
$bundles = $bundles ?? [];
$appName = $this->e($app_name ?? 'OpenNIT');
$today   = date('d.m.Y H:i');
$priceLabel = $disp === 'gross' ? 'Brutto' : 'Netto';
$palette = ['#4f46e5', '#059669', '#d97706', '#dc2626', '#2563eb', '#7c3aed', '#0891b2', '#db2777'];
$rec = fn($t) => \OpenNIT\Catalog\CatalogManager::recurringYearly($t);

$grandOnce = 0.0; $grandRec = 0.0;
foreach ($bundles as $b) { $grandOnce += (float)$b['totals']['once'][$disp]; $r = $rec($b['totals']); $grandRec += (float)$r[$disp]; }
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title><?= $this->te('Profilübersicht') ?></title>
<style>
@page { margin: 1.5cm 1.5cm 2cm; }
body { font-family: DejaVu Sans, Arial, sans-serif; font-size: 9pt; color: #1f2937; }
h1 { font-size: 16pt; margin: 0 0 2px; color: #1a1a2e; }
.meta { font-size: 8pt; color: #6b7280; margin-bottom: 12px; }
.price-mode { display: inline-block; font-size: 7.5pt; font-weight: bold; background: #dbeafe; color: #1e40af; border: 1px solid #bfdbfe; border-radius: 3px; padding: 1px 5px; letter-spacing: 0.04em; vertical-align: middle; margin-left: 6px; }
.summary-box { display: table; width: 100%; border-collapse: collapse; margin-bottom: 14px; }
.summary-cell { display: table-cell; background: #f0f7ff; border: 1px solid #bfd9f2; padding: 8px 14px; width: 33%; text-align: center; }
.summary-cell .label { font-size: 7.5pt; color: #6b7280; text-transform: uppercase; letter-spacing: 0.04em; }
.summary-cell .value { font-size: 14pt; font-weight: bold; color: #1a4a7a; }
table.main { width: 100%; border-collapse: collapse; }
table.main th { background: #1a4a7a; color: #fff; font-size: 8pt; padding: 5px 6px; text-align: left; }
table.main th.num { text-align: right; }
table.main td { font-size: 8pt; padding: 4px 6px; border-bottom: 1px solid #e5e7eb; vertical-align: top; }
td.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
tr.cat-header td { background: #f3f4f6; font-weight: bold; font-size: 8.5pt; padding: 6px 6px; border-top: 2px solid #d1d5db; }
tr.subtotal td { background: #eff6ff; font-size: 8pt; border-top: 1px solid #bfd9f2; padding: 4px 6px; }
tr.total-row td { background: #1a4a7a; color: #fff; font-weight: bold; font-size: 9pt; padding: 6px 6px; }
.cat-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 5px; vertical-align: middle; }
.cat-count { font-size: 7.5pt; color: #6b7280; font-weight: normal; margin-left: 6px; }
.pill-req { display: inline-block; font-size: 6.8pt; font-weight: bold; background: #eef2ff; color: #4338ca; border: 1px solid #c7d2fe; border-radius: 3px; padding: 0 4px; margin-left: 4px; vertical-align: middle; }
.pill-off { display: inline-block; font-size: 6.8pt; font-weight: bold; background: #f3f4f6; color: #6b7280; border: 1px solid #d1d5db; border-radius: 3px; padding: 0 4px; margin-left: 4px; vertical-align: middle; }
.muted { color: #9ca3af; }
.desc { color: #6b7280; font-size: 7.5pt; }
.empty { color: #6b7280; padding: 10px 0; }
.footer { border-top: 1px solid #e5e7eb; font-size: 7.5pt; color: #9ca3af; padding-top: 4px; margin-top: 14px; }
</style>
</head>
<body>
<h1><?= $this->te('Profilübersicht') ?> <span class="price-mode"><?= $priceLabel ?></span></h1>
<div class="meta"><?= $this->t('Exportiert am') ?> <?= $today ?> &bull; <?= $appName ?> &bull; <?= $this->te(count($bundles) === 1 ? '{anzahl} Profil' : '{anzahl} Profile', ['anzahl' => count($bundles)]) ?> &bull; „Laufend" = wiederkehrende Kosten pro Jahr (monatliche ×12)</div>

<table class="summary-box">
  <tr>
    <td class="summary-cell">
      <div class="label"><?= $this->te('Profile') ?></div>
      <div class="value"><?= count($bundles) ?></div>
    </td>
    <td class="summary-cell" style="border-left:none">
      <div class="label"><?= $this->t('Einmalig gesamt') ?> (<?= $priceLabel ?>)</div>
      <div class="value"><?= $money($grandOnce) ?></div>
    </td>
    <td class="summary-cell" style="border-left:none">
      <div class="label"><?= $this->t('Laufend gesamt / Jahr') ?> (<?= $priceLabel ?>)</div>
      <div class="value"><?= $money($grandRec) ?></div>
    </td>
  </tr>
</table>

<?php if (empty($bundles)): ?>
<div class="empty"><?= $this->te('Noch keine Profile angelegt.') ?></div>
<?php else: ?>
<table class="main">
  <thead>
    <tr>
      <th scope="col" style="width:8%"><?= $this->te('Menge') ?></th>
      <th scope="col" style="width:48%"><?= $this->te('Artikel') ?></th>
      <th scope="col" style="width:16%"><?= $this->te('Abrechnung') ?></th>
      <th scope="col" class="num" style="width:14%"><?= $this->t('Einzel') ?> (<?= $priceLabel ?>)</th>
      <th scope="col" class="num" style="width:14%"><?= $this->t('Summe') ?> (<?= $priceLabel ?>)</th>
    </tr>
  </thead>
  <tbody>
  <?php $ci = 0; foreach ($bundles as $b): $bd = $b['bundle']; $t = $b['totals']; $r = $rec($t); $dot = $palette[$ci % count($palette)]; $ci++;
        $posCount = count($b['items']) + count($b['requirements']); ?>
    <tr class="cat-header">
      <td colspan="5">
        <span class="cat-dot" style="background:<?= $dot ?>"></span><?= $this->e($bd['name']) ?>
        <?php if ((int)($bd['is_active'] ?? 1) !== 1): ?><span class="pill-off"><?= $this->te('inaktiv') ?></span><?php endif; ?>
        <span class="cat-count"><?= $this->te($posCount === 1 ? '{anzahl} Position' : '{anzahl} Positionen', ['anzahl' => $posCount]) ?></span>
        <?php if (!empty($bd['description'])): ?><span class="desc"> &bull; <?= $this->e($bd['description']) ?></span><?php endif; ?>
      </td>
    </tr>
    <?php foreach ($b['items'] as $bi): $pv = $bi['price_' . $disp]; $q = (int)$bi['quantity']; ?>
    <tr>
      <td><?= $q ?>×</td>
      <td><?= $this->e($bi['name']) ?></td>
      <td><?= $this->e($billingLabels[$bi['billing_cycle'] ?? 'once'] ?? 'einmalig') ?></td>
      <td class="num"><?= $money($pv) ?></td>
      <td class="num"><?= $pv === null ? '–' : $money((float)$pv * $q) ?></td>
    </tr>
    <?php endforeach; ?>
    <?php foreach ($b['requirements'] as $rq): $pv = $rq['price_' . $disp]; $q = (int)$rq['quantity']; ?>
    <tr>
      <td><?= $q ?>×</td>
      <td><?= $this->e($rq['name']) ?><span class="pill-req"><?= $this->te('Pflicht') ?></span></td>
      <td><?= $this->e($billingLabels[$rq['billing_cycle'] ?? 'once'] ?? 'einmalig') ?></td>
      <td class="num"><?= $money($pv) ?></td>
      <td class="num"><?= $pv === null ? '–' : $money((float)$pv * $q) ?></td>
    </tr>
    <?php endforeach; ?>
    <tr class="subtotal">
      <td colspan="3"><strong><?= $this->te('Zwischensumme {name}', ['name' => $bd['name']]) ?></strong></td>
      <td class="num"><strong><?= $money($t['once'][$disp]) ?></strong><br><span class="muted"><?= $this->te('einmalig') ?></span></td>
      <td class="num"><strong><?= $money($r[$disp]) ?></strong><br><span class="muted"><?= $this->te('laufend/Jahr') ?></span></td>
    </tr>
  <?php endforeach; ?>
    <tr class="total-row">
      <td colspan="3"><?= $this->t('Gesamt') ?> <?= $this->te('({anzahl} Profile)', ['anzahl' => count($bundles)]) ?></td>
      <td class="num"><?= $money($grandOnce) ?><br><?= $this->te('einmalig') ?></td>
      <td class="num"><?= $money($grandRec) ?><br><?= $this->te('laufend/Jahr') ?></td>
    </tr>
  </tbody>
</table>
<?php endif; ?>

<div class="footer"><?= $appName ?> &bull; <?= $this->te('Unverbindliche Richtpreise, Änderungen vorbehalten') ?> &bull; <?= $today ?></div>
</body>
</html>