Merge pull request 'Feature: UI: Drag-and-Drop Reihenfolge der Direktverbindungs-Kacheln' (#80) from fds2610-bot/Steuerkram:main into main
Reviewed-on: https://codeberg.org/fds2610/Steuerkram/pulls/80
This commit is contained in:
commit
86ab3b6152
5 changed files with 168 additions and 4 deletions
7
migrations/071_bank_sources_sort_order.sql
Normal file
7
migrations/071_bank_sources_sort_order.sql
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Sortierreihenfolge der Bank-Importquellen für das Kachelraster.
|
||||||
|
-- Initial = id, damit Bestandsdaten ihre bisherige Reihenfolge behalten.
|
||||||
|
|
||||||
|
ALTER TABLE bank_sources
|
||||||
|
ADD COLUMN IF NOT EXISTS sort_order INT NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
UPDATE bank_sources SET sort_order = id WHERE sort_order = 0;
|
||||||
|
|
@ -68,6 +68,7 @@ $router->post('/belege/{id}/vorschlag/reset', fn($p) => (new BelegController(
|
||||||
$router->get('/bank', fn($p) => (new BankController())->index($p));
|
$router->get('/bank', fn($p) => (new BankController())->index($p));
|
||||||
$router->get('/bank/direkt', fn($p) => (new BankController())->direkt($p));
|
$router->get('/bank/direkt', fn($p) => (new BankController())->direkt($p));
|
||||||
$router->post('/bank/direkt', fn($p) => (new BankController())->direktStore($p));
|
$router->post('/bank/direkt', fn($p) => (new BankController())->direktStore($p));
|
||||||
|
$router->post('/bank/direkt/_order', fn($p) => (new BankController())->direktOrder($p));
|
||||||
$router->get('/bank/direkt/{id}/run', fn($p) => (new BankController())->direktRun($p));
|
$router->get('/bank/direkt/{id}/run', fn($p) => (new BankController())->direktRun($p));
|
||||||
$router->post('/bank/direkt/{id}', fn($p) => (new BankController())->direktUpdate($p));
|
$router->post('/bank/direkt/{id}', fn($p) => (new BankController())->direktUpdate($p));
|
||||||
$router->post('/bank/direkt/{id}/delete', fn($p) => (new BankController())->direktDelete($p));
|
$router->post('/bank/direkt/{id}/delete', fn($p) => (new BankController())->direktDelete($p));
|
||||||
|
|
|
||||||
|
|
@ -550,6 +550,22 @@ class BankController extends BaseController
|
||||||
View::redirect('/bank/direkt', 'Verbindung gelöscht.');
|
View::redirect('/bank/direkt', 'Verbindung gelöscht.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Speichert die per Drag-and-Drop geänderte Reihenfolge der Direktverbindungs-Kacheln. */
|
||||||
|
public function direktOrder(array $params): void
|
||||||
|
{
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$raw = $_POST['ids'] ?? '[]';
|
||||||
|
$ids = is_array($raw) ? $raw : (json_decode((string)$raw, true) ?: []);
|
||||||
|
if (!is_array($ids)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Ungültige Reihenfolge.']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BankSource::updateOrder(array_map('intval', $ids));
|
||||||
|
echo json_encode(['ok' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
/** Nimmt die im Browser eingegebene TAN für einen wartenden FinTS-Abruf entgegen. */
|
/** Nimmt die im Browser eingegebene TAN für einen wartenden FinTS-Abruf entgegen. */
|
||||||
public function direktTan(array $params): void
|
public function direktTan(array $params): void
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ class BankSource
|
||||||
$sql .= ' WHERE bs.type = ?';
|
$sql .= ' WHERE bs.type = ?';
|
||||||
$params[] = $type;
|
$params[] = $type;
|
||||||
}
|
}
|
||||||
$sql .= ' ORDER BY bs.type, bs.bezeichnung';
|
$sql .= ' ORDER BY bs.sort_order, bs.type, bs.bezeichnung';
|
||||||
$rows = Database::fetchAll($sql, $params);
|
$rows = Database::fetchAll($sql, $params);
|
||||||
return array_map([self::class, 'hydrate'], $rows);
|
return array_map([self::class, 'hydrate'], $rows);
|
||||||
}
|
}
|
||||||
|
|
@ -90,6 +90,19 @@ class BankSource
|
||||||
Database::execute('UPDATE bank_sources SET last_fetched_at = NOW() WHERE id = ?', [$id]);
|
Database::execute('UPDATE bank_sources SET last_fetched_at = NOW() WHERE id = ?', [$id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Aktualisiert sort_order anhand der übergebenen ID-Reihenfolge (Position 0..N). */
|
||||||
|
public static function updateOrder(array $orderedIds): void
|
||||||
|
{
|
||||||
|
foreach (array_values($orderedIds) as $pos => $id) {
|
||||||
|
$id = (int)$id;
|
||||||
|
if ($id <= 0) continue;
|
||||||
|
Database::execute(
|
||||||
|
'UPDATE bank_sources SET sort_order = ? WHERE id = ?',
|
||||||
|
[$pos, $id]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static function hydrate(array $row): array
|
private static function hydrate(array $row): array
|
||||||
{
|
{
|
||||||
$row['name'] = $row['bezeichnung'];
|
$row['name'] = $row['bezeichnung'];
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,29 @@ $connectionsJson = json_encode(array_map(fn($c) => [
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:1rem">
|
<style>
|
||||||
|
#connection-grid .card.drop-before::before,
|
||||||
|
#connection-grid .card.drop-after::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: .25rem;
|
||||||
|
bottom: .25rem;
|
||||||
|
width: 4px;
|
||||||
|
background: #6366f1;
|
||||||
|
border-radius: 2px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
#connection-grid .card.drop-before::before { left: -10px; }
|
||||||
|
#connection-grid .card.drop-after::after { right: -10px; }
|
||||||
|
</style>
|
||||||
|
<div id="connection-grid" style="display:grid;grid-template-columns:repeat(3,1fr);gap:1rem">
|
||||||
<?php foreach ($connections as $conn): ?>
|
<?php foreach ($connections as $conn): ?>
|
||||||
<div class="card btn-run-connection" data-id="<?= (int)$conn['id'] ?>" style="margin:0;cursor:pointer" title="Abruf starten">
|
<div class="card btn-run-connection" data-id="<?= (int)$conn['id'] ?>" style="margin:0;cursor:pointer;position:relative" title="Abruf starten">
|
||||||
|
<span class="drag-handle" draggable="true"
|
||||||
|
title="Ziehen zum Sortieren"
|
||||||
|
style="position:absolute;top:.4rem;left:.4rem;color:var(--color-muted);cursor:grab;user-select:none;padding:.15rem .35rem;font-size:.95rem;line-height:1">⠿</span>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:.5rem">
|
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:.5rem;padding-left:1rem">
|
||||||
<div>
|
<div>
|
||||||
<div style="display:flex;align-items:center;gap:.5rem;flex-wrap:wrap">
|
<div style="display:flex;align-items:center;gap:.5rem;flex-wrap:wrap">
|
||||||
<?php if (!empty($conn['kuerzel'])): ?>
|
<?php if (!empty($conn['kuerzel'])): ?>
|
||||||
|
|
@ -574,9 +592,118 @@ $connectionsJson = json_encode(array_map(fn($c) => [
|
||||||
btnTanSubmit.addEventListener('click', submitTan);
|
btnTanSubmit.addEventListener('click', submitTan);
|
||||||
tanInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') submitTan(); });
|
tanInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') submitTan(); });
|
||||||
|
|
||||||
|
// ── Drag & Drop für Kachel-Reihenfolge ──────────────────────
|
||||||
|
const grid = document.getElementById('connection-grid');
|
||||||
|
let dragSrc = null;
|
||||||
|
let didDrag = false;
|
||||||
|
if (grid) {
|
||||||
|
grid.querySelectorAll('.drag-handle').forEach(handle => {
|
||||||
|
handle.addEventListener('dragstart', (e) => {
|
||||||
|
dragSrc = handle.closest('.card');
|
||||||
|
didDrag = false;
|
||||||
|
if (!dragSrc) return;
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
e.dataTransfer.setData('text/plain', dragSrc.dataset.id || '');
|
||||||
|
// Komplette Kachel als Drag-Image, am Mauszeiger an der Klick-Position
|
||||||
|
const rect = dragSrc.getBoundingClientRect();
|
||||||
|
e.dataTransfer.setDragImage(dragSrc, e.clientX - rect.left, e.clientY - rect.top);
|
||||||
|
// Ursprung subtil markieren, erst nach Snapshot (sonst landet das Highlight im Ghost)
|
||||||
|
setTimeout(() => {
|
||||||
|
if (dragSrc) {
|
||||||
|
dragSrc.style.outline = '2px dashed #6366f1';
|
||||||
|
dragSrc.style.outlineOffset = '2px';
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
handle.addEventListener('dragend', () => {
|
||||||
|
if (dragSrc) {
|
||||||
|
dragSrc.style.outline = '';
|
||||||
|
dragSrc.style.outlineOffset = '';
|
||||||
|
}
|
||||||
|
grid.querySelectorAll('.drop-before, .drop-after').forEach(c => {
|
||||||
|
c.classList.remove('drop-before', 'drop-after');
|
||||||
|
});
|
||||||
|
dragSrc = null;
|
||||||
|
setTimeout(() => { didDrag = false; }, 50);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
function clearDropMarkers() {
|
||||||
|
grid.querySelectorAll('.drop-before, .drop-after').forEach(c => {
|
||||||
|
c.classList.remove('drop-before', 'drop-after');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function findDropTarget(clientX, clientY) {
|
||||||
|
const cards = [...grid.querySelectorAll('.card')].filter(c => c !== dragSrc);
|
||||||
|
// Erst: Karte unter dem Cursor (bevorzugt)
|
||||||
|
for (const card of cards) {
|
||||||
|
const r = card.getBoundingClientRect();
|
||||||
|
if (clientX >= r.left && clientX <= r.right && clientY >= r.top && clientY <= r.bottom) {
|
||||||
|
return { card, side: (clientX < r.left + r.width / 2) ? 'before' : 'after' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback: nächste Karte in derselben Zeile (anhand Y)
|
||||||
|
let best = null, bestDist = Infinity;
|
||||||
|
for (const card of cards) {
|
||||||
|
const r = card.getBoundingClientRect();
|
||||||
|
if (clientY >= r.top && clientY <= r.bottom) {
|
||||||
|
const center = r.left + r.width / 2;
|
||||||
|
const dist = Math.abs(clientX - center);
|
||||||
|
if (dist < bestDist) {
|
||||||
|
bestDist = dist;
|
||||||
|
best = { card, side: (clientX < center) ? 'before' : 'after' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
grid.addEventListener('dragover', (e) => {
|
||||||
|
if (!dragSrc) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = 'move';
|
||||||
|
const target = findDropTarget(e.clientX, e.clientY);
|
||||||
|
if (!target) { clearDropMarkers(); return; }
|
||||||
|
const cls = target.side === 'before' ? 'drop-before' : 'drop-after';
|
||||||
|
// Nur neu setzen wenn nötig (vermeidet Flackern)
|
||||||
|
if (target.card.classList.contains(cls)) return;
|
||||||
|
clearDropMarkers();
|
||||||
|
target.card.classList.add(cls);
|
||||||
|
});
|
||||||
|
grid.addEventListener('dragleave', (e) => {
|
||||||
|
// Nur wenn der Grid komplett verlassen wird
|
||||||
|
if (e.target === grid && !grid.contains(e.relatedTarget)) {
|
||||||
|
clearDropMarkers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
grid.addEventListener('drop', (e) => {
|
||||||
|
if (!dragSrc) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const marker = grid.querySelector('.drop-before, .drop-after');
|
||||||
|
if (marker) {
|
||||||
|
const before = marker.classList.contains('drop-before');
|
||||||
|
marker.parentNode.insertBefore(dragSrc, before ? marker : marker.nextSibling);
|
||||||
|
didDrag = true;
|
||||||
|
persistOrder();
|
||||||
|
}
|
||||||
|
clearDropMarkers();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function persistOrder() {
|
||||||
|
const ids = Array.from(grid.querySelectorAll('.card[data-id]')).map(c => c.dataset.id);
|
||||||
|
const form = new FormData();
|
||||||
|
form.append('ids', JSON.stringify(ids));
|
||||||
|
try {
|
||||||
|
await fetch('/bank/direkt/_order', { method: 'POST', body: form });
|
||||||
|
} catch (e) { /* still local */ }
|
||||||
|
}
|
||||||
|
|
||||||
document.querySelectorAll('.btn-run-connection').forEach(card => {
|
document.querySelectorAll('.btn-run-connection').forEach(card => {
|
||||||
card.addEventListener('click', (e) => {
|
card.addEventListener('click', (e) => {
|
||||||
|
if (didDrag) { didDrag = false; return; }
|
||||||
if (e.target.closest('.btn-edit-connection')) return;
|
if (e.target.closest('.btn-edit-connection')) return;
|
||||||
|
if (e.target.closest('.drag-handle')) return;
|
||||||
|
|
||||||
const id = card.dataset.id;
|
const id = card.dataset.id;
|
||||||
currentSourceId = id;
|
currentSourceId = id;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue