- Auth: TOTP replay protection with counter tracking, constant-time backup code comparison, atomic lockout increment, per-token logout - Invoices/PDFs: net-based VAT calculation, dangerous URL scheme stripping in cleanQuillHtml, orders-pdf error handling - Orders: reject item changes on status transition, cascading delete cleanup, take:1 with orderBy - Projects: atomic rename collision handling, MIME/extension validation, empty customer name rejection - Attendance: Czech public holiday awareness in frontend fund calculation, leave_hours 0 handling, invalid date NaN guard, bounded per-month queries in workfund - Users/Admin: profile audit logging + password validation, session revocation guard, session ID validation, dashboard DB aggregation, soft-deleted record protection in scope templates - Frontend: FormField label linkage, Pagination ARIA, error handling in OrderConfirmationModal, 401 propagation, GPS emoji hidden from screen readers, table sort state fix, geolocation race/abort cleanup, Leaflet popup DOM safety, Vehicles toggleActive minimal body, CompanySettings ref mutation fix, OfferDetail unlock abort, AttendanceBalances combined fetches - Utils: env validation, Puppeteer concurrency mutex, invoice alert cron cleanup on shutdown, body limit alignment, TOTP error logging, trustProxy from env, symlink rejection, rate cache Map usage Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
114 lines
2.9 KiB
TypeScript
114 lines
2.9 KiB
TypeScript
interface PaginationProps {
|
|
pagination: {
|
|
total: number;
|
|
page: number;
|
|
per_page: number;
|
|
total_pages: number;
|
|
} | null;
|
|
onPageChange: (page: number) => void;
|
|
onPerPageChange?: (perPage: number) => void;
|
|
}
|
|
|
|
export default function Pagination({
|
|
pagination,
|
|
onPageChange,
|
|
onPerPageChange,
|
|
}: PaginationProps) {
|
|
if (!pagination || pagination.total_pages <= 1) return null;
|
|
|
|
const { page, total_pages, total } = pagination;
|
|
|
|
const getPages = () => {
|
|
const pages: (number | string)[] = [];
|
|
const delta = 2;
|
|
for (let i = 1; i <= total_pages; i++) {
|
|
if (
|
|
i === 1 ||
|
|
i === total_pages ||
|
|
(i >= page - delta && i <= page + delta)
|
|
) {
|
|
pages.push(i);
|
|
} else if (pages[pages.length - 1] !== "...") {
|
|
pages.push("...");
|
|
}
|
|
}
|
|
return pages;
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className="admin-pagination"
|
|
role="navigation"
|
|
aria-label="Stránkování"
|
|
>
|
|
<div className="admin-pagination-info">{total} záznamů</div>
|
|
<div className="admin-pagination-controls">
|
|
<button
|
|
disabled={page <= 1}
|
|
onClick={() => onPageChange(page - 1)}
|
|
className="admin-pagination-page"
|
|
aria-label="Předchozí stránka"
|
|
>
|
|
<svg
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
>
|
|
<path d="M15 18l-6-6 6-6" />
|
|
</svg>
|
|
</button>
|
|
{getPages().map((p, i) =>
|
|
typeof p === "string" ? (
|
|
<span key={`dots-${i}`} className="admin-pagination-ellipsis">
|
|
...
|
|
</span>
|
|
) : (
|
|
<button
|
|
key={p}
|
|
onClick={() => onPageChange(p)}
|
|
className={`admin-pagination-page ${p === page ? "active" : ""}`}
|
|
aria-label={`Stránka ${p}`}
|
|
aria-current={p === page ? "page" : undefined}
|
|
>
|
|
{p}
|
|
</button>
|
|
),
|
|
)}
|
|
<button
|
|
disabled={page >= total_pages}
|
|
onClick={() => onPageChange(page + 1)}
|
|
className="admin-pagination-page"
|
|
aria-label="Další stránka"
|
|
>
|
|
<svg
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
>
|
|
<path d="M9 18l6-6-6-6" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
{onPerPageChange && (
|
|
<select
|
|
value={pagination.per_page}
|
|
onChange={(e) => onPerPageChange(Number(e.target.value))}
|
|
className="admin-pagination-select"
|
|
>
|
|
{[10, 25, 50, 100].map((n) => (
|
|
<option key={n} value={n}>
|
|
{n} / stránka
|
|
</option>
|
|
))}
|
|
</select>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|