Add configurable author price calculator
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
# Änderungsprotokoll
|
||||
|
||||
## 2026-09-18
|
||||
|
||||
Seit Commit `0eb32fb` wurden folgende Änderungen vorgenommen:
|
||||
|
||||
- Neue geschützte Admin-Seite `author_price.php` für die Berechnung von Autorenpreisen.
|
||||
- Printbücher können per Dropdown ausgewählt werden.
|
||||
- Berechnung des Autorenpreises auf Grundlage von Brutto-Preis, 7 % Mehrwertsteuer, Druckkosten und einem frei wählbaren Rabatt.
|
||||
- Der Rabatt ist standardmäßig mit 85 % vorbelegt und kann zwischen 0 und 100 % eingegeben werden.
|
||||
- Die Ersparnis pro Exemplar wird separat ausgewiesen.
|
||||
- Der Autorenrabatt wird als eigener Rechenposten angezeigt.
|
||||
- Checkbox ergänzt, um alternativ die kumulierte Autorenmarge aller am Buch beteiligten Autoren als Rabatt zu verwenden.
|
||||
- Das manuelle Rabattfeld wird bei aktivierter Margen-Checkbox deaktiviert.
|
||||
- Die kumulierte Autorenmarge wird aus allen Buch-Autor-Zuordnungen berechnet.
|
||||
- JavaScript für die unmittelbare Aktivierung und Deaktivierung des Rabattfelds ergänzt.
|
||||
- Navigation im Admin-Bereich um den Link zum Autorenpreis-Rechner erweitert.
|
||||
- CSS für die Anordnung von Rabattfeld und Checkbox ergänzt.
|
||||
- PHP-, SQL- und Zugriffsschutz-Prüfungen für die neue Funktion durchgeführt.
|
||||
@@ -156,7 +156,7 @@ $token = csrf();
|
||||
?><!doctype html>
|
||||
<html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Autorenmargen – Admin</title><link rel="stylesheet" href="css/admin.css"></head>
|
||||
<body data-admin-app data-csrf="<?= e($token) ?>" data-author-search="author_search.php">
|
||||
<header><h1>Autorenmargen – Admin</h1><nav><a class="button secondary" href="billing.php">Abrechnung erstellen</a><form method="post" class="inline-form"><input type="hidden" name="action" value="logout"><input type="hidden" name="csrf_token" value="<?= e($token) ?>"><button class="secondary" type="submit">Abmelden</button></form></nav></header>
|
||||
<header><h1>Autorenmargen – Admin</h1><nav><a class="button secondary" href="author_price.php">Autorenpreis-Rechner</a><a class="button secondary" href="billing.php">Abrechnung erstellen</a><form method="post" class="inline-form"><input type="hidden" name="action" value="logout"><input type="hidden" name="csrf_token" value="<?= e($token) ?>"><button class="secondary" type="submit">Abmelden</button></form></nav></header>
|
||||
<main><?php if ($message): ?><p class="notice"><?= e($message) ?></p><?php endif; ?><?php if ($error): ?><p class="notice error"><?= e($error) ?></p><?php endif; ?>
|
||||
<div class="grid">
|
||||
<section class="panel"><h2><?= $author['id'] ? 'Autor bearbeiten' : 'Autor anlegen' ?></h2><form method="post"><input type="hidden" name="action" value="save_author"><input type="hidden" name="csrf_token" value="<?= e($token) ?>"><input type="hidden" name="author_id" value="<?= e($author['id']) ?>"><label for="author-name">Name</label><input id="author-name" name="name" maxlength="255" value="<?= e($author['name']) ?>" required><label for="address">Adresse</label><input id="address" name="address" maxlength="255" value="<?= e($author['address']) ?>"><label for="postal-code">PLZ</label><input id="postal-code" name="postal_code" maxlength="20" value="<?= e($author['postal_code']) ?>"><label for="city">Ort</label><input id="city" name="city" maxlength="120" value="<?= e($author['city']) ?>"><label for="bank-account">IBAN</label><input id="bank-account" name="bank_account" maxlength="255" value="<?= e($author['bank_account']) ?>"><label for="bic">BIC</label><input id="bic" name="bic" maxlength="20" value="<?= e($author['bic']) ?>"><label for="bank-name">Bankname</label><input id="bank-name" name="bank_name" maxlength="255" value="<?= e($author['bank_name']) ?>"><label for="paypal-address">PayPal-Adresse</label><input id="paypal-address" name="paypal_address" maxlength="255" value="<?= e($author['paypal_address']) ?>"><div class="actions"><button type="submit">Speichern</button><?php if ($author['id']): ?><a class="button secondary" href="admin.php">Abbrechen</a><?php endif; ?></div></form></section>
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/inc/admin_bootstrap.php';
|
||||
requireAdmin();
|
||||
|
||||
const AUTHOR_PRICE_VAT_RATE = 0.07;
|
||||
const DEFAULT_AUTHOR_DISCOUNT_PERCENT = 85.0;
|
||||
|
||||
function authorPriceMoney(float $value): string
|
||||
{
|
||||
return number_format($value, 2, ',', '.') . ' €';
|
||||
}
|
||||
|
||||
$books = $pdo->query(
|
||||
'SELECT b.id, b.title, b.selling_price, b.printing_costs, b.isbn_print,
|
||||
COALESCE(SUM(ba.margin_percent), 0) AS author_margin_percent
|
||||
FROM books b
|
||||
LEFT JOIN book_authors ba ON ba.book_id = b.id
|
||||
WHERE b.isbn_print IS NOT NULL AND b.isbn_print <> ""
|
||||
GROUP BY b.id
|
||||
ORDER BY b.title'
|
||||
)->fetchAll();
|
||||
|
||||
$selectedId = filter_var($_GET['book_id'] ?? null, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]) ?: null;
|
||||
$useAuthorMargin = ($_GET['use_author_margin'] ?? '') === '1';
|
||||
$discountInput = $_GET['discount_percent'] ?? (string) DEFAULT_AUTHOR_DISCOUNT_PERCENT;
|
||||
$discountPercent = filter_var($discountInput, FILTER_VALIDATE_FLOAT);
|
||||
$discountError = $discountPercent === false || $discountPercent < 0 || $discountPercent > 100;
|
||||
if ($discountError) {
|
||||
$discountPercent = DEFAULT_AUTHOR_DISCOUNT_PERCENT;
|
||||
}
|
||||
$discountRate = (float) $discountPercent / 100;
|
||||
$retainedRate = 1 - $discountRate;
|
||||
$selectedBook = null;
|
||||
foreach ($books as $book) {
|
||||
if ((int) $book['id'] === $selectedId) {
|
||||
$selectedBook = $book;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$calculation = null;
|
||||
if ($selectedBook !== null) {
|
||||
if ($useAuthorMargin) {
|
||||
$discountPercent = (float) $selectedBook['author_margin_percent'];
|
||||
$discountError = false;
|
||||
}
|
||||
$discountRate = (float) $discountPercent / 100;
|
||||
$retainedRate = 1 - $discountRate;
|
||||
$grossPrice = (float) $selectedBook['selling_price'];
|
||||
$printingCosts = (float) $selectedBook['printing_costs'];
|
||||
$netPrice = $grossPrice / (1 + AUTHOR_PRICE_VAT_RATE);
|
||||
$revenueBeforeAuthorDiscount = $netPrice - $printingCosts;
|
||||
$authorDiscount = $revenueBeforeAuthorDiscount * $discountRate;
|
||||
$retainedRevenue = $revenueBeforeAuthorDiscount * $retainedRate;
|
||||
$authorPriceNet = $printingCosts + $retainedRevenue;
|
||||
$authorPriceVat = $authorPriceNet * AUTHOR_PRICE_VAT_RATE;
|
||||
$authorPriceGross = $authorPriceNet + $authorPriceVat;
|
||||
$saving = $grossPrice - $authorPriceGross;
|
||||
|
||||
$calculation = [
|
||||
'gross_price' => $grossPrice,
|
||||
'net_price' => $netPrice,
|
||||
'printing_costs' => $printingCosts,
|
||||
'revenue_before_author_discount' => $revenueBeforeAuthorDiscount,
|
||||
'author_discount' => $authorDiscount,
|
||||
'discount_percent' => (float) $discountPercent,
|
||||
'retained_percent' => $retainedRate * 100,
|
||||
'use_author_margin' => $useAuthorMargin,
|
||||
'author_margin_percent' => (float) $selectedBook['author_margin_percent'],
|
||||
'retained_revenue' => $retainedRevenue,
|
||||
'author_price_net' => $authorPriceNet,
|
||||
'author_price_vat' => $authorPriceVat,
|
||||
'author_price_gross' => $authorPriceGross,
|
||||
'saving' => $saving,
|
||||
];
|
||||
}
|
||||
|
||||
$token = csrf();
|
||||
?><!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Autorenpreis-Rechner</title>
|
||||
<link rel="stylesheet" href="css/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Autorenpreis-Rechner</h1>
|
||||
<nav>
|
||||
<a class="button secondary" href="admin.php">Stammdaten</a>
|
||||
<a class="button secondary" href="billing.php">Abrechnung</a>
|
||||
<form method="post" action="admin.php" class="inline-form">
|
||||
<input type="hidden" name="action" value="logout">
|
||||
<input type="hidden" name="csrf_token" value="<?= e($token) ?>">
|
||||
<button class="secondary" type="submit">Abmelden</button>
|
||||
</form>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<section class="panel">
|
||||
<h2>Buch auswählen</h2>
|
||||
<form method="get">
|
||||
<label for="book-id">Printbuch</label>
|
||||
<select id="book-id" name="book_id" required>
|
||||
<option value="">Bitte auswählen</option>
|
||||
<?php foreach ($books as $book): ?>
|
||||
<option value="<?= (int) $book['id'] ?>"<?= (int) $book['id'] === $selectedId ? ' selected' : '' ?>><?= e($book['title']) ?> (<?= e($book['isbn_print']) ?>)</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<div class="discount-input-row">
|
||||
<div>
|
||||
<label for="discount-percent">Autorenrabatt (%)</label>
|
||||
<input id="discount-percent" type="number" name="discount_percent" min="0" max="100" step="0.01" value="<?= e(number_format((float) $discountPercent, 2, '.', '')) ?>"<?= $useAuthorMargin ? ' disabled' : '' ?> required>
|
||||
</div>
|
||||
<label class="checkbox-label" for="use-author-margin"><input id="use-author-margin" type="checkbox" name="use_author_margin" value="1"<?= $useAuthorMargin ? ' checked' : '' ?>> Kumulierte Autorenmarge als Rabatt verwenden</label>
|
||||
</div>
|
||||
<?php if ($discountError): ?><p class="notice error">Der Rabatt muss zwischen 0 und 100 Prozent liegen. Es wurde 85 Prozent verwendet.</p><?php endif; ?>
|
||||
<p class="muted">Hinterlegte Autorenmarge für dieses Buch: <?= number_format((float) ($selectedBook['author_margin_percent'] ?? 0), 2, ',', '.') ?> %</p>
|
||||
<div class="actions"><button type="submit">Preis berechnen</button></div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<?php if ($calculation !== null): ?>
|
||||
<section class="panel">
|
||||
<h2>Autorenpreis: <?= e($selectedBook['title']) ?></h2>
|
||||
<p class="muted">Berechnung pro Exemplar. Die Zwischenwerte werden ungerundet berechnet und erst für die Anzeige auf Cent gerundet.</p>
|
||||
<div class="table-wrap">
|
||||
<table class="billing-table">
|
||||
<tbody>
|
||||
<tr><th>Regulärer Brutto-Preis</th><td><?= authorPriceMoney($calculation['gross_price']) ?></td></tr>
|
||||
<tr><th>Regulärer Netto-Preis (Brutto ÷ 1,07)</th><td><?= authorPriceMoney($calculation['net_price']) ?></td></tr>
|
||||
<tr><th>Abzüglich Druckkosten</th><td><?= authorPriceMoney($calculation['printing_costs']) ?></td></tr>
|
||||
<tr><th>Erlös vor Autorenrabatt</th><td><?= authorPriceMoney($calculation['revenue_before_author_discount']) ?></td></tr>
|
||||
<tr><th>Autorenrabatt <?= number_format($calculation['discount_percent'], 2, ',', '.') ?> %<?= $calculation['use_author_margin'] ? ' (kumulierte Autorenmarge)' : '' ?></th><td><?= authorPriceMoney($calculation['author_discount']) ?></td></tr>
|
||||
<tr><th>Verbleibender Erlösanteil <?= number_format($calculation['retained_percent'], 2, ',', '.') ?> %</th><td><?= authorPriceMoney($calculation['retained_revenue']) ?></td></tr>
|
||||
<tr><th>Autorenpreis netto (Druckkosten + verbleibender Anteil)</th><td><?= authorPriceMoney($calculation['author_price_net']) ?></td></tr>
|
||||
<tr><th>Mehrwertsteuer 7 % auf Autorenpreis</th><td><?= authorPriceMoney($calculation['author_price_vat']) ?></td></tr>
|
||||
<tr><th>Autorenpreis brutto</th><td><strong><?= authorPriceMoney($calculation['author_price_gross']) ?></strong></td></tr>
|
||||
<tr><th>Ersparnis pro Exemplar</th><td><strong><?= authorPriceMoney($calculation['saving']) ?></strong></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<?php elseif ($selectedId !== null): ?>
|
||||
<section class="panel"><p class="notice error">Das ausgewählte Buch ist nicht als Printbuch vorhanden.</p></section>
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
<script src="scripts/author_price.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -31,6 +31,10 @@ th { background: #e0e7ff; color: #172554; }
|
||||
nav { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
|
||||
nav a { color: #fff; }
|
||||
.billing-table input[type=number] { min-width: 7rem; }
|
||||
.discount-input-row { display: flex; gap: 1rem; align-items: end; flex-wrap: wrap; }
|
||||
.discount-input-row > div { flex: 1 1 15rem; }
|
||||
.checkbox-label { display: flex; align-items: center; gap: .45rem; padding-bottom: .65rem; }
|
||||
.checkbox-label input { width: auto; }
|
||||
.muted { color: #64748b; }
|
||||
.visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
||||
@media (max-width: 600px) { .author-row { grid-template-columns: 1fr; } header { align-items: flex-start; flex-direction: column; } }
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
(() => {
|
||||
const checkbox = document.querySelector('#use-author-margin');
|
||||
const discountInput = document.querySelector('#discount-percent');
|
||||
if (!checkbox || !discountInput) return;
|
||||
|
||||
const updateDiscountInput = () => {
|
||||
discountInput.disabled = checkbox.checked;
|
||||
};
|
||||
|
||||
checkbox.addEventListener('change', updateDiscountInput);
|
||||
updateDiscountInput();
|
||||
})();
|
||||
Reference in New Issue
Block a user