Verbessere Mitgliederfilter und Zahlungszuordnung

This commit is contained in:
2026-08-13 15:40:43 +02:00
parent 0f263c3a19
commit a93e067e04
6 changed files with 892 additions and 29 deletions
+192 -5
View File
@@ -1,5 +1,7 @@
<?php
ob_start();
include "functions.php";
require_once __DIR__ . "/app/ledger.php";
require_once __DIR__ . "/app/imports.php";
@@ -103,22 +105,88 @@ function csv_status_label(string $status): string
'unmatched' => 'Mitglied nicht gefunden',
'invalid' => 'Ungültige Zeile',
'imported' => 'Importiert',
'ignored' => 'Ignoriert',
default => $status,
};
}
function csv_status_class(string $status): string
{
return match ($status) {
'matched', 'imported' => 'success',
'duplicate' => 'warning',
'unmatched', 'invalid' => 'error',
'ignored' => 'muted',
default => 'muted',
};
}
function csv_redirect_to_batch(?int $batchId = null): void
{
$target = 'csvupload.php';
if ($batchId !== null && $batchId > 0) {
$target .= '?batch_id=' . urlencode((string)$batchId);
}
header('Location: ' . $target);
exit;
}
function csv_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;
}
$meldung = null;
$fehler = null;
$vorschauBatchId = null;
$vorschauBatchId = isset($_GET['batch_id']) ? (int)$_GET['batch_id'] : null;
if (isset($_SESSION['flash_csvupload']) && is_array($_SESSION['flash_csvupload'])) {
$flash = $_SESSION['flash_csvupload'];
unset($_SESSION['flash_csvupload']);
$meldung = isset($flash['meldung']) ? (string)$flash['meldung'] : null;
$fehler = isset($flash['fehler']) ? (string)$flash['fehler'] : null;
}
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$aktion = $_POST['aktion'] ?? 'hochladen';
$vorschauBatchId = (int)($_POST['batch_id'] ?? 0);
if ($aktion === 'hochladen' && isset($_FILES["csv_file"])) {
[$csvFile, $uploadError] = csv_prepare_upload($_FILES["csv_file"]);
if ($uploadError !== null) {
$fehler = $uploadError;
$_SESSION['flash_csvupload'] = ['fehler' => $fehler];
csv_redirect_to_batch();
} else {
try {
$checksum = hash_file('sha256', $csvFile);
@@ -160,6 +228,7 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
$pdo->commit();
$vorschauBatchId = $batchId;
$meldung = 'CSV-Datei wurde eingelesen. Bitte die Vorschau prüfen.';
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
@@ -168,9 +237,43 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
} finally {
@unlink($csvFile);
}
$_SESSION['flash_csvupload'] = ['meldung' => $meldung, 'fehler' => $fehler];
csv_redirect_to_batch($vorschauBatchId);
}
} elseif ($aktion === 'zeile_zuordnen') {
$rowId = (int)($_POST['row_id'] ?? 0);
$participantId = (int)($_POST['participant_id'] ?? 0);
if ($vorschauBatchId <= 0 || $rowId <= 0 || $participantId <= 0) {
$fehler = 'Bitte eine Importzeile und ein Mitglied auswählen.';
} else {
$result = imports_assign_row($pdo, $tenantId, $vorschauBatchId, $rowId, $participantId);
if ($result['ok']) {
$meldung = 'Importzeile wurde zugeordnet.';
} else {
$fehler = $result['error'] ?? 'Die Importzeile konnte nicht zugeordnet werden.';
}
}
$_SESSION['flash_csvupload'] = ['meldung' => $meldung, 'fehler' => $fehler];
csv_redirect_to_batch($vorschauBatchId);
} elseif ($aktion === 'zeile_ignorieren') {
$rowId = (int)($_POST['row_id'] ?? 0);
if ($vorschauBatchId <= 0 || $rowId <= 0) {
$fehler = 'Bitte eine Importzeile auswählen.';
} else {
$result = imports_ignore_row($pdo, $tenantId, $vorschauBatchId, $rowId);
if ($result['ok']) {
$meldung = 'Importzeile wurde ignoriert.';
} else {
$fehler = $result['error'] ?? 'Die Importzeile konnte nicht ignoriert werden.';
}
}
$_SESSION['flash_csvupload'] = ['meldung' => $meldung, 'fehler' => $fehler];
csv_redirect_to_batch($vorschauBatchId);
} elseif ($aktion === 'importieren') {
$batchId = (int)($_POST['batch_id'] ?? 0);
$batchId = $vorschauBatchId;
try {
$ergebnis = imports_commit_batch($pdo, $tenantId, $batchId);
app_audit_log($pdo, $tenantId, $saasUser['user_id'] ?? null, 'csv_import.committed', 'payment_import_batch', $batchId, ['imported' => $ergebnis['imported']]);
@@ -179,10 +282,22 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
$fehler = $e->getMessage();
$vorschauBatchId = $batchId;
}
$_SESSION['flash_csvupload'] = ['meldung' => $meldung, 'fehler' => $fehler];
csv_redirect_to_batch($vorschauBatchId);
}
}
$vorschau = $vorschauBatchId !== null ? imports_fetch_batch($pdo, $tenantId, $vorschauBatchId) : null;
$letzteBatches = imports_fetch_recent_batches($pdo, $tenantId, 8);
$mitglieder = ledger_fetch_participant_summaries($pdo, $tenantId, ['active_only' => true]);
$vorschauStatusZaehler = [];
if ($vorschau !== null) {
foreach ($vorschau['rows'] as $row) {
$status = (string)$row['status'];
$vorschauStatusZaehler[$status] = ($vorschauStatusZaehler[$status] ?? 0) + 1;
}
}
?>
@@ -197,9 +312,15 @@ $vorschau = $vorschauBatchId !== null ? imports_fetch_batch($pdo, $tenantId, $vo
<?php if ($vorschau !== null): ?>
<h3>Vorschau: <?php echo saas_html($vorschau['batch']['original_filename']); ?></h3>
<p>Status: <?php echo saas_html($vorschau['batch']['status']); ?></p>
<p>Status: <?php echo saas_html($vorschau['batch']['status']); ?> ·
<?php echo (int)($vorschauStatusZaehler['matched'] ?? 0); ?> bereit ·
<?php echo (int)($vorschauStatusZaehler['unmatched'] ?? 0); ?> ohne Zuordnung ·
<?php echo (int)($vorschauStatusZaehler['duplicate'] ?? 0); ?> Duplikatverdacht ·
<?php echo (int)($vorschauStatusZaehler['invalid'] ?? 0); ?> ungültig
</p>
<table>
<div class="table-wrapper">
<table class="admin-table">
<tr>
<th>Zeile</th>
<th>Name (CSV)</th>
@@ -207,18 +328,58 @@ $vorschau = $vorschauBatchId !== null ? imports_fetch_batch($pdo, $tenantId, $vo
<th>Betrag</th>
<th>Datum</th>
<th>Status</th>
<th>Aktion</th>
</tr>
<?php foreach ($vorschau['rows'] as $row): ?>
<?php
$rowStatus = (string)$row['status'];
$suggestions = in_array($rowStatus, ['unmatched', 'duplicate'], true)
? imports_suggest_participants($pdo, $tenantId, (string)$row['raw_name'], null, 4)
: [];
$selectedId = count($suggestions) === 1 ? (int)$suggestions[0]['participant_id'] : 0;
?>
<tr>
<td><?php echo (int)$row['row_num']; ?></td>
<td><?php echo saas_html($row['raw_name']); ?></td>
<td><?php echo saas_html($row['display_name'] ?? '—'); ?></td>
<td><?php echo saas_html(number_format(((int)$row['amount_cents']) / 100, 2, ',', '.')); ?> €</td>
<td><?php echo saas_html($row['booked_at']); ?></td>
<td><?php echo saas_html(csv_status_label($row['status'])); ?></td>
<td><span class="status-badge <?php echo saas_html(csv_status_class($rowStatus)); ?>"><?php echo saas_html(csv_status_label($rowStatus)); ?></span></td>
<td>
<?php if ($vorschau['batch']['status'] === 'previewed' && in_array($rowStatus, ['unmatched', 'duplicate'], true)): ?>
<form method="post" action="csvupload.php" class="inline-admin-form"<?php echo $rowStatus === 'duplicate' ? ' onsubmit="return confirm(\'Diese Zeile wurde als Duplikat erkannt. Trotzdem als neue Einzahlung buchen?\');"' : ''; ?>>
<?php echo app_csrf_field(); ?>
<input type="hidden" name="aktion" value="zeile_zuordnen">
<input type="hidden" name="batch_id" value="<?php echo (int)$vorschau['batch']['id']; ?>">
<input type="hidden" name="row_id" value="<?php echo (int)$row['id']; ?>">
<select name="participant_id" required>
<?php echo csv_select_options($mitglieder, $suggestions, $selectedId); ?>
</select>
<button type="submit">Zuordnen</button>
</form>
<form method="post" action="csvupload.php" class="inline-admin-form" onsubmit="return confirm('Diese Importzeile ignorieren?');">
<?php echo app_csrf_field(); ?>
<input type="hidden" name="aktion" value="zeile_ignorieren">
<input type="hidden" name="batch_id" value="<?php echo (int)$vorschau['batch']['id']; ?>">
<input type="hidden" name="row_id" value="<?php echo (int)$row['id']; ?>">
<button type="submit" class="alt">Ignorieren</button>
</form>
<?php elseif ($vorschau['batch']['status'] === 'previewed' && $rowStatus === 'invalid'): ?>
<form method="post" action="csvupload.php" class="inline-admin-form" onsubmit="return confirm('Diese ungültige Importzeile ignorieren?');">
<?php echo app_csrf_field(); ?>
<input type="hidden" name="aktion" value="zeile_ignorieren">
<input type="hidden" name="batch_id" value="<?php echo (int)$vorschau['batch']['id']; ?>">
<input type="hidden" name="row_id" value="<?php echo (int)$row['id']; ?>">
<button type="submit" class="alt">Ignorieren</button>
</form>
<?php else: ?>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</table>
</div>
<?php if ($vorschau['batch']['status'] === 'previewed'): ?>
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">
@@ -231,6 +392,32 @@ $vorschau = $vorschauBatchId !== null ? imports_fetch_batch($pdo, $tenantId, $vo
<br>
<?php endif; ?>
<?php if ($letzteBatches !== []): ?>
<h3>Letzte Importvorschauen</h3>
<div class="table-wrapper">
<table class="admin-table">
<tr>
<th>Datei</th>
<th>Status</th>
<th>Zeilen</th>
<th>Bereit</th>
<th>Offen</th>
<th></th>
</tr>
<?php foreach ($letzteBatches as $batch): ?>
<tr>
<td><?php echo saas_html($batch['original_filename']); ?><br><small><?php echo saas_html($batch['created_at']); ?></small></td>
<td><?php echo saas_html($batch['status']); ?></td>
<td><?php echo (int)$batch['total_rows']; ?></td>
<td><?php echo (int)$batch['matched_rows'] + (int)$batch['imported_rows']; ?></td>
<td><?php echo (int)$batch['unresolved_rows']; ?></td>
<td><a class="button small" href="csvupload.php?batch_id=<?php echo (int)$batch['id']; ?>">Öffnen</a></td>
</tr>
<?php endforeach; ?>
</table>
</div>
<?php endif; ?>
<form action="csvupload.php" method="post" enctype="multipart/form-data">
<?php echo app_csrf_field(); ?>
<input type="hidden" name="aktion" value="hochladen">