Quelltext des aktuellen Standes
<?php
$this->layout('layouts/main', [
'page_title' => $page_title,
'current_route' => $current_route,
'full_width' => true,
'extra_css' => [
'/assets/css/catalog.css?v=' . filemtime(dirname(__DIR__, 2) . '/public/assets/css/catalog.css'),
'/assets/css/company-search.css',
],
'extra_js' => ['/assets/js/company-search.js'],
]);
// Der Vorschlag, eine unbekannte Firma anzulegen, setzt das Adressbuch und
// das Recht voraus, es zu pflegen.
$kannFirmaAnlegen = !empty($supplier_options) && ($modules['contacts'] ?? false)
&& $this->hasCapability('manage_contacts');
$disp = ($display ?? 'net') === 'gross' ? 'gross' : 'net';
$money = fn($v) => $v === null ? '–' : $this->money($v);
$billingLabels = $billing ?? ['once' => 'einmalig', 'monthly' => 'pro Monat', 'yearly' => 'pro Jahr'];
// Kurzes Preis-Suffix (wie in der Übersicht): unterscheidet einmalige von
// wiederkehrenden Positionen (z. B. Monats-/Jahres-Tarifen).
$cycleSuffix = ['once' => '', 'monthly' => '/Monat', 'yearly' => '/Jahr'];
$history = $history ?? [];
// ── Preisverlauf-Diagramm (Linie mit Punkten je Lieferant) ─────────────
$field = $disp === 'gross' ? 'price_gross' : 'price_net';
$series = [];
foreach ($history as $h) {
$key = (string)($h['supplier_name'] ?? 'Ohne Lieferant');
$series[$key][] = ['t' => strtotime((string)$h['valid_from']), 'v' => (float)$h[$field], 'd' => (string)$h['valid_from']];
}
$allPts = array_merge(...array_values($series ?: [[]]));
$chart = null;
if (count($allPts) >= 2) {
$ts = array_column($allPts, 't');
$vs = array_column($allPts, 'v');
$tMin = min($ts); $tMax = max($ts); $vMin = min($vs); $vMax = max($vs);
if ($tMax === $tMin) { $tMax = $tMin + 86400; }
$pad = max(1.0, ($vMax - $vMin) * 0.12);
$vLo = max(0.0, $vMin - $pad); $vHi = $vMax + $pad;
if ($vHi === $vLo) { $vHi = $vLo + 1; }
$W = 720; $H = 240; $L = 60; $R = 16; $T = 16; $B = 34;
$px = fn($t) => $L + ($t - $tMin) / ($tMax - $tMin) * ($W - $L - $R);
$py = fn($v) => $T + (1 - ($v - $vLo) / ($vHi - $vLo)) * ($H - $T - $B);
// Farben kommen aus Design-Tokens über die CSS-Klassen cat-s0…cat-s5.
$chart = compact('series', 'W', 'H', 'L', 'R', 'T', 'B', 'vLo', 'vHi', 'tMin', 'tMax') + ['px' => $px, 'py' => $py];
}
?>
<div class="cat-wrap">
<div class="content-header">
<div>
<h1><?= $this->e($item['name']) ?>
<?php if ((int)$item['is_standard'] === 1): ?><span class="status-pill status-pill--info"><i class="fas fa-star"></i> <?= $this->te('Standard') ?></span><?php endif; ?>
<?php if ((int)$item['is_active'] !== 1): ?><span class="status-pill status-pill--neutral"><?= $this->te('Inaktiv') ?></span><?php endif; ?>
</h1>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/"><?= $this->te('Home') ?></a></li>
<li class="breadcrumb-item"><a href="/catalog"><?= $this->te('Ausstattung & Preise') ?></a></li>
<li class="breadcrumb-item active"><?= $this->e($item['name']) ?></li>
</ol>
</div>
<div class="content-header-actions">
<a href="/catalog" class="btn btn-default"><i class="fas fa-arrow-left"></i> <?= $this->te('Zurück') ?></a>
<?php if (!empty($can_manage)): ?>
<a href="/catalog/items/<?= (int)$item['id'] ?>/edit" class="btn btn-default"><i class="fas fa-pencil-alt"></i> <?= $this->te('Bearbeiten') ?></a>
<form method="post" action="/catalog/items/<?= (int)$item['id'] ?>/delete" onsubmit="return confirm('Artikel wirklich löschen? Alle Preis-Einträge gehen verloren.');" style="display:inline;">
<input type="hidden" name="_csrf" value="<?= $this->e($this->csrf()) ?>">
<button aria-label="<?= $this->te('Löschen') ?>" type="submit" class="btn btn-outline-danger"><i class="fas fa-trash"></i></button>
</form>
<?php endif; ?>
</div>
</div>
<div class="page-body cat-detail-grid">
<div class="cat-detail-main">
<!-- Eckdaten -->
<div class="detail-card">
<div class="detail-card-header"><span class="detail-card-title"><?= $this->te('Eckdaten') ?></span></div>
<div class="detail-card-body">
<?php if (!empty($item['image_effective'])): ?>
<div style="margin-bottom:1rem;text-align:center;">
<img src="<?= $this->e($item['image_effective']) ?>" alt="<?= $this->e($item['name']) ?>" class="cat-detail-img">
</div>
<?php endif; ?>
<dl class="cat-dl">
<?php
$modelLink = trim((string)($item['asset_type_manufacturer'] ?? '') . ' ' . (string)($item['asset_type_name'] ?? ''));
$facts = [
'Kategorie' => $item['category_name'] ?? '–',
'Hersteller' => trim((string)($item['manufacturer'] ?? '') . ' ' . (string)($item['model'] ?? '')) ?: '–',
'Artikel-Nr.' => $item['sku'] ?? '–',
'Einheit' => $item['unit'] ?? 'Stück',
'Abrechnung' => $billingLabels[$item['billing_cycle'] ?? 'once'] ?? 'einmalig',
'Standard-Lieferant' => $item['default_supplier_name'] ?? '–',
];
if ($modelLink !== '') { $facts['Inventar-Modell'] = $modelLink; }
foreach ($facts as $k => $v): ?>
<div class="cat-dl-row"><dt><?= $this->e($k) ?></dt><dd><?= $this->e((string)($v ?: '–')) ?></dd></div>
<?php endforeach; ?>
</dl>
<?php if (!empty($item['spec'])): ?>
<div class="cat-spec-box"><strong><?= $this->te('Spezifikation:') ?></strong><br><?= nl2br($this->e($item['spec'])) ?></div>
<?php endif; ?>
<?php if (!empty($item['notes'])): ?>
<div class="cat-spec-box text-muted"><strong><?= $this->te('Notiz:') ?></strong><br><?= nl2br($this->e($item['notes'])) ?></div>
<?php endif; ?>
</div>
</div>
<!-- Aktuelle Preise je Lieferant -->
<div class="detail-card">
<div class="detail-card-header"><span class="detail-card-title"><?= $this->te('Aktueller Preis je Lieferant') ?></span></div>
<div class="detail-card-body">
<?php if (empty($suppliers_now)): ?>
<div class="empty-state"><p><?= $this->te('Noch kein Preis erfasst.') ?></p></div>
<?php else: ?>
<div class="table-wrapper">
<table class="data-table">
<thead><tr><th scope="col"><?= $this->te('Lieferant') ?></th><th scope="col"><?= $this->te('Netto') ?></th><th scope="col"><?= $this->te('Brutto') ?></th><th scope="col"><?= $this->te('MwSt.') ?></th><th scope="col"><?= $this->te('Stand') ?></th><?php if (!empty($can_manage)): ?><th scope="col"></th><?php endif; ?></tr></thead>
<tbody>
<?php foreach ($suppliers_now as $s): ?>
<tr>
<td><?= $this->e($s['supplier_name'] ?? '–') ?: '–' ?></td>
<td><strong><?= $money($s['price_net']) ?></strong></td>
<td><?= $money($s['price_gross']) ?></td>
<td><?= number_format((float)$s['vat_rate'], 0) ?> %</td>
<td class="text-muted"><?= date('d.m.Y', strtotime((string)$s['valid_from'])) ?></td>
<?php if (!empty($can_manage)): ?>
<td>
<form method="post" action="/catalog/prices/<?= (int)$s['id'] ?>/delete" onsubmit="return confirm('Preis-Eintrag entfernen?');" style="display:inline;">
<input type="hidden" name="_csrf" value="<?= $this->e($this->csrf()) ?>">
<button type="submit" class="btn btn-outline-danger btn-xs" title="<?= $this->te('Entfernen') ?>"><i class="fas fa-times"></i></button>
</form>
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<!-- Preisverlauf -->
<div class="detail-card">
<div class="detail-card-header"><span class="detail-card-title"><?= $this->te($disp === 'gross' ? 'Preisverlauf (brutto)' : 'Preisverlauf (netto)') ?></span></div>
<div class="detail-card-body">
<?php if ($chart === null): ?>
<p class="text-muted"><?= $this->te('Für einen Verlauf werden mindestens zwei Preis-Einträge benötigt.') ?></p>
<?php else: ?>
<div class="cat-chart-scroll">
<svg class="cat-chart" viewBox="0 0 <?= $chart['W'] ?> <?= $chart['H'] ?>" role="img" aria-label="<?= $this->te('Preisverlauf') ?>">
<?php
// Y-Gitter + Beschriftung (4 Stufen)
for ($i = 0; $i <= 4; $i++):
$v = $chart['vLo'] + ($chart['vHi'] - $chart['vLo']) * $i / 4;
$y = ($chart['py'])($v);
?>
<line class="cat-grid" x1="<?= $chart['L'] ?>" y1="<?= round($y, 1) ?>" x2="<?= $chart['W'] - $chart['R'] ?>" y2="<?= round($y, 1) ?>"/>
<text class="cat-axis" x="<?= $chart['L'] - 8 ?>" y="<?= round($y + 4, 1) ?>" text-anchor="end" font-size="11"><?= $this->number($v, 0) ?></text>
<?php endfor; ?>
<!-- X-Beschriftung: Start/Ende -->
<text class="cat-axis" x="<?= $chart['L'] ?>" y="<?= $chart['H'] - 10 ?>" font-size="11"><?= date('m/Y', $chart['tMin']) ?></text>
<text class="cat-axis" x="<?= $chart['W'] - $chart['R'] ?>" y="<?= $chart['H'] - 10 ?>" text-anchor="end" font-size="11"><?= date('m/Y', $chart['tMax']) ?></text>
<?php $ci = 0; foreach ($chart['series'] as $supName => $pts):
usort($pts, fn($a, $b) => $a['t'] <=> $b['t']);
$sc = 'cat-s' . ($ci % 6);
$poly = [];
foreach ($pts as $p) { $poly[] = round(($chart['px'])($p['t']), 1) . ',' . round(($chart['py'])($p['v']), 1); }
?>
<polyline class="cat-line <?= $sc ?>" points="<?= implode(' ', $poly) ?>"/>
<?php foreach ($pts as $p): ?>
<circle class="cat-dot <?= $sc ?>" cx="<?= round(($chart['px'])($p['t']), 1) ?>" cy="<?= round(($chart['py'])($p['v']), 1) ?>" r="3.5">
<title><?= $this->e($supName) ?>: <?= $money($p['v']) ?> (<?= date('d.m.Y', $p['t']) ?>)</title>
</circle>
<?php endforeach; $ci++; endforeach; ?>
</svg>
</div>
<div class="cat-legend">
<?php $ci = 0; foreach (array_keys($chart['series']) as $supName): $sc = 'cat-s' . ($ci % 6); $ci++; ?>
<span class="cat-legend-item"><span class="cat-legend-dot <?= $sc ?>"></span><?= $this->e($supName) ?></span>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!empty($history)): ?>
<div class="table-wrapper" style="margin-top:1rem;">
<table class="data-table">
<thead><tr><th scope="col"><?= $this->te('Gültig ab') ?></th><th scope="col"><?= $this->te('Lieferant') ?></th><th scope="col"><?= $this->te('Netto') ?></th><th scope="col"><?= $this->te('Brutto') ?></th><th scope="col"><?= $this->te('MwSt.') ?></th><th scope="col"><?= $this->te('Notiz') ?></th><?php if (!empty($can_manage)): ?><th scope="col"></th><?php endif; ?></tr></thead>
<tbody>
<?php foreach (array_reverse($history) as $h): ?>
<tr>
<td><?= date('d.m.Y', strtotime((string)$h['valid_from'])) ?><?php if (strtotime((string)$h['valid_from']) > time()): ?> <span class="status-pill status-pill--info"><?= $this->te('angekündigt') ?></span><?php endif; ?></td>
<td><?= $this->e($h['supplier_name'] ?? '–') ?: '–' ?></td>
<td><?= $money($h['price_net']) ?></td>
<td><?= $money($h['price_gross']) ?></td>
<td><?= number_format((float)$h['vat_rate'], 0) ?> %</td>
<td class="text-muted"><?= $this->e($h['note'] ?? '') ?></td>
<?php if (!empty($can_manage)): ?>
<td>
<form method="post" action="/catalog/prices/<?= (int)$h['id'] ?>/delete" onsubmit="return confirm('Preis-Eintrag entfernen?');" style="display:inline;">
<input type="hidden" name="_csrf" value="<?= $this->e($this->csrf()) ?>">
<button type="submit" class="btn btn-outline-danger btn-xs" title="<?= $this->te('Entfernen') ?>"><i class="fas fa-times"></i></button>
</form>
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<!-- Pflicht-Abhängigkeiten -->
<?php if (!empty($requirements)): ?>
<div class="detail-card">
<div class="detail-card-header"><span class="detail-card-title"><i class="fas fa-link"></i> <?= $this->te('Benötigt zwingend') ?></span></div>
<div class="detail-card-body">
<p class="text-muted" style="margin-top:0;"><?= $this->te('Diese Positionen sind für den Betrieb erforderlich und werden in Preisliste, Kalkulation und Profilen automatisch mitgerechnet.') ?></p>
<div class="table-wrapper">
<table class="data-table">
<thead><tr><th scope="col" class="cat-col-num"><?= $this->te('Menge') ?></th><th scope="col"><?= $this->te('Artikel') ?></th><th scope="col"><?= $this->te('Abrechnung') ?></th><th scope="col" class="cat-col-price"><?= $this->t('Einzel') ?> (<?= $disp === 'gross' ? 'brutto' : 'netto' ?>)</th><th scope="col" class="cat-col-price"><?= $this->te('Summe') ?></th></tr></thead>
<tbody>
<?php foreach ($requirements as $r):
$pv = $disp === 'gross' ? $r['price_gross'] : $r['price_net'];
$q = (int)$r['quantity'];
?>
<tr>
<td class="cat-col-num"><?= $q ?>×</td>
<td>
<a class="table-link" href="/catalog/items/<?= (int)$r['id'] ?>"><?= $this->e($r['name']) ?></a>
<?php if (!empty($r['note'])): ?><br><span class="text-muted cat-spec"><?= $this->e($r['note']) ?></span><?php endif; ?>
</td>
<td><?= $this->e($billingLabels[$r['billing_cycle'] ?? 'once'] ?? 'einmalig') ?></td>
<td class="cat-col-price"><?= $pv === null ? '<span class="text-muted">kein Preis</span>' : $money($pv) ?></td>
<td class="cat-col-price"><?= $pv === null ? '<span class="text-muted">–</span>' : '<strong>' . $money((float)$pv * $q) . '</strong>' ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php endif; ?>
<!-- Zubehör -->
<?php if (!empty($accessories)): ?>
<div class="detail-card">
<div class="detail-card-header"><span class="detail-card-title"><?= $this->te('Typisches Zubehör') ?></span></div>
<div class="detail-card-body">
<div class="table-wrapper">
<table class="data-table">
<thead><tr><th scope="col"><?= $this->te('Artikel') ?></th><th scope="col" class="cat-col-price"><?= $this->t('Preis') ?> (<?= $disp === 'gross' ? 'brutto' : 'netto' ?>)</th></tr></thead>
<tbody>
<?php foreach ($accessories as $a): $pv = $disp === 'gross' ? $a['price_gross'] : $a['price_net']; $suf = $cycleSuffix[$a['billing_cycle'] ?? 'once'] ?? ''; ?>
<tr>
<td><a class="table-link" href="/catalog/items/<?= (int)$a['id'] ?>"><?= $this->e($a['name']) ?></a></td>
<td class="cat-col-price"><?= $pv === null ? '<span class="text-muted">kein Preis</span>' : '<strong>' . $money($pv) . $suf . '</strong>' ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php endif; ?>
</div>
<!-- Seitenspalte: Preis erfassen (nur manage) -->
<?php if (!empty($can_manage)): ?>
<div class="cat-detail-side">
<div class="detail-card">
<div class="detail-card-header"><span class="detail-card-title"><?= $this->te('Preis erfassen') ?></span></div>
<div class="detail-card-body">
<form method="post" action="/catalog/items/<?= (int)$item['id'] ?>/prices" class="cat-price-form">
<input type="hidden" name="_csrf" value="<?= $this->e($this->csrf()) ?>">
<div class="form-group">
<label for="f-5aee-supplier_company_id"><?= $this->te('Lieferant') ?></label>
<?php if (!empty($supplier_options)): ?>
<select id="f-5aee-supplier_company_id" name="supplier_company_id" class="form-control form-control-sm" data-searchable <?= $kannFirmaAnlegen ? 'data-create-company' : '' ?>>
<option value=""><?= $this->te('– Freitext / keiner –') ?></option>
<?php foreach ($supplier_options as $so): ?>
<option value="<?= (int)$so['id'] ?>" <?= (int)($item['default_supplier_company_id'] ?? 0) === (int)$so['id'] ? 'selected' : '' ?>><?= $this->e($so['name']) ?></option>
<?php endforeach; ?>
</select>
<?php else: ?>
<input type="text" name="supplier_name" class="form-control form-control-sm" placeholder="<?= $this->te('Lieferant (Firma)') ?>">
<small class="form-hint"><?= $this->te('Kontakte-Modul aktivieren, um Firmen auszuwählen.') ?></small>
<?php endif; ?>
</div>
<div class="form-group">
<label for="cat-valid-from"><?= $this->te('Gültig ab') ?> <span class="text-danger">*</span></label>
<input type="date" id="cat-valid-from" name="valid_from" class="form-control form-control-sm" value="<?= date('Y-m-d') ?>" required>
<small class="form-hint"><?= $this->te('Auch Vergangenheit (Altpreis) oder Zukunft (angekündigt) möglich.') ?></small>
</div>
<div class="form-group">
<label for="cat-entered-as"><?= $this->te('Erfassen als') ?></label>
<select id="cat-entered-as" name="entered_as" class="form-control form-control-sm" data-searchable>
<option value="net"><?= $this->te('Netto') ?></option>
<option value="gross"><?= $this->te('Brutto') ?></option>
</select>
</div>
<div class="form-row-2">
<div class="form-group">
<label for="cat-amount"><?= $this->te('Betrag') ?> <span class="text-danger">*</span></label>
<input type="text" inputmode="decimal" id="cat-amount" name="amount" class="form-control form-control-sm" placeholder="z. B. 899,00" required>
</div>
<div class="form-group">
<label for="cat-vat"><?= $this->te('MwSt. %') ?></label>
<input type="text" inputmode="decimal" id="cat-vat" name="vat_rate" class="form-control form-control-sm" value="<?= number_format((float)($vat_default ?? 19), 0) ?>">
</div>
</div>
<p class="cat-calc-preview" id="cat-calc-preview"><?= $this->t('Netto <span data-net>–</span> · Brutto <span data-gross>–</span>') ?></p>
<div class="form-group">
<label for="cat-note"><?= $this->te('Notiz') ?></label>
<input type="text" id="cat-note" name="note" class="form-control form-control-sm" maxlength="255">
</div>
<button type="submit" class="btn btn-primary btn-sm"><i class="fas fa-plus"></i> <?= $this->te('Preis speichern') ?></button>
</form>
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
<?php if (!empty($can_manage)): ?>
<script>
// Live-Vorschau Netto/Brutto beim Preis erfassen.
(function () {
var amount = document.getElementById('cat-amount');
var vat = document.getElementById('cat-vat');
var mode = document.getElementById('cat-entered-as');
var out = document.getElementById('cat-calc-preview');
if (!amount || !vat || !mode || !out) { return; }
var netEl = out.querySelector('[data-net]');
var grossEl = out.querySelector('[data-gross]');
function fmt(n) { return isFinite(n) ? n.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : '–'; }
function calc() {
var a = parseFloat(String(amount.value).replace(',', '.'));
var v = parseFloat(String(vat.value).replace(',', '.'));
if (!isFinite(a) || a <= 0) { netEl.textContent = '–'; grossEl.textContent = '–'; return; }
if (!isFinite(v) || v < 0) { v = 0; }
var f = 1 + v / 100;
var net, gross;
if (mode.value === 'gross') { gross = a; net = a / f; } else { net = a; gross = a * f; }
netEl.textContent = fmt(net); grossEl.textContent = fmt(gross);
}
[amount, vat, mode].forEach(function (el) { el.addEventListener('input', calc); el.addEventListener('change', calc); });
})();
</script>
<?php endif; ?>
<?php if ($kannFirmaAnlegen): ?>
<?= $this->render('partials/_company_quick_create') ?>
<?php endif; ?>