feat: P2 strankovani - PaginationHelper, Pagination komponenta, integrace do 4 modulu
- PaginationHelper.php: parseParams() + paginate() - DRY backend pagination logika - Pagination.jsx: frontend strankovaci komponenta (prev/next/cisla/info) - CSS: .admin-pagination styly v admin.css - Refaktor handleru: offers, orders, invoices, projects pouzivaji PaginationHelper - Default 25 zaznamu na stranku (misto 500), max 500 - Frontend: page state + reset na search/filter zmenu - useListData: pagination data v response Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -169,12 +169,7 @@ function handleGetStats(PDO $pdo): void
|
|||||||
|
|
||||||
function handleGetList(PDO $pdo): void
|
function handleGetList(PDO $pdo): void
|
||||||
{
|
{
|
||||||
$search = trim($_GET['search'] ?? '');
|
|
||||||
$statusFilter = trim($_GET['status'] ?? '');
|
$statusFilter = trim($_GET['status'] ?? '');
|
||||||
$sort = $_GET['sort'] ?? 'created_at';
|
|
||||||
$order = strtoupper($_GET['order'] ?? 'DESC') === 'ASC' ? 'ASC' : 'DESC';
|
|
||||||
$page = max(1, (int) ($_GET['page'] ?? 1));
|
|
||||||
$perPage = min(500, max(1, (int) ($_GET['per_page'] ?? 500)));
|
|
||||||
|
|
||||||
$sortMap = [
|
$sortMap = [
|
||||||
'InvoiceNumber' => 'i.invoice_number',
|
'InvoiceNumber' => 'i.invoice_number',
|
||||||
@@ -188,10 +183,8 @@ function handleGetList(PDO $pdo): void
|
|||||||
'IssueDate' => 'i.issue_date',
|
'IssueDate' => 'i.issue_date',
|
||||||
'issue_date' => 'i.issue_date',
|
'issue_date' => 'i.issue_date',
|
||||||
];
|
];
|
||||||
if (!isset($sortMap[$sort])) {
|
|
||||||
errorResponse('Neplatný parametr řazení', 400);
|
$p = PaginationHelper::parseParams($sortMap);
|
||||||
}
|
|
||||||
$sortCol = $sortMap[$sort];
|
|
||||||
|
|
||||||
// Lazy overdue detekce
|
// Lazy overdue detekce
|
||||||
$pdo->exec("UPDATE invoices SET status = 'overdue' WHERE status = 'issued' AND due_date < CURDATE()");
|
$pdo->exec("UPDATE invoices SET status = 'overdue' WHERE status = 'issued' AND due_date < CURDATE()");
|
||||||
@@ -199,10 +192,9 @@ function handleGetList(PDO $pdo): void
|
|||||||
$where = 'WHERE 1=1';
|
$where = 'WHERE 1=1';
|
||||||
$params = [];
|
$params = [];
|
||||||
|
|
||||||
if ($search) {
|
if ($p['search']) {
|
||||||
$search = mb_substr($search, 0, 100);
|
|
||||||
$where .= ' AND (i.invoice_number LIKE ? OR c.name LIKE ? OR c.company_id LIKE ?)';
|
$where .= ' AND (i.invoice_number LIKE ? OR c.name LIKE ? OR c.company_id LIKE ?)';
|
||||||
$searchParam = "%{$search}%";
|
$searchParam = "%{$p['search']}%";
|
||||||
$params = array_merge($params, [$searchParam, $searchParam, $searchParam]);
|
$params = array_merge($params, [$searchParam, $searchParam, $searchParam]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,36 +207,26 @@ function handleGetList(PDO $pdo): void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$countSql = "
|
$from = "FROM invoices i
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM invoices i
|
|
||||||
LEFT JOIN customers c ON i.customer_id = c.id
|
LEFT JOIN customers c ON i.customer_id = c.id
|
||||||
$where
|
LEFT JOIN orders o ON i.order_id = o.id";
|
||||||
";
|
|
||||||
$stmt = $pdo->prepare($countSql);
|
|
||||||
$stmt->execute($params);
|
|
||||||
$total = (int) $stmt->fetchColumn();
|
|
||||||
|
|
||||||
$offset = ($page - 1) * $perPage;
|
$result = PaginationHelper::paginate(
|
||||||
|
$pdo,
|
||||||
$sql = "
|
"SELECT COUNT(*) FROM invoices i LEFT JOIN customers c ON i.customer_id = c.id {$where}",
|
||||||
SELECT i.id, i.invoice_number, i.order_id, i.status, i.currency,
|
"SELECT i.id, i.invoice_number, i.order_id, i.status, i.currency,
|
||||||
i.issue_date, i.due_date, i.paid_date, i.created_at, i.apply_vat,
|
i.issue_date, i.due_date, i.paid_date, i.created_at, i.apply_vat,
|
||||||
c.name as customer_name,
|
c.name as customer_name,
|
||||||
(SELECT COALESCE(SUM(ii.quantity * ii.unit_price), 0)
|
(SELECT COALESCE(SUM(ii.quantity * ii.unit_price), 0)
|
||||||
FROM invoice_items ii WHERE ii.invoice_id = i.id) as subtotal,
|
FROM invoice_items ii WHERE ii.invoice_id = i.id) as subtotal,
|
||||||
o.order_number
|
o.order_number
|
||||||
FROM invoices i
|
{$from} {$where}
|
||||||
LEFT JOIN customers c ON i.customer_id = c.id
|
ORDER BY {$p['sort']} {$p['order']}",
|
||||||
LEFT JOIN orders o ON i.order_id = o.id
|
$params,
|
||||||
$where
|
$p
|
||||||
ORDER BY $sortCol $order
|
);
|
||||||
LIMIT $perPage OFFSET $offset
|
|
||||||
";
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare($sql);
|
$invoices = $result['items'];
|
||||||
$stmt->execute($params);
|
|
||||||
$invoices = $stmt->fetchAll();
|
|
||||||
|
|
||||||
// Dopocitat celkovou castku s DPH
|
// Dopocitat celkovou castku s DPH
|
||||||
foreach ($invoices as &$inv) {
|
foreach ($invoices as &$inv) {
|
||||||
@@ -265,9 +247,7 @@ function handleGetList(PDO $pdo): void
|
|||||||
|
|
||||||
successResponse([
|
successResponse([
|
||||||
'invoices' => $invoices,
|
'invoices' => $invoices,
|
||||||
'total' => $total,
|
'pagination' => $result['pagination'],
|
||||||
'page' => $page,
|
|
||||||
'per_page' => $perPage,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
function handleGetList(PDO $pdo): void
|
function handleGetList(PDO $pdo): void
|
||||||
{
|
{
|
||||||
$search = trim($_GET['search'] ?? '');
|
|
||||||
$sort = $_GET['sort'] ?? 'created_at';
|
|
||||||
$order = strtoupper($_GET['order'] ?? 'DESC') === 'ASC' ? 'ASC' : 'DESC';
|
|
||||||
$page = max(1, (int) ($_GET['page'] ?? 1));
|
|
||||||
$perPage = min(500, max(1, (int) ($_GET['per_page'] ?? 500)));
|
|
||||||
|
|
||||||
$sortMap = [
|
$sortMap = [
|
||||||
'Date' => 'q.created_at',
|
'Date' => 'q.created_at',
|
||||||
'CreatedAt' => 'q.created_at',
|
'CreatedAt' => 'q.created_at',
|
||||||
@@ -23,57 +17,37 @@ function handleGetList(PDO $pdo): void
|
|||||||
'Currency' => 'q.currency',
|
'Currency' => 'q.currency',
|
||||||
'currency' => 'q.currency',
|
'currency' => 'q.currency',
|
||||||
];
|
];
|
||||||
if (!isset($sortMap[$sort])) {
|
|
||||||
errorResponse('Neplatný parametr řazení', 400);
|
|
||||||
}
|
|
||||||
$sortCol = $sortMap[$sort];
|
|
||||||
|
|
||||||
|
$p = PaginationHelper::parseParams($sortMap);
|
||||||
$where = 'WHERE 1=1';
|
$where = 'WHERE 1=1';
|
||||||
$params = [];
|
$params = [];
|
||||||
|
|
||||||
if ($search) {
|
if ($p['search']) {
|
||||||
$search = mb_substr($search, 0, 100);
|
|
||||||
$where .= ' AND (q.quotation_number LIKE ? OR q.project_code LIKE ? OR c.name LIKE ?)';
|
$where .= ' AND (q.quotation_number LIKE ? OR q.project_code LIKE ? OR c.name LIKE ?)';
|
||||||
$searchParam = "%{$search}%";
|
$searchParam = "%{$p['search']}%";
|
||||||
$params = [$searchParam, $searchParam, $searchParam];
|
$params = [$searchParam, $searchParam, $searchParam];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Celkovy pocet pro pagination
|
$from = "FROM quotations q LEFT JOIN customers c ON q.customer_id = c.id";
|
||||||
$countSql = "
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM quotations q
|
|
||||||
LEFT JOIN customers c ON q.customer_id = c.id
|
|
||||||
$where
|
|
||||||
";
|
|
||||||
$stmt = $pdo->prepare($countSql);
|
|
||||||
$stmt->execute($params);
|
|
||||||
$total = (int) $stmt->fetchColumn();
|
|
||||||
|
|
||||||
$offset = ($page - 1) * $perPage;
|
$result = PaginationHelper::paginate(
|
||||||
|
$pdo,
|
||||||
$sql = "
|
"SELECT COUNT(*) {$from} {$where}",
|
||||||
SELECT q.id, q.quotation_number, q.project_code, q.created_at, q.valid_until,
|
"SELECT q.id, q.quotation_number, q.project_code, q.created_at, q.valid_until,
|
||||||
q.currency, q.language, q.apply_vat, q.vat_rate, q.exchange_rate,
|
q.currency, q.language, q.apply_vat, q.vat_rate, q.exchange_rate,
|
||||||
q.customer_id, q.order_id, q.status,
|
q.customer_id, q.order_id, q.status,
|
||||||
c.name as customer_name,
|
c.name as customer_name,
|
||||||
(SELECT COALESCE(SUM(CASE WHEN qi.is_included_in_total THEN qi.quantity * qi.unit_price ELSE 0 END), 0)
|
(SELECT COALESCE(SUM(CASE WHEN qi.is_included_in_total THEN qi.quantity * qi.unit_price ELSE 0 END), 0)
|
||||||
FROM quotation_items qi WHERE qi.quotation_id = q.id) as total
|
FROM quotation_items qi WHERE qi.quotation_id = q.id) as total
|
||||||
FROM quotations q
|
{$from} {$where}
|
||||||
LEFT JOIN customers c ON q.customer_id = c.id
|
ORDER BY {$p['sort']} {$p['order']}",
|
||||||
$where
|
$params,
|
||||||
ORDER BY $sortCol $order
|
$p
|
||||||
LIMIT $perPage OFFSET $offset
|
);
|
||||||
";
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute($params);
|
|
||||||
$quotations = $stmt->fetchAll();
|
|
||||||
|
|
||||||
successResponse([
|
successResponse([
|
||||||
'quotations' => $quotations,
|
'quotations' => $result['items'],
|
||||||
'total' => $total,
|
'pagination' => $result['pagination'],
|
||||||
'page' => $page,
|
|
||||||
'per_page' => $perPage,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,12 +25,6 @@ function generateOrderNumber(PDO $pdo): string
|
|||||||
|
|
||||||
function handleGetList(PDO $pdo): void
|
function handleGetList(PDO $pdo): void
|
||||||
{
|
{
|
||||||
$search = trim($_GET['search'] ?? '');
|
|
||||||
$sort = $_GET['sort'] ?? 'created_at';
|
|
||||||
$order = strtoupper($_GET['order'] ?? 'DESC') === 'ASC' ? 'ASC' : 'DESC';
|
|
||||||
$page = max(1, (int) ($_GET['page'] ?? 1));
|
|
||||||
$perPage = min(500, max(1, (int) ($_GET['per_page'] ?? 500)));
|
|
||||||
|
|
||||||
$sortMap = [
|
$sortMap = [
|
||||||
'OrderNumber' => 'o.order_number',
|
'OrderNumber' => 'o.order_number',
|
||||||
'order_number' => 'o.order_number',
|
'order_number' => 'o.order_number',
|
||||||
@@ -41,36 +35,25 @@ function handleGetList(PDO $pdo): void
|
|||||||
'Currency' => 'o.currency',
|
'Currency' => 'o.currency',
|
||||||
'currency' => 'o.currency',
|
'currency' => 'o.currency',
|
||||||
];
|
];
|
||||||
if (!isset($sortMap[$sort])) {
|
|
||||||
errorResponse('Neplatný parametr řazení', 400);
|
|
||||||
}
|
|
||||||
$sortCol = $sortMap[$sort];
|
|
||||||
|
|
||||||
|
$p = PaginationHelper::parseParams($sortMap);
|
||||||
$where = 'WHERE 1=1';
|
$where = 'WHERE 1=1';
|
||||||
$params = [];
|
$params = [];
|
||||||
|
|
||||||
if ($search) {
|
if ($p['search']) {
|
||||||
$search = mb_substr($search, 0, 100);
|
|
||||||
$where .= ' AND (o.order_number LIKE ? OR q.quotation_number LIKE ? OR q.project_code LIKE ? OR c.name LIKE ?)';
|
$where .= ' AND (o.order_number LIKE ? OR q.quotation_number LIKE ? OR q.project_code LIKE ? OR c.name LIKE ?)';
|
||||||
$searchParam = "%{$search}%";
|
$searchParam = "%{$p['search']}%";
|
||||||
$params = [$searchParam, $searchParam, $searchParam, $searchParam];
|
$params = [$searchParam, $searchParam, $searchParam, $searchParam];
|
||||||
}
|
}
|
||||||
|
|
||||||
$countSql = "
|
$from = "FROM orders o
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM orders o
|
|
||||||
LEFT JOIN quotations q ON o.quotation_id = q.id
|
LEFT JOIN quotations q ON o.quotation_id = q.id
|
||||||
LEFT JOIN customers c ON o.customer_id = c.id
|
LEFT JOIN customers c ON o.customer_id = c.id";
|
||||||
$where
|
|
||||||
";
|
|
||||||
$stmt = $pdo->prepare($countSql);
|
|
||||||
$stmt->execute($params);
|
|
||||||
$total = (int) $stmt->fetchColumn();
|
|
||||||
|
|
||||||
$offset = ($page - 1) * $perPage;
|
$result = PaginationHelper::paginate(
|
||||||
|
$pdo,
|
||||||
$sql = "
|
"SELECT COUNT(*) {$from} {$where}",
|
||||||
SELECT o.id, o.order_number, o.quotation_id, o.status, o.currency,
|
"SELECT o.id, o.order_number, o.quotation_id, o.status, o.currency,
|
||||||
o.created_at, o.apply_vat, o.vat_rate,
|
o.created_at, o.apply_vat, o.vat_rate,
|
||||||
q.quotation_number, q.project_code,
|
q.quotation_number, q.project_code,
|
||||||
c.name as customer_name,
|
c.name as customer_name,
|
||||||
@@ -78,23 +61,15 @@ function handleGetList(PDO $pdo): void
|
|||||||
FROM order_items oi WHERE oi.order_id = o.id) as total,
|
FROM order_items oi WHERE oi.order_id = o.id) as total,
|
||||||
(SELECT inv.id FROM invoices inv WHERE inv.order_id = o.id LIMIT 1) as invoice_id,
|
(SELECT inv.id FROM invoices inv WHERE inv.order_id = o.id LIMIT 1) as invoice_id,
|
||||||
(SELECT inv.invoice_number FROM invoices inv WHERE inv.order_id = o.id LIMIT 1) as invoice_number
|
(SELECT inv.invoice_number FROM invoices inv WHERE inv.order_id = o.id LIMIT 1) as invoice_number
|
||||||
FROM orders o
|
{$from} {$where}
|
||||||
LEFT JOIN quotations q ON o.quotation_id = q.id
|
ORDER BY {$p['sort']} {$p['order']}",
|
||||||
LEFT JOIN customers c ON o.customer_id = c.id
|
$params,
|
||||||
$where
|
$p
|
||||||
ORDER BY $sortCol $order
|
);
|
||||||
LIMIT $perPage OFFSET $offset
|
|
||||||
";
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute($params);
|
|
||||||
$orders = $stmt->fetchAll();
|
|
||||||
|
|
||||||
successResponse([
|
successResponse([
|
||||||
'orders' => $orders,
|
'orders' => $result['items'],
|
||||||
'total' => $total,
|
'pagination' => $result['pagination'],
|
||||||
'page' => $page,
|
|
||||||
'per_page' => $perPage,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -156,12 +156,6 @@ function handleDeleteProject(PDO $pdo, int $id): void
|
|||||||
|
|
||||||
function handleGetList(PDO $pdo): void
|
function handleGetList(PDO $pdo): void
|
||||||
{
|
{
|
||||||
$search = trim($_GET['search'] ?? '');
|
|
||||||
$sort = $_GET['sort'] ?? 'created_at';
|
|
||||||
$order = strtoupper($_GET['order'] ?? 'DESC') === 'ASC' ? 'ASC' : 'DESC';
|
|
||||||
$page = max(1, (int) ($_GET['page'] ?? 1));
|
|
||||||
$perPage = min(500, max(1, (int) ($_GET['per_page'] ?? 500)));
|
|
||||||
|
|
||||||
$sortMap = [
|
$sortMap = [
|
||||||
'ProjectNumber' => 'p.project_number',
|
'ProjectNumber' => 'p.project_number',
|
||||||
'project_number' => 'p.project_number',
|
'project_number' => 'p.project_number',
|
||||||
@@ -176,56 +170,37 @@ function handleGetList(PDO $pdo): void
|
|||||||
'CreatedAt' => 'p.created_at',
|
'CreatedAt' => 'p.created_at',
|
||||||
'created_at' => 'p.created_at',
|
'created_at' => 'p.created_at',
|
||||||
];
|
];
|
||||||
if (!isset($sortMap[$sort])) {
|
|
||||||
errorResponse('Neplatný parametr řazení', 400);
|
|
||||||
}
|
|
||||||
$sortCol = $sortMap[$sort];
|
|
||||||
|
|
||||||
|
$p = PaginationHelper::parseParams($sortMap);
|
||||||
$where = 'WHERE 1=1';
|
$where = 'WHERE 1=1';
|
||||||
$params = [];
|
$params = [];
|
||||||
|
|
||||||
if ($search) {
|
if ($p['search']) {
|
||||||
$search = mb_substr($search, 0, 100);
|
|
||||||
$where .= ' AND (p.project_number LIKE ? OR p.name LIKE ? OR c.name LIKE ?)';
|
$where .= ' AND (p.project_number LIKE ? OR p.name LIKE ? OR c.name LIKE ?)';
|
||||||
$searchParam = "%{$search}%";
|
$searchParam = "%{$p['search']}%";
|
||||||
$params = [$searchParam, $searchParam, $searchParam];
|
$params = [$searchParam, $searchParam, $searchParam];
|
||||||
}
|
}
|
||||||
|
|
||||||
$countSql = "
|
$from = "FROM projects p
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM projects p
|
|
||||||
LEFT JOIN customers c ON p.customer_id = c.id
|
LEFT JOIN customers c ON p.customer_id = c.id
|
||||||
LEFT JOIN orders o ON p.order_id = o.id
|
LEFT JOIN orders o ON p.order_id = o.id";
|
||||||
$where
|
|
||||||
";
|
|
||||||
$stmt = $pdo->prepare($countSql);
|
|
||||||
$stmt->execute($params);
|
|
||||||
$total = (int) $stmt->fetchColumn();
|
|
||||||
|
|
||||||
$offset = ($page - 1) * $perPage;
|
$result = PaginationHelper::paginate(
|
||||||
|
$pdo,
|
||||||
$sql = "
|
"SELECT COUNT(*) {$from} {$where}",
|
||||||
SELECT p.id, p.project_number, p.name, p.status, p.start_date, p.end_date,
|
"SELECT p.id, p.project_number, p.name, p.status, p.start_date, p.end_date,
|
||||||
p.order_id, p.quotation_id, p.created_at,
|
p.order_id, p.quotation_id, p.created_at,
|
||||||
c.name as customer_name,
|
c.name as customer_name,
|
||||||
o.order_number
|
o.order_number
|
||||||
FROM projects p
|
{$from} {$where}
|
||||||
LEFT JOIN customers c ON p.customer_id = c.id
|
ORDER BY {$p['sort']} {$p['order']}",
|
||||||
LEFT JOIN orders o ON p.order_id = o.id
|
$params,
|
||||||
$where
|
$p
|
||||||
ORDER BY $sortCol $order
|
);
|
||||||
LIMIT $perPage OFFSET $offset
|
|
||||||
";
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute($params);
|
|
||||||
$projects = $stmt->fetchAll();
|
|
||||||
|
|
||||||
successResponse([
|
successResponse([
|
||||||
'projects' => $projects,
|
'projects' => $result['items'],
|
||||||
'total' => $total,
|
'pagination' => $result['pagination'],
|
||||||
'page' => $page,
|
|
||||||
'per_page' => $perPage,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ require_once dirname(__DIR__) . '/config.php';
|
|||||||
require_once dirname(__DIR__) . '/includes/JWTAuth.php';
|
require_once dirname(__DIR__) . '/includes/JWTAuth.php';
|
||||||
require_once dirname(__DIR__) . '/includes/AuditLog.php';
|
require_once dirname(__DIR__) . '/includes/AuditLog.php';
|
||||||
require_once dirname(__DIR__) . '/includes/CnbRates.php';
|
require_once dirname(__DIR__) . '/includes/CnbRates.php';
|
||||||
|
require_once dirname(__DIR__) . '/includes/PaginationHelper.php';
|
||||||
require_once __DIR__ . '/handlers/invoices-handlers.php';
|
require_once __DIR__ . '/handlers/invoices-handlers.php';
|
||||||
|
|
||||||
setCorsHeaders();
|
setCorsHeaders();
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ declare(strict_types=1);
|
|||||||
require_once dirname(__DIR__) . '/config.php';
|
require_once dirname(__DIR__) . '/config.php';
|
||||||
require_once dirname(__DIR__) . '/includes/JWTAuth.php';
|
require_once dirname(__DIR__) . '/includes/JWTAuth.php';
|
||||||
require_once dirname(__DIR__) . '/includes/AuditLog.php';
|
require_once dirname(__DIR__) . '/includes/AuditLog.php';
|
||||||
|
require_once dirname(__DIR__) . '/includes/PaginationHelper.php';
|
||||||
require_once __DIR__ . '/handlers/offers-handlers.php';
|
require_once __DIR__ . '/handlers/offers-handlers.php';
|
||||||
|
|
||||||
setCorsHeaders();
|
setCorsHeaders();
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ declare(strict_types=1);
|
|||||||
require_once dirname(__DIR__) . '/config.php';
|
require_once dirname(__DIR__) . '/config.php';
|
||||||
require_once dirname(__DIR__) . '/includes/JWTAuth.php';
|
require_once dirname(__DIR__) . '/includes/JWTAuth.php';
|
||||||
require_once dirname(__DIR__) . '/includes/AuditLog.php';
|
require_once dirname(__DIR__) . '/includes/AuditLog.php';
|
||||||
|
require_once dirname(__DIR__) . '/includes/PaginationHelper.php';
|
||||||
require_once __DIR__ . '/handlers/orders-handlers.php';
|
require_once __DIR__ . '/handlers/orders-handlers.php';
|
||||||
|
|
||||||
setCorsHeaders();
|
setCorsHeaders();
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ declare(strict_types=1);
|
|||||||
require_once dirname(__DIR__) . '/config.php';
|
require_once dirname(__DIR__) . '/config.php';
|
||||||
require_once dirname(__DIR__) . '/includes/JWTAuth.php';
|
require_once dirname(__DIR__) . '/includes/JWTAuth.php';
|
||||||
require_once dirname(__DIR__) . '/includes/AuditLog.php';
|
require_once dirname(__DIR__) . '/includes/AuditLog.php';
|
||||||
|
require_once dirname(__DIR__) . '/includes/PaginationHelper.php';
|
||||||
require_once __DIR__ . '/handlers/projects-handlers.php';
|
require_once __DIR__ . '/handlers/projects-handlers.php';
|
||||||
|
|
||||||
setCorsHeaders();
|
setCorsHeaders();
|
||||||
|
|||||||
82
api/includes/PaginationHelper.php
Normal file
82
api/includes/PaginationHelper.php
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pagination helper - extrakce spolecne logiky pro strankovani seznamu.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
class PaginationHelper
|
||||||
|
{
|
||||||
|
private const DEFAULT_PER_PAGE = 25;
|
||||||
|
private const MAX_PER_PAGE = 500;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nacte pagination parametry z GET requestu.
|
||||||
|
*
|
||||||
|
* @return array{page: int, per_page: int, sort: string, order: string, search: string}
|
||||||
|
*/
|
||||||
|
public static function parseParams(array $sortMap, string $defaultSort = 'created_at'): array
|
||||||
|
{
|
||||||
|
$sort = $_GET['sort'] ?? $defaultSort;
|
||||||
|
$order = strtoupper($_GET['order'] ?? 'DESC') === 'ASC' ? 'ASC' : 'DESC';
|
||||||
|
$page = max(1, (int) ($_GET['page'] ?? 1));
|
||||||
|
$perPage = min(self::MAX_PER_PAGE, max(1, (int) ($_GET['per_page'] ?? self::DEFAULT_PER_PAGE)));
|
||||||
|
$search = trim($_GET['search'] ?? '');
|
||||||
|
|
||||||
|
if (!isset($sortMap[$sort])) {
|
||||||
|
errorResponse('Neplatný parametr řazení', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'page' => $page,
|
||||||
|
'per_page' => $perPage,
|
||||||
|
'sort' => $sortMap[$sort],
|
||||||
|
'order' => $order,
|
||||||
|
'search' => $search ? mb_substr($search, 0, 100) : '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spusti COUNT + SELECT dotazy s pagination a vrati vysledek.
|
||||||
|
*
|
||||||
|
* @param PDO $pdo
|
||||||
|
* @param string $countSql - COUNT(*) dotaz
|
||||||
|
* @param string $dataSql - SELECT dotaz (bez LIMIT/OFFSET)
|
||||||
|
* @param array $params - parametry pro prepared statement
|
||||||
|
* @param array{page: int, per_page: int, sort: string, order: string} $pagination
|
||||||
|
* @return array{items: array, pagination: array}
|
||||||
|
*/
|
||||||
|
public static function paginate(
|
||||||
|
PDO $pdo,
|
||||||
|
string $countSql,
|
||||||
|
string $dataSql,
|
||||||
|
array $params,
|
||||||
|
array $pagination
|
||||||
|
): array {
|
||||||
|
$page = $pagination['page'];
|
||||||
|
$perPage = $pagination['per_page'];
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare($countSql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$total = (int) $stmt->fetchColumn();
|
||||||
|
|
||||||
|
$offset = ($page - 1) * $perPage;
|
||||||
|
$totalPages = (int) ceil($total / $perPage);
|
||||||
|
|
||||||
|
$sql = "{$dataSql} LIMIT {$perPage} OFFSET {$offset}";
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$items = $stmt->fetchAll();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'items' => $items,
|
||||||
|
'pagination' => [
|
||||||
|
'total' => $total,
|
||||||
|
'page' => $page,
|
||||||
|
'per_page' => $perPage,
|
||||||
|
'total_pages' => $totalPages,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2332,6 +2332,93 @@ img {
|
|||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
Pagination
|
||||||
|
============================================================================ */
|
||||||
|
|
||||||
|
.admin-pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.75rem 0;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-pagination-info {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-pagination-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-pagination-page {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 6px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
background: none;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-pagination-page:hover {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-pagination-page.active {
|
||||||
|
background: var(--accent-color);
|
||||||
|
color: #fff;
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-pagination-ellipsis {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-pagination-select {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.admin-pagination {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-pagination-info {
|
||||||
|
order: 2;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Keyboard shortcut badge */
|
/* Keyboard shortcut badge */
|
||||||
.admin-kbd {
|
.admin-kbd {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|||||||
106
src/admin/components/Pagination.jsx
Normal file
106
src/admin/components/Pagination.jsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strankovaci komponenta pro seznamove stranky.
|
||||||
|
*
|
||||||
|
* @param {object} pagination - {total, page, per_page, total_pages}
|
||||||
|
* @param {function} onPageChange - callback(newPage)
|
||||||
|
* @param {function} [onPerPageChange] - callback(newPerPage)
|
||||||
|
*/
|
||||||
|
export default function Pagination({ pagination, onPageChange, onPerPageChange }) {
|
||||||
|
const page = pagination?.page ?? 1
|
||||||
|
const totalPages = pagination?.total_pages ?? 1
|
||||||
|
const total = pagination?.total ?? 0
|
||||||
|
const perPage = pagination?.per_page ?? 25
|
||||||
|
|
||||||
|
const visiblePages = useMemo(() => {
|
||||||
|
const pages = []
|
||||||
|
const maxVisible = 5
|
||||||
|
let start = Math.max(1, page - Math.floor(maxVisible / 2))
|
||||||
|
const end = Math.min(totalPages, start + maxVisible - 1)
|
||||||
|
|
||||||
|
if (end - start < maxVisible - 1) {
|
||||||
|
start = Math.max(1, end - maxVisible + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start > 1) {
|
||||||
|
pages.push(1)
|
||||||
|
if (start > 2) { pages.push('...') }
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
pages.push(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (end < totalPages) {
|
||||||
|
if (end < totalPages - 1) { pages.push('...') }
|
||||||
|
pages.push(totalPages)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages
|
||||||
|
}, [page, totalPages])
|
||||||
|
|
||||||
|
if (!pagination || totalPages <= 1) { return null }
|
||||||
|
|
||||||
|
const from = (page - 1) * perPage + 1
|
||||||
|
const to = Math.min(page * perPage, total)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="admin-pagination">
|
||||||
|
<span className="admin-pagination-info">
|
||||||
|
{from}–{to} z {total}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="admin-pagination-controls">
|
||||||
|
<button
|
||||||
|
className="admin-btn-secondary admin-btn-sm"
|
||||||
|
disabled={page <= 1}
|
||||||
|
onClick={() => onPageChange(page - 1)}
|
||||||
|
aria-label="Předchozí stránka"
|
||||||
|
>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<polyline points="15 18 9 12 15 6" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{visiblePages.map((p, i) => (
|
||||||
|
p === '...'
|
||||||
|
? <span key={`ellipsis-${i}`} className="admin-pagination-ellipsis">…</span>
|
||||||
|
: (
|
||||||
|
<button
|
||||||
|
key={p}
|
||||||
|
className={`admin-pagination-page${p === page ? ' active' : ''}`}
|
||||||
|
onClick={() => onPageChange(p)}
|
||||||
|
>
|
||||||
|
{p}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
))}
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="admin-btn-secondary admin-btn-sm"
|
||||||
|
disabled={page >= totalPages}
|
||||||
|
onClick={() => onPageChange(page + 1)}
|
||||||
|
aria-label="Další stránka"
|
||||||
|
>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<polyline points="9 18 15 12 9 6" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{onPerPageChange && (
|
||||||
|
<select
|
||||||
|
className="admin-pagination-select"
|
||||||
|
value={perPage}
|
||||||
|
onChange={(e) => onPerPageChange(Number(e.target.value))}
|
||||||
|
aria-label="Záznamů na stránku"
|
||||||
|
>
|
||||||
|
{[10, 25, 50, 100].map((n) => (
|
||||||
|
<option key={n} value={n}>{n} / strana</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import { formatCurrency, formatDate, czechPlural } from '../utils/formatters'
|
|||||||
import SortIcon from '../components/SortIcon'
|
import SortIcon from '../components/SortIcon'
|
||||||
import useTableSort from '../hooks/useTableSort'
|
import useTableSort from '../hooks/useTableSort'
|
||||||
import useListData from '../hooks/useListData'
|
import useListData from '../hooks/useListData'
|
||||||
|
import Pagination from '../components/Pagination'
|
||||||
|
|
||||||
const ReceivedInvoices = lazy(() => import('./ReceivedInvoices'))
|
const ReceivedInvoices = lazy(() => import('./ReceivedInvoices'))
|
||||||
const API_BASE = '/api/admin'
|
const API_BASE = '/api/admin'
|
||||||
@@ -65,6 +66,7 @@ export default function Invoices() {
|
|||||||
const [receivedUploadOpen, setReceivedUploadOpen] = useState(false)
|
const [receivedUploadOpen, setReceivedUploadOpen] = useState(false)
|
||||||
const { sort, order, handleSort, activeSort } = useTableSort('invoice_number')
|
const { sort, order, handleSort, activeSort } = useTableSort('invoice_number')
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
const [statusFilter, setStatusFilter] = useState('')
|
const [statusFilter, setStatusFilter] = useState('')
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
@@ -139,8 +141,8 @@ export default function Invoices() {
|
|||||||
setDraft(null)
|
setDraft(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { items: invoices, loading, refetch: fetchData } = useListData('invoices.php', {
|
const { items: invoices, loading, pagination, refetch: fetchData } = useListData('invoices.php', {
|
||||||
dataKey: 'invoices', search, sort, order,
|
dataKey: 'invoices', search, sort, order, page,
|
||||||
extraParams: statusFilter ? { status: statusFilter } : {},
|
extraParams: statusFilter ? { status: statusFilter } : {},
|
||||||
errorMsg: 'Nepodařilo se načíst faktury'
|
errorMsg: 'Nepodařilo se načíst faktury'
|
||||||
})
|
})
|
||||||
@@ -269,7 +271,7 @@ export default function Invoices() {
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="admin-page-title">Faktury</h1>
|
<h1 className="admin-page-title">Faktury</h1>
|
||||||
<p className="admin-page-subtitle">
|
<p className="admin-page-subtitle">
|
||||||
{invoices.length} {czechPlural(invoices.length, 'faktura', 'faktury', 'faktur')}
|
{pagination?.total ?? invoices.length} {czechPlural(pagination?.total ?? invoices.length, 'faktura', 'faktury', 'faktur')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{hasPermission('invoices.create') && (
|
{hasPermission('invoices.create') && (
|
||||||
@@ -430,7 +432,7 @@ export default function Invoices() {
|
|||||||
<button
|
<button
|
||||||
key={f.value}
|
key={f.value}
|
||||||
className={`offers-tab ${statusFilter === f.value ? 'active' : ''}`}
|
className={`offers-tab ${statusFilter === f.value ? 'active' : ''}`}
|
||||||
onClick={() => setStatusFilter(f.value)}
|
onClick={() => { setStatusFilter(f.value); setPage(1) }}
|
||||||
>
|
>
|
||||||
{f.label}
|
{f.label}
|
||||||
</button>
|
</button>
|
||||||
@@ -450,7 +452,7 @@ export default function Invoices() {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => { setSearch(e.target.value); setPage(1) }}
|
||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
placeholder="Hledat podle čísla faktury, zákazníka nebo IČ..."
|
placeholder="Hledat podle čísla faktury, zákazníka nebo IČ..."
|
||||||
/>
|
/>
|
||||||
@@ -622,6 +624,7 @@ export default function Invoices() {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<Pagination pagination={pagination} onPageChange={setPage} />
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import SortIcon from '../components/SortIcon'
|
|||||||
import useTableSort from '../hooks/useTableSort'
|
import useTableSort from '../hooks/useTableSort'
|
||||||
import useListData from '../hooks/useListData'
|
import useListData from '../hooks/useListData'
|
||||||
import useModalLock from '../hooks/useModalLock'
|
import useModalLock from '../hooks/useModalLock'
|
||||||
|
import Pagination from '../components/Pagination'
|
||||||
const API_BASE = '/api/admin'
|
const API_BASE = '/api/admin'
|
||||||
const DRAFT_KEY = 'boha_offer_draft'
|
const DRAFT_KEY = 'boha_offer_draft'
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ export default function Offers() {
|
|||||||
|
|
||||||
const { sort, order, handleSort, activeSort } = useTableSort('quotation_number')
|
const { sort, order, handleSort, activeSort } = useTableSort('quotation_number')
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
|
||||||
const [deleteConfirm, setDeleteConfirm] = useState({ show: false, quotation: null })
|
const [deleteConfirm, setDeleteConfirm] = useState({ show: false, quotation: null })
|
||||||
const [deleting, setDeleting] = useState(false)
|
const [deleting, setDeleting] = useState(false)
|
||||||
@@ -36,8 +38,8 @@ export default function Offers() {
|
|||||||
const [orderAttachment, setOrderAttachment] = useState(null)
|
const [orderAttachment, setOrderAttachment] = useState(null)
|
||||||
const [draft, setDraft] = useState(null)
|
const [draft, setDraft] = useState(null)
|
||||||
|
|
||||||
const { items: quotations, loading, refetch: fetchData } = useListData('offers.php', {
|
const { items: quotations, loading, pagination, refetch: fetchData } = useListData('offers.php', {
|
||||||
dataKey: 'quotations', search, sort, order,
|
dataKey: 'quotations', search, sort, order, page,
|
||||||
errorMsg: 'Nepodařilo se načíst nabídky'
|
errorMsg: 'Nepodařilo se načíst nabídky'
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -232,7 +234,7 @@ export default function Offers() {
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="admin-page-title">Nabídky</h1>
|
<h1 className="admin-page-title">Nabídky</h1>
|
||||||
<p className="admin-page-subtitle">
|
<p className="admin-page-subtitle">
|
||||||
{quotations.length} {czechPlural(quotations.length, 'nabídka', 'nabídky', 'nabídek')}
|
{pagination?.total ?? quotations.length} {czechPlural(pagination?.total ?? quotations.length, 'nabídka', 'nabídky', 'nabídek')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="admin-page-actions">
|
<div className="admin-page-actions">
|
||||||
@@ -268,7 +270,7 @@ export default function Offers() {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => { setSearch(e.target.value); setPage(1) }}
|
||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
placeholder="Hledat podle čísla, projektu nebo zákazníka..."
|
placeholder="Hledat podle čísla, projektu nebo zákazníka..."
|
||||||
/>
|
/>
|
||||||
@@ -509,6 +511,7 @@ export default function Offers() {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<Pagination pagination={pagination} onPageChange={setPage} />
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { formatCurrency, formatDate, czechPlural } from '../utils/formatters'
|
|||||||
import SortIcon from '../components/SortIcon'
|
import SortIcon from '../components/SortIcon'
|
||||||
import useTableSort from '../hooks/useTableSort'
|
import useTableSort from '../hooks/useTableSort'
|
||||||
import useListData from '../hooks/useListData'
|
import useListData from '../hooks/useListData'
|
||||||
|
import Pagination from '../components/Pagination'
|
||||||
const API_BASE = '/api/admin'
|
const API_BASE = '/api/admin'
|
||||||
|
|
||||||
const STATUS_LABELS = {
|
const STATUS_LABELS = {
|
||||||
@@ -33,12 +34,13 @@ export default function Orders() {
|
|||||||
|
|
||||||
const { sort, order, handleSort, activeSort } = useTableSort('order_number')
|
const { sort, order, handleSort, activeSort } = useTableSort('order_number')
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
|
||||||
const [deleteConfirm, setDeleteConfirm] = useState({ show: false, order: null })
|
const [deleteConfirm, setDeleteConfirm] = useState({ show: false, order: null })
|
||||||
const [deleting, setDeleting] = useState(false)
|
const [deleting, setDeleting] = useState(false)
|
||||||
|
|
||||||
const { items: orders, loading, refetch: fetchData } = useListData('orders.php', {
|
const { items: orders, loading, pagination, refetch: fetchData } = useListData('orders.php', {
|
||||||
dataKey: 'orders', search, sort, order,
|
dataKey: 'orders', search, sort, order, page,
|
||||||
errorMsg: 'Nepodařilo se načíst objednávky'
|
errorMsg: 'Nepodařilo se načíst objednávky'
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -137,7 +139,7 @@ export default function Orders() {
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="admin-page-title">Objednávky</h1>
|
<h1 className="admin-page-title">Objednávky</h1>
|
||||||
<p className="admin-page-subtitle">
|
<p className="admin-page-subtitle">
|
||||||
{orders.length} {czechPlural(orders.length, 'objednávka', 'objednávky', 'objednávek')}
|
{pagination?.total ?? orders.length} {czechPlural(pagination?.total ?? orders.length, 'objednávka', 'objednávky', 'objednávek')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -153,7 +155,7 @@ export default function Orders() {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => { setSearch(e.target.value); setPage(1) }}
|
||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
placeholder="Hledat podle čísla, nabídky, projektu nebo zákazníka..."
|
placeholder="Hledat podle čísla, nabídky, projektu nebo zákazníka..."
|
||||||
/>
|
/>
|
||||||
@@ -264,6 +266,7 @@ export default function Orders() {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<Pagination pagination={pagination} onPageChange={setPage} />
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { formatDate, czechPlural } from '../utils/formatters'
|
|||||||
import SortIcon from '../components/SortIcon'
|
import SortIcon from '../components/SortIcon'
|
||||||
import useTableSort from '../hooks/useTableSort'
|
import useTableSort from '../hooks/useTableSort'
|
||||||
import useListData from '../hooks/useListData'
|
import useListData from '../hooks/useListData'
|
||||||
|
import Pagination from '../components/Pagination'
|
||||||
const API_BASE = '/api/admin'
|
const API_BASE = '/api/admin'
|
||||||
|
|
||||||
const STATUS_LABELS = {
|
const STATUS_LABELS = {
|
||||||
@@ -31,11 +32,12 @@ export default function Projects() {
|
|||||||
|
|
||||||
const { sort, order, handleSort, activeSort } = useTableSort('project_number')
|
const { sort, order, handleSort, activeSort } = useTableSort('project_number')
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
const [deletingId, setDeletingId] = useState(null)
|
const [deletingId, setDeletingId] = useState(null)
|
||||||
const [deleteTarget, setDeleteTarget] = useState(null)
|
const [deleteTarget, setDeleteTarget] = useState(null)
|
||||||
|
|
||||||
const { items: projects, setItems: setProjects, loading } = useListData('projects.php', {
|
const { items: projects, setItems: setProjects, loading, pagination } = useListData('projects.php', {
|
||||||
dataKey: 'projects', search, sort, order,
|
dataKey: 'projects', search, sort, order, page,
|
||||||
errorMsg: 'Nepodařilo se načíst projekty'
|
errorMsg: 'Nepodařilo se načíst projekty'
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -132,7 +134,7 @@ export default function Projects() {
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="admin-page-title">Projekty</h1>
|
<h1 className="admin-page-title">Projekty</h1>
|
||||||
<p className="admin-page-subtitle">
|
<p className="admin-page-subtitle">
|
||||||
{projects.length} {czechPlural(projects.length, 'projekt', 'projekty', 'projektů')}
|
{pagination?.total ?? projects.length} {czechPlural(pagination?.total ?? projects.length, 'projekt', 'projekty', 'projektů')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{hasPermission('projects.create') && (
|
{hasPermission('projects.create') && (
|
||||||
@@ -157,7 +159,7 @@ export default function Projects() {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => { setSearch(e.target.value); setPage(1) }}
|
||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
placeholder="Hledat podle čísla, názvu nebo zákazníka..."
|
placeholder="Hledat podle čísla, názvu nebo zákazníka..."
|
||||||
/>
|
/>
|
||||||
@@ -258,6 +260,7 @@ export default function Projects() {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<Pagination pagination={pagination} onPageChange={setPage} />
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user