Merge pull request 'Feature: Buchungsvorlagen, E-Mail-Vorlagen pro Kunde, UI-Fixes' (#95) from fds2610-bot/Steuerkram:feature/buchungsvorlagen into main
Reviewed-on: https://codeberg.org/fds2610/Steuerkram/pulls/95
This commit is contained in:
commit
cf2d2b6e3c
18 changed files with 137 additions and 152 deletions
11
migrations/081_buchungsvorlage_standard_lieferant.sql
Normal file
11
migrations/081_buchungsvorlage_standard_lieferant.sql
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
-- 081_buchungsvorlage_standard_lieferant.sql
|
||||
-- Standard-Lieferant pro Vorlage; wird im Buchungsdialog vorbelegt.
|
||||
|
||||
ALTER TABLE buchungsvorlagen
|
||||
ADD COLUMN IF NOT EXISTS standard_lieferant VARCHAR(255) NULL DEFAULT NULL AFTER beschreibung;
|
||||
|
||||
-- Bestehende Vorlage "Essensbeleg" mit Standardlieferant "Lebensmittel" vorbelegen.
|
||||
UPDATE buchungsvorlagen
|
||||
SET standard_lieferant = 'Lebensmittel'
|
||||
WHERE name = 'Essensbeleg n. abzugsfähig (nur USt.)'
|
||||
AND (standard_lieferant IS NULL OR standard_lieferant = '');
|
||||
6
migrations/082_customer_mail_templates.sql
Normal file
6
migrations/082_customer_mail_templates.sql
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
-- 082_customer_mail_templates.sql
|
||||
-- E-Mail-Betreff und -Text pro Kunde statt global in company_settings.
|
||||
|
||||
ALTER TABLE customers
|
||||
ADD COLUMN IF NOT EXISTS mail_subject_template VARCHAR(255) NULL DEFAULT NULL AFTER notes,
|
||||
ADD COLUMN IF NOT EXISTS mail_body_template TEXT NULL DEFAULT NULL AFTER mail_subject_template;
|
||||
|
|
@ -161,7 +161,6 @@ $router->get('/tax/euer/{year}/anlageliste/pdf', fn($p) => (new EuerController()
|
|||
// ── Einstellungen ────────────────────────────────────────────
|
||||
$router->get('/settings', fn($p) => (new SettingsController())->show($p));
|
||||
$router->post('/settings', fn($p) => (new SettingsController())->update($p));
|
||||
$router->post('/settings/mail-template', fn($p) => (new SettingsController())->mailTemplate($p));
|
||||
$router->post('/settings/mail-test', fn($p) => (new SettingsController())->mailTest($p));
|
||||
$router->post('/settings/logo', fn($p) => (new SettingsController())->uploadLogo($p));
|
||||
$router->post('/settings/logo/delete', fn($p) => (new SettingsController())->deleteLogo($p));
|
||||
|
|
|
|||
|
|
@ -524,10 +524,11 @@ document.addEventListener('input', function(e) {
|
|||
if (!inp.matches('#vorlage-modal .vorlage-netto, #vorlage-modal .vorlage-brutto')) return;
|
||||
const row = inp.closest('.vorlage-ust-row');
|
||||
if (!row) return;
|
||||
const satz = parseFloat(row.dataset.ustSatz) / 100;
|
||||
const netto = row.querySelector('.vorlage-netto');
|
||||
const brutto = row.querySelector('.vorlage-brutto');
|
||||
const ust = row.querySelector('.vorlage-ust');
|
||||
const satz = parseFloat(row.dataset.ustSatz) / 100;
|
||||
const netto = row.querySelector('.vorlage-netto');
|
||||
const brutto = row.querySelector('.vorlage-brutto');
|
||||
const ust = row.querySelector('.vorlage-ust');
|
||||
const ustAnzeige = row.querySelector('.vorlage-ust-anzeige');
|
||||
if (inp.classList.contains('vorlage-netto')) {
|
||||
const n = parseFloat(netto.value) || 0;
|
||||
brutto.value = (n * (1 + satz)).toFixed(2);
|
||||
|
|
@ -538,6 +539,7 @@ document.addEventListener('input', function(e) {
|
|||
netto.value = n.toFixed(2);
|
||||
ust.value = (b - n).toFixed(2);
|
||||
}
|
||||
if (ustAnzeige) ustAnzeige.textContent = parseFloat(ust.value).toFixed(2).replace('.', ',');
|
||||
vorlageUpdateBruttoGesamt();
|
||||
});
|
||||
|
||||
|
|
@ -548,19 +550,11 @@ function vorlageUpdateBruttoGesamt() {
|
|||
const summe = Array.from(bruttoFelder).reduce((s, el) => s + (parseFloat(el.value) || 0), 0);
|
||||
const anzeige = modal.querySelector('[data-feldname="brutto_gesamt"]');
|
||||
if (anzeige) anzeige.textContent = summe.toFixed(2).replace('.', ',') + ' €';
|
||||
// Auch das buchungs_datum-Hidden synchronisieren mit belegdatum-Feld falls vorhanden
|
||||
const datumFeld = modal.querySelector('input[name="belegdatum"]');
|
||||
const datumHidden = modal.querySelector('input[name="buchungs_datum"]');
|
||||
if (datumFeld && datumHidden && datumFeld.value) {
|
||||
datumHidden.value = datumFeld.value;
|
||||
}
|
||||
}
|
||||
|
||||
// Datum-Feld Änderung → buchungs_datum synchronisieren
|
||||
// Zahldatum-Feld Änderung → buchungs_datum-Hidden synchronisieren
|
||||
document.addEventListener('change', function(e) {
|
||||
if (!e.target.matches('#vorlage-modal input[name="belegdatum"]')) return;
|
||||
const modal = document.getElementById('vorlage-modal');
|
||||
if (!modal) return;
|
||||
const hidden = modal.querySelector('input[name="buchungs_datum"]');
|
||||
if (!e.target.matches('#vorlage-modal #vorlage-zahldatum')) return;
|
||||
const hidden = document.getElementById('vorlage-buchungs-datum');
|
||||
if (hidden) hidden.value = e.target.value;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -209,10 +209,9 @@ class BookingController extends BaseController
|
|||
|
||||
if ($newAssetId !== null) {
|
||||
View::redirect("/assets/{$newAssetId}/edit", 'Buchung erfasst. Bitte Wirtschaftsgut-Details ergänzen.');
|
||||
} elseif ($bankTxId > 0) {
|
||||
View::redirect('/bank', 'Buchung erfasst und Bankbuchung verknüpft.');
|
||||
} else {
|
||||
View::redirect("/belege/{$belegId}", 'Buchung erfasst.');
|
||||
$msg = $bankTxId > 0 ? 'Buchung erfasst und Bankbuchung verknüpft.' : 'Buchung erfasst.';
|
||||
View::redirect("/belege/{$belegId}", $msg);
|
||||
}
|
||||
} catch (\PDOException $e) {
|
||||
error_log('[Steuerkram] PDO-Fehler in BookingController::store(): ' . $e->getMessage());
|
||||
|
|
|
|||
|
|
@ -141,14 +141,20 @@ class BuchungsvorlageController extends BaseController
|
|||
$zahlungskonten = Account::all(['kontenart' => 'Zahlungskonto', 'aktiv' => 1]);
|
||||
$zahlungskonten = array_filter($zahlungskonten, fn($a) => (int)$a['kontonummer'] >= 1200 && (int)$a['kontonummer'] <= 1299);
|
||||
|
||||
$bankTxId = (int)($_GET['bank_tx'] ?? 0);
|
||||
$bankTxDatum = '';
|
||||
$hatBankbuchung = false;
|
||||
if ($beleg) {
|
||||
$linked = BankTransaction::findByBelegId((int)$beleg['id']);
|
||||
$hatBankbuchung = !empty($linked);
|
||||
if (!$hatBankbuchung && !empty($_GET['bank_tx'])) {
|
||||
if (!$hatBankbuchung && $bankTxId > 0) {
|
||||
$hatBankbuchung = true;
|
||||
}
|
||||
}
|
||||
if ($bankTxId > 0) {
|
||||
$bankTx = BankTransaction::find($bankTxId);
|
||||
$bankTxDatum = $bankTx['wertstellung'] ?? '';
|
||||
}
|
||||
|
||||
echo $this->renderView('buchungsvorlagen/modal', [
|
||||
'vorlage' => $vorlage,
|
||||
|
|
@ -157,6 +163,8 @@ class BuchungsvorlageController extends BaseController
|
|||
'belegId' => $belegId,
|
||||
'zahlungskonten' => array_values($zahlungskonten),
|
||||
'hatBankbuchung' => $hatBankbuchung,
|
||||
'bankTxId' => $bankTxId,
|
||||
'bankTxDatum' => $bankTxDatum,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -170,17 +178,24 @@ class BuchungsvorlageController extends BaseController
|
|||
$beleg = Beleg::find($belegId);
|
||||
if (!$beleg) { http_response_code(404); return; }
|
||||
|
||||
$datum = !empty($_POST['buchungs_datum']) ? $_POST['buchungs_datum'] : date('Y-m-d');
|
||||
$datum = !empty($_POST['buchungs_datum']) ? $_POST['buchungs_datum'] : date('Y-m-d');
|
||||
$bankTxId = (int)($_POST['bank_tx_id'] ?? 0);
|
||||
|
||||
try {
|
||||
$svc = new VorlageBuchungsService();
|
||||
$svc->buchen($belegId, $vorlageId, $_POST, $datum);
|
||||
|
||||
if ($bankTxId > 0) {
|
||||
$totalBrutto = \Steuerkram\Models\Booking::sumBruttoForBeleg($belegId);
|
||||
BankTransaction::linkBeleg($bankTxId, $belegId, $totalBrutto);
|
||||
}
|
||||
} catch (\RuntimeException $e) {
|
||||
View::redirect("/belege/{$belegId}", $e->getMessage(), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
View::redirect("/belege/{$belegId}", 'Buchungen aus Vorlage erfasst.');
|
||||
$msg = $bankTxId > 0 ? 'Buchungen aus Vorlage erfasst und Bankbuchung verknüpft.' : 'Buchungen aus Vorlage erfasst.';
|
||||
View::redirect("/belege/{$belegId}", $msg);
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ──────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -258,7 +258,10 @@ class InvoiceController extends BaseController
|
|||
try {
|
||||
$lines = Invoice::lines($id);
|
||||
$settings = Setting::get();
|
||||
(new EmailService())->sendInvoice($invoice, $lines, $settings, $recipients);
|
||||
$customer = !empty($invoice['customer_id'])
|
||||
? \Steuerkram\Models\Customer::find((int)$invoice['customer_id'])
|
||||
: null;
|
||||
(new EmailService())->sendInvoice($invoice, $lines, $settings, $recipients, $customer ?: null);
|
||||
|
||||
if ($invoice['status'] === 'Finalisiert') {
|
||||
\Steuerkram\Database::execute(
|
||||
|
|
|
|||
|
|
@ -95,25 +95,6 @@ class SettingsController extends BaseController
|
|||
View::redirect('/settings', 'Logo hochgeladen.');
|
||||
}
|
||||
|
||||
public function mailTemplate(array $params): void
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
$subject = trim($_POST['mail_subject_template'] ?? '');
|
||||
$body = trim($_POST['mail_body_template'] ?? '');
|
||||
|
||||
if ($subject === '') {
|
||||
echo json_encode(['error' => 'Betreff darf nicht leer sein.']);
|
||||
return;
|
||||
}
|
||||
|
||||
\Steuerkram\Database::execute(
|
||||
'UPDATE company_settings SET mail_subject_template=?, mail_body_template=? WHERE id=1',
|
||||
[$subject, $body ?: null]
|
||||
);
|
||||
|
||||
echo json_encode(['ok' => true]);
|
||||
}
|
||||
|
||||
public function mailTest(array $params): void
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
|
|
|
|||
|
|
@ -54,11 +54,12 @@ class Buchungsvorlage
|
|||
public static function create(array $data): int
|
||||
{
|
||||
Database::execute(
|
||||
'INSERT INTO buchungsvorlagen (name, beschreibung, aktiv, sort_order)
|
||||
VALUES (?, ?, ?, ?)',
|
||||
'INSERT INTO buchungsvorlagen (name, beschreibung, standard_lieferant, aktiv, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?)',
|
||||
[
|
||||
trim($data['name']),
|
||||
$data['beschreibung'] ?? null,
|
||||
$data['standard_lieferant'] !== '' ? trim($data['standard_lieferant'] ?? '') : null,
|
||||
isset($data['aktiv']) ? (int)$data['aktiv'] : 1,
|
||||
(int)($data['sort_order'] ?? 0),
|
||||
]
|
||||
|
|
@ -70,11 +71,12 @@ class Buchungsvorlage
|
|||
{
|
||||
Database::execute(
|
||||
'UPDATE buchungsvorlagen
|
||||
SET name = ?, beschreibung = ?, aktiv = ?, sort_order = ?
|
||||
SET name = ?, beschreibung = ?, standard_lieferant = ?, aktiv = ?, sort_order = ?
|
||||
WHERE id = ?',
|
||||
[
|
||||
trim($data['name']),
|
||||
$data['beschreibung'] ?? null,
|
||||
$data['standard_lieferant'] !== '' ? trim($data['standard_lieferant'] ?? '') : null,
|
||||
isset($data['aktiv']) ? (int)$data['aktiv'] : 1,
|
||||
(int)($data['sort_order'] ?? 0),
|
||||
$id,
|
||||
|
|
|
|||
|
|
@ -39,8 +39,8 @@ class Customer
|
|||
$nr = self::generateKundennummer();
|
||||
Database::execute(
|
||||
'INSERT INTO customers (kundennummer, firma, anrede, vorname, nachname, strasse, plz, ort, land,
|
||||
email, telefon, ust_id, steuernummer, payment_days, notes)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
email, telefon, ust_id, steuernummer, payment_days, notes, mail_subject_template, mail_body_template)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[
|
||||
$data['kundennummer'] ?? $nr,
|
||||
$data['firma'] ?? null,
|
||||
|
|
@ -57,6 +57,8 @@ class Customer
|
|||
$data['steuernummer'] ?? null,
|
||||
(int)($data['payment_days'] ?? 14),
|
||||
$data['notes'] ?? null,
|
||||
($data['mail_subject_template'] ?? '') !== '' ? trim($data['mail_subject_template']) : null,
|
||||
($data['mail_body_template'] ?? '') !== '' ? trim($data['mail_body_template']) : null,
|
||||
]
|
||||
);
|
||||
return (int)Database::lastInsertId();
|
||||
|
|
@ -66,7 +68,8 @@ class Customer
|
|||
{
|
||||
Database::execute(
|
||||
'UPDATE customers SET firma=?, anrede=?, vorname=?, nachname=?, strasse=?, plz=?, ort=?, land=?,
|
||||
email=?, telefon=?, ust_id=?, steuernummer=?, payment_days=?, notes=?, updated_at=NOW()
|
||||
email=?, telefon=?, ust_id=?, steuernummer=?, payment_days=?, notes=?,
|
||||
mail_subject_template=?, mail_body_template=?, updated_at=NOW()
|
||||
WHERE id=?',
|
||||
[
|
||||
$data['firma'] ?? null,
|
||||
|
|
@ -83,6 +86,8 @@ class Customer
|
|||
$data['steuernummer'] ?? null,
|
||||
(int)($data['payment_days'] ?? 14),
|
||||
$data['notes'] ?? null,
|
||||
($data['mail_subject_template'] ?? '') !== '' ? trim($data['mail_subject_template']) : null,
|
||||
($data['mail_body_template'] ?? '') !== '' ? trim($data['mail_body_template']) : null,
|
||||
$id,
|
||||
]
|
||||
);
|
||||
|
|
|
|||
|
|
@ -9,10 +9,11 @@ class EmailService
|
|||
/**
|
||||
* Sendet eine Rechnung als PDF-Anhang an alle Empfänger.
|
||||
*
|
||||
* @param string[] $recipients E-Mail-Adressen (1–3)
|
||||
* @param string[] $recipients E-Mail-Adressen (1–3)
|
||||
* @param array|null $customer Kundendatensatz (für kundenspezifische E-Mail-Vorlage)
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function sendInvoice(array $invoice, array $lines, array $settings, array $recipients): void
|
||||
public function sendInvoice(array $invoice, array $lines, array $settings, array $recipients, ?array $customer = null): void
|
||||
{
|
||||
$mailer = $this->buildMailer($settings);
|
||||
|
||||
|
|
@ -28,8 +29,8 @@ class EmailService
|
|||
. "Mit freundlichen Grüßen\n"
|
||||
. "%INHABER%";
|
||||
|
||||
$subjectTpl = ($settings['mail_subject_template'] ?? '') ?: 'Rechnung %RNR%';
|
||||
$bodyTpl = ($settings['mail_body_template'] ?? '') ?: $defaultBody;
|
||||
$subjectTpl = ($customer['mail_subject_template'] ?? '') ?: 'Rechnung %RNR%';
|
||||
$bodyTpl = ($customer['mail_body_template'] ?? '') ?: $defaultBody;
|
||||
|
||||
$subject = $this->replacePlaceholders($subjectTpl, $invoice, $settings);
|
||||
$body = $this->replacePlaceholders($bodyTpl, $invoice, $settings);
|
||||
|
|
|
|||
|
|
@ -224,9 +224,7 @@ $restFormatted = number_format($remaining, 2, ',', '.');
|
|||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Belegnr.</th>
|
||||
<th>Datum</th>
|
||||
<th>Typ</th>
|
||||
<th>Beleg</th>
|
||||
<th>Lieferant</th>
|
||||
<th class="td-right">Brutto</th>
|
||||
<th></th>
|
||||
|
|
@ -239,20 +237,22 @@ $restFormatted = number_format($remaining, 2, ',', '.');
|
|||
data-beleg-id="<?= (int)$b['id'] ?>"
|
||||
style="cursor:<?= $hatBeleg ? 'pointer' : 'default' ?>"
|
||||
<?= $hatBeleg ? 'title="Klicken zum Vorschau anzeigen"' : '' ?>>
|
||||
<td class="td-mono" onclick="event.stopPropagation()">
|
||||
<?php if ($hatBeleg): ?>
|
||||
<a href="/belege/<?= (int)$b['id'] ?>" target="_blank"><?= $h($b['beleg_nr']) ?></a>
|
||||
<?php else: ?>
|
||||
<a href="/invoices/<?= (int)$b['invoice_id'] ?>" target="_blank"><?= $h($b['beleg_nr']) ?></a>
|
||||
<?php endif; ?>
|
||||
<td onclick="event.stopPropagation()" style="white-space:nowrap">
|
||||
<div class="td-mono" style="font-size:.8rem">
|
||||
<?php if ($hatBeleg): ?>
|
||||
<a href="/belege/<?= (int)$b['id'] ?>" target="_blank"><?= $h($b['beleg_nr']) ?></a>
|
||||
<?php else: ?>
|
||||
<a href="/invoices/<?= (int)$b['invoice_id'] ?>" target="_blank"><?= $h($b['beleg_nr']) ?></a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="td-mono text-muted" style="font-size:.78rem"><?= $h(View::formatDate($b['beleg_datum'])) ?></div>
|
||||
<div style="margin-top:.15rem"><span class="badge badge-gray" style="font-size:.7rem"><?= $h($b['belegtyp']) ?></span></div>
|
||||
</td>
|
||||
<td class="td-mono"><?= $h(View::formatDate($b['beleg_datum'])) ?></td>
|
||||
<td><span class="badge badge-gray"><?= $h($b['belegtyp']) ?></span></td>
|
||||
<td>
|
||||
<?= $b['supplier_name'] ? $h($b['supplier_name']) : '<span class="text-muted">–</span>' ?>
|
||||
</td>
|
||||
<td class="td-right td-mono">
|
||||
<?= $b['betrag_brutto'] ? $fmt($b['betrag_brutto']) . ' €' : '<span class="text-muted">–</span>' ?>
|
||||
<td class="td-right td-mono" style="white-space:nowrap">
|
||||
<?= $b['betrag_brutto'] ? $fmt($b['betrag_brutto']) : '<span class="text-muted">–</span>' ?>
|
||||
</td>
|
||||
<td onclick="event.stopPropagation()">
|
||||
<?php if ($hatBeleg): ?>
|
||||
|
|
|
|||
|
|
@ -484,7 +484,7 @@ $suppliers = $suppliers ?? [];
|
|||
<select name="steuerart" id="steuerart_select">
|
||||
<?php
|
||||
$steuerartOptionen = [
|
||||
'normal' => 'Normal (Inland)',
|
||||
'normal' => 'Normal (Inland/Drittland)',
|
||||
'ig_lieferung' => 'Steuerfreie ig. Lieferung § 4 Nr. 1b (Kz 41)',
|
||||
'export' => 'Steuerfreie Ausfuhr § 4 Nr. 1a (Kz 44)',
|
||||
'ig_erwerb_19' => 'Innergemeinschaftlicher Erwerb 19 % – ReverseCharge (Kz 89/91/67)',
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@ $bedOptn = ['neq_zero' => 'wenn ≠ 0', 'always' => 'immer'];
|
|||
<label>Beschreibung</label>
|
||||
<textarea name="beschreibung" rows="2" class="form-control"><?= $h($vorlage['beschreibung'] ?? '') ?></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Standard-Lieferant</label>
|
||||
<input type="text" name="standard_lieferant" value="<?= $h($vorlage['standard_lieferant'] ?? '') ?>"
|
||||
class="form-control" placeholder="Wird im Buchungsdialog vorbelegt">
|
||||
</div>
|
||||
<div style="display:flex;gap:1.5rem">
|
||||
<div class="form-group" style="flex:0 0 auto">
|
||||
<label>Reihenfolge</label>
|
||||
|
|
|
|||
|
|
@ -7,13 +7,17 @@ $gruppe1 = array_filter($felder, fn($f) => $f['gruppe_nr'] == 1);
|
|||
$gruppe2 = array_filter($felder, fn($f) => $f['gruppe_nr'] == 2);
|
||||
$gruppe3 = array_filter($felder, fn($f) => $f['gruppe_nr'] == 3);
|
||||
|
||||
// Vorbelegen aus Beleg-Metadaten
|
||||
// Vorbelegen: Vorlage-Standard-Lieferant hat Vorrang vor Beleg-Lieferant
|
||||
$standardLieferant = trim($vorlage['standard_lieferant'] ?? '');
|
||||
$vorbelegt = [
|
||||
'lieferant' => $beleg['supplier_name'] ?? '',
|
||||
'lieferant' => $standardLieferant !== '' ? $standardLieferant : ($beleg['supplier_name'] ?? ''),
|
||||
'rechnungsnr' => $beleg['rechnungs_nr'] ?? '',
|
||||
'belegdatum' => $beleg['beleg_datum'] ?? date('Y-m-d'),
|
||||
];
|
||||
|
||||
// Zahldatum: Bankbuchungs-Wertstellung, Fallback heute
|
||||
$zahldatum = ($bankTxDatum ?? '') ?: date('Y-m-d');
|
||||
|
||||
// Vorauswahl Zahlungskonto
|
||||
$defaultKontonummer = $hatBankbuchung ? '1200' : '1210';
|
||||
$defaultKontoId = 0;
|
||||
|
|
@ -35,7 +39,10 @@ if (!$defaultKontoId && !empty($zahlungskonten)) {
|
|||
</div>
|
||||
<div class="modal-body" style="padding:1.25rem 1.5rem">
|
||||
<form method="post" action="/belege/<?= $h($belegId) ?>/vorlage/<?= $h($vorlage['id']) ?>/buchen" id="vorlage-buchen-form">
|
||||
<input type="hidden" name="buchungs_datum" value="<?= $h($vorbelegt['belegdatum'] ?: date('Y-m-d')) ?>">
|
||||
<input type="hidden" name="buchungs_datum" id="vorlage-buchungs-datum" value="<?= $h($zahldatum) ?>">
|
||||
<?php if (!empty($bankTxId)): ?>
|
||||
<input type="hidden" name="bank_tx_id" value="<?= $h($bankTxId) ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($gruppe1)): ?>
|
||||
<!-- Gruppe 1: Metadaten -->
|
||||
|
|
@ -50,11 +57,17 @@ if (!$defaultKontoId && !empty($zahlungskonten)) {
|
|||
style="font-weight:600;font-size:.95rem">0,00 €</span>
|
||||
</div>
|
||||
<?php elseif ($feld['typ'] === 'date'): ?>
|
||||
<div class="form-group" style="display:flex;align-items:center;gap:.75rem;margin-bottom:.6rem">
|
||||
<div class="form-group" style="display:flex;align-items:center;gap:.75rem;margin-bottom:.6rem;flex-wrap:wrap">
|
||||
<label style="min-width:130px;margin:0;font-size:.875rem"><?= $h($feld['label']) ?></label>
|
||||
<input type="date" name="<?= $h($feld['feldname']) ?>"
|
||||
value="<?= $h($vorbelegt[$feld['feldname']] ?? '') ?>"
|
||||
style="flex:1;max-width:200px" class="form-control form-control-sm">
|
||||
style="width:150px" class="form-control form-control-sm">
|
||||
<?php if ($feld['feldname'] === 'belegdatum'): ?>
|
||||
<label style="margin:0;font-size:.875rem;white-space:nowrap">Zahldatum</label>
|
||||
<input type="date" id="vorlage-zahldatum"
|
||||
value="<?= $h($zahldatum) ?>"
|
||||
style="width:150px" class="form-control form-control-sm">
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="form-group" style="display:flex;align-items:center;gap:.75rem;margin-bottom:.6rem">
|
||||
|
|
@ -78,7 +91,7 @@ if (!$defaultKontoId && !empty($zahlungskonten)) {
|
|||
<th style="text-align:left;padding:.3rem .5rem;font-weight:500">Steuer</th>
|
||||
<th style="text-align:right;padding:.3rem .5rem;font-weight:500">Netto (€)</th>
|
||||
<th style="text-align:right;padding:.3rem .5rem;font-weight:500">Brutto (€)</th>
|
||||
<th style="text-align:right;padding:.3rem .5rem;font-weight:500">USt (€)</th>
|
||||
<th style="text-align:right;padding:.3rem .5rem;font-weight:500;color:var(--color-text-muted)">USt (€)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -105,11 +118,10 @@ if (!$defaultKontoId && !empty($zahlungskonten)) {
|
|||
style="text-align:right;width:110px"
|
||||
placeholder="0,00">
|
||||
</td>
|
||||
<td style="padding:.3rem .5rem">
|
||||
<input type="number" name="<?= $h($uKey) ?>" step="0.01" value=""
|
||||
class="form-control form-control-sm vorlage-ust"
|
||||
style="text-align:right;width:90px;background:var(--color-bg-subtle)"
|
||||
readonly placeholder="0,00">
|
||||
<td style="padding:.3rem .5rem;text-align:right">
|
||||
<span class="vorlage-ust-anzeige"
|
||||
style="display:inline-block;width:90px;text-align:right;color:var(--color-text-muted);font-size:.875rem">0,00</span>
|
||||
<input type="hidden" name="<?= $h($uKey) ?>" class="vorlage-ust" value="">
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
|
|
|
|||
|
|
@ -82,6 +82,32 @@
|
|||
<textarea id="notes" name="notes"><?= $h($customer['notes'] ?? '') ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:1.25rem;border:1px solid var(--color-border)">
|
||||
<div class="card-header" style="padding:.6rem 1rem">
|
||||
<div class="card-title" style="font-size:.85rem">E-Mail-Vorlage für Rechnungsversand</div>
|
||||
</div>
|
||||
<div class="card-body" style="padding:.75rem 1rem">
|
||||
<p style="font-size:.78rem;color:var(--color-text-muted);margin:0 0 .75rem">
|
||||
Platzhalter: <code>%RNR%</code> = Rechnungsnummer ·
|
||||
<code>%FNAME%</code> = Firmenname ·
|
||||
<code>%INHABER%</code> = Inhaber.
|
||||
Leer lassen = Standard-Vorlage verwenden.
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<label for="mail_subject_template">Betreff</label>
|
||||
<input type="text" id="mail_subject_template" name="mail_subject_template"
|
||||
value="<?= $h($customer['mail_subject_template'] ?? '') ?>"
|
||||
placeholder="Rechnung %RNR%">
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:0">
|
||||
<label for="mail_body_template">Text</label>
|
||||
<textarea id="mail_body_template" name="mail_body_template"
|
||||
rows="6" style="resize:vertical;font-family:inherit;font-size:.85rem"
|
||||
placeholder="Guten Tag, anbei erhalten Sie …"><?= $h($customer['mail_body_template'] ?? '') ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:.75rem;margin-top:1rem">
|
||||
<button type="submit" class="btn btn-primary">Speichern</button>
|
||||
<a href="/customers" class="btn btn-secondary">Abbrechen</a>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
<?php use Steuerkram\View; $h = fn($v) => View::h($v); ?>
|
||||
|
||||
<form id="settings-form" method="post" action="/settings">
|
||||
<input type="hidden" id="hf-mail-subject" name="mail_subject_template"
|
||||
value="<?= $h($settings['mail_subject_template'] ?? 'Rechnung %RNR%') ?>">
|
||||
<input type="hidden" id="hf-mail-body" name="mail_body_template"
|
||||
value="<?= $h($settings['mail_body_template'] ?? '') ?>">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;align-items:start">
|
||||
|
||||
<!-- Firmendaten -->
|
||||
|
|
@ -368,7 +364,6 @@
|
|||
</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:.5rem;flex-wrap:wrap">
|
||||
<button type="button" class="btn btn-secondary" id="btn-mail-text">Text bearbeiten</button>
|
||||
<button type="button" class="btn btn-secondary" id="btn-mail-test">Test-Mail senden</button>
|
||||
<span id="mail-test-result" style="font-size:.8rem"></span>
|
||||
</div>
|
||||
|
|
@ -482,29 +477,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: E-Mail-Text bearbeiten -->
|
||||
<div id="mail-text-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:1000;align-items:center;justify-content:center">
|
||||
<div style="background:#fff;border-radius:8px;padding:1.5rem;width:520px;max-width:95vw;box-shadow:0 8px 32px rgba(0,0,0,.2)">
|
||||
<h3 style="margin:0 0 1rem">E-Mail-Text bearbeiten</h3>
|
||||
<p style="font-size:.78rem;color:#666;margin:0 0 1rem">
|
||||
Platzhalter: <code>%RNR%</code> = Rechnungsnummer · <code>%FNAME%</code> = Firmenname · <code>%INHABER%</code> = Inhaber
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<label>Betreff</label>
|
||||
<input type="text" id="mail-text-subject" style="width:100%"
|
||||
value="<?= $h($settings['mail_subject_template'] ?? 'Rechnung %RNR%') ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Text</label>
|
||||
<textarea id="mail-text-body" rows="8" style="width:100%;resize:vertical;font-family:inherit;font-size:.85rem"><?= $h($settings['mail_body_template'] ?? '') ?></textarea>
|
||||
</div>
|
||||
<div style="display:flex;gap:.75rem;justify-content:flex-end;margin-top:1.25rem">
|
||||
<button type="button" class="btn btn-secondary" id="btn-mail-text-cancel">Abbrechen</button>
|
||||
<button type="button" class="btn btn-primary" id="btn-mail-text-save">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('btn-mail-test')?.addEventListener('click', function () {
|
||||
var btn = this;
|
||||
|
|
@ -564,52 +536,6 @@ function logoPositionChanged() {
|
|||
(pos === 'left' || pos === 'right') ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// E-Mail-Text-Modal
|
||||
document.getElementById('btn-mail-text')?.addEventListener('click', function () {
|
||||
document.getElementById('mail-text-modal').style.display = 'flex';
|
||||
});
|
||||
document.getElementById('btn-mail-text-cancel')?.addEventListener('click', function () {
|
||||
document.getElementById('mail-text-modal').style.display = 'none';
|
||||
});
|
||||
document.getElementById('btn-mail-text-save')?.addEventListener('click', function () {
|
||||
var btn = this;
|
||||
var subject = document.getElementById('mail-text-subject').value.trim();
|
||||
var body = document.getElementById('mail-text-body').value;
|
||||
var result = document.getElementById('mail-test-result');
|
||||
|
||||
if (!subject) {
|
||||
document.getElementById('mail-text-subject').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
var fd = new FormData();
|
||||
fd.append('mail_subject_template', subject);
|
||||
fd.append('mail_body_template', body);
|
||||
|
||||
fetch('/settings/mail-template', { method: 'POST', body: fd })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
if (data.ok) {
|
||||
// Versteckte Felder im Hauptformular synchron halten
|
||||
document.getElementById('hf-mail-subject').value = subject;
|
||||
document.getElementById('hf-mail-body').value = body;
|
||||
document.getElementById('mail-text-modal').style.display = 'none';
|
||||
result.textContent = '✓ E-Mail-Text gespeichert.';
|
||||
result.style.color = 'green';
|
||||
setTimeout(function () { result.textContent = ''; }, 3000);
|
||||
} else {
|
||||
result.textContent = '✗ ' + (data.error || 'Fehler beim Speichern.');
|
||||
result.style.color = 'red';
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
result.textContent = '✗ Netzwerkfehler.';
|
||||
result.style.color = 'red';
|
||||
})
|
||||
.finally(function () { btn.disabled = false; });
|
||||
});
|
||||
|
||||
document.getElementById('btn-generate-app-key')?.addEventListener('click', function () {
|
||||
const btn = this;
|
||||
const result = document.getElementById('app-key-result');
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
1-260528
|
||||
1-260530
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue