Bisher blendete das Menue nur aus, was der Betreiber gesperrt hatte. Hat der Mandant selbst eine Funktion abgeschaltet - etwa "PayPal anbieten" in den Mandant-Einstellungen - blieb der Menuepunkt stehen und fuehrte auf eine Seite ohne Zweck. app_feature_available() prueft nun beide Ebenen, app_feature_tenant_switch() haelt die Zuordnung Funktion -> Mandanten-Schalter. Navigation und Anleitung nutzen die neue Pruefung; paypal-zuordnung.php sperrt sich beim direkten Aufruf ebenfalls und verweist dabei auf die Mandant-Einstellungen statt auf den Betreiber. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
235 lines
9.1 KiB
PHP
235 lines
9.1 KiB
PHP
<?php
|
|
|
|
include "functions.php";
|
|
require_once __DIR__ . "/app/ledger.php";
|
|
require_once __DIR__ . "/app/paypal-inbox.php";
|
|
require_once __DIR__ . "/app/features.php";
|
|
app_require_csrf();
|
|
|
|
$pdo = app_db_pdo();
|
|
$saasUser = saas_current_user($pdo);
|
|
$tenantId = 0;
|
|
$hasAccess = false;
|
|
|
|
if ($saasUser !== null && saas_user_has_role(['owner', 'admin', 'treasurer'], $saasUser)) {
|
|
$tenantId = (int)$saasUser['tenant_id'];
|
|
$hasAccess = true;
|
|
}
|
|
|
|
// Gesperrte Funktion: $hasAccess faellt zurueck, damit auch die
|
|
// POST-Verarbeitung weiter unten nicht mehr laeuft. Der Menuepunkt ist in
|
|
// diesem Fall ausgeblendet - die Seite bleibt aber ueber die direkte URL
|
|
// erreichbar und muss deshalb selbst pruefen.
|
|
$sperrHinweis = null;
|
|
if ($hasAccess && !app_feature_enabled($pdo, $tenantId, 'paypal_inbox')) {
|
|
// Der Betreiber hat die Funktion fuer diesen Mandanten nicht freigeschaltet.
|
|
$sperrHinweis = app_feature_notice_html('paypal_inbox');
|
|
$hasAccess = false;
|
|
} elseif ($hasAccess && !app_feature_available($pdo, $tenantId, 'paypal_inbox')) {
|
|
// Der Mandant selbst bietet PayPal nicht als Zahlungsweg an.
|
|
$sperrHinweis = app_feature_tenant_notice_html('paypal_inbox');
|
|
$hasAccess = false;
|
|
}
|
|
|
|
$flash = null;
|
|
if ($hasAccess && isset($_SESSION['flash_paypal'])) {
|
|
$flash = $_SESSION['flash_paypal'];
|
|
unset($_SESSION['flash_paypal']);
|
|
}
|
|
|
|
$suchbegriff = trim((string)($_SERVER['REQUEST_METHOD'] === 'POST' ? ($_POST['return_q'] ?? '') : ($_GET['q'] ?? '')));
|
|
$returnQuery = $suchbegriff !== '' ? '?q=' . urlencode($suchbegriff) : '';
|
|
|
|
function paypal_select_options(array $members, array $suggestions = [], int $selectedId = 0): string
|
|
{
|
|
$suggestedIds = [];
|
|
foreach ($suggestions as $suggestion) {
|
|
$suggestedIds[(int)$suggestion['participant_id']] = true;
|
|
}
|
|
|
|
$html = '<option value="">- Mitglied wählen -</option>';
|
|
if ($suggestions !== []) {
|
|
$html .= '<optgroup label="Vorschläge">';
|
|
foreach ($suggestions as $suggestion) {
|
|
$id = (int)$suggestion['participant_id'];
|
|
$html .= '<option value="' . $id . '"' . ($selectedId === $id ? ' selected' : '') . '>'
|
|
. saas_html((string)$suggestion['display_name'])
|
|
. '</option>';
|
|
}
|
|
$html .= '</optgroup>';
|
|
}
|
|
|
|
$html .= '<optgroup label="Alle aktiven Mitglieder">';
|
|
foreach ($members as $member) {
|
|
$id = (int)$member['participant_id'];
|
|
if (isset($suggestedIds[$id])) {
|
|
continue;
|
|
}
|
|
$html .= '<option value="' . $id . '"' . ($selectedId === $id ? ' selected' : '') . '>'
|
|
. saas_html((string)$member['display_name'])
|
|
. '</option>';
|
|
}
|
|
$html .= '</optgroup>';
|
|
|
|
return $html;
|
|
}
|
|
|
|
function paypal_payment_matches_query(array $payment, string $query): bool
|
|
{
|
|
$query = imports_normalize_match_text($query);
|
|
if ($query === '') {
|
|
return true;
|
|
}
|
|
|
|
$text = imports_normalize_match_text(
|
|
(string)$payment['payer_name'] . ' '
|
|
. (string)($payment['note'] ?? '') . ' '
|
|
. (string)($payment['transaction_code'] ?? '')
|
|
);
|
|
|
|
return str_contains($text, $query);
|
|
}
|
|
|
|
if ($hasAccess && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
$aktion = (string)($_POST['aktion'] ?? '');
|
|
$paymentId = (int)($_POST['payment_id'] ?? 0);
|
|
$actorUserId = $saasUser['user_id'] ?? null;
|
|
|
|
if ($aktion === 'zuordnen') {
|
|
$participantId = (int)($_POST['participant_id'] ?? 0);
|
|
if ($participantId <= 0) {
|
|
$flash = ['type' => 'error', 'text' => 'Bitte ein Mitglied auswählen.'];
|
|
} else {
|
|
$result = paypal_assign_payment($pdo, $tenantId, $paymentId, $participantId, $actorUserId);
|
|
$flash = $result['ok']
|
|
? ['type' => 'success', 'text' => 'Zahlung wurde zugeordnet und als Einzahlung gebucht.']
|
|
: ['type' => 'error', 'text' => $result['error'] ?? 'Die Zuordnung ist fehlgeschlagen.'];
|
|
}
|
|
} elseif ($aktion === 'ignorieren') {
|
|
$result = paypal_ignore_payment($pdo, $tenantId, $paymentId, $actorUserId);
|
|
$flash = $result['ok']
|
|
? ['type' => 'success', 'text' => 'Zahlung wurde als erledigt markiert.']
|
|
: ['type' => 'error', 'text' => $result['error'] ?? 'Die Aktion ist fehlgeschlagen.'];
|
|
}
|
|
|
|
// Post-Redirect-Get gegen Doppelbuchung per Refresh.
|
|
$_SESSION['flash_paypal'] = $flash;
|
|
header('Location: paypal-zuordnung.php' . $returnQuery);
|
|
exit;
|
|
}
|
|
|
|
$inboxAddress = null;
|
|
$offene = [];
|
|
$mitglieder = [];
|
|
if ($hasAccess) {
|
|
$token = paypal_inbox_ensure_token($pdo, $tenantId);
|
|
$inboxAddress = paypal_inbox_address_for_token($token);
|
|
$offene = paypal_fetch_unmatched($pdo, $tenantId);
|
|
$mitglieder = ledger_fetch_participant_summaries($pdo, $tenantId, ['active_only' => true]);
|
|
}
|
|
$offeneGefiltert = array_values(array_filter($offene, static fn(array $payment): bool => paypal_payment_matches_query($payment, $suchbegriff)));
|
|
|
|
include "header.php";
|
|
include "headerline.php";
|
|
include "nav.php";
|
|
?>
|
|
|
|
<section id="banner">
|
|
<div class="content">
|
|
|
|
<?php if ($sperrHinweis !== null): ?>
|
|
<?php echo $sperrHinweis; ?>
|
|
<?php elseif (!$hasAccess): ?>
|
|
<h2>Kein Zugriff</h2>
|
|
<?php else: ?>
|
|
|
|
<h2>PayPal-Zahlungen zuordnen</h2>
|
|
|
|
<?php if ($flash !== null): ?>
|
|
<div class="hint-box <?php echo $flash['type'] === 'success' ? 'success' : 'error'; ?>"><p><?php echo saas_html($flash['text']); ?></p></div>
|
|
<?php endif; ?>
|
|
|
|
<h3>So richtet ihr die automatische Verbuchung ein</h3>
|
|
<?php if ($inboxAddress !== null): ?>
|
|
<p>Leitet die PayPal-Benachrichtigungsmails („Du hast eine Zahlung erhalten") automatisch an eure persönliche Eingangsadresse weiter:</p>
|
|
<p><b><?php echo saas_html($inboxAddress); ?></b></p>
|
|
<p>Eingehende Zahlungen werden dann automatisch dem passenden Mitglied gutgeschrieben (gebuchter Betrag = tatsächlich eingegangener Betrag nach PayPal-Gebühr). Zahlungen, die sich nicht eindeutig zuordnen lassen, erscheinen unten zur manuellen Zuordnung.</p>
|
|
<p><small>Tipp: Am zuverlässigsten trefft ihr die Zuordnung, wenn beim Mitglied der PayPal-Name hinterlegt ist (unter „Mitglieder verwalten").</small></p>
|
|
<?php else: ?>
|
|
<div class="hint-box error"><p>Die zentrale Eingangsadresse ist noch nicht konfiguriert. Bitte wende dich an den Betreiber (Server-Einstellung <code>PAYPAL_INBOX_BASE</code>).</p></div>
|
|
<?php endif; ?>
|
|
|
|
<h3>Offene Zahlungen (<?php echo count($offene); ?>)</h3>
|
|
<form method="get" action="paypal-zuordnung.php" class="admin-filter-bar">
|
|
<label for="paypal-q">Suche</label>
|
|
<input type="search" name="q" id="paypal-q" value="<?php echo saas_html($suchbegriff); ?>" placeholder="Zahler, Mitteilung oder Transaktion">
|
|
<button type="submit">Filtern</button>
|
|
<a class="button alt" href="paypal-zuordnung.php">Zurücksetzen</a>
|
|
</form>
|
|
<?php if ($offene === []): ?>
|
|
<p>Keine offenen Zahlungen. Alles automatisch zugeordnet.</p>
|
|
<?php elseif ($offeneGefiltert === []): ?>
|
|
<p>Keine offenen Zahlungen für diesen Filter gefunden.</p>
|
|
<?php else: ?>
|
|
<p><?php echo count($offeneGefiltert); ?> von <?php echo count($offene); ?> offenen Zahlungen angezeigt.</p>
|
|
<div class="table-wrapper">
|
|
<table class="admin-table">
|
|
<tr><th>Eingang</th><th>Zahlung</th><th>Betrag</th><th>Vorschläge</th><th>Zuordnen</th><th></th></tr>
|
|
<?php foreach ($offeneGefiltert as $z): ?>
|
|
<?php
|
|
$suggestions = imports_suggest_participants($pdo, $tenantId, (string)$z['payer_name'], (string)($z['note'] ?? ''), 4);
|
|
$selectedId = count($suggestions) === 1 ? (int)$suggestions[0]['participant_id'] : 0;
|
|
?>
|
|
<tr>
|
|
<td><?php echo saas_html((string)($z['paid_at'] ?? $z['created_at'])); ?></td>
|
|
<td>
|
|
<strong><?php echo saas_html($z['payer_name']); ?></strong>
|
|
<?php if (($z['note'] ?? '') !== null && trim((string)$z['note']) !== ''): ?>
|
|
<br><small><?php echo saas_html((string)$z['note']); ?></small>
|
|
<?php endif; ?>
|
|
<br><small>Transaktion: <?php echo saas_html((string)$z['transaction_code']); ?></small>
|
|
</td>
|
|
<td><?php echo saas_html(saas_format_money_cents((int)$z['net_cents'])); ?> €</td>
|
|
<td>
|
|
<?php if ($suggestions === []): ?>
|
|
<span class="status-badge muted">Kein Vorschlag</span>
|
|
<?php else: ?>
|
|
<?php foreach ($suggestions as $suggestion): ?>
|
|
<span class="status-badge info"><?php echo saas_html((string)$suggestion['display_name']); ?></span>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td>
|
|
<form method="post" action="paypal-zuordnung.php" class="inline-admin-form">
|
|
<?php echo app_csrf_field(); ?>
|
|
<input type="hidden" name="aktion" value="zuordnen">
|
|
<input type="hidden" name="payment_id" value="<?php echo (int)$z['id']; ?>">
|
|
<input type="hidden" name="return_q" value="<?php echo saas_html($suchbegriff); ?>">
|
|
<select name="participant_id" required>
|
|
<?php echo paypal_select_options($mitglieder, $suggestions, $selectedId); ?>
|
|
</select>
|
|
<button type="submit">Buchen</button>
|
|
</form>
|
|
</td>
|
|
<td>
|
|
<form method="post" action="paypal-zuordnung.php" style="margin:0" onsubmit="return confirm('Diese Zahlung als erledigt/ignoriert markieren?');">
|
|
<?php echo app_csrf_field(); ?>
|
|
<input type="hidden" name="aktion" value="ignorieren">
|
|
<input type="hidden" name="payment_id" value="<?php echo (int)$z['id']; ?>">
|
|
<input type="hidden" name="return_q" value="<?php echo saas_html($suchbegriff); ?>">
|
|
<button type="submit" class="alt">Ignorieren</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</table>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php endif; ?>
|
|
|
|
</div>
|
|
</section>
|
|
|
|
<?php include "footer.php"; ?>
|