0'; } elseif ($filter['typ'] === 'ausgabe') { $where[] = 'bt.wert < 0'; } } if (!empty($filter['search'])) { $where[] = '(bt.buchungstext LIKE ? OR bt.vorgang LIKE ?)'; $s = '%' . $filter['search'] . '%'; $params[] = $s; $params[] = $s; } if (!empty($filter['format_id'])) { $where[] = 'bib.format_id = ?'; $params[] = (int)$filter['format_id']; } $sortMap = [ 'wertstellung' => 'bt.wertstellung', 'buchungstag' => 'bt.buchungstag', 'betrag' => 'bt.wert', 'buchungstext' => 'bt.buchungstext', 'status' => 'bt.status', ]; $sortCol = $sortMap[$filter['sort'] ?? ''] ?? 'bt.wertstellung'; $sortDir = ($filter['dir'] ?? 'desc') === 'asc' ? 'ASC' : 'DESC'; $whereStr = implode(' AND ', $where); $offset = ($page - 1) * $perPage; $total = (int)(Database::fetchOne( "SELECT COUNT(*) AS n FROM bank_transactions bt JOIN bank_import_batches bib ON bt.batch_id = bib.id WHERE $whereStr", $params )['n'] ?? 0); $rows = Database::fetchAll( "SELECT bt.*, bib.filename AS batch_filename, bif.name AS format_name, bif.kuerzel, bif.farbe, btb_agg.beleg_id AS beleg_id, btb_agg.beleg_count, b.beleg_nr FROM bank_transactions bt JOIN bank_import_batches bib ON bt.batch_id = bib.id LEFT JOIN bank_import_formats bif ON bib.format_id = bif.id LEFT JOIN ( SELECT bank_tx_id, MIN(beleg_id) AS beleg_id, COUNT(*) AS beleg_count FROM bank_transaction_belege GROUP BY bank_tx_id ) btb_agg ON btb_agg.bank_tx_id = bt.id LEFT JOIN belege b ON b.id = btb_agg.beleg_id WHERE $whereStr ORDER BY $sortCol $sortDir, bt.wertstellung $sortDir, bt.buchungstag $sortDir, bt.id $sortDir LIMIT ? OFFSET ?", array_merge($params, [$perPage, $offset]) ); return [ 'rows' => $rows, 'total' => $total, 'pages' => (int)ceil($total / $perPage), ]; } public static function find(int $id): ?array { $row = Database::fetchOne( 'SELECT bt.*, bib.filename AS batch_filename, bif.name AS format_name, bif.kuerzel, bif.farbe FROM bank_transactions bt JOIN bank_import_batches bib ON bt.batch_id = bib.id JOIN bank_import_formats bif ON bib.format_id = bif.id WHERE bt.id = ?', [$id] ); return $row ?: null; } public static function updateStatus(int $id, string $status): void { Database::execute( "UPDATE bank_transactions SET status = ? WHERE id = ?", [$status, $id] ); } /** Verknüpft einen Beleg mit zugeordnetem Betrag und aktualisiert den Status. */ public static function linkBeleg(int $txId, int $belegId, float $betrag): void { Database::execute( 'INSERT INTO bank_transaction_belege (bank_tx_id, beleg_id, betrag) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE betrag = VALUES(betrag)', [$txId, $belegId, $betrag] ); self::recalcStatus($txId); } /** Entfernt eine Beleg-Verknüpfung und aktualisiert den Status. */ public static function unlinkBeleg(int $txId, int $belegId): void { Database::execute( 'DELETE FROM bank_transaction_belege WHERE bank_tx_id = ? AND beleg_id = ?', [$txId, $belegId] ); self::recalcStatus($txId); } /** Berechnet Status neu: 0 Links→offen, Restsaldo>0→teilweise, Restsaldo=0→gebucht. */ public static function recalcStatus(int $txId): void { $row = Database::fetchOne( 'SELECT ABS(bt.wert) AS brutto, COALESCE(SUM(btb.betrag), 0) AS allocated FROM bank_transactions bt LEFT JOIN bank_transaction_belege btb ON btb.bank_tx_id = bt.id WHERE bt.id = ? GROUP BY bt.id', [$txId] ); if (!$row) return; $remaining = round((float)$row['brutto'] - (float)$row['allocated'], 2); $allocated = (float)$row['allocated']; if ($allocated <= 0) { $status = 'offen'; } elseif ($remaining > 0.01) { $status = 'teilweise'; } else { $status = 'gebucht'; } Database::execute( "UPDATE bank_transactions SET status = ? WHERE id = ?", [$status, $txId] ); } /** Gibt alle verknüpften Belege mit ihren Beträgen zurück. */ public static function getLinkedBelege(int $txId): array { return Database::fetchAll( 'SELECT btb.betrag, b.id AS beleg_id, b.beleg_nr, b.beleg_datum, b.belegtyp, b.betrag_brutto, s.name AS supplier_name FROM bank_transaction_belege btb JOIN belege b ON b.id = btb.beleg_id LEFT JOIN suppliers s ON s.id = b.supplier_id WHERE btb.bank_tx_id = ? ORDER BY b.beleg_nr', [$txId] ); } /** Gibt den noch nicht zugeordneten Restbetrag zurück. */ public static function getRemainingBetrag(int $txId): float { $row = Database::fetchOne( 'SELECT ABS(bt.wert) AS brutto, COALESCE(SUM(btb.betrag), 0) AS allocated FROM bank_transactions bt LEFT JOIN bank_transaction_belege btb ON btb.bank_tx_id = bt.id WHERE bt.id = ? GROUP BY bt.id', [$txId] ); if (!$row) return 0.0; return round(max(0.0, (float)$row['brutto'] - (float)$row['allocated']), 2); } /** Sucht die Bankbuchung, die mit einem bestimmten Beleg verknüpft ist. */ public static function findByBelegId(int $belegId): ?array { $row = Database::fetchOne( 'SELECT bt.*, bib.filename AS batch_filename, bif.kuerzel, bif.farbe, btb.betrag AS alloc_betrag FROM bank_transaction_belege btb JOIN bank_transactions bt ON bt.id = btb.bank_tx_id JOIN bank_import_batches bib ON bib.id = bt.batch_id JOIN bank_import_formats bif ON bif.id = bib.format_id WHERE btb.beleg_id = ? LIMIT 1', [$belegId] ); return $row ?: null; } /** * Teilt $rows in ['new' => [...], 'dup' => [...]] auf. */ public static function findDuplicates(array $rows): array { if (empty($rows)) { return ['new' => [], 'dup' => []]; } $placeholders = implode(',', array_fill(0, count($rows), '(?,?,?)')); $params = []; foreach ($rows as $r) { $params[] = $r['buchungstag']; $params[] = $r['wertstellung']; $params[] = (string)$r['wert']; } $existing = Database::fetchAll( "SELECT buchungstag, wertstellung, CAST(wert AS CHAR) AS wert FROM bank_transactions WHERE (buchungstag, wertstellung, CAST(wert AS CHAR)) IN ($placeholders)", $params ); $lookup = []; foreach ($existing as $e) { $lookup[$e['buchungstag'] . '|' . $e['wertstellung'] . '|' . $e['wert']] = true; } $new = []; $dup = []; foreach ($rows as $row) { $key = $row['buchungstag'] . '|' . $row['wertstellung'] . '|' . (string)$row['wert']; if (isset($lookup[$key])) { $dup[] = $row; } else { $new[] = $row; } } return ['new' => $new, 'dup' => $dup]; } public static function createBatch(int $formatId, string $filename, int $rowCount): int { Database::execute( 'INSERT INTO bank_import_batches (format_id, filename, row_count) VALUES (?, ?, ?)', [$formatId, $filename, $rowCount] ); return (int)Database::lastInsertId(); } public static function insertRow(int $batchId, array $data): void { Database::execute( 'INSERT INTO bank_transactions (batch_id, buchungstag, wertstellung, vorgang, buchungstext, wert, raw_line) VALUES (?, ?, ?, ?, ?, ?, ?)', [ $batchId, $data['buchungstag'], $data['wertstellung'], $data['vorgang'], $data['buchungstext'], $data['wert'], $data['raw_line'] ?? null, ] ); } }