Verbessere Mitgliederfilter und Zahlungszuordnung
This commit is contained in:
+189
@@ -56,6 +56,99 @@ function imports_find_participant(PDO $pdo, int $tenantId, string $name): ?array
|
||||
];
|
||||
}
|
||||
|
||||
function imports_normalize_match_text(string $value): string
|
||||
{
|
||||
$value = trim($value);
|
||||
$value = preg_replace('/\s+/', ' ', $value) ?? $value;
|
||||
|
||||
return function_exists('mb_strtolower')
|
||||
? mb_strtolower($value, 'UTF-8')
|
||||
: strtolower($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert wahrscheinliche Teilnehmer fuer eine manuelle Zuordnung. Die
|
||||
* automatische Buchung bleibt strenger; diese Vorschlaege sind nur eine Hilfe
|
||||
* fuer Admins/Kassenwarte in CSV- und PayPal-Warteschlangen.
|
||||
*
|
||||
* @return list<array{participant_id:int, display_name:string, email:?string, paypal_name:?string, score:int}>
|
||||
*/
|
||||
function imports_suggest_participants(PDO $pdo, int $tenantId, string $rawName, ?string $note = null, int $limit = 5): array
|
||||
{
|
||||
$haystack = imports_normalize_match_text(trim($rawName . ' ' . (string)$note));
|
||||
if ($haystack === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$tokens = array_values(array_unique(array_filter(
|
||||
preg_split('/\s+/', $haystack) ?: [],
|
||||
static fn(string $token): bool => strlen($token) >= 3
|
||||
)));
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT id, display_name, email, paypal_name
|
||||
FROM participants
|
||||
WHERE tenant_id = ? AND active = 1
|
||||
ORDER BY display_name'
|
||||
);
|
||||
$stmt->execute([$tenantId]);
|
||||
|
||||
$candidates = [];
|
||||
foreach ($stmt->fetchAll() as $row) {
|
||||
$displayName = (string)$row['display_name'];
|
||||
$paypalName = (string)($row['paypal_name'] ?? '');
|
||||
$email = (string)($row['email'] ?? '');
|
||||
$displayNorm = imports_normalize_match_text($displayName);
|
||||
$paypalNorm = imports_normalize_match_text($paypalName);
|
||||
$emailNorm = imports_normalize_match_text($email);
|
||||
$score = 0;
|
||||
|
||||
if ($displayNorm !== '' && $displayNorm === imports_normalize_match_text($rawName)) {
|
||||
$score += 90;
|
||||
}
|
||||
if ($paypalNorm !== '' && $paypalNorm === imports_normalize_match_text($rawName)) {
|
||||
$score += 100;
|
||||
}
|
||||
if ($displayNorm !== '' && str_contains($haystack, $displayNorm)) {
|
||||
$score += 45;
|
||||
}
|
||||
if ($paypalNorm !== '' && str_contains($haystack, $paypalNorm)) {
|
||||
$score += 55;
|
||||
}
|
||||
|
||||
foreach ($tokens as $token) {
|
||||
if ($displayNorm !== '' && str_contains($displayNorm, $token)) {
|
||||
$score += 12;
|
||||
}
|
||||
if ($paypalNorm !== '' && str_contains($paypalNorm, $token)) {
|
||||
$score += 16;
|
||||
}
|
||||
if ($emailNorm !== '' && str_contains($emailNorm, $token)) {
|
||||
$score += 6;
|
||||
}
|
||||
}
|
||||
|
||||
if ($score <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$candidates[] = [
|
||||
'participant_id' => (int)$row['id'],
|
||||
'display_name' => $displayName,
|
||||
'email' => $row['email'] !== null ? (string)$row['email'] : null,
|
||||
'paypal_name' => $row['paypal_name'] !== null ? (string)$row['paypal_name'] : null,
|
||||
'score' => $score,
|
||||
];
|
||||
}
|
||||
|
||||
usort($candidates, static function (array $a, array $b): int {
|
||||
return $b['score'] <=> $a['score']
|
||||
?: strcasecmp((string)$a['display_name'], (string)$b['display_name']);
|
||||
});
|
||||
|
||||
return array_slice($candidates, 0, max(1, $limit));
|
||||
}
|
||||
|
||||
/**
|
||||
* A payment counts as a duplicate if the same participant already has a
|
||||
* non-voided ledger payment of the same amount on the same day.
|
||||
@@ -188,6 +281,102 @@ function imports_fetch_batch(PDO $pdo, int $tenantId, int $batchId): ?array
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{id:int, original_filename:string, status:string, created_at:string, committed_at:?string, total_rows:int, matched_rows:int, unresolved_rows:int, imported_rows:int, ignored_rows:int}>
|
||||
*/
|
||||
function imports_fetch_recent_batches(PDO $pdo, int $tenantId, int $limit = 10): array
|
||||
{
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT b.id, b.original_filename, b.status, b.created_at, b.committed_at,
|
||||
COUNT(r.id) AS total_rows,
|
||||
SUM(CASE WHEN r.status = 'matched' THEN 1 ELSE 0 END) AS matched_rows,
|
||||
SUM(CASE WHEN r.status IN ('unmatched', 'duplicate', 'invalid') THEN 1 ELSE 0 END) AS unresolved_rows,
|
||||
SUM(CASE WHEN r.status = 'imported' THEN 1 ELSE 0 END) AS imported_rows,
|
||||
SUM(CASE WHEN r.status = 'ignored' THEN 1 ELSE 0 END) AS ignored_rows
|
||||
FROM payment_import_batches b
|
||||
LEFT JOIN payment_import_rows r ON r.batch_id = b.id
|
||||
WHERE b.tenant_id = ?
|
||||
GROUP BY b.id, b.original_filename, b.status, b.created_at, b.committed_at
|
||||
ORDER BY b.created_at DESC, b.id DESC
|
||||
LIMIT " . max(1, min(50, $limit))
|
||||
);
|
||||
$stmt->execute([$tenantId]);
|
||||
|
||||
return array_map(static function (array $row): array {
|
||||
return [
|
||||
'id' => (int)$row['id'],
|
||||
'original_filename' => (string)$row['original_filename'],
|
||||
'status' => (string)$row['status'],
|
||||
'created_at' => (string)$row['created_at'],
|
||||
'committed_at' => $row['committed_at'] !== null ? (string)$row['committed_at'] : null,
|
||||
'total_rows' => (int)$row['total_rows'],
|
||||
'matched_rows' => (int)$row['matched_rows'],
|
||||
'unresolved_rows' => (int)$row['unresolved_rows'],
|
||||
'imported_rows' => (int)$row['imported_rows'],
|
||||
'ignored_rows' => (int)$row['ignored_rows'],
|
||||
];
|
||||
}, $stmt->fetchAll());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{ok:bool, error?:string}
|
||||
*/
|
||||
function imports_assign_row(PDO $pdo, int $tenantId, int $batchId, int $rowId, int $participantId): array
|
||||
{
|
||||
$batch = imports_fetch_batch($pdo, $tenantId, $batchId);
|
||||
if ($batch === null || $batch['batch']['status'] !== 'previewed') {
|
||||
return ['ok' => false, 'error' => 'Dieser Import kann nicht mehr bearbeitet werden.'];
|
||||
}
|
||||
|
||||
$summaries = ledger_fetch_participant_summaries($pdo, $tenantId, ['participant_ids' => [$participantId]]);
|
||||
$participant = $summaries[0] ?? null;
|
||||
if ($participant === null || empty($participant['active'])) {
|
||||
return ['ok' => false, 'error' => 'Das gewählte Mitglied wurde nicht gefunden.'];
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
"UPDATE payment_import_rows r
|
||||
JOIN payment_import_batches b ON b.id = r.batch_id
|
||||
SET r.participant_id = ?, r.status = 'matched'
|
||||
WHERE r.id = ?
|
||||
AND r.batch_id = ?
|
||||
AND b.tenant_id = ?
|
||||
AND b.status = 'previewed'
|
||||
AND r.status IN ('unmatched', 'duplicate')"
|
||||
);
|
||||
$stmt->execute([$participantId, $rowId, $batchId, $tenantId]);
|
||||
|
||||
if ($stmt->rowCount() !== 1) {
|
||||
return ['ok' => false, 'error' => 'Diese Zeile kann nicht zugeordnet werden.'];
|
||||
}
|
||||
|
||||
return ['ok' => true];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{ok:bool, error?:string}
|
||||
*/
|
||||
function imports_ignore_row(PDO $pdo, int $tenantId, int $batchId, int $rowId): array
|
||||
{
|
||||
$stmt = $pdo->prepare(
|
||||
"UPDATE payment_import_rows r
|
||||
JOIN payment_import_batches b ON b.id = r.batch_id
|
||||
SET r.participant_id = NULL, r.status = 'ignored'
|
||||
WHERE r.id = ?
|
||||
AND r.batch_id = ?
|
||||
AND b.tenant_id = ?
|
||||
AND b.status = 'previewed'
|
||||
AND r.status IN ('unmatched', 'duplicate', 'invalid')"
|
||||
);
|
||||
$stmt->execute([$rowId, $batchId, $tenantId]);
|
||||
|
||||
if ($stmt->rowCount() !== 1) {
|
||||
return ['ok' => false, 'error' => 'Diese Zeile kann nicht ignoriert werden.'];
|
||||
}
|
||||
|
||||
return ['ok' => true];
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits every 'matched' row of a previewed batch straight into the ledger.
|
||||
* Already-committed batches are rejected.
|
||||
|
||||
Reference in New Issue
Block a user