Feature: Pagination für Quarantäne und Mail-Queue (25 pro Seite)

- Quarantäne und Queue zeigen je 25 Einträge pro Seite
- Pagination-Bar mit Seitenanzeige (X-Y von Z) und Blätter-Buttons
- Seite wird bei Filter- oder Suchwechsel auf 1 zurückgesetzt
- Quarantäne: rows-Select entfernt (API holt intern 500, UI paginiert)
- CSS-Klassen mq-pagination, mq-pag-btn passend zum Dark-Design

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main v1.1.143
boban 2026-04-23 02:06:14 +02:00
parent b52ea46f22
commit bc66870681
5 changed files with 81 additions and 12 deletions

View File

@ -18,12 +18,18 @@ class QuarantineList extends Component
#[Url(as: 'q', keep: true)] #[Url(as: 'q', keep: true)]
public string $search = ''; public string $search = '';
#[Url(as: 'rows', keep: true)] #[Url(as: 'page', keep: true)]
public int $rows = 200; public int $page = 1;
public int $perPage = 25;
public int $rows = 500;
#[On('quarantine:updated')] #[On('quarantine:updated')]
public function refresh(): void {} public function refresh(): void {}
public function updatedFilter(): void { $this->page = 1; }
public function updatedSearch(): void { $this->page = 1; }
public function openMessage(string $msgId): void public function openMessage(string $msgId): void
{ {
$this->dispatch('openModal', $this->dispatch('openModal',
@ -61,7 +67,17 @@ class QuarantineList extends Component
)); ));
} }
return view('livewire.ui.nx.mail.quarantine-list', compact('messages', 'counts')); $total = count($messages);
$totalPages = max(1, (int) ceil($total / $this->perPage));
$this->page = max(1, min($this->page, $totalPages));
$paged = array_slice($messages, ($this->page - 1) * $this->perPage, $this->perPage);
return view('livewire.ui.nx.mail.quarantine-list', [
'messages' => $paged,
'counts' => $counts,
'total' => $total,
'totalPages' => $totalPages,
]);
} }
// ── helpers ────────────────────────────────────────────────────────────── // ── helpers ──────────────────────────────────────────────────────────────

View File

@ -18,12 +18,19 @@ class QueueList extends Component
#[Url(as: 'q', keep: true)] #[Url(as: 'q', keep: true)]
public string $search = ''; public string $search = '';
#[Url(as: 'page', keep: true)]
public int $page = 1;
public int $perPage = 25;
public array $selected = []; public array $selected = [];
public bool $selectAll = false; public bool $selectAll = false;
#[On('queue:updated')] #[On('queue:updated')]
public function refresh(): void {} public function refresh(): void {}
public function updatedFilter(): void { $this->page = 1; $this->selected = []; $this->selectAll = false; }
public function updatedSearch(): void { $this->page = 1; }
public function updatedSelectAll(bool $val): void public function updatedSelectAll(bool $val): void
{ {
$messages = $this->fetchQueue(); $messages = $this->fetchQueue();
@ -89,7 +96,17 @@ class QueueList extends Component
)); ));
} }
return view('livewire.ui.nx.mail.queue-list', compact('messages', 'counts')); $total = count($messages);
$totalPages = max(1, (int) ceil($total / $this->perPage));
$this->page = max(1, min($this->page, $totalPages));
$paged = array_slice($messages, ($this->page - 1) * $this->perPage, $this->perPage);
return view('livewire.ui.nx.mail.queue-list', [
'messages' => $paged,
'counts' => $counts,
'total' => $total,
'totalPages' => $totalPages,
]);
} }
// ── helpers ────────────────────────────────────────────────────────────── // ── helpers ──────────────────────────────────────────────────────────────

View File

@ -2168,3 +2168,12 @@ textarea.sec-input { height: auto; line-height: 1.55; padding: 8px 10px; resize:
.audit-level.error { border-color: rgba(239,68,68,.3); background: rgba(239,68,68,.08); color: #fca5a5; } .audit-level.error { border-color: rgba(239,68,68,.3); background: rgba(239,68,68,.08); color: #fca5a5; }
.audit-level.critical { border-color: rgba(239,68,68,.5); background: rgba(239,68,68,.15); color: #f87171; font-weight: 700; } .audit-level.critical { border-color: rgba(239,68,68,.5); background: rgba(239,68,68,.15); color: #f87171; font-weight: 700; }
.audit-msg { color: var(--mw-t2); min-width: 0; word-break: break-word; line-height: 1.55; font-family: ui-monospace,monospace; font-size: 11.5px; } .audit-msg { color: var(--mw-t2); min-width: 0; word-break: break-word; line-height: 1.55; font-family: ui-monospace,monospace; font-size: 11.5px; }
/* ── Pagination (Queue / Quarantine) ────────────────────────────── */
.mq-pagination { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; border-top: 1px solid var(--mw-b2); }
.mq-pag-info { font-size: 11.5px; color: var(--mw-t4); }
.mq-pag-btns { display: flex; align-items: center; gap: 4px; }
.mq-pag-btn { display: flex; align-items: center; justify-content: center; min-width: 28px; height: 26px; padding: 0 6px; border-radius: 5px; border: 1px solid var(--mw-b2); background: transparent; color: var(--mw-t3); font-size: 12px; cursor: pointer; transition: background .15s, border-color .15s, color .15s; }
.mq-pag-btn:hover:not(:disabled) { background: var(--mw-bg3); color: var(--mw-t1); }
.mq-pag-btn.active { background: rgba(124,58,237,.2); border-color: rgba(124,58,237,.5); color: #c4b5fd; }
.mq-pag-btn:disabled { opacity: .35; cursor: default; }

View File

@ -8,18 +8,13 @@
<svg width="16" height="16" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="7" r="5.5" stroke="currentColor" stroke-width="1.2"/><path d="M7 4v4l2.5 2" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg> <svg width="16" height="16" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="7" r="5.5" stroke="currentColor" stroke-width="1.2"/><path d="M7 4v4l2.5 2" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
Quarantäne Quarantäne
<span class="mq-total-badge">{{ $counts['all'] }}</span> <span class="mq-total-badge">{{ $counts['all'] }}</span>
<span class="mq-page-sub">Rspamd · letzte {{ count($messages) }} Einträge</span> <span class="mq-page-sub">Rspamd · {{ $total }} Einträge</span>
</div> </div>
<div class="mq-page-actions"> <div class="mq-page-actions">
<div class="mq-search-wrap"> <div class="mq-search-wrap">
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><circle cx="5" cy="5" r="3.8" stroke="currentColor" stroke-width="1.2"/><path d="M8 8l2.5 2.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg> <svg width="12" height="12" viewBox="0 0 12 12" fill="none"><circle cx="5" cy="5" r="3.8" stroke="currentColor" stroke-width="1.2"/><path d="M8 8l2.5 2.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
<input type="text" wire:model.live.debounce.300ms="search" class="mq-search-input" placeholder="Von, An, Betreff …"> <input type="text" wire:model.live.debounce.300ms="search" class="mq-search-input" placeholder="Von, An, Betreff …">
</div> </div>
<select wire:model.live="rows" class="mq-select">
<option value="100">100 Einträge</option>
<option value="200">200 Einträge</option>
<option value="500">500 Einträge</option>
</select>
</div> </div>
</div> </div>
@ -104,6 +99,22 @@
</tbody> </tbody>
</table> </table>
</div> </div>
@if($totalPages > 1)
<div class="mq-pagination">
<span class="mq-pag-info">{{ ($page - 1) * $perPage + 1 }}{{ min($page * $perPage, $total) }} von {{ $total }}</span>
<div class="mq-pag-btns">
<button wire:click="$set('page', {{ max(1, $page - 1) }})" class="mq-pag-btn" @if($page <= 1) disabled @endif>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M7.5 2L3.5 6l4 4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
@for($i = max(1, $page - 2); $i <= min($totalPages, $page + 2); $i++)
<button wire:click="$set('page', {{ $i }})" class="mq-pag-btn {{ $i === $page ? 'active' : '' }}">{{ $i }}</button>
@endfor
<button wire:click="$set('page', {{ min($totalPages, $page + 1) }})" class="mq-pag-btn" @if($page >= $totalPages) disabled @endif>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M4.5 2L8.5 6l-4 4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</div>
</div>
@endif
@elseif($counts['all'] === 0) @elseif($counts['all'] === 0)
<div class="mq-empty"> <div class="mq-empty">
<svg width="36" height="36" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="7" r="5.5" stroke="currentColor" stroke-width="1" opacity=".3"/><path d="M7 4v4l2.5 2" stroke="currentColor" stroke-width="1" stroke-linecap="round" opacity=".3"/></svg> <svg width="36" height="36" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="7" r="5.5" stroke="currentColor" stroke-width="1" opacity=".3"/><path d="M7 4v4l2.5 2" stroke="currentColor" stroke-width="1" stroke-linecap="round" opacity=".3"/></svg>

View File

@ -92,6 +92,22 @@
</tbody> </tbody>
</table> </table>
</div> </div>
@if($totalPages > 1)
<div class="mq-pagination">
<span class="mq-pag-info">{{ ($page - 1) * $perPage + 1 }}{{ min($page * $perPage, $total) }} von {{ $total }}</span>
<div class="mq-pag-btns">
<button wire:click="$set('page', {{ max(1, $page - 1) }})" class="mq-pag-btn" @if($page <= 1) disabled @endif>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M7.5 2L3.5 6l4 4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
@for($i = max(1, $page - 2); $i <= min($totalPages, $page + 2); $i++)
<button wire:click="$set('page', {{ $i }})" class="mq-pag-btn {{ $i === $page ? 'active' : '' }}">{{ $i }}</button>
@endfor
<button wire:click="$set('page', {{ min($totalPages, $page + 1) }})" class="mq-pag-btn" @if($page >= $totalPages) disabled @endif>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M4.5 2L8.5 6l-4 4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</div>
</div>
@endif
@else @else
<div class="mq-empty"> <div class="mq-empty">
<svg width="36" height="36" viewBox="0 0 14 14" fill="none"><path d="M1 4h12M4 4V2h6v2M4 12V7m6 5V7" stroke="currentColor" stroke-width="1" stroke-linecap="round" opacity=".3"/></svg> <svg width="36" height="36" viewBox="0 0 14 14" fill="none"><path d="M1 4h12M4 4V2h6v2M4 12V7m6 5V7" stroke="currentColor" stroke-width="1" stroke-linecap="round" opacity=".3"/></svg>