OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

item.php

24 KB
<?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 &amp; 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; ?>