From b2a2937a357d51e9262db762c2ca769e816c7200 Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 13 Mar 2026 09:19:40 +0100 Subject: [PATCH] feat: dist/ pridan do repa pro server deploy Co-Authored-By: Claude Opus 4.6 --- .gitignore | 2 +- dist/api/admin/attendance.php | 152 +++ dist/api/admin/audit-log.php | 94 ++ dist/api/admin/bank-accounts.php | 70 ++ dist/api/admin/company-settings.php | 71 ++ dist/api/admin/customers.php | 79 ++ dist/api/admin/dashboard.php | 281 +++++ .../admin/handlers/attendance-handlers.php | 613 +++++++++ .../admin/handlers/bank-accounts-handlers.php | 174 +++ .../handlers/company-settings-handlers.php | 253 ++++ .../api/admin/handlers/customers-handlers.php | 287 +++++ dist/api/admin/handlers/invoices-handlers.php | 696 +++++++++++ .../handlers/leave-requests-handlers.php | 481 +++++++ dist/api/admin/handlers/offers-handlers.php | 586 +++++++++ .../handlers/offers-templates-handlers.php | 273 ++++ dist/api/admin/handlers/orders-handlers.php | 528 ++++++++ dist/api/admin/handlers/projects-handlers.php | 406 ++++++ .../handlers/received-invoices-handlers.php | 511 ++++++++ dist/api/admin/handlers/roles-handlers.php | 242 ++++ dist/api/admin/handlers/session-handlers.php | 21 + dist/api/admin/handlers/sessions-handlers.php | 180 +++ dist/api/admin/handlers/totp-handlers.php | 426 +++++++ dist/api/admin/handlers/trips-handlers.php | 685 ++++++++++ dist/api/admin/handlers/users-handlers.php | 277 ++++ dist/api/admin/invoices-pdf.php | 1113 +++++++++++++++++ dist/api/admin/invoices.php | 110 ++ dist/api/admin/leave-requests.php | 80 ++ dist/api/admin/login.php | 180 +++ dist/api/admin/logout.php | 51 + dist/api/admin/offers-pdf.php | 879 +++++++++++++ dist/api/admin/offers-templates.php | 101 ++ dist/api/admin/offers.php | 96 ++ dist/api/admin/orders.php | 92 ++ dist/api/admin/profile.php | 117 ++ dist/api/admin/projects.php | 115 ++ dist/api/admin/received-invoices.php | 97 ++ dist/api/admin/refresh.php | 59 + dist/api/admin/roles.php | 67 + dist/api/admin/session.php | 94 ++ dist/api/admin/sessions.php | 67 + dist/api/admin/totp.php | 72 ++ dist/api/admin/trips.php | 132 ++ dist/api/admin/users.php | 73 ++ dist/api/cleanup.php | 58 + dist/api/config.php | 25 + dist/api/includes/AttendanceAdmin.php | 991 +++++++++++++++ dist/api/includes/AttendanceHelpers.php | 386 ++++++ dist/api/includes/AuditLog.php | 560 +++++++++ dist/api/includes/CnbRates.php | 205 +++ dist/api/includes/CzechHolidays.php | 117 ++ dist/api/includes/Encryption.php | 98 ++ dist/api/includes/JWTAuth.php | 663 ++++++++++ dist/api/includes/LeaveNotification.php | 90 ++ dist/api/includes/Mailer.php | 45 + dist/api/includes/PaginationHelper.php | 84 ++ dist/api/includes/RateLimiter.php | 220 ++++ dist/api/includes/Validator.php | 139 ++ dist/api/includes/constants.php | 38 + dist/api/includes/helpers.php | 357 ++++++ dist/api/rate_limits/.htaccess | 1 + .../8d5c008a5d9ce3db19bbd930d2b86dd1.json | 1 + .../92f2e2a3520918dc3a4f54cfed15fbfe.json | 1 + .../95bc5d544df53a813f55a3a2ae270497.json | 1 + .../ddb677adee83b940eedb4fd82821f581.json | 1 + dist/apple-touch-icon.png | Bin 0 -> 10312 bytes dist/assets/Attendance-Bq3ErxVC.js | 1 + dist/assets/AttendanceAdmin-CN6S51Mm.js | 125 ++ dist/assets/AttendanceBalances-BBDz3NFV.js | 1 + dist/assets/AttendanceCreate-j72Gsy_8.js | 1 + dist/assets/AttendanceHistory-DQLQHe_C.js | 88 ++ dist/assets/AttendanceLocation-C1MPClO-.js | 1 + dist/assets/AuditLog-DGV9ABTZ.js | 1 + dist/assets/CompanySettings-Cac8Rr8l.js | 1 + dist/assets/Forbidden-D25jV3Oq.js | 1 + dist/assets/InvoiceCreate-D7azSaER.js | 2 + dist/assets/InvoiceDetail-CxmXBolF.js | 2 + dist/assets/Invoices-BxKVmNYN.js | 2 + dist/assets/LeaveApproval-BQyC3i8M.js | 1 + dist/assets/LeaveRequests-CJA9No9B.js | 1 + dist/assets/NotFound-Cm3yLPlV.js | 1 + dist/assets/OfferDetail-TQHeNuC6.js | 1 + dist/assets/Offers-DwUrbYu8.js | 1 + dist/assets/OffersCustomers-BjvYTLYl.js | 1 + dist/assets/OffersTemplates-bzE8pdbp.js | 1 + dist/assets/OrderDetail-3O2WshUa.js | 1 + dist/assets/Orders-CSsExPPr.js | 1 + dist/assets/Pagination-B1sbY6V7.js | 1 + dist/assets/ProjectCreate-B8awV2Y4.js | 1 + dist/assets/ProjectDetail-BWBiBOHM.js | 1 + dist/assets/Projects-DRnqfGWv.js | 1 + dist/assets/ReceivedInvoices-Cbz7NucU.js | 1 + dist/assets/RichEditor-7oN3-GhD.css | 7 + dist/assets/RichEditor-Bfur5pi6.js | 49 + dist/assets/Settings-WU5LlT1S.js | 1 + dist/assets/Trips-BXj-7zce.js | 1 + dist/assets/TripsAdmin-yiBDyemU.js | 81 ++ dist/assets/TripsHistory-BBeF9ORG.js | 1 + dist/assets/Users-_q0u-jiE.js | 1 + dist/assets/Vehicles-drdX9CTA.js | 1 + dist/assets/attendanceHelpers-D6sLEw0q.js | 1 + dist/assets/index-BBlIrj2z.js | 7 + dist/assets/index-BazDZfA0.css | 1 + dist/assets/qrcode-CBP_ltkV.js | 8 + dist/assets/useListData-BVkTFDdr.js | 1 + dist/assets/useSortableList-CgbuKaxB.js | 5 + dist/assets/vendor-animation-0s3FMHwK.js | 17 + dist/assets/vendor-react-BVs3cwbi.js | 59 + dist/assets/vendor-utils-Dyr8OjFr.js | 2 + dist/favicon-96x96.png | Bin 0 -> 3853 bytes dist/favicon.ico | Bin 0 -> 15086 bytes dist/favicon.svg | 3 + dist/images/logo-dark.png | Bin 0 -> 16982 bytes dist/images/logo-light.png | Bin 0 -> 14621 bytes dist/index.html | 43 + dist/robots.txt | 2 + dist/router.php | 13 + dist/site.webmanifest | 21 + dist/web-app-manifest-192x192.png | Bin 0 -> 11523 bytes dist/web-app-manifest-512x512.png | Bin 0 -> 51005 bytes 119 files changed, 15628 insertions(+), 1 deletion(-) create mode 100644 dist/api/admin/attendance.php create mode 100644 dist/api/admin/audit-log.php create mode 100644 dist/api/admin/bank-accounts.php create mode 100644 dist/api/admin/company-settings.php create mode 100644 dist/api/admin/customers.php create mode 100644 dist/api/admin/dashboard.php create mode 100644 dist/api/admin/handlers/attendance-handlers.php create mode 100644 dist/api/admin/handlers/bank-accounts-handlers.php create mode 100644 dist/api/admin/handlers/company-settings-handlers.php create mode 100644 dist/api/admin/handlers/customers-handlers.php create mode 100644 dist/api/admin/handlers/invoices-handlers.php create mode 100644 dist/api/admin/handlers/leave-requests-handlers.php create mode 100644 dist/api/admin/handlers/offers-handlers.php create mode 100644 dist/api/admin/handlers/offers-templates-handlers.php create mode 100644 dist/api/admin/handlers/orders-handlers.php create mode 100644 dist/api/admin/handlers/projects-handlers.php create mode 100644 dist/api/admin/handlers/received-invoices-handlers.php create mode 100644 dist/api/admin/handlers/roles-handlers.php create mode 100644 dist/api/admin/handlers/session-handlers.php create mode 100644 dist/api/admin/handlers/sessions-handlers.php create mode 100644 dist/api/admin/handlers/totp-handlers.php create mode 100644 dist/api/admin/handlers/trips-handlers.php create mode 100644 dist/api/admin/handlers/users-handlers.php create mode 100644 dist/api/admin/invoices-pdf.php create mode 100644 dist/api/admin/invoices.php create mode 100644 dist/api/admin/leave-requests.php create mode 100644 dist/api/admin/login.php create mode 100644 dist/api/admin/logout.php create mode 100644 dist/api/admin/offers-pdf.php create mode 100644 dist/api/admin/offers-templates.php create mode 100644 dist/api/admin/offers.php create mode 100644 dist/api/admin/orders.php create mode 100644 dist/api/admin/profile.php create mode 100644 dist/api/admin/projects.php create mode 100644 dist/api/admin/received-invoices.php create mode 100644 dist/api/admin/refresh.php create mode 100644 dist/api/admin/roles.php create mode 100644 dist/api/admin/session.php create mode 100644 dist/api/admin/sessions.php create mode 100644 dist/api/admin/totp.php create mode 100644 dist/api/admin/trips.php create mode 100644 dist/api/admin/users.php create mode 100644 dist/api/cleanup.php create mode 100644 dist/api/config.php create mode 100644 dist/api/includes/AttendanceAdmin.php create mode 100644 dist/api/includes/AttendanceHelpers.php create mode 100644 dist/api/includes/AuditLog.php create mode 100644 dist/api/includes/CnbRates.php create mode 100644 dist/api/includes/CzechHolidays.php create mode 100644 dist/api/includes/Encryption.php create mode 100644 dist/api/includes/JWTAuth.php create mode 100644 dist/api/includes/LeaveNotification.php create mode 100644 dist/api/includes/Mailer.php create mode 100644 dist/api/includes/PaginationHelper.php create mode 100644 dist/api/includes/RateLimiter.php create mode 100644 dist/api/includes/Validator.php create mode 100644 dist/api/includes/constants.php create mode 100644 dist/api/includes/helpers.php create mode 100644 dist/api/rate_limits/.htaccess create mode 100644 dist/api/rate_limits/8d5c008a5d9ce3db19bbd930d2b86dd1.json create mode 100644 dist/api/rate_limits/92f2e2a3520918dc3a4f54cfed15fbfe.json create mode 100644 dist/api/rate_limits/95bc5d544df53a813f55a3a2ae270497.json create mode 100644 dist/api/rate_limits/ddb677adee83b940eedb4fd82821f581.json create mode 100644 dist/apple-touch-icon.png create mode 100644 dist/assets/Attendance-Bq3ErxVC.js create mode 100644 dist/assets/AttendanceAdmin-CN6S51Mm.js create mode 100644 dist/assets/AttendanceBalances-BBDz3NFV.js create mode 100644 dist/assets/AttendanceCreate-j72Gsy_8.js create mode 100644 dist/assets/AttendanceHistory-DQLQHe_C.js create mode 100644 dist/assets/AttendanceLocation-C1MPClO-.js create mode 100644 dist/assets/AuditLog-DGV9ABTZ.js create mode 100644 dist/assets/CompanySettings-Cac8Rr8l.js create mode 100644 dist/assets/Forbidden-D25jV3Oq.js create mode 100644 dist/assets/InvoiceCreate-D7azSaER.js create mode 100644 dist/assets/InvoiceDetail-CxmXBolF.js create mode 100644 dist/assets/Invoices-BxKVmNYN.js create mode 100644 dist/assets/LeaveApproval-BQyC3i8M.js create mode 100644 dist/assets/LeaveRequests-CJA9No9B.js create mode 100644 dist/assets/NotFound-Cm3yLPlV.js create mode 100644 dist/assets/OfferDetail-TQHeNuC6.js create mode 100644 dist/assets/Offers-DwUrbYu8.js create mode 100644 dist/assets/OffersCustomers-BjvYTLYl.js create mode 100644 dist/assets/OffersTemplates-bzE8pdbp.js create mode 100644 dist/assets/OrderDetail-3O2WshUa.js create mode 100644 dist/assets/Orders-CSsExPPr.js create mode 100644 dist/assets/Pagination-B1sbY6V7.js create mode 100644 dist/assets/ProjectCreate-B8awV2Y4.js create mode 100644 dist/assets/ProjectDetail-BWBiBOHM.js create mode 100644 dist/assets/Projects-DRnqfGWv.js create mode 100644 dist/assets/ReceivedInvoices-Cbz7NucU.js create mode 100644 dist/assets/RichEditor-7oN3-GhD.css create mode 100644 dist/assets/RichEditor-Bfur5pi6.js create mode 100644 dist/assets/Settings-WU5LlT1S.js create mode 100644 dist/assets/Trips-BXj-7zce.js create mode 100644 dist/assets/TripsAdmin-yiBDyemU.js create mode 100644 dist/assets/TripsHistory-BBeF9ORG.js create mode 100644 dist/assets/Users-_q0u-jiE.js create mode 100644 dist/assets/Vehicles-drdX9CTA.js create mode 100644 dist/assets/attendanceHelpers-D6sLEw0q.js create mode 100644 dist/assets/index-BBlIrj2z.js create mode 100644 dist/assets/index-BazDZfA0.css create mode 100644 dist/assets/qrcode-CBP_ltkV.js create mode 100644 dist/assets/useListData-BVkTFDdr.js create mode 100644 dist/assets/useSortableList-CgbuKaxB.js create mode 100644 dist/assets/vendor-animation-0s3FMHwK.js create mode 100644 dist/assets/vendor-react-BVs3cwbi.js create mode 100644 dist/assets/vendor-utils-Dyr8OjFr.js create mode 100644 dist/favicon-96x96.png create mode 100644 dist/favicon.ico create mode 100644 dist/favicon.svg create mode 100644 dist/images/logo-dark.png create mode 100644 dist/images/logo-light.png create mode 100644 dist/index.html create mode 100644 dist/robots.txt create mode 100644 dist/router.php create mode 100644 dist/site.webmanifest create mode 100644 dist/web-app-manifest-192x192.png create mode 100644 dist/web-app-manifest-512x512.png diff --git a/.gitignore b/.gitignore index 51bdbc9..43c024f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ example_design/ sql/ # Build -dist/ +# dist/ # IDE .vscode/ diff --git a/dist/api/admin/attendance.php b/dist/api/admin/attendance.php new file mode 100644 index 0000000..e4ce058 --- /dev/null +++ b/dist/api/admin/attendance.php @@ -0,0 +1,152 @@ +getMessage()); + errorResponse('Chyba databáze', 500); +} + +// ============================================================================ +// User-facing handlers +// ============================================================================ diff --git a/dist/api/admin/audit-log.php b/dist/api/admin/audit-log.php new file mode 100644 index 0000000..aabca73 --- /dev/null +++ b/dist/api/admin/audit-log.php @@ -0,0 +1,94 @@ +query('DELETE FROM audit_logs'); + $deleted = $stmt->rowCount(); + $msg = $deleted > 0 + ? "Smazáno všech $deleted záznamů" + : 'Audit log je prázdný'; + } else { + $days = max(1, $days); + $stmt = $pdo->prepare( + 'DELETE FROM audit_logs WHERE created_at < DATE_SUB(NOW(), INTERVAL ? DAY)' + ); + $stmt->execute([$days]); + $deleted = $stmt->rowCount(); + $msg = $deleted > 0 + ? "Smazáno $deleted záznamů starších $days dní" + : "Žádné záznamy starší než $days dní nebyly nalezeny"; + } + + successResponse(['deleted' => $deleted], $msg); +} + +$page = max(1, (int) ($_GET['page'] ?? 1)); +$perPage = max(1, min(100, (int) ($_GET['per_page'] ?? 50))); + +$filters = []; + +if (!empty($_GET['search'])) { + $filters['search'] = (string) $_GET['search']; +} + +if (!empty($_GET['action'])) { + $filters['action'] = (string) $_GET['action']; +} + +if (!empty($_GET['entity_type'])) { + $filters['entity_type'] = (string) $_GET['entity_type']; +} + +if (!empty($_GET['date_from'])) { + $filters['date_from'] = (string) $_GET['date_from']; +} + +if (!empty($_GET['date_to'])) { + $filters['date_to'] = (string) $_GET['date_to']; +} + +$result = AuditLog::getLogs($filters, $page, $perPage); + +successResponse($result); diff --git a/dist/api/admin/bank-accounts.php b/dist/api/admin/bank-accounts.php new file mode 100644 index 0000000..f8a9a5b --- /dev/null +++ b/dist/api/admin/bank-accounts.php @@ -0,0 +1,70 @@ +getMessage()); + if (DEBUG_MODE) { + errorResponse('Chyba databáze: ' . $e->getMessage(), 500); + } else { + errorResponse('Chyba databáze', 500); + } +} diff --git a/dist/api/admin/company-settings.php b/dist/api/admin/company-settings.php new file mode 100644 index 0000000..b14c724 --- /dev/null +++ b/dist/api/admin/company-settings.php @@ -0,0 +1,71 @@ +getMessage()); + if (DEBUG_MODE) { + errorResponse('Chyba databáze: ' . $e->getMessage(), 500); + } else { + errorResponse('Chyba databáze', 500); + } +} diff --git a/dist/api/admin/customers.php b/dist/api/admin/customers.php new file mode 100644 index 0000000..01c9deb --- /dev/null +++ b/dist/api/admin/customers.php @@ -0,0 +1,79 @@ +getMessage()); + if (DEBUG_MODE) { + errorResponse('Chyba databáze: ' . $e->getMessage(), 500); + } else { + errorResponse('Chyba databáze', 500); + } +} diff --git a/dist/api/admin/dashboard.php b/dist/api/admin/dashboard.php new file mode 100644 index 0000000..3bb75a6 --- /dev/null +++ b/dist/api/admin/dashboard.php @@ -0,0 +1,281 @@ +prepare(" + SELECT id FROM attendance + WHERE user_id = ? AND departure_time IS NULL AND (leave_type IS NULL OR leave_type = 'work') + ORDER BY created_at DESC LIMIT 1 + "); + $stmt->execute([$userId]); + $result['my_shift'] = [ + 'has_ongoing' => (bool) $stmt->fetch(), + ]; +} + +// --- Docházka dnes (attendance.admin) --- +if (hasPermission($authData, 'attendance.admin')) { + // Poslední pracovní záznam per uživatel (vyloučit ty co mají leave dnes) + $stmt = $pdo->query(" + SELECT u.id, CONCAT(u.first_name, ' ', u.last_name) as name, + CONCAT(LEFT(u.first_name, 1), LEFT(u.last_name, 1)) as initials, + a.arrival_time, a.departure_time, a.break_start, a.break_end + FROM users u + LEFT JOIN ( + SELECT a1.* + FROM attendance a1 + INNER JOIN ( + SELECT user_id, MAX(id) as max_id + FROM attendance + WHERE shift_date = CURDATE() + AND (leave_type IS NULL OR leave_type = 'work') + GROUP BY user_id + ) a2 ON a1.id = a2.max_id + ) a ON u.id = a.user_id + WHERE u.is_active = 1 + AND u.id NOT IN ( + SELECT user_id FROM attendance + WHERE shift_date = CURDATE() AND leave_type IN ('vacation', 'sick', 'holiday', 'unpaid') + ) + ORDER BY a.arrival_time IS NULL, a.arrival_time ASC + "); + $users = $stmt->fetchAll(); + + $present = 0; + $away = 0; + $attendanceUsers = []; + + foreach ($users as $u) { + $status = 'out'; + $arrivedAt = null; + + if ($u['arrival_time'] !== null) { + if ($u['departure_time'] !== null) { + $status = 'out'; + } elseif ($u['break_start'] !== null && $u['break_end'] === null) { + $status = 'away'; + $away++; + } else { + $status = 'in'; + $present++; + } + $arrivedAt = date('H:i', strtotime($u['arrival_time'])); + } + + $attendanceUsers[] = [ + 'name' => $u['name'], + 'initials' => $u['initials'], + 'status' => $status, + 'arrived_at' => $arrivedAt, + ]; + } + + // Dnes na dovolene/nemocenske + $stmtLeave = $pdo->query(" + SELECT CONCAT(u.first_name, ' ', u.last_name) as name, + CONCAT(LEFT(u.first_name, 1), LEFT(u.last_name, 1)) as initials, + a.leave_type + FROM attendance a + JOIN users u ON a.user_id = u.id + WHERE a.shift_date = CURDATE() AND a.leave_type IN ('vacation', 'sick', 'holiday', 'unpaid') + "); + $onLeave = $stmtLeave->fetchAll(); + + foreach ($onLeave as $leave) { + $attendanceUsers[] = [ + 'name' => $leave['name'], + 'initials' => $leave['initials'], + 'status' => 'leave', + 'arrived_at' => null, + 'leave_type' => $leave['leave_type'], + ]; + } + + $result['attendance'] = [ + 'present_today' => $present, + 'away_today' => $away, + 'total_active' => count($users), + 'on_leave' => count($onLeave), + 'users' => $attendanceUsers, + ]; +} + +// --- Nabídky (offers.view) --- +if (hasPermission($authData, 'offers.view')) { + $stmt = $pdo->query(" + SELECT + COUNT(*) as total, + SUM(CASE WHEN q.order_id IS NULL + AND (q.valid_until IS NULL OR q.valid_until >= CURDATE()) + THEN 1 ELSE 0 END) as open_count, + SUM(CASE WHEN q.order_id IS NULL + AND q.valid_until < CURDATE() + THEN 1 ELSE 0 END) as expired_count, + SUM(CASE WHEN q.order_id IS NOT NULL + THEN 1 ELSE 0 END) as converted_count + FROM quotations q + "); + $counts = $stmt->fetch(); + + $stmtMonth = $pdo->query(" + SELECT COUNT(*) as count FROM quotations + WHERE YEAR(created_at) = YEAR(CURDATE()) AND MONTH(created_at) = MONTH(CURDATE()) + "); + $monthData = $stmtMonth->fetch(); + + $result['offers'] = [ + 'total' => (int) $counts['total'], + 'open_count' => (int) $counts['open_count'], + 'expired_count' => (int) $counts['expired_count'], + 'converted_count' => (int) $counts['converted_count'], + 'created_this_month' => (int) $monthData['count'], + ]; +} + +// --- Projekty (projects.view) --- +if (hasPermission($authData, 'projects.view')) { + $stmt = $pdo->query(" + SELECT p.id, p.name, p.status, c.name as customer_name + FROM projects p + LEFT JOIN customers c ON p.customer_id = c.id + WHERE p.status = 'aktivni' + ORDER BY p.modified_at DESC + LIMIT 5 + "); + $activeProjects = $stmt->fetchAll(); + + $stmtCounts = $pdo->query(" + SELECT + SUM(CASE WHEN status = 'aktivni' THEN 1 ELSE 0 END) as active_count, + SUM(CASE WHEN status = 'dokonceny' THEN 1 ELSE 0 END) as completed_count + FROM projects WHERE status != 'deleted' + "); + $projectCounts = $stmtCounts->fetch(); + + $result['projects'] = [ + 'active_count' => (int) ($projectCounts['active_count'] ?? 0), + 'completed_count' => (int) ($projectCounts['completed_count'] ?? 0), + 'active_projects' => $activeProjects, + ]; +} + +// --- Faktury (invoices.view) --- +if (hasPermission($authData, 'invoices.view')) { + $stmt = $pdo->query(" + SELECT + COUNT(*) as total, + SUM(CASE WHEN i.status = 'paid' + AND YEAR(i.paid_date) = YEAR(CURDATE()) + AND MONTH(i.paid_date) = MONTH(CURDATE()) + THEN 1 ELSE 0 END) as paid_this_month, + SUM(CASE WHEN i.status IN ('issued', 'overdue') + THEN 1 ELSE 0 END) as unpaid_count + FROM invoices i + "); + $invCounts = $stmt->fetch(); + + // Tržby tento měsíc per faktura (pro kurz k datu vystaveni) + $stmtRevenue = $pdo->query(" + SELECT i.id, i.currency, i.issue_date, + COALESCE(SUM(ii.quantity * ii.unit_price), 0) as revenue + FROM invoices i + JOIN invoice_items ii ON i.id = ii.invoice_id + WHERE i.status = 'paid' + AND YEAR(i.paid_date) = YEAR(CURDATE()) + AND MONTH(i.paid_date) = MONTH(CURDATE()) + GROUP BY i.id, i.currency, i.issue_date + ORDER BY revenue DESC + "); + + $revByCurrency = []; + $revCzkItems = []; + foreach ($stmtRevenue->fetchAll() as $row) { + $cur = $row['currency']; + $amt = (float) $row['revenue']; + $revByCurrency[$cur] = ($revByCurrency[$cur] ?? 0) + $amt; + $revCzkItems[] = [ + 'amount' => $amt, + 'currency' => $cur, + 'date' => $row['issue_date'], + ]; + } + + $revenueByCurrency = []; + foreach ($revByCurrency as $cur => $total) { + $revenueByCurrency[] = [ + 'currency' => $cur, + 'amount' => round($total, 2), + ]; + } + + $cnb = CnbRates::getInstance(); + + $result['invoices'] = [ + 'total' => (int) $invCounts['total'], + 'paid_this_month' => (int) $invCounts['paid_this_month'], + 'unpaid_count' => (int) $invCounts['unpaid_count'], + 'revenue_this_month' => $revenueByCurrency, + 'revenue_czk' => $cnb->sumToCzk($revCzkItems), + ]; +} + +// --- Čekající žádosti (attendance.approve) --- +if (hasPermission($authData, 'attendance.approve')) { + $stmt = $pdo->query(" + SELECT COUNT(*) as count FROM leave_requests WHERE status = 'pending' + "); + $pending = $stmt->fetch(); + + $result['leave_pending'] = [ + 'count' => (int) $pending['count'], + ]; +} + +// --- Poslední aktivita (settings.roles = admin přehled) --- +if (hasPermission($authData, 'settings.roles')) { + $stmt = $pdo->query(" + SELECT username, action, entity_type, description, created_at + FROM audit_logs + WHERE action IN ('create', 'update', 'delete', 'login') + ORDER BY created_at DESC + LIMIT 8 + "); + $result['recent_activity'] = $stmt->fetchAll(); +} + +jsonResponse($result); diff --git a/dist/api/admin/handlers/attendance-handlers.php b/dist/api/admin/handlers/attendance-handlers.php new file mode 100644 index 0000000..bd49619 --- /dev/null +++ b/dist/api/admin/handlers/attendance-handlers.php @@ -0,0 +1,613 @@ +prepare(" + SELECT id, user_id, shift_date, arrival_time, arrival_lat, arrival_lng, + arrival_accuracy, arrival_address, break_start, break_end, + departure_time, departure_lat, departure_lng, departure_accuracy, + departure_address, notes, project_id, leave_type, leave_hours, created_at + FROM attendance + WHERE user_id = ? AND departure_time IS NULL AND (leave_type IS NULL OR leave_type = 'work') + ORDER BY created_at DESC LIMIT 1 + "); + $stmt->execute([$userId]); + $ongoingShift = $stmt->fetch(); + + $projectLogs = []; + $activeProjectId = null; + if ($ongoingShift) { + $stmt = $pdo->prepare( + 'SELECT id, attendance_id, project_id, started_at, ended_at, hours, minutes + FROM attendance_project_logs WHERE attendance_id = ? ORDER BY started_at ASC' + ); + $stmt->execute([$ongoingShift['id']]); + $projectLogs = $stmt->fetchAll(); + foreach ($projectLogs as $log) { + if ($log['ended_at'] === null) { + $activeProjectId = (int)$log['project_id']; + break; + } + } + } + + $stmt = $pdo->prepare(" + SELECT id, user_id, shift_date, arrival_time, arrival_lat, arrival_lng, + arrival_accuracy, arrival_address, break_start, break_end, + departure_time, departure_lat, departure_lng, departure_accuracy, + departure_address, notes, project_id, leave_type, leave_hours, created_at + FROM attendance + WHERE user_id = ? AND shift_date = ? + AND departure_time IS NOT NULL + AND (leave_type IS NULL OR leave_type = 'work') + ORDER BY arrival_time DESC + "); + $stmt->execute([$userId, $today]); + $todayShifts = $stmt->fetchAll(); + + $completedShiftIds = array_column($todayShifts, 'id'); + $completedProjectLogs = []; + if (!empty($completedShiftIds)) { + $placeholders = implode(',', array_fill(0, count($completedShiftIds), '?')); + $stmt = $pdo->prepare( + "SELECT id, attendance_id, project_id, started_at, ended_at, hours, minutes + FROM attendance_project_logs + WHERE attendance_id IN ($placeholders) + ORDER BY started_at ASC" + ); + $stmt->execute($completedShiftIds); + $allLogs = $stmt->fetchAll(); + foreach ($allLogs as $log) { + $completedProjectLogs[$log['attendance_id']][] = $log; + } + } + + $leaveBalance = getLeaveBalance($pdo, $userId); + + $currentYear = $dbTime['year']; + $currentMonth = $dbTime['month']; + $fund = CzechHolidays::getMonthlyWorkFund($currentYear, $currentMonth); + $businessDays = CzechHolidays::getBusinessDaysInMonth($currentYear, $currentMonth); + + $startDate = substr($dbTime['today'], 0, 7) . '-01'; + $endDate = date('Y-m-t', strtotime($startDate)); + + $stmt = $pdo->prepare(' + SELECT id, user_id, shift_date, arrival_time, break_start, break_end, + departure_time, notes, project_id, leave_type, leave_hours + FROM attendance + WHERE user_id = ? AND shift_date BETWEEN ? AND ? + '); + $stmt->execute([$userId, $startDate, $endDate]); + $monthRecords = $stmt->fetchAll(); + + $workedMinutes = 0; + $leaveHoursMonth = 0; + $vacationHours = 0; + $sickHours = 0; + $holidayHours = 0; + $unpaidHours = 0; + foreach ($monthRecords as $rec) { + $lt = $rec['leave_type'] ?? 'work'; + $lh = (float)($rec['leave_hours'] ?? 0); + if ($lt === 'work') { + if ($rec['departure_time']) { + $workedMinutes += calculateWorkMinutes($rec); + } + } elseif ($lt === 'vacation') { + $vacationHours += $lh; + $leaveHoursMonth += $lh; + } elseif ($lt === 'sick') { + $sickHours += $lh; + $leaveHoursMonth += $lh; + } elseif ($lt === 'holiday') { + $holidayHours += $lh; + } elseif ($lt === 'unpaid') { + $unpaidHours += $lh; + } + } + + $workedHours = round($workedMinutes / 60, 1); + $covered = $workedHours + $leaveHoursMonth; + $remaining = max(0, $fund - $covered); + $overtime = max(0, round($covered - $fund, 1)); + + $monthlyFund = [ + 'fund' => $fund, + 'business_days' => $businessDays, + 'worked' => $workedHours, + 'leave_hours' => $leaveHoursMonth, + 'vacation_hours' => $vacationHours, + 'sick_hours' => $sickHours, + 'holiday_hours' => $holidayHours, + 'unpaid_hours' => $unpaidHours, + 'covered' => $covered, + 'remaining' => $remaining, + 'overtime' => $overtime, + 'month_name' => getCzechMonthName($currentMonth) . ' ' . $currentYear, + ]; + + // Enrich project logs with names + $allLogProjectIds = []; + foreach ($projectLogs as $l) { + $allLogProjectIds[$l['project_id']] = $l['project_id']; + } + foreach ($completedProjectLogs as $logs) { + foreach ($logs as $l) { + $allLogProjectIds[$l['project_id']] = $l['project_id']; + } + } + $projNameMap = fetchProjectNames($allLogProjectIds); + + foreach ($projectLogs as &$l) { + $l['project_name'] = $projNameMap[$l['project_id']] ?? null; + } + unset($l); + foreach ($completedProjectLogs as &$logs) { + foreach ($logs as &$l) { + $l['project_name'] = $projNameMap[$l['project_id']] ?? null; + } + unset($l); + } + unset($logs); + + foreach ($todayShifts as &$shift) { + $shift['project_logs'] = $completedProjectLogs[$shift['id']] ?? []; + } + unset($shift); + + successResponse([ + 'ongoing_shift' => $ongoingShift, + 'today_shifts' => $todayShifts, + 'date' => $today, + 'leave_balance' => $leaveBalance, + 'monthly_fund' => $monthlyFund, + 'project_logs' => $projectLogs, + 'active_project_id' => $activeProjectId, + ]); +} + +function handleGetHistory(PDO $pdo, int $userId): void +{ + $month = validateMonth(); + $year = (int)substr($month, 0, 4); + $monthNum = (int)substr($month, 5, 2); + + $startDate = "{$month}-01"; + $endDate = date('Y-m-t', strtotime($startDate)); + + $stmt = $pdo->prepare(' + SELECT id, user_id, shift_date, arrival_time, arrival_address, + break_start, break_end, departure_time, departure_address, + notes, project_id, leave_type, leave_hours, created_at + FROM attendance + WHERE user_id = ? AND shift_date BETWEEN ? AND ? + ORDER BY shift_date DESC + '); + $stmt->execute([$userId, $startDate, $endDate]); + $records = $stmt->fetchAll(); + + enrichRecordsWithProjectLogs($pdo, $records); + + $totalMinutes = 0; + $vacationHours = 0; + $sickHours = 0; + $holidayHours = 0; + $unpaidHours = 0; + + foreach ($records as $record) { + $leaveType = $record['leave_type'] ?? 'work'; + $leaveHours = (float)($record['leave_hours'] ?? 0); + + if ($leaveType === 'vacation') { + $vacationHours += $leaveHours; + } elseif ($leaveType === 'sick') { + $sickHours += $leaveHours; + } elseif ($leaveType === 'holiday') { + $holidayHours += $leaveHours; + } elseif ($leaveType === 'unpaid') { + $unpaidHours += $leaveHours; + } else { + $totalMinutes += calculateWorkMinutes($record); + } + } + + $fund = CzechHolidays::getMonthlyWorkFund($year, $monthNum); + $businessDays = CzechHolidays::getBusinessDaysInMonth($year, $monthNum); + $workedHours = round($totalMinutes / 60, 1); + $leaveHoursCovered = $vacationHours + $sickHours; + $covered = $workedHours + $leaveHoursCovered; + $remaining = max(0, round($fund - $covered, 1)); + $overtime = max(0, round($covered - $fund, 1)); + + $leaveBalance = getLeaveBalance($pdo, $userId, $year); + + successResponse([ + 'records' => $records, + 'month' => $month, + 'year' => $year, + 'month_name' => getCzechMonthName($monthNum) . ' ' . $year, + 'total_minutes' => $totalMinutes, + 'vacation_hours' => $vacationHours, + 'sick_hours' => $sickHours, + 'holiday_hours' => $holidayHours, + 'unpaid_hours' => $unpaidHours, + 'leave_balance' => $leaveBalance, + 'monthly_fund' => [ + 'fund' => $fund, + 'business_days' => $businessDays, + 'worked' => $workedHours, + 'leave_hours' => $leaveHoursCovered, + 'covered' => $covered, + 'remaining' => $remaining, + 'overtime' => $overtime, + ], + ]); +} + +function handlePunch(PDO $pdo, int $userId): void +{ + $input = getJsonInput(); + $action = $input['punch_action'] ?? ''; + $dbTime = getDbNow($pdo); + $today = $dbTime['today']; + $rawNow = $dbTime['now']; + + $lat = isset($input['latitude']) && $input['latitude'] !== '' ? (float)$input['latitude'] : null; + $lng = isset($input['longitude']) && $input['longitude'] !== '' ? (float)$input['longitude'] : null; + $accuracy = isset($input['accuracy']) && $input['accuracy'] !== '' ? (float)$input['accuracy'] : null; + $address = !empty($input['address']) ? $input['address'] : null; + + $stmt = $pdo->prepare(" + SELECT id, user_id, shift_date, arrival_time, break_start, break_end, + departure_time, notes, project_id, leave_type, created_at + FROM attendance + WHERE user_id = ? AND departure_time IS NULL AND (leave_type IS NULL OR leave_type = 'work') + ORDER BY created_at DESC LIMIT 1 + "); + $stmt->execute([$userId]); + $ongoingShift = $stmt->fetch(); + + if ($action === 'arrival' && !$ongoingShift) { + $now = roundUpTo15Minutes($rawNow); + $stmt = $pdo->prepare(' + INSERT INTO attendance + (user_id, shift_date, arrival_time, arrival_lat, arrival_lng, arrival_accuracy, arrival_address) + VALUES (?, ?, ?, ?, ?, ?, ?) + '); + $stmt->execute([$userId, $today, $now, $lat, $lng, $accuracy, $address]); + + AuditLog::logCreate('attendance', (int)$pdo->lastInsertId(), [ + 'arrival_time' => $now, + 'location' => $address, + ], 'Příchod zaznamenán'); + + successResponse(null, 'Příchod zaznamenán'); + } elseif ($ongoingShift) { + switch ($action) { + case 'break_start': + if ($ongoingShift['arrival_time'] && !$ongoingShift['break_start']) { + $breakStart = roundToNearest10Minutes($rawNow); + $breakEnd = date('Y-m-d H:i:s', strtotime($breakStart) + (30 * 60)); + + $stmt = $pdo->prepare('UPDATE attendance SET break_start = ?, break_end = ? WHERE id = ?'); + $stmt->execute([$breakStart, $breakEnd, $ongoingShift['id']]); + + successResponse(null, 'Pauza zaznamenána'); + } else { + errorResponse('Nelze zadat pauzu'); + } + break; + + case 'departure': + if ($ongoingShift['arrival_time'] && !$ongoingShift['departure_time']) { + $now = roundDownTo15Minutes($rawNow); + + // Auto-add break if shift is longer than 6h and no break + if (!$ongoingShift['break_start'] && !$ongoingShift['break_end']) { + $arrivalTime = strtotime($ongoingShift['arrival_time']); + $departureTime = strtotime($now); + $hoursWorked = ($departureTime - $arrivalTime) / 3600; + + if ($hoursWorked > 12) { + $midPoint = $arrivalTime + (($departureTime - $arrivalTime) / 2); + $breakStart = roundToNearest10Minutes(date('Y-m-d H:i:s', $midPoint - (30 * 60))); + $breakEnd = roundToNearest10Minutes(date('Y-m-d H:i:s', $midPoint + (30 * 60))); + + $stmt = $pdo->prepare('UPDATE attendance SET break_start = ?, break_end = ? WHERE id = ?'); + $stmt->execute([$breakStart, $breakEnd, $ongoingShift['id']]); + } elseif ($hoursWorked > 6) { + $midPoint = $arrivalTime + (($departureTime - $arrivalTime) / 2); + $breakStart = roundToNearest10Minutes(date('Y-m-d H:i:s', $midPoint - (15 * 60))); + $breakEnd = roundToNearest10Minutes(date('Y-m-d H:i:s', $midPoint + (15 * 60))); + + $stmt = $pdo->prepare('UPDATE attendance SET break_start = ?, break_end = ? WHERE id = ?'); + $stmt->execute([$breakStart, $breakEnd, $ongoingShift['id']]); + } + } + + $stmt = $pdo->prepare(' + UPDATE attendance + SET departure_time = ?, departure_lat = ?, departure_lng = ?, + departure_accuracy = ?, departure_address = ? + WHERE id = ? + '); + $stmt->execute([$now, $lat, $lng, $accuracy, $address, $ongoingShift['id']]); + + // Close any open project log + $stmt = $pdo->prepare(' + UPDATE attendance_project_logs SET ended_at = ? WHERE attendance_id = ? AND ended_at IS NULL + '); + $stmt->execute([$now, $ongoingShift['id']]); + + AuditLog::logUpdate('attendance', $ongoingShift['id'], [], [ + 'departure_time' => $now, + 'location' => $address, + ], 'Odchod zaznamenán'); + + successResponse(null, 'Odchod zaznamenán'); + } else { + errorResponse('Nelze zadat odchod'); + } + break; + + default: + errorResponse('Neplatná akce'); + } + } else { + errorResponse('Neplatná akce - nemáte aktivní směnu'); + } +} + +function handleUpdateAddress(PDO $pdo, int $userId): void +{ + $input = getJsonInput(); + $address = trim($input['address'] ?? ''); + $punchAction = $input['punch_action'] ?? ''; + + if (!$address) { + successResponse(null); + return; + } + + if ($punchAction === 'arrival') { + $stmt = $pdo->prepare(" + UPDATE attendance SET arrival_address = ? + WHERE id = ( + SELECT id FROM ( + SELECT id FROM attendance + WHERE user_id = ? AND (arrival_address IS NULL OR arrival_address = '') + ORDER BY created_at DESC LIMIT 1 + ) t + ) + "); + } else { + $stmt = $pdo->prepare(" + UPDATE attendance SET departure_address = ? + WHERE id = ( + SELECT id FROM ( + SELECT id FROM attendance + WHERE user_id = ? AND (departure_address IS NULL OR departure_address = '') + AND departure_time IS NOT NULL + ORDER BY created_at DESC LIMIT 1 + ) t + ) + "); + } + $stmt->execute([$address, $userId]); + + successResponse(null); +} + +function handleAddLeave(PDO $pdo, int $userId): void +{ + $input = getJsonInput(); + + $leaveType = $input['leave_type'] ?? ''; + $leaveDate = $input['leave_date'] ?? ''; + $leaveHours = (float)($input['leave_hours'] ?? 8); + $notes = trim($input['notes'] ?? ''); + + if (!$leaveType || !$leaveDate || $leaveHours <= 0) { + errorResponse('Vyplňte všechna povinná pole'); + } + + if (!in_array($leaveType, ['vacation', 'sick', 'unpaid'])) { + errorResponse('Neplatný typ nepřítomnosti'); + } + + if ($leaveType === 'vacation') { + $year = (int)date('Y', strtotime($leaveDate)); + $balance = getLeaveBalance($pdo, $userId, $year); + + if ($balance['vacation_remaining'] < $leaveHours) { + errorResponse( + "Nemáte dostatek hodin dovolené. Zbývá vám " + . "{$balance['vacation_remaining']} hodin, požadujete {$leaveHours} hodin." + ); + } + } + + $stmt = $pdo->prepare(' + INSERT INTO attendance (user_id, shift_date, leave_type, leave_hours, notes) + VALUES (?, ?, ?, ?, ?) + '); + $stmt->execute([$userId, $leaveDate, $leaveType, $leaveHours, $notes ?: null]); + + updateLeaveBalance($pdo, $userId, $leaveDate, $leaveType, $leaveHours); + + AuditLog::logCreate('attendance', (int)$pdo->lastInsertId(), [ + 'leave_type' => $leaveType, + 'leave_hours' => $leaveHours, + ], "Zaznamenána nepřítomnost: $leaveType"); + + successResponse(null, 'Nepřítomnost byla zaznamenána'); +} + +function handleSaveNotes(PDO $pdo, int $userId): void +{ + $input = getJsonInput(); + $notes = trim($input['notes'] ?? ''); + + $stmt = $pdo->prepare(' + SELECT id FROM attendance + WHERE user_id = ? AND departure_time IS NULL + ORDER BY created_at DESC LIMIT 1 + '); + $stmt->execute([$userId]); + $currentShift = $stmt->fetch(); + + if (!$currentShift) { + errorResponse('Nemáte aktivní směnu'); + } + + $stmt = $pdo->prepare('UPDATE attendance SET notes = ? WHERE id = ?'); + $stmt->execute([$notes, $currentShift['id']]); + + successResponse(null, 'Poznámka byla uložena'); +} + +function handleGetProjects(): void +{ + try { + $pdo = db(); + $stmt = $pdo->query( + "SELECT id, project_number, name FROM projects + WHERE status = 'aktivni' ORDER BY project_number ASC" + ); + $projects = $stmt->fetchAll(); + successResponse(['projects' => $projects]); + } catch (\Exception $e) { + error_log('Failed to fetch projects: ' . $e->getMessage()); + successResponse(['projects' => []]); + } +} + +function handleSwitchProject(PDO $pdo, int $userId): void +{ + $input = getJsonInput(); + /** @var mixed $rawProjectId */ + $rawProjectId = $input['project_id'] ?? null; + $projectId = isset($input['project_id']) && $rawProjectId !== '' && $rawProjectId !== null + ? (int)$rawProjectId + : null; + + $stmt = $pdo->prepare(" + SELECT id FROM attendance + WHERE user_id = ? AND departure_time IS NULL AND (leave_type IS NULL OR leave_type = 'work') + ORDER BY created_at DESC LIMIT 1 + "); + $stmt->execute([$userId]); + $currentShift = $stmt->fetch(); + + if (!$currentShift) { + errorResponse('Nemáte aktivní směnu'); + } + + $attendanceId = $currentShift['id']; + $now = getDbNow($pdo)['now']; + + $stmt = $pdo->prepare( + 'UPDATE attendance_project_logs SET ended_at = ? + WHERE attendance_id = ? AND ended_at IS NULL' + ); + $stmt->execute([$now, $attendanceId]); + + if ($projectId) { + $stmt = $pdo->prepare( + 'INSERT INTO attendance_project_logs + (attendance_id, project_id, started_at) VALUES (?, ?, ?)' + ); + $stmt->execute([$attendanceId, $projectId, $now]); + } + + $stmt = $pdo->prepare('UPDATE attendance SET project_id = ? WHERE id = ?'); + $stmt->execute([$projectId, $attendanceId]); + + successResponse(null, $projectId ? 'Projekt přepnut' : 'Projekt zastaven'); +} + +/** @param array $authData */ +function handleGetProjectLogs(PDO $pdo, int $currentUserId, array $authData): void +{ + $attendanceId = (int)($_GET['attendance_id'] ?? 0); + if (!$attendanceId) { + errorResponse('attendance_id je povinné'); + } + + // Ověření vlastnictví záznamu nebo admin oprávnění + if (!hasPermission($authData, 'attendance.admin')) { + $ownerStmt = $pdo->prepare('SELECT user_id FROM attendance WHERE id = ?'); + $ownerStmt->execute([$attendanceId]); + $owner = $ownerStmt->fetch(); + if (!$owner || (int)$owner['user_id'] !== $currentUserId) { + errorResponse('Nemáte oprávnění zobrazit tyto záznamy', 403); + } + } + + $stmt = $pdo->prepare( + 'SELECT id, attendance_id, project_id, started_at, ended_at, hours, minutes + FROM attendance_project_logs WHERE attendance_id = ? ORDER BY started_at ASC' + ); + $stmt->execute([$attendanceId]); + $logs = $stmt->fetchAll(); + + $projectIds = []; + foreach ($logs as $l) { + $projectIds[$l['project_id']] = $l['project_id']; + } + $projNameMap = fetchProjectNames($projectIds); + foreach ($logs as &$l) { + $l['project_name'] = $projNameMap[$l['project_id']] ?? null; + } + unset($l); + + successResponse(['logs' => $logs]); +} + +function handleSaveProjectLogs(PDO $pdo): void +{ + $input = getJsonInput(); + $attendanceId = (int)($input['attendance_id'] ?? 0); + $logs = $input['project_logs'] ?? []; + + if (!$attendanceId) { + errorResponse('attendance_id je povinné'); + } + + $stmt = $pdo->prepare('SELECT id FROM attendance WHERE id = ?'); + $stmt->execute([$attendanceId]); + $record = $stmt->fetch(); + if (!$record) { + errorResponse('Záznam nebyl nalezen', 404); + } + + $stmt = $pdo->prepare('DELETE FROM attendance_project_logs WHERE attendance_id = ?'); + $stmt->execute([$attendanceId]); + + if (!empty($logs)) { + $stmt = $pdo->prepare( + 'INSERT INTO attendance_project_logs + (attendance_id, project_id, hours, minutes) VALUES (?, ?, ?, ?)' + ); + foreach ($logs as $log) { + $projectId = (int)($log['project_id'] ?? 0); + if (!$projectId) { + continue; + } + $h = (int)($log['hours'] ?? 0); + $m = (int)($log['minutes'] ?? 0); + if ($h === 0 && $m === 0) { + continue; + } + $stmt->execute([$attendanceId, $projectId, $h, $m]); + } + } + + successResponse(null, 'Projektové záznamy byly uloženy'); +} diff --git a/dist/api/admin/handlers/bank-accounts-handlers.php b/dist/api/admin/handlers/bank-accounts-handlers.php new file mode 100644 index 0000000..1a9a829 --- /dev/null +++ b/dist/api/admin/handlers/bank-accounts-handlers.php @@ -0,0 +1,174 @@ +query( + 'SELECT id, account_name, bank_name, account_number, iban, bic, + currency, is_default, position, created_at, modified_at + FROM bank_accounts ORDER BY position, id' + ); + successResponse($stmt->fetchAll()); +} + +function handleCreateBankAccount(PDO $pdo): void +{ + $input = getJsonInput(); + + $accountName = trim($input['account_name'] ?? ''); + $bankName = trim($input['bank_name'] ?? ''); + $accountNumber = trim($input['account_number'] ?? ''); + $iban = trim($input['iban'] ?? ''); + $bic = trim($input['bic'] ?? ''); + $currency = trim($input['currency'] ?? 'CZK'); + $isDefault = !empty($input['is_default']) ? 1 : 0; + + if (!$accountName) { + errorResponse('Název účtu je povinný'); + } + if (mb_strlen($accountName) > 100) { + errorResponse('Název účtu je příliš dlouhý (max 100 znaků)'); + } + if (mb_strlen($bankName) > 255) { + errorResponse('Název banky je příliš dlouhý (max 255 znaků)'); + } + if (mb_strlen($accountNumber) > 50) { + errorResponse('Číslo účtu je příliš dlouhé (max 50 znaků)'); + } + if (mb_strlen($iban) > 50) { + errorResponse('IBAN je příliš dlouhý (max 50 znaků)'); + } + if (mb_strlen($bic) > 20) { + errorResponse('BIC/SWIFT je příliš dlouhý (max 20 znaků)'); + } + if (!in_array($currency, ['CZK', 'EUR', 'USD', 'GBP'])) { + errorResponse('Neplatná měna'); + } + + // Zjistit dalsi pozici + $maxPos = (int) $pdo->query('SELECT COALESCE(MAX(position), 0) FROM bank_accounts')->fetchColumn(); + + $pdo->beginTransaction(); + try { + // Pokud je default, zrusit ostatnim + if ($isDefault) { + $pdo->exec('UPDATE bank_accounts SET is_default = 0'); + } + + $stmt = $pdo->prepare(' + INSERT INTO bank_accounts + (account_name, bank_name, account_number, iban, bic, currency, is_default, position) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + '); + $stmt->execute([$accountName, $bankName, $accountNumber, $iban, $bic, $currency, $isDefault, $maxPos + 1]); + $newId = (int) $pdo->lastInsertId(); + + $pdo->commit(); + + AuditLog::logCreate( + 'bank_account', + $newId, + ['account_name' => $accountName], + "Vytvořen bankovní účet '$accountName'" + ); + + successResponse(['id' => $newId], 'Bankovní účet byl vytvořen'); + } catch (PDOException $e) { + $pdo->rollBack(); + throw $e; + } +} + +function handleUpdateBankAccount(PDO $pdo, int $id): void +{ + $stmt = $pdo->prepare( + 'SELECT id, account_name, bank_name, account_number, iban, bic, + currency, is_default, position + FROM bank_accounts WHERE id = ?' + ); + $stmt->execute([$id]); + $account = $stmt->fetch(); + + if (!$account) { + errorResponse('Bankovní účet nebyl nalezen', 404); + } + + $input = getJsonInput(); + + // Delkove limity a validace + $maxLengths = ['account_name' => 100, 'bank_name' => 255, 'account_number' => 50, 'iban' => 50, 'bic' => 20]; + foreach ($maxLengths as $f => $max) { + if (isset($input[$f]) && mb_strlen(trim((string)$input[$f])) > $max) { + errorResponse("Pole $f je příliš dlouhé (max $max znaků)"); + } + } + if (isset($input['currency']) && !in_array($input['currency'], ['CZK', 'EUR', 'USD', 'GBP'])) { + errorResponse('Neplatná měna'); + } + + $fields = ['account_name', 'bank_name', 'account_number', 'iban', 'bic', 'currency']; + $updates = []; + $params = []; + + foreach ($fields as $field) { + if (array_key_exists($field, $input)) { + $updates[] = "$field = ?"; + $params[] = trim((string) $input[$field]); + } + } + + $pdo->beginTransaction(); + try { + if (array_key_exists('is_default', $input)) { + $isDefault = !empty($input['is_default']) ? 1 : 0; + if ($isDefault) { + $pdo->exec('UPDATE bank_accounts SET is_default = 0'); + } + $updates[] = 'is_default = ?'; + $params[] = $isDefault; + } + + if (empty($updates)) { + errorResponse('Žádná data k aktualizaci'); + } + + $updates[] = 'modified_at = NOW()'; + $params[] = $id; + + $sql = 'UPDATE bank_accounts SET ' . implode(', ', $updates) . ' WHERE id = ?'; + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + + $pdo->commit(); + + AuditLog::logUpdate('bank_account', $id, [], $input, "Aktualizován bankovní účet #{$id}"); + + successResponse(null, 'Bankovní účet byl aktualizován'); + } catch (PDOException $e) { + $pdo->rollBack(); + throw $e; + } +} + +function handleDeleteBankAccount(PDO $pdo, int $id): void +{ + $stmt = $pdo->prepare('SELECT id, account_name FROM bank_accounts WHERE id = ?'); + $stmt->execute([$id]); + $account = $stmt->fetch(); + + if (!$account) { + errorResponse('Bankovní účet nebyl nalezen', 404); + } + + $pdo->prepare('DELETE FROM bank_accounts WHERE id = ?')->execute([$id]); + + AuditLog::logDelete( + 'bank_account', + $id, + ['account_name' => $account['account_name']], + "Smazán bankovní účet '{$account['account_name']}'" + ); + + successResponse(null, 'Bankovní účet byl smazán'); +} diff --git a/dist/api/admin/handlers/company-settings-handlers.php b/dist/api/admin/handlers/company-settings-handlers.php new file mode 100644 index 0000000..c38c035 --- /dev/null +++ b/dist/api/admin/handlers/company-settings-handlers.php @@ -0,0 +1,253 @@ + + */ +function getOrCreateSettings(PDO $pdo, bool $includeLogo = false): array +{ + if ($includeLogo) { + $stmt = $pdo->query(' + SELECT id, company_name, company_id, vat_id, street, city, postal_code, + country, quotation_prefix, default_currency, default_vat_rate, + custom_fields, logo_data, uuid, modified_at, sync_version, + order_type_code, invoice_type_code, is_deleted, require_2fa + FROM company_settings LIMIT 1 + '); + } else { + $stmt = $pdo->query(' + SELECT id, company_name, company_id, vat_id, street, city, postal_code, country, + quotation_prefix, default_currency, default_vat_rate, + custom_fields, uuid, modified_at, sync_version, + order_type_code, invoice_type_code, is_deleted, + CASE WHEN logo_data IS NOT NULL AND LENGTH(logo_data) > 0 THEN 1 ELSE 0 END as has_logo + FROM company_settings LIMIT 1 + '); + } + $settings = $stmt->fetch(); + + if (!$settings) { + $uuid = sprintf( + '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + random_int(0, 0xffff), + random_int(0, 0xffff), + random_int(0, 0xffff), + random_int(0, 0x0fff) | 0x4000, + random_int(0, 0x3fff) | 0x8000, + random_int(0, 0xffff), + random_int(0, 0xffff), + random_int(0, 0xffff) + ); + $pdo->prepare( + "INSERT INTO company_settings + (id, company_name, quotation_prefix, default_currency, + default_vat_rate, uuid, modified_at, sync_version) + VALUES (1, '', 'N', 'EUR', 21.0, ?, NOW(), 1)" + )->execute([$uuid]); + return getOrCreateSettings($pdo, $includeLogo); + } + + return $settings; +} + +function handleGetOffersSettings(PDO $pdo): void +{ + $settings = getOrCreateSettings($pdo, false); + /** @var array|null $cfRaw */ + $cfRaw = !empty($settings['custom_fields']) + ? json_decode($settings['custom_fields'], true) + : null; + if (is_array($cfRaw) && !isset($cfRaw['fields'])) { + $settings['custom_fields'] = $cfRaw; + $settings['supplier_field_order'] = null; + } elseif (is_array($cfRaw) && isset($cfRaw['fields'])) { + $settings['custom_fields'] = $cfRaw['fields']; + $settings['supplier_field_order'] = $cfRaw['field_order'] ?? $cfRaw['fieldOrder'] ?? null; + } else { + $settings['custom_fields'] = []; + $settings['supplier_field_order'] = null; + } + + $settings['has_logo'] = (bool)($settings['has_logo'] ?? false); + + successResponse($settings); +} + +function handleUpdateOffersSettings(PDO $pdo): void +{ + $input = getJsonInput(); + $settings = getOrCreateSettings($pdo); + + // Delkove limity + $maxLengths = [ + 'company_name' => 255, 'street' => 255, 'city' => 255, + 'postal_code' => 20, 'country' => 100, + 'company_id' => 50, 'vat_id' => 50, + 'default_currency' => 5, + ]; + foreach ($maxLengths as $f => $max) { + if (isset($input[$f]) && mb_strlen(trim((string)$input[$f])) > $max) { + errorResponse("Pole $f je příliš dlouhé (max $max znaků)"); + } + } + // Validace meny + if (isset($input['default_currency']) && !in_array($input['default_currency'], ['EUR', 'USD', 'CZK', 'GBP'])) { + errorResponse('Neplatná měna'); + } + + $fields = [ + 'company_name', 'street', 'city', 'postal_code', 'country', + 'company_id', 'vat_id', + 'quotation_prefix', 'default_currency', + 'order_type_code', 'invoice_type_code', + ]; + + $setClauses = []; + $params = []; + + foreach ($fields as $field) { + if (array_key_exists($field, $input)) { + $setClauses[] = "$field = ?"; + $params[] = $input[$field]; + } + } + + // custom_fields + SupplierFieldOrder - ulozeny dohromady jako JSON + if (array_key_exists('custom_fields', $input) || array_key_exists('supplier_field_order', $input)) { + /** @var array|null $currentRaw */ + $currentRaw = !empty($settings['custom_fields']) + ? json_decode($settings['custom_fields'], true) + : null; + if (is_array($currentRaw) && !isset($currentRaw['fields'])) { + /** @var array $stored */ + $stored = ['fields' => $currentRaw, 'field_order' => null]; + } elseif (is_array($currentRaw) && isset($currentRaw['fields'])) { + /** @var array $stored */ + $stored = $currentRaw; + } else { + $stored = ['fields' => [], 'field_order' => null]; + } + + if (array_key_exists('custom_fields', $input) && is_array($input['custom_fields'])) { + $stored['fields'] = $input['custom_fields']; + } + if (array_key_exists('supplier_field_order', $input)) { + $stored['field_order'] = is_array($input['supplier_field_order']) ? $input['supplier_field_order'] : null; + } + + // Odstranit stary klic + unset($stored['fieldOrder']); + + $setClauses[] = 'custom_fields = ?'; + $params[] = json_encode($stored, JSON_UNESCAPED_UNICODE); + } + + // Validace prefixu + if (isset($input['quotation_prefix']) && !preg_match('/^[A-Za-z0-9]{0,10}$/', $input['quotation_prefix'])) { + errorResponse('Prefix nabídky může obsahovat pouze alfanumerické znaky (max 10)'); + } + if (isset($input['order_type_code']) && !preg_match('/^[0-9]{0,10}$/', $input['order_type_code'])) { + errorResponse('Typový kód objednávek může obsahovat pouze čísla (max 10)'); + } + if (isset($input['invoice_type_code']) && !preg_match('/^[0-9]{0,10}$/', $input['invoice_type_code'])) { + errorResponse('Typový kód faktur může obsahovat pouze čísla (max 10)'); + } + + $numericFields = ['default_vat_rate']; + foreach ($numericFields as $field) { + if (array_key_exists($field, $input)) { + $val = is_numeric($input[$field]) ? floatval($input[$field]) : 0; + if ($val < 0 || $val > 100) { + errorResponse('Sazba DPH musí být mezi 0 a 100'); + } + $setClauses[] = "$field = ?"; + $params[] = $val; + } + } + + if (empty($setClauses)) { + errorResponse('Žádná data k aktualizaci'); + } + + $setClauses[] = 'modified_at = NOW()'; + $setClauses[] = 'sync_version = sync_version + 1'; + + $sql = 'UPDATE company_settings SET ' . implode(', ', $setClauses) . ' WHERE id = ?'; + $params[] = $settings['id']; + + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + + + AuditLog::logUpdate('company_settings', (int)$settings['id'], [], $input, 'Aktualizováno nastavení firmy'); + + successResponse(null, 'Nastavení bylo uloženo'); +} + +function handleUploadLogo(PDO $pdo): void +{ + if (!isset($_FILES['logo']) || $_FILES['logo']['error'] !== UPLOAD_ERR_OK) { + errorResponse('Nebyl nahrán žádný soubor'); + } + + $file = $_FILES['logo']; + $allowedTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp']; + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mimeType = finfo_file($finfo, $file['tmp_name']); + finfo_close($finfo); + + if (!in_array($mimeType, $allowedTypes)) { + errorResponse('Nepodporovaný formát obrázku. Povolené: PNG, JPEG, GIF, WebP'); + } + + if ($file['size'] > 5 * 1024 * 1024) { + errorResponse('Soubor je příliš velký (max 5 MB)'); + } + + $logoData = file_get_contents($file['tmp_name']); + + $settings = getOrCreateSettings($pdo); + $stmt = $pdo->prepare( + 'UPDATE company_settings SET logo_data = ?, modified_at = NOW(), sync_version = sync_version + 1 WHERE id = ?' + ); + $stmt->execute([$logoData, $settings['id']]); + + + AuditLog::logUpdate( + 'company_settings', + (int)$settings['id'], + [], + ['logo' => 'uploaded'], + 'Aktualizováno logo firmy' + ); + + successResponse(null, 'Logo bylo nahráno'); +} + +function handleGetLogo(PDO $pdo): void +{ + $stmt = $pdo->query('SELECT logo_data FROM company_settings LIMIT 1'); + $row = $stmt->fetch(); + + if (!$row || empty($row['logo_data'])) { + http_response_code(404); + header('Content-Type: application/json; charset=utf-8'); + echo json_encode(['success' => false, 'error' => 'Logo nenalezeno']); + exit(); + } + + $logoData = $row['logo_data']; + + // Detect image type from binary data + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mimeType = finfo_buffer($finfo, $logoData); + finfo_close($finfo); + + header('Content-Type: ' . $mimeType); + header('Content-Length: ' . strlen($logoData)); + header('Cache-Control: public, max-age=3600'); + echo $logoData; + exit(); +} diff --git a/dist/api/admin/handlers/customers-handlers.php b/dist/api/admin/handlers/customers-handlers.php new file mode 100644 index 0000000..3970c9d --- /dev/null +++ b/dist/api/admin/handlers/customers-handlers.php @@ -0,0 +1,287 @@ + $customer */ +function parseCustomerCustomFields(array &$customer): void +{ + /** @var array|null $cfRaw */ + $cfRaw = !empty($customer['custom_fields']) + ? json_decode($customer['custom_fields'], true) + : null; + if (is_array($cfRaw) && !isset($cfRaw['fields'])) { + $customer['custom_fields'] = $cfRaw; + $customer['customer_field_order'] = null; + } elseif (is_array($cfRaw) && isset($cfRaw['fields'])) { + $customer['custom_fields'] = $cfRaw['fields']; + $customer['customer_field_order'] = $cfRaw['field_order'] ?? $cfRaw['fieldOrder'] ?? null; + } else { + $customer['custom_fields'] = []; + $customer['customer_field_order'] = null; + } +} + +/** @param array $input */ +function encodeCustomerCustomFields(array $input, ?string $existingJson): ?string +{ + if (!array_key_exists('custom_fields', $input) && !array_key_exists('customer_field_order', $input)) { + return $existingJson; + } + /** @var array|null $currentRaw */ + $currentRaw = !empty($existingJson) ? json_decode($existingJson, true) : null; + if (is_array($currentRaw) && !isset($currentRaw['fields'])) { + /** @var array $stored */ + $stored = ['fields' => $currentRaw, 'field_order' => null]; + } elseif (is_array($currentRaw) && isset($currentRaw['fields'])) { + /** @var array $stored */ + $stored = $currentRaw; + } else { + $stored = ['fields' => [], 'field_order' => null]; + } + + if (array_key_exists('custom_fields', $input) && is_array($input['custom_fields'])) { + $stored['fields'] = $input['custom_fields']; + } + if (array_key_exists('customer_field_order', $input)) { + $stored['field_order'] = is_array($input['customer_field_order']) ? $input['customer_field_order'] : null; + } + + unset($stored['fieldOrder']); + + return json_encode($stored, JSON_UNESCAPED_UNICODE); +} + +function handleGetAll(PDO $pdo): void +{ + $stmt = $pdo->query(' + SELECT c.id, c.name, c.street, c.city, c.postal_code, c.country, + c.company_id, c.vat_id, c.custom_fields, c.created_at, + COUNT(q.id) as quotation_count + FROM customers c + LEFT JOIN quotations q ON q.customer_id = c.id + GROUP BY c.id + ORDER BY c.name ASC + '); + $customers = $stmt->fetchAll(); + + foreach ($customers as &$c) { + parseCustomerCustomFields($c); + } + unset($c); + + successResponse(['customers' => $customers]); +} + +function handleGetOne(PDO $pdo, int $id): void +{ + $stmt = $pdo->prepare( + 'SELECT id, name, street, city, postal_code, country, + company_id, vat_id, custom_fields, created_at + FROM customers WHERE id = ?' + ); + $stmt->execute([$id]); + $customer = $stmt->fetch(); + + if (!$customer) { + errorResponse('Zákazník nebyl nalezen', 404); + } + + parseCustomerCustomFields($customer); + successResponse($customer); +} + +function handleSearch(PDO $pdo): void +{ + $q = trim($_GET['q'] ?? ''); + if (strlen($q) < 1 || mb_strlen($q) > 100) { + successResponse(['customers' => []]); + return; + } + + $stmt = $pdo->prepare(' + SELECT id, name, street, city, postal_code, country, + company_id, vat_id, custom_fields + FROM customers + WHERE name LIKE ? OR company_id LIKE ? OR city LIKE ? + ORDER BY name ASC + LIMIT 20 + '); + $search = "%{$q}%"; + $stmt->execute([$search, $search, $search]); + + $results = $stmt->fetchAll(); + foreach ($results as &$c) { + parseCustomerCustomFields($c); + } + unset($c); + successResponse(['customers' => $results]); +} + +function handleCreateCustomer(PDO $pdo): void +{ + $input = getJsonInput(); + + if (empty($input['name'])) { + errorResponse('Název zákazníka je povinný'); + } + if (mb_strlen($input['name']) > 255) { + errorResponse('Název zákazníka je příliš dlouhý (max 255 znaků)'); + } + foreach (['street', 'city', 'country'] as $f) { + if (isset($input[$f]) && mb_strlen($input[$f]) > 255) { + errorResponse("Pole $f je příliš dlouhé (max 255 znaků)"); + } + } + if (isset($input['postal_code']) && mb_strlen($input['postal_code']) > 20) { + errorResponse('PSČ je příliš dlouhé (max 20 znaků)'); + } + if (isset($input['company_id']) && mb_strlen($input['company_id']) > 50) { + errorResponse('IČO je příliš dlouhé (max 50 znaků)'); + } + if (isset($input['vat_id']) && mb_strlen($input['vat_id']) > 50) { + errorResponse('DIČ je příliš dlouhé (max 50 znaků)'); + } + + $uuid = sprintf( + '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + random_int(0, 0xffff), + random_int(0, 0xffff), + random_int(0, 0xffff), + random_int(0, 0x0fff) | 0x4000, + random_int(0, 0x3fff) | 0x8000, + random_int(0, 0xffff), + random_int(0, 0xffff), + random_int(0, 0xffff) + ); + + $customFieldsJson = encodeCustomerCustomFields($input, null); + + $stmt = $pdo->prepare(' + INSERT INTO customers (name, street, city, postal_code, country, + company_id, vat_id, custom_fields, created_at, uuid, modified_at, sync_version) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?, NOW(), 1) + '); + $stmt->execute([ + $input['name'], + $input['street'] ?? '', + $input['city'] ?? '', + $input['postal_code'] ?? '', + $input['country'] ?? '', + $input['company_id'] ?? '', + $input['vat_id'] ?? '', + $customFieldsJson, + $uuid, + ]); + + $newId = (int)$pdo->lastInsertId(); + + + AuditLog::logCreate('customer', (int)$newId, [ + 'name' => $input['name'], + ], "Vytvořen zákazník '{$input['name']}'"); + + successResponse(['id' => $newId], 'Zákazník byl vytvořen'); +} + +function handleUpdateCustomer(PDO $pdo, int $id): void +{ + $stmt = $pdo->prepare( + 'SELECT id, name, street, city, postal_code, country, + company_id, vat_id, custom_fields + FROM customers WHERE id = ?' + ); + $stmt->execute([$id]); + $existing = $stmt->fetch(); + + if (!$existing) { + errorResponse('Zákazník nebyl nalezen', 404); + } + + $input = getJsonInput(); + + // Delkove limity + if (isset($input['name']) && mb_strlen($input['name']) > 255) { + errorResponse('Název je příliš dlouhý (max 255 znaků)'); + } + foreach (['street', 'city', 'country'] as $f) { + if (isset($input[$f]) && mb_strlen($input[$f]) > 255) { + errorResponse("Pole $f je příliš dlouhé (max 255 znaků)"); + } + } + if (isset($input['postal_code']) && mb_strlen($input['postal_code']) > 20) { + errorResponse('PSČ je příliš dlouhé (max 20 znaků)'); + } + if (isset($input['company_id']) && mb_strlen($input['company_id']) > 50) { + errorResponse('IČO je příliš dlouhé (max 50 znaků)'); + } + if (isset($input['vat_id']) && mb_strlen($input['vat_id']) > 50) { + errorResponse('DIČ je příliš dlouhé (max 50 znaků)'); + } + + $customFieldsJson = encodeCustomerCustomFields($input, $existing['custom_fields'] ?? null); + + $stmt = $pdo->prepare(' + UPDATE customers SET + name = ?, + street = ?, + city = ?, + postal_code = ?, + country = ?, + company_id = ?, + vat_id = ?, + custom_fields = ?, + modified_at = NOW(), + sync_version = sync_version + 1 + WHERE id = ? + '); + $stmt->execute([ + $input['name'] ?? $existing['name'], + $input['street'] ?? $existing['street'], + $input['city'] ?? $existing['city'], + $input['postal_code'] ?? $existing['postal_code'], + $input['country'] ?? $existing['country'], + $input['company_id'] ?? $existing['company_id'], + $input['vat_id'] ?? $existing['vat_id'], + $customFieldsJson, + $id, + ]); + + + AuditLog::logUpdate( + 'customer', + $id, + ['name' => $existing['name']], + ['name' => $input['name'] ?? $existing['name']], + "Upraven zákazník #$id" + ); + + successResponse(null, 'Zákazník byl aktualizován'); +} + +function handleDeleteCustomer(PDO $pdo, int $id): void +{ + $stmt = $pdo->prepare('SELECT id, name FROM customers WHERE id = ?'); + $stmt->execute([$id]); + $customer = $stmt->fetch(); + + if (!$customer) { + errorResponse('Zákazník nebyl nalezen', 404); + } + + // Check if customer has quotations + $stmt = $pdo->prepare('SELECT COUNT(*) FROM quotations WHERE customer_id = ?'); + $stmt->execute([$id]); + $count = (int)$stmt->fetchColumn(); + + if ($count > 0) { + errorResponse("Zákazníka nelze smazat, má $count nabídek"); + } + + $stmt = $pdo->prepare('DELETE FROM customers WHERE id = ?'); + $stmt->execute([$id]); + + + AuditLog::logDelete('customer', $id, ['name' => $customer['name']], "Smazán zákazník '{$customer['name']}'"); + + successResponse(null, 'Zákazník byl smazán'); +} diff --git a/dist/api/admin/handlers/invoices-handlers.php b/dist/api/admin/handlers/invoices-handlers.php new file mode 100644 index 0000000..44d15dc --- /dev/null +++ b/dist/api/admin/handlers/invoices-handlers.php @@ -0,0 +1,696 @@ + */ +function getValidTransitions(string $status): array +{ + return match ($status) { + 'issued' => ['paid'], + 'overdue' => ['paid'], + default => [] + }; +} + +// --- Invoice number generation --- + +function generateInvoiceNumber(PDO $pdo): string +{ + $yy = date('y'); + + $settings = $pdo->query('SELECT invoice_type_code FROM company_settings LIMIT 1')->fetch(); + $typeCode = ($settings && !empty($settings['invoice_type_code'])) ? $settings['invoice_type_code'] : '81'; + + $prefix = $yy . $typeCode; + $prefixLen = strlen($prefix); + $likePattern = $prefix . '%'; + + $stmt = $pdo->prepare(' + SELECT COALESCE(MAX(CAST(SUBSTRING(invoice_number, ? + 1) AS UNSIGNED)), 0) + FROM invoices WHERE invoice_number LIKE ? + '); + $stmt->execute([$prefixLen, $likePattern]); + $max = (int) $stmt->fetchColumn(); + + return sprintf('%s%04d', $prefix, $max + 1); +} + +// --- Stats --- + +/** + * Spocita celkovou castku faktur seskupenou podle meny + CZK prepocet dle kurzu k datu faktury. + * + * @param array $params + * @return array{amounts: array, count: int, total_czk: float} + */ +function sumInvoicesByCurrency(PDO $pdo, string $where, array $params): array +{ + // Per-faktura pro presny prepocet kurzem k datu + $perInvoiceSql = " + SELECT i.id, i.currency, i.issue_date, + COALESCE(SUM(ii.quantity * ii.unit_price), 0) + + COALESCE(SUM(CASE WHEN i.apply_vat + THEN ii.quantity * ii.unit_price * ii.vat_rate / 100 + ELSE 0 END), 0) AS total + FROM invoices i + JOIN invoice_items ii ON ii.invoice_id = i.id + $where + GROUP BY i.id, i.currency, i.issue_date + "; + $stmt = $pdo->prepare($perInvoiceSql); + $stmt->execute($params); + $rows = $stmt->fetchAll(); + + // Seskupit podle meny pro zobrazeni + $byCurrency = []; + $czkItems = []; + foreach ($rows as $r) { + $cur = $r['currency']; + $amt = round((float) $r['total'], 2); + $byCurrency[$cur] = ($byCurrency[$cur] ?? 0) + $amt; + $czkItems[] = [ + 'amount' => $amt, + 'currency' => $cur, + 'date' => $r['issue_date'], + ]; + } + + $amounts = []; + foreach ($byCurrency as $cur => $total) { + $amounts[] = ['amount' => round($total, 2), 'currency' => $cur]; + } + + $cnb = CnbRates::getInstance(); + $totalCzk = $cnb->sumToCzk($czkItems); + + $countSql = "SELECT COUNT(*) FROM invoices i $where"; + $countStmt = $pdo->prepare($countSql); + $countStmt->execute($params); + + return [ + 'amounts' => $amounts, + 'count' => (int) $countStmt->fetchColumn(), + 'total_czk' => $totalCzk, + ]; +} + +function handleGetStats(PDO $pdo): void +{ + $month = max(1, min(12, (int) ($_GET['month'] ?? (int) date('n')))); + $year = max(2020, min(2099, (int) ($_GET['year'] ?? (int) date('Y')))); + + $monthStart = sprintf('%04d-%02d-01', $year, $month); + $monthEnd = date('Y-m-t', strtotime($monthStart)); + + // a) Uhrazeno v danem mesici (dle data vystaveni, ne uhrazeni) + $paidWhere = "WHERE i.status = 'paid' AND i.issue_date BETWEEN ? AND ?"; + $paid = sumInvoicesByCurrency($pdo, $paidWhere, [$monthStart, $monthEnd]); + + // b) Ceka uhrada (aktualni stav) + $awaiting = sumInvoicesByCurrency($pdo, "WHERE i.status = 'issued'", []); + + // c) Po splatnosti (aktualni stav) + $overdue = sumInvoicesByCurrency($pdo, "WHERE i.status = 'overdue'", []); + + // d) DPH v danem mesici - per faktura pro kurz k datu + $vatSql = " + SELECT i.id, i.currency, i.issue_date, + COALESCE(SUM(ii.quantity * ii.unit_price * ii.vat_rate / 100), 0) AS vat_total + FROM invoices i + JOIN invoice_items ii ON ii.invoice_id = i.id + WHERE i.apply_vat = 1 AND i.issue_date BETWEEN ? AND ? + GROUP BY i.id, i.currency, i.issue_date + "; + $vatStmt = $pdo->prepare($vatSql); + $vatStmt->execute([$monthStart, $monthEnd]); + $vatRows = $vatStmt->fetchAll(); + + $vatByCurrency = []; + $vatCzkItems = []; + foreach ($vatRows as $r) { + $cur = $r['currency']; + $amt = round((float) $r['vat_total'], 2); + $vatByCurrency[$cur] = ($vatByCurrency[$cur] ?? 0) + $amt; + $vatCzkItems[] = [ + 'amount' => $amt, + 'currency' => $cur, + 'date' => $r['issue_date'], + ]; + } + + $vatAmounts = []; + foreach ($vatByCurrency as $cur => $total) { + $vatAmounts[] = ['amount' => round($total, 2), 'currency' => $cur]; + } + + $cnb = CnbRates::getInstance(); + + successResponse([ + 'paid_month' => $paid['amounts'], + 'paid_month_czk' => $paid['total_czk'], + 'paid_month_count' => $paid['count'], + 'awaiting' => $awaiting['amounts'], + 'awaiting_czk' => $awaiting['total_czk'], + 'awaiting_count' => $awaiting['count'], + 'overdue' => $overdue['amounts'], + 'overdue_czk' => $overdue['total_czk'], + 'overdue_count' => $overdue['count'], + 'vat_month' => $vatAmounts, + 'vat_month_czk' => $cnb->sumToCzk($vatCzkItems), + 'month' => $month, + 'year' => $year, + ]); +} + +// --- Handlers --- + +function handleGetList(PDO $pdo): void +{ + $statusFilter = trim($_GET['status'] ?? ''); + + $sortMap = [ + 'InvoiceNumber' => 'i.invoice_number', + 'invoice_number' => 'i.invoice_number', + 'CreatedAt' => 'i.created_at', + 'created_at' => 'i.created_at', + 'Status' => 'i.status', + 'status' => 'i.status', + 'DueDate' => 'i.due_date', + 'due_date' => 'i.due_date', + 'IssueDate' => 'i.issue_date', + 'issue_date' => 'i.issue_date', + ]; + + $p = PaginationHelper::parseParams($sortMap); + + $where = 'WHERE 1=1'; + $params = []; + + if ($p['search']) { + $where .= ' AND (i.invoice_number LIKE ? OR c.name LIKE ? OR c.company_id LIKE ?)'; + $searchParam = "%{$p['search']}%"; + $params = array_merge($params, [$searchParam, $searchParam, $searchParam]); + } + + if ($statusFilter) { + $statuses = array_filter(explode(',', $statusFilter)); + if ($statuses) { + $placeholders = implode(',', array_fill(0, count($statuses), '?')); + $where .= " AND i.status IN ($placeholders)"; + $params = array_merge($params, $statuses); + } + } + + $from = "FROM invoices i + LEFT JOIN customers c ON i.customer_id = c.id + LEFT JOIN orders o ON i.order_id = o.id"; + + $result = PaginationHelper::paginate( + $pdo, + "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, + i.issue_date, i.due_date, i.paid_date, i.created_at, i.apply_vat, + c.name as customer_name, + (SELECT COALESCE(SUM(ii.quantity * ii.unit_price), 0) + FROM invoice_items ii WHERE ii.invoice_id = i.id) as subtotal, + o.order_number + {$from} {$where} + ORDER BY {$p['sort']} {$p['order']}", + $params, + $p + ); + + $invoices = $result['items']; + + // Dopocitat celkovou castku s DPH + foreach ($invoices as &$inv) { + $subtotal = (float) $inv['subtotal']; + if ($inv['apply_vat']) { + $vatStmt = $pdo->prepare(' + SELECT COALESCE(SUM(quantity * unit_price * vat_rate / 100), 0) + FROM invoice_items WHERE invoice_id = ? + '); + $vatStmt->execute([$inv['id']]); + $vatAmount = (float) $vatStmt->fetchColumn(); + $inv['total'] = $subtotal + $vatAmount; + } else { + $inv['total'] = $subtotal; + } + } + unset($inv); + + successResponse([ + 'invoices' => $invoices, + 'pagination' => $result['pagination'], + ]); +} + +function handleGetDetail(PDO $pdo, int $id): void +{ + $stmt = $pdo->prepare(' + SELECT i.id, i.invoice_number, i.order_id, i.customer_id, i.status, + i.currency, i.vat_rate, i.apply_vat, i.payment_method, + i.constant_symbol, i.bank_name, i.bank_swift, i.bank_iban, + i.bank_account, i.issue_date, i.due_date, i.tax_date, + i.paid_date, i.issued_by, i.notes, i.internal_notes, + i.created_at, i.modified_at, + c.name as customer_name, o.order_number + FROM invoices i + LEFT JOIN customers c ON i.customer_id = c.id + LEFT JOIN orders o ON i.order_id = o.id + WHERE i.id = ? + '); + $stmt->execute([$id]); + $invoice = $stmt->fetch(); + + if (!$invoice) { + errorResponse('Faktura nebyla nalezena', 404); + } + + // Polozky + $stmt = $pdo->prepare( + 'SELECT id, invoice_id, description, quantity, unit, unit_price, vat_rate, position + FROM invoice_items WHERE invoice_id = ? ORDER BY position' + ); + $stmt->execute([$id]); + $invoice['items'] = $stmt->fetchAll(); + + // Zakaznik + if ($invoice['customer_id']) { + $stmt = $pdo->prepare( + 'SELECT id, name, company_id, vat_id, street, city, postal_code, country, custom_fields + FROM customers WHERE id = ?' + ); + $stmt->execute([$invoice['customer_id']]); + $invoice['customer'] = $stmt->fetch(); + } + + $invoice['valid_transitions'] = getValidTransitions($invoice['status']); + + successResponse($invoice); +} + +function handleGetNextNumber(PDO $pdo): void +{ + $number = generateInvoiceNumber($pdo); + successResponse(['number' => $number]); +} + +function handleGetOrderData(PDO $pdo, int $id): void +{ + $stmt = $pdo->prepare(' + SELECT o.id, o.order_number, o.customer_id, o.status, o.currency, + o.language, o.vat_rate, o.apply_vat, o.exchange_rate, + o.created_at, o.modified_at, + c.name as customer_name + FROM orders o + LEFT JOIN customers c ON o.customer_id = c.id + WHERE o.id = ? + '); + $stmt->execute([$id]); + $order = $stmt->fetch(); + + if (!$order) { + errorResponse('Objednávka nebyla nalezena', 404); + } + + // Polozky objednavky + $stmt = $pdo->prepare( + 'SELECT id, order_id, description, item_description, quantity, unit, + unit_price, is_included_in_total, position + FROM order_items WHERE order_id = ? ORDER BY position' + ); + $stmt->execute([$id]); + $order['items'] = $stmt->fetchAll(); + + successResponse($order); +} + +/** @param array $authData */ +function handleCreateInvoice(PDO $pdo, array $authData): void +{ + $input = getJsonInput(); + + $customerId = isset($input['customer_id']) ? (int) $input['customer_id'] : null; + $orderId = !empty($input['order_id']) ? (int) $input['order_id'] : null; + $issueDate = trim($input['issue_date'] ?? ''); + $dueDate = trim($input['due_date'] ?? ''); + $taxDate = trim($input['tax_date'] ?? ''); + $currency = trim($input['currency'] ?? 'CZK'); + $applyVat = isset($input['apply_vat']) ? (int) $input['apply_vat'] : 1; + $paymentMethod = trim($input['payment_method'] ?? 'Příkazem'); + $constantSymbol = trim($input['constant_symbol'] ?? '0308'); + $issuedBy = trim($input['issued_by'] ?? ''); + $notes = trim($input['notes'] ?? ''); + $items = $input['items'] ?? []; + + // Bankovni udaje + $bankName = trim($input['bank_name'] ?? ''); + $bankSwift = trim($input['bank_swift'] ?? ''); + $bankIban = trim($input['bank_iban'] ?? ''); + $bankAccount = trim($input['bank_account'] ?? ''); + + if (!$customerId) { + errorResponse('Zákazník je povinný'); + } + if (!$issueDate || !$dueDate || !$taxDate) { + errorResponse('Všechna data (vystavení, splatnost, DÚZP) jsou povinná'); + } + + // Validace formatu dat + foreach (['issue_date' => $issueDate, 'due_date' => $dueDate, 'tax_date' => $taxDate] as $label => $date) { + if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) || !strtotime($date)) { + errorResponse("Neplatný formát data: $label"); + } + } + + // Validace meny + $validCurrencies = ['CZK', 'EUR', 'USD', 'GBP']; + if (!in_array($currency, $validCurrencies)) { + errorResponse('Neplatná měna'); + } + + // Delkove limity + if (mb_strlen($paymentMethod) > 50) { + errorResponse('Forma úhrady je příliš dlouhá (max 50 znaků)'); + } + if (mb_strlen($issuedBy) > 255) { + errorResponse('Vystavil je příliš dlouhé (max 255 znaků)'); + } + if (mb_strlen($notes) > 5000) { + errorResponse('Poznámky jsou příliš dlouhé (max 5000 znaků)'); + } + if (mb_strlen($bankName) > 255) { + errorResponse('Název banky je příliš dlouhý'); + } + if (mb_strlen($bankIban) > 50) { + errorResponse('IBAN je příliš dlouhý'); + } + if (mb_strlen($bankSwift) > 20) { + errorResponse('BIC/SWIFT je příliš dlouhý'); + } + if (mb_strlen($bankAccount) > 50) { + errorResponse('Číslo účtu je příliš dlouhé'); + } + if (!$bankAccount && !$bankIban) { + errorResponse('Bankovní účet je povinný'); + } + + if (empty($items)) { + errorResponse('Faktura musí mít alespoň jednu položku'); + } + + // Validace polozek + foreach ($items as $i => $item) { + $qty = $item['quantity'] ?? 1; + $price = $item['unit_price'] ?? 0; + $vatRate = $item['vat_rate'] ?? 21; + if (!is_numeric($qty) || $qty < 0) { + errorResponse('Položka #' . ($i + 1) . ': neplatné množství'); + } + if (!is_numeric($price)) { + errorResponse('Položka #' . ($i + 1) . ': neplatná cena'); + } + if (!is_numeric($vatRate) || $vatRate < 0 || $vatRate > 100) { + errorResponse('Položka #' . ($i + 1) . ': neplatná sazba DPH'); + } + if (mb_strlen($item['description'] ?? '') > 500) { + errorResponse('Položka #' . ($i + 1) . ': popis je příliš dlouhý (max 500 znaků)'); + } + } + + // Overit zakaznika + $stmt = $pdo->prepare('SELECT id FROM customers WHERE id = ?'); + $stmt->execute([$customerId]); + if (!$stmt->fetch()) { + errorResponse('Zákazník nebyl nalezen', 404); + } + + // Lock pro cislovani + $locked = $pdo->query("SELECT GET_LOCK('boha_invoice_number', 5)")->fetchColumn(); + if (!$locked) { + errorResponse('Nepodařilo se získat zámek pro číslo faktury, zkuste to znovu', 503); + } + + $pdo->beginTransaction(); + try { + $invoiceNumber = !empty($input['invoice_number']) + ? trim($input['invoice_number']) + : generateInvoiceNumber($pdo); + + $stmt = $pdo->prepare(" + INSERT INTO invoices ( + invoice_number, order_id, customer_id, status, currency, + vat_rate, apply_vat, payment_method, constant_symbol, + bank_name, bank_swift, bank_iban, bank_account, + issue_date, due_date, tax_date, issued_by, notes, + created_at, modified_at + ) VALUES (?, ?, ?, 'issued', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW()) + "); + $stmt->execute([ + $invoiceNumber, + $orderId, + $customerId, + $currency, + $input['vat_rate'] ?? 21, + $applyVat, + $paymentMethod, + $constantSymbol, + $bankName, + $bankSwift, + $bankIban, + $bankAccount, + $issueDate, + $dueDate, + $taxDate, + $issuedBy, + $notes, + ]); + $invoiceId = (int) $pdo->lastInsertId(); + + // Vlozit polozky + $itemStmt = $pdo->prepare(' + INSERT INTO invoice_items ( + invoice_id, description, quantity, unit, unit_price, vat_rate, position + ) VALUES (?, ?, ?, ?, ?, ?, ?) + '); + foreach ($items as $i => $item) { + $itemStmt->execute([ + $invoiceId, + trim($item['description'] ?? ''), + $item['quantity'] ?? 1, + trim($item['unit'] ?? ''), + $item['unit_price'] ?? 0, + $item['vat_rate'] ?? 21, + $item['position'] ?? $i, + ]); + } + + $pdo->commit(); + $pdo->query("SELECT RELEASE_LOCK('boha_invoice_number')"); + + AuditLog::logCreate('invoices_invoice', $invoiceId, [ + 'invoice_number' => $invoiceNumber, + 'customer_id' => $customerId, + 'order_id' => $orderId, + ], "Vytvořena faktura '$invoiceNumber'"); + + successResponse([ + 'invoice_id' => $invoiceId, + 'invoice_number' => $invoiceNumber, + ], 'Faktura byla vystavena'); + } catch (PDOException $e) { + $pdo->rollBack(); + $pdo->query("SELECT RELEASE_LOCK('boha_invoice_number')"); + throw $e; + } +} + +function handleUpdateInvoice(PDO $pdo, int $id): void +{ + $stmt = $pdo->prepare( + 'SELECT id, invoice_number, order_id, customer_id, status, currency, + vat_rate, apply_vat, payment_method, constant_symbol, + bank_name, bank_swift, bank_iban, bank_account, + issue_date, due_date, tax_date, paid_date, + issued_by, notes, internal_notes + FROM invoices WHERE id = ?' + ); + $stmt->execute([$id]); + $invoice = $stmt->fetch(); + + if (!$invoice) { + errorResponse('Faktura nebyla nalezena', 404); + } + + $input = getJsonInput(); + $newStatus = $input['status'] ?? null; + $isDraft = $invoice['status'] === 'issued'; + + // Zmena stavu + if ($newStatus && $newStatus !== $invoice['status']) { + $valid = getValidTransitions($invoice['status']); + if (!in_array($newStatus, $valid)) { + errorResponse("Neplatný přechod stavu z '{$invoice['status']}' na '$newStatus'"); + } + } + + $pdo->beginTransaction(); + try { + $updates = []; + $params = []; + + if ($newStatus !== null && $newStatus !== $invoice['status']) { + $updates[] = 'status = ?'; + $params[] = $newStatus; + + if ($newStatus === 'paid') { + $updates[] = 'paid_date = CURDATE()'; + } + } + + // V issued stavu lze editovat vsechna pole + if ($isDraft) { + // Validace dat + foreach (['issue_date', 'due_date', 'tax_date'] as $dateField) { + if ( + isset($input[$dateField]) + && (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $input[$dateField]) || !strtotime($input[$dateField])) + ) { + errorResponse("Neplatný formát data: $dateField"); + } + } + // Validace meny + if (isset($input['currency']) && !in_array($input['currency'], ['CZK', 'EUR', 'USD', 'GBP'])) { + errorResponse('Neplatná měna'); + } + // Validace DPH + if ( + isset($input['vat_rate']) + && (!is_numeric($input['vat_rate']) || $input['vat_rate'] < 0 || $input['vat_rate'] > 100) + ) { + errorResponse('Neplatná sazba DPH'); + } + // Validace zakaznika + if (isset($input['customer_id'])) { + $custStmt = $pdo->prepare('SELECT id FROM customers WHERE id = ?'); + $custStmt->execute([(int)$input['customer_id']]); + if (!$custStmt->fetch()) { + errorResponse('Zákazník nebyl nalezen', 404); + } + } + + $stringFields = [ + 'issue_date' => 20, 'due_date' => 20, 'tax_date' => 20, + 'payment_method' => 50, 'constant_symbol' => 10, + 'bank_name' => 255, 'bank_swift' => 20, 'bank_iban' => 50, 'bank_account' => 50, + 'issued_by' => 255, + ]; + foreach ($stringFields as $field => $maxLen) { + if (array_key_exists($field, $input)) { + $val = trim((string)$input[$field]); + if (mb_strlen($val) > $maxLen) { + errorResponse("Pole $field je příliš dlouhé (max $maxLen znaků)"); + } + $updates[] = "$field = ?"; + $params[] = $val; + } + } + $numericFields = ['currency', 'vat_rate', 'apply_vat', 'customer_id']; + foreach ($numericFields as $field) { + if (array_key_exists($field, $input)) { + $updates[] = "$field = ?"; + $params[] = $input[$field]; + } + } + + // Aktualizace polozek + if (isset($input['items']) && is_array($input['items'])) { + $pdo->prepare('DELETE FROM invoice_items WHERE invoice_id = ?')->execute([$id]); + + $itemStmt = $pdo->prepare(' + INSERT INTO invoice_items ( + invoice_id, description, quantity, unit, unit_price, vat_rate, position + ) VALUES (?, ?, ?, ?, ?, ?, ?) + '); + foreach ($input['items'] as $i => $item) { + $itemStmt->execute([ + $id, + trim($item['description'] ?? ''), + $item['quantity'] ?? 1, + trim($item['unit'] ?? ''), + $item['unit_price'] ?? 0, + $item['vat_rate'] ?? 21, + $item['position'] ?? $i, + ]); + } + } + } + + // Poznamky lze editovat jen v issued/overdue stavu + if ($isDraft || $invoice['status'] === 'overdue') { + if (array_key_exists('notes', $input)) { + $updates[] = 'notes = ?'; + $params[] = $input['notes']; + } + if (array_key_exists('internal_notes', $input)) { + $updates[] = 'internal_notes = ?'; + $params[] = $input['internal_notes']; + } + } + + if (!empty($updates)) { + $updates[] = 'modified_at = NOW()'; + $params[] = $id; + $sql = 'UPDATE invoices SET ' . implode(', ', $updates) . ' WHERE id = ?'; + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + } + + $pdo->commit(); + + AuditLog::logUpdate( + 'invoices_invoice', + $id, + ['status' => $invoice['status']], + ['status' => $newStatus ?? $invoice['status']], + "Aktualizována faktura '{$invoice['invoice_number']}'" + ); + + successResponse(null, 'Faktura byla aktualizována'); + } catch (PDOException $e) { + $pdo->rollBack(); + throw $e; + } +} + +function handleDeleteInvoice(PDO $pdo, int $id): void +{ + $stmt = $pdo->prepare( + 'SELECT id, invoice_number, customer_id FROM invoices WHERE id = ?' + ); + $stmt->execute([$id]); + $invoice = $stmt->fetch(); + + if (!$invoice) { + errorResponse('Faktura nebyla nalezena', 404); + } + + $pdo->beginTransaction(); + try { + $pdo->prepare('DELETE FROM invoice_items WHERE invoice_id = ?')->execute([$id]); + $pdo->prepare('DELETE FROM invoices WHERE id = ?')->execute([$id]); + + $pdo->commit(); + + AuditLog::logDelete('invoices_invoice', $id, [ + 'invoice_number' => $invoice['invoice_number'], + 'customer_id' => $invoice['customer_id'], + ], "Smazána faktura '{$invoice['invoice_number']}'"); + + successResponse(null, 'Faktura byla smazána'); + } catch (PDOException $e) { + $pdo->rollBack(); + throw $e; + } +} diff --git a/dist/api/admin/handlers/leave-requests-handlers.php b/dist/api/admin/handlers/leave-requests-handlers.php new file mode 100644 index 0000000..d0836b8 --- /dev/null +++ b/dist/api/admin/handlers/leave-requests-handlers.php @@ -0,0 +1,481 @@ +modify('+1 day'); // include the end date + + $days = 0; + $period = new DatePeriod($start, new DateInterval('P1D'), $end); + foreach ($period as $date) { + $dayOfWeek = (int)$date->format('N'); // 1=Mon, 7=Sun + if ($dayOfWeek <= 5) { + $days++; + } + } + return $days; +} + +/** + * Get leave balance for user (reuse logic from attendance.php) + * + * @return array + */ +function getLeaveBalanceForRequest(PDO $pdo, int $userId, ?int $year = null): array +{ + $year = $year ?: (int)date('Y'); + + $stmt = $pdo->prepare( + 'SELECT id, user_id, year, vacation_total, vacation_used, sick_used + FROM leave_balances WHERE user_id = ? AND year = ?' + ); + $stmt->execute([$userId, $year]); + $balance = $stmt->fetch(); + + if (!$balance) { + return [ + 'vacation_total' => 160, + 'vacation_used' => 0, + 'vacation_remaining' => 160, + 'sick_used' => 0, + ]; + } + + return [ + 'vacation_total' => (float)$balance['vacation_total'], + 'vacation_used' => (float)$balance['vacation_used'], + 'vacation_remaining' => (float)$balance['vacation_total'] - (float)$balance['vacation_used'], + 'sick_used' => (float)$balance['sick_used'], + ]; +} + +/** + * Get hours already locked in pending requests for vacation + */ +function getPendingVacationHours(PDO $pdo, int $userId, int $year): float +{ + $stmt = $pdo->prepare(" + SELECT COALESCE(SUM(total_hours), 0) as pending_hours + FROM leave_requests + WHERE user_id = ? AND leave_type = 'vacation' AND status = 'pending' + AND YEAR(date_from) = ? + "); + $stmt->execute([$userId, $year]); + return (float)$stmt->fetchColumn(); +} + +// ============================================================================ +// GET Handlers +// ============================================================================ + +/** + * GET - Own leave requests + */ +function handleGetMyRequests(PDO $pdo, int $userId): void +{ + $stmt = $pdo->prepare(" + SELECT lr.id, lr.user_id, lr.leave_type, lr.date_from, lr.date_to, + lr.total_hours, lr.total_days, lr.notes, lr.status, + lr.reviewer_id, lr.reviewer_note, lr.reviewed_at, lr.created_at, + CONCAT(u.first_name, ' ', u.last_name) as reviewer_name + FROM leave_requests lr + LEFT JOIN users u ON lr.reviewer_id = u.id + WHERE lr.user_id = ? + ORDER BY lr.created_at DESC + "); + $stmt->execute([$userId]); + $requests = $stmt->fetchAll(); + + successResponse($requests); +} + +/** + * GET - All pending requests (for approver) + */ +function handleGetPending(PDO $pdo): void +{ + $stmt = $pdo->prepare(" + SELECT lr.id, lr.user_id, lr.leave_type, lr.date_from, lr.date_to, + lr.total_hours, lr.total_days, lr.notes, lr.status, + lr.reviewer_id, lr.reviewer_note, lr.reviewed_at, lr.created_at, + CONCAT(u.first_name, ' ', u.last_name) as employee_name, + CONCAT(rv.first_name, ' ', rv.last_name) as reviewer_name + FROM leave_requests lr + JOIN users u ON lr.user_id = u.id + LEFT JOIN users rv ON lr.reviewer_id = rv.id + WHERE lr.status = 'pending' + ORDER BY lr.created_at ASC + "); + $stmt->execute(); + $requests = $stmt->fetchAll(); + + successResponse([ + 'requests' => $requests, + 'count' => count($requests), + ]); +} + +/** + * GET - All requests with filters (for approver) + */ +function handleGetAll(PDO $pdo): void +{ + $status = $_GET['status'] ?? ''; + $userId = isset($_GET['user_id']) ? (int)$_GET['user_id'] : null; + + $where = []; + $params = []; + + if ($status && in_array($status, ['pending', 'approved', 'rejected', 'cancelled'])) { + $where[] = 'lr.status = ?'; + $params[] = $status; + } + + if ($userId) { + $where[] = 'lr.user_id = ?'; + $params[] = $userId; + } + + $whereClause = $where ? 'WHERE ' . implode(' AND ', $where) : ''; + + $stmt = $pdo->prepare(" + SELECT lr.id, lr.user_id, lr.leave_type, lr.date_from, lr.date_to, + lr.total_hours, lr.total_days, lr.notes, lr.status, + lr.reviewer_id, lr.reviewer_note, lr.reviewed_at, lr.created_at, + CONCAT(u.first_name, ' ', u.last_name) as employee_name, + CONCAT(rv.first_name, ' ', rv.last_name) as reviewer_name + FROM leave_requests lr + JOIN users u ON lr.user_id = u.id + LEFT JOIN users rv ON lr.reviewer_id = rv.id + $whereClause + ORDER BY lr.created_at DESC + LIMIT 200 + "); + $stmt->execute($params); + $requests = $stmt->fetchAll(); + + successResponse($requests); +} + +// ============================================================================ +// POST Handlers +// ============================================================================ + +/** + * POST - Submit new leave request + */ +function handleSubmitRequest(PDO $pdo, int $userId): void +{ + $input = getJsonInput(); + + $leaveType = $input['leave_type'] ?? ''; + $dateFrom = $input['date_from'] ?? ''; + $dateTo = $input['date_to'] ?? ''; + $notes = trim($input['notes'] ?? ''); + + if (!$leaveType || !$dateFrom || !$dateTo) { + errorResponse('Vyplňte všechna povinná pole'); + } + + if (!in_array($leaveType, ['vacation', 'sick', 'unpaid'])) { + errorResponse('Neplatný typ nepřítomnosti'); + } + + // Validate dates + $from = new DateTime($dateFrom); + $to = new DateTime($dateTo); + if ($to < $from) { + errorResponse('Datum "do" nesmí být před datem "od"'); + } + + // Calculate business days + $businessDays = calculateBusinessDays($dateFrom, $dateTo); + if ($businessDays === 0) { + errorResponse('Zvolené období neobsahuje žádné pracovní dny'); + } + + $totalHours = $businessDays * 8; + + // Check vacation balance + if ($leaveType === 'vacation') { + $year = (int)$from->format('Y'); + $balance = getLeaveBalanceForRequest($pdo, $userId, $year); + $pendingHours = getPendingVacationHours($pdo, $userId, $year); + $availableHours = $balance['vacation_remaining'] - $pendingHours; + + if ($availableHours < $totalHours) { + errorResponse( + "Nemáte dostatek hodin dovolené. Dostupné: {$availableHours}h" + . " (zbývá {$balance['vacation_remaining']}h, v čekajících žádostech: {$pendingHours}h)," + . " požadujete: {$totalHours}h." + ); + } + } + + // Check overlapping requests + $stmt = $pdo->prepare(" + SELECT id FROM leave_requests + WHERE user_id = ? AND status IN ('pending', 'approved') + AND date_from <= ? AND date_to >= ? + "); + $stmt->execute([$userId, $dateTo, $dateFrom]); + if ($stmt->fetch()) { + errorResponse('Pro toto období již existuje žádost o nepřítomnost'); + } + + // Insert request + $stmt = $pdo->prepare(" + INSERT INTO leave_requests (user_id, leave_type, date_from, date_to, total_hours, total_days, notes, status) + VALUES (?, ?, ?, ?, ?, ?, ?, 'pending') + "); + $stmt->execute([$userId, $leaveType, $dateFrom, $dateTo, $totalHours, $businessDays, $notes ?: null]); + + $requestId = (int)$pdo->lastInsertId(); + + AuditLog::logCreate('leave_request', $requestId, [ + 'leave_type' => $leaveType, + 'date_from' => $dateFrom, + 'date_to' => $dateTo, + 'total_days' => $businessDays, + 'total_hours' => $totalHours, + ], "Podána žádost o nepřítomnost: $leaveType ($dateFrom - $dateTo)"); + + // Send email notification + try { + $stmt = $pdo->prepare("SELECT CONCAT(first_name, ' ', last_name) as name FROM users WHERE id = ?"); + $stmt->execute([$userId]); + $employeeName = $stmt->fetchColumn() ?: 'Neznámý'; + + LeaveNotification::notifyNewRequest([ + 'leave_type' => $leaveType, + 'date_from' => $dateFrom, + 'date_to' => $dateTo, + 'total_days' => $businessDays, + 'total_hours' => $totalHours, + 'notes' => $notes, + ], $employeeName); + } catch (\Exception $e) { + error_log('Leave notification error: ' . $e->getMessage()); + } + + successResponse(['id' => $requestId], 'Žádost byla odeslána ke schválení'); +} + +/** + * POST - Cancel own pending request + */ +function handleCancelRequest(PDO $pdo, int $userId): void +{ + $input = getJsonInput(); + $requestId = (int)($input['request_id'] ?? 0); + + if (!$requestId) { + errorResponse('ID žádosti je povinné'); + } + + $stmt = $pdo->prepare( + 'SELECT id, user_id, leave_type, date_from, date_to, total_hours, + total_days, notes, status + FROM leave_requests WHERE id = ? AND user_id = ?' + ); + $stmt->execute([$requestId, $userId]); + $request = $stmt->fetch(); + + if (!$request) { + errorResponse('Žádost nebyla nalezena'); + } + + if ($request['status'] !== 'pending') { + errorResponse('Lze zrušit pouze čekající žádosti'); + } + + $stmt = $pdo->prepare("UPDATE leave_requests SET status = 'cancelled' WHERE id = ?"); + $stmt->execute([$requestId]); + + AuditLog::logUpdate( + 'leave_request', + $requestId, + ['status' => 'pending'], + ['status' => 'cancelled'], + 'Žádost o nepřítomnost zrušena zaměstnancem' + ); + + successResponse(null, 'Žádost byla zrušena'); +} + +/** + * POST - Approve a leave request + * + * @param array $authData + */ +function handleApproveRequest(PDO $pdo, int $reviewerId, array $authData): void +{ + $input = getJsonInput(); + $requestId = (int)($input['request_id'] ?? 0); + + if (!$requestId) { + errorResponse('ID žádosti je povinné'); + } + + $stmt = $pdo->prepare( + 'SELECT id, user_id, leave_type, date_from, date_to, total_hours, + total_days, status + FROM leave_requests WHERE id = ?' + ); + $stmt->execute([$requestId]); + $request = $stmt->fetch(); + + if (!$request) { + errorResponse('Žádost nebyla nalezena'); + } + + if ($request['status'] !== 'pending') { + errorResponse('Lze schválit pouze čekající žádosti'); + } + + if ((int)$request['user_id'] === $reviewerId && !($authData['user']['is_admin'] ?? false)) { + errorResponse('Nemůžete schválit vlastní žádost', 403); + } + + // Re-check vacation balance + if ($request['leave_type'] === 'vacation') { + $year = (int)date('Y', strtotime($request['date_from'])); + $balance = getLeaveBalanceForRequest($pdo, (int)$request['user_id'], $year); + + if ($balance['vacation_remaining'] < (float)$request['total_hours']) { + errorResponse( + "Zaměstnanec nemá dostatek hodin dovolené." + . " Zbývá: {$balance['vacation_remaining']}h, požadováno: {$request['total_hours']}h." + ); + } + } + + // Begin transaction + $pdo->beginTransaction(); + + try { + // Create attendance records for each business day + $start = new DateTime($request['date_from']); + $end = new DateTime($request['date_to']); + $end->modify('+1 day'); + + $period = new DatePeriod($start, new DateInterval('P1D'), $end); + $insertStmt = $pdo->prepare(' + INSERT INTO attendance (user_id, shift_date, leave_type, leave_hours, notes) + VALUES (?, ?, ?, 8, ?) + '); + + $leaveNote = "Schválená žádost #$requestId"; + $totalBusinessDays = 0; + + foreach ($period as $date) { + $dayOfWeek = (int)$date->format('N'); + if ($dayOfWeek <= 5) { + $shiftDate = $date->format('Y-m-d'); + $insertStmt->execute([ + $request['user_id'], + $shiftDate, + $request['leave_type'], + $leaveNote, + ]); + $totalBusinessDays++; + } + } + + // Update leave balance ONCE with total hours (was N queries, one per day) + if ($totalBusinessDays > 0) { + updateLeaveBalance( + $pdo, + (int)$request['user_id'], + $request['date_from'], + $request['leave_type'], + (float)($totalBusinessDays * 8) + ); + } + + // Update request status + $stmt = $pdo->prepare(" + UPDATE leave_requests + SET status = 'approved', reviewer_id = ?, reviewed_at = NOW() + WHERE id = ? + "); + $stmt->execute([$reviewerId, $requestId]); + + $pdo->commit(); + + AuditLog::logUpdate( + 'leave_request', + $requestId, + ['status' => 'pending'], + ['status' => 'approved', 'reviewer_id' => $reviewerId], + 'Žádost o nepřítomnost schválena' + ); + + successResponse(null, 'Žádost byla schválena'); + } catch (\Exception $e) { + $pdo->rollBack(); + error_log('Approve request error: ' . $e->getMessage()); + errorResponse('Chyba při schvalování žádosti', 500); + } +} + +/** + * POST - Reject a leave request + * + * @param array $authData + */ +function handleRejectRequest(PDO $pdo, int $reviewerId, array $authData): void +{ + $input = getJsonInput(); + $requestId = (int)($input['request_id'] ?? 0); + $note = trim($input['note'] ?? ''); + + if (!$requestId) { + errorResponse('ID žádosti je povinné'); + } + + if (!$note) { + errorResponse('Důvod zamítnutí je povinný'); + } + + $stmt = $pdo->prepare( + 'SELECT id, user_id, status FROM leave_requests WHERE id = ?' + ); + $stmt->execute([$requestId]); + $request = $stmt->fetch(); + + if (!$request) { + errorResponse('Žádost nebyla nalezena'); + } + + if ($request['status'] !== 'pending') { + errorResponse('Lze zamítnout pouze čekající žádosti'); + } + + if ((int)$request['user_id'] === $reviewerId && !($authData['user']['is_admin'] ?? false)) { + errorResponse('Nemůžete zamítnout vlastní žádost', 403); + } + + $stmt = $pdo->prepare(" + UPDATE leave_requests + SET status = 'rejected', reviewer_id = ?, reviewer_note = ?, reviewed_at = NOW() + WHERE id = ? + "); + $stmt->execute([$reviewerId, $note, $requestId]); + + AuditLog::logUpdate( + 'leave_request', + $requestId, + ['status' => 'pending'], + ['status' => 'rejected', 'reviewer_id' => $reviewerId, 'reviewer_note' => $note], + "Žádost o nepřítomnost zamítnuta: $note" + ); + + successResponse(null, 'Žádost byla zamítnuta'); +} diff --git a/dist/api/admin/handlers/offers-handlers.php b/dist/api/admin/handlers/offers-handlers.php new file mode 100644 index 0000000..ba5c327 --- /dev/null +++ b/dist/api/admin/handlers/offers-handlers.php @@ -0,0 +1,586 @@ + 'q.created_at', + 'CreatedAt' => 'q.created_at', + 'created_at' => 'q.created_at', + 'QuotationNumber' => 'q.quotation_number', + 'quotation_number' => 'q.quotation_number', + 'ProjectCode' => 'q.project_code', + 'project_code' => 'q.project_code', + 'ValidUntil' => 'q.valid_until', + 'valid_until' => 'q.valid_until', + 'Currency' => 'q.currency', + 'currency' => 'q.currency', + ]; + + $p = PaginationHelper::parseParams($sortMap); + $where = 'WHERE 1=1'; + $params = []; + + if ($p['search']) { + $where .= ' AND (q.quotation_number LIKE ? OR q.project_code LIKE ? OR c.name LIKE ?)'; + $searchParam = "%{$p['search']}%"; + $params = [$searchParam, $searchParam, $searchParam]; + } + + $from = "FROM quotations q LEFT JOIN customers c ON q.customer_id = c.id"; + + $result = PaginationHelper::paginate( + $pdo, + "SELECT COUNT(*) {$from} {$where}", + "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.customer_id, q.order_id, q.status, + 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) + FROM quotation_items qi WHERE qi.quotation_id = q.id) as total + {$from} {$where} + ORDER BY {$p['sort']} {$p['order']}", + $params, + $p + ); + + successResponse([ + 'quotations' => $result['items'], + 'pagination' => $result['pagination'], + ]); +} + +function handleGetDetail(PDO $pdo, int $id): void +{ + $stmt = $pdo->prepare(' + SELECT q.id, q.quotation_number, q.project_code, q.customer_id, + q.created_at, q.valid_until, q.currency, q.language, + q.vat_rate, q.apply_vat, q.exchange_rate, q.order_id, + q.status, q.scope_title, q.scope_description, + c.name as customer_name + FROM quotations q + LEFT JOIN customers c ON q.customer_id = c.id + WHERE q.id = ? + '); + $stmt->execute([$id]); + $quotation = $stmt->fetch(); + + if (!$quotation) { + errorResponse('Nabídka nebyla nalezena', 404); + } + + // Get items + $stmt = $pdo->prepare(' + SELECT id, quotation_id, position, description, item_description, + quantity, unit, unit_price, is_included_in_total + FROM quotation_items + WHERE quotation_id = ? + ORDER BY position + '); + $stmt->execute([$id]); + $quotation['items'] = $stmt->fetchAll(); + + // Get scope sections + $stmt = $pdo->prepare(' + SELECT id, quotation_id, position, title, title_cz, content + FROM scope_sections + WHERE quotation_id = ? + ORDER BY position + '); + $stmt->execute([$id]); + $quotation['sections'] = $stmt->fetchAll(); + + // Get customer + if ($quotation['customer_id']) { + $stmt = $pdo->prepare( + 'SELECT id, name, company_id, vat_id, street, city, postal_code, country, custom_fields + FROM customers WHERE id = ?' + ); + $stmt->execute([$quotation['customer_id']]); + $quotation['customer'] = $stmt->fetch(); + } + + // Get linked order info + if ($quotation['order_id']) { + $stmt = $pdo->prepare('SELECT id, order_number, status FROM orders WHERE id = ?'); + $stmt->execute([$quotation['order_id']]); + $quotation['order'] = $stmt->fetch() ?: null; + } else { + $quotation['order'] = null; + } + + successResponse($quotation); +} + +function handleGetNextNumber(PDO $pdo): void +{ + $settings = $pdo->query('SELECT quotation_prefix FROM company_settings LIMIT 1')->fetch(); + if (!$settings) { + errorResponse('Nastavení firmy nenalezeno'); + } + + $year = date('Y'); + $prefix = $settings['quotation_prefix'] ?: 'N'; + $number = getMaxQuotationNumber($pdo, $year, $prefix) + 1; + + $formatted = sprintf('%s/%s/%03d', $year, $prefix, $number); + + successResponse([ + 'number' => $formatted, + 'raw_number' => $number, + 'prefix' => $prefix, + 'year' => $year, + ]); +} + +function getMaxQuotationNumber(PDO $pdo, string $year, string $prefix): int +{ + $likePattern = "{$year}/{$prefix}/%"; + $stmt = $pdo->prepare(" + SELECT COALESCE(MAX(CAST(SUBSTRING_INDEX(quotation_number, '/', -1) AS UNSIGNED)), 0) + FROM quotations + WHERE quotation_number LIKE ? + "); + $stmt->execute([$likePattern]); + return (int) $stmt->fetchColumn(); +} + +function generateNextNumber(PDO $pdo): string +{ + $settings = $pdo->query('SELECT quotation_prefix FROM company_settings LIMIT 1')->fetch(); + + $year = date('Y'); + $prefix = $settings['quotation_prefix'] ?: 'N'; + $number = getMaxQuotationNumber($pdo, $year, $prefix) + 1; + + return sprintf('%s/%s/%03d', $year, $prefix, $number); +} + +/** @param array $q */ +function validateQuotationInput(array $q): void +{ + if (empty($q['customer_id'])) { + errorResponse('Vyberte zákazníka'); + } + if (empty($q['created_at'])) { + errorResponse('Zadejte datum vytvoření'); + } + if (empty($q['valid_until'])) { + errorResponse('Zadejte datum platnosti'); + } + if (!empty($q['created_at']) && !empty($q['valid_until']) && $q['valid_until'] < $q['created_at']) { + errorResponse('Datum platnosti nesmí být před datem vytvoření'); + } + if (empty($q['currency'])) { + errorResponse('Vyberte měnu'); + } + + // Validace formatu dat + foreach (['created_at', 'valid_until'] as $dateField) { + if (!empty($q[$dateField]) && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $q[$dateField])) { + errorResponse("Neplatný formát data: $dateField"); + } + } + // Validace meny a jazyka + if (!in_array($q['currency'] ?? '', ['EUR', 'USD', 'CZK', 'GBP'])) { + errorResponse('Neplatná měna'); + } + if (!empty($q['language']) && !in_array($q['language'], ['EN', 'CZ'])) { + errorResponse('Neplatný jazyk'); + } + // Validace DPH + if (isset($q['vat_rate'])) { + $rate = floatval($q['vat_rate']); + if ($rate < 0 || $rate > 100) { + errorResponse('Sazba DPH musí být mezi 0 a 100'); + } + } + // Delkove limity + if (!empty($q['project_code']) && mb_strlen($q['project_code']) > 100) { + errorResponse('Kód projektu je příliš dlouhý (max 100 znaků)'); + } +} + +function handleCreateOffer(PDO $pdo): void +{ + $input = getJsonInput(); + $quotation = $input['quotation'] ?? $input; + $items = $input['items'] ?? []; + $sections = $input['sections'] ?? []; + + validateQuotationInput($quotation); + + // Serialize number generation across concurrent requests + $locked = $pdo->query("SELECT GET_LOCK('boha_quotation_number', 5)")->fetchColumn(); + if (!$locked) { + errorResponse('Nepodařilo se získat zámek pro číslo nabídky, zkuste to znovu', 503); + } + + $pdo->beginTransaction(); + try { + $quotationNumber = generateNextNumber($pdo); + + $stmt = $pdo->prepare(' + INSERT INTO quotations ( + quotation_number, project_code, customer_id, created_at, valid_until, + currency, language, vat_rate, apply_vat, exchange_rate, + scope_title, scope_description, modified_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW()) + '); + + $stmt->execute([ + $quotationNumber, + $quotation['project_code'] ?? '', + $quotation['customer_id'] ? (int)$quotation['customer_id'] : null, + $quotation['created_at'] ?? date('Y-m-d H:i:s'), + $quotation['valid_until'] ?? date('Y-m-d H:i:s', strtotime('+30 days')), + $quotation['currency'] ?? 'EUR', + $quotation['language'] ?? 'EN', + $quotation['vat_rate'] ?? 21, + isset($quotation['apply_vat']) ? ($quotation['apply_vat'] ? 1 : 0) : 0, + $quotation['exchange_rate'] ?? null, + $quotation['scope_title'] ?? '', + $quotation['scope_description'] ?? '', + ]); + + $quotationId = (int)$pdo->lastInsertId(); + + saveItems($pdo, $quotationId, $items); + saveSections($pdo, $quotationId, $sections); + + + $pdo->commit(); + $pdo->query("SELECT RELEASE_LOCK('boha_quotation_number')"); + + AuditLog::logCreate('offers_quotation', $quotationId, [ + 'quotation_number' => $quotationNumber, + 'project_code' => $quotation['project_code'] ?? '', + ], "Vytvořena nabídka '$quotationNumber'"); + + successResponse([ + 'id' => $quotationId, + 'number' => $quotationNumber, + ], 'Nabídka byla vytvořena'); + } catch (PDOException $e) { + $pdo->rollBack(); + $pdo->query("SELECT RELEASE_LOCK('boha_quotation_number')"); + throw $e; + } +} + +function handleUpdateOffer(PDO $pdo, int $id): void +{ + $stmt = $pdo->prepare( + 'SELECT id, quotation_number, project_code, customer_id, created_at, + valid_until, currency, language, vat_rate, apply_vat, + exchange_rate, order_id, status, scope_title, scope_description + FROM quotations WHERE id = ?' + ); + $stmt->execute([$id]); + $existing = $stmt->fetch(); + + if (!$existing) { + errorResponse('Nabídka nebyla nalezena', 404); + } + + if ($existing['status'] === 'invalidated') { + errorResponse('Zneplatněnou nabídku nelze upravovat', 403); + } + + $input = getJsonInput(); + $quotation = $input['quotation'] ?? $input; + $items = $input['items'] ?? []; + $sections = $input['sections'] ?? []; + + validateQuotationInput($quotation); + + $pdo->beginTransaction(); + try { + $stmt = $pdo->prepare(' + UPDATE quotations SET + project_code = ?, + customer_id = ?, + created_at = ?, + valid_until = ?, + currency = ?, + language = ?, + vat_rate = ?, + apply_vat = ?, + exchange_rate = ?, + scope_title = ?, + scope_description = ?, + modified_at = NOW() + WHERE id = ? + '); + + $stmt->execute([ + $quotation['project_code'] ?? $existing['project_code'], + isset($quotation['customer_id']) + ? ($quotation['customer_id'] ? (int)$quotation['customer_id'] : null) + : $existing['customer_id'], + $quotation['created_at'] ?? $existing['created_at'], + $quotation['valid_until'] ?? $existing['valid_until'], + $quotation['currency'] ?? $existing['currency'], + $quotation['language'] ?? $existing['language'], + $quotation['vat_rate'] ?? $existing['vat_rate'], + isset($quotation['apply_vat']) ? ($quotation['apply_vat'] ? 1 : 0) : $existing['apply_vat'], + array_key_exists('exchange_rate', $quotation) ? $quotation['exchange_rate'] : $existing['exchange_rate'], + $quotation['scope_title'] ?? $existing['scope_title'], + $quotation['scope_description'] ?? $existing['scope_description'], + $id, + ]); + + // Replace items + $stmt = $pdo->prepare('DELETE FROM quotation_items WHERE quotation_id = ?'); + $stmt->execute([$id]); + saveItems($pdo, $id, $items); + + // Replace sections + $stmt = $pdo->prepare('DELETE FROM scope_sections WHERE quotation_id = ?'); + $stmt->execute([$id]); + saveSections($pdo, $id, $sections); + + + $pdo->commit(); + + AuditLog::logUpdate( + 'offers_quotation', + $id, + ['quotation_number' => $existing['quotation_number']], + ['project_code' => $quotation['project_code'] ?? $existing['project_code']], + "Upravena nabídka '{$existing['quotation_number']}'" + ); + + successResponse(null, 'Nabídka byla aktualizována'); + } catch (PDOException $e) { + $pdo->rollBack(); + throw $e; + } +} + +function handleDuplicate(PDO $pdo, int $sourceId): void +{ + $stmt = $pdo->prepare( + 'SELECT id, quotation_number, project_code, customer_id, currency, + language, vat_rate, apply_vat, exchange_rate, + scope_title, scope_description + FROM quotations WHERE id = ?' + ); + $stmt->execute([$sourceId]); + $source = $stmt->fetch(); + + if (!$source) { + errorResponse('Zdrojová nabídka nebyla nalezena', 404); + } + + $stmt = $pdo->prepare( + 'SELECT description, item_description, quantity, unit, unit_price, + is_included_in_total, position + FROM quotation_items WHERE quotation_id = ? ORDER BY position' + ); + $stmt->execute([$sourceId]); + $sourceItems = $stmt->fetchAll(); + + $stmt = $pdo->prepare( + 'SELECT title, title_cz, content, position + FROM scope_sections WHERE quotation_id = ? ORDER BY position' + ); + $stmt->execute([$sourceId]); + $sourceSections = $stmt->fetchAll(); + + $locked = $pdo->query("SELECT GET_LOCK('boha_quotation_number', 5)")->fetchColumn(); + if (!$locked) { + errorResponse('Nepodařilo se získat zámek pro číslo nabídky, zkuste to znovu', 503); + } + + $pdo->beginTransaction(); + try { + $newNumber = generateNextNumber($pdo); + + $stmt = $pdo->prepare(' + INSERT INTO quotations ( + quotation_number, project_code, customer_id, created_at, valid_until, + currency, language, vat_rate, apply_vat, exchange_rate, + scope_title, scope_description, modified_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW()) + '); + + $stmt->execute([ + $newNumber, + $source['project_code'], + $source['customer_id'], + date('Y-m-d H:i:s'), + date('Y-m-d H:i:s', strtotime('+30 days')), + $source['currency'], + $source['language'], + $source['vat_rate'], + $source['apply_vat'], + $source['exchange_rate'], + $source['scope_title'], + $source['scope_description'], + ]); + + $newId = (int)$pdo->lastInsertId(); + + $items = array_map(function ($item) { + return [ + 'description' => $item['description'], + 'item_description' => $item['item_description'], + 'quantity' => $item['quantity'], + 'unit_price' => $item['unit_price'], + 'is_included_in_total' => $item['is_included_in_total'], + 'position' => $item['position'], + ]; + }, $sourceItems); + saveItems($pdo, $newId, $items); + + $sections = array_map(function ($section) { + return [ + 'title' => $section['title'], + 'title_cz' => $section['title_cz'], + 'content' => $section['content'], + 'position' => $section['position'], + ]; + }, $sourceSections); + saveSections($pdo, $newId, $sections); + + $pdo->commit(); + $pdo->query("SELECT RELEASE_LOCK('boha_quotation_number')"); + + AuditLog::logCreate('offers_quotation', $newId, [ + 'quotation_number' => $newNumber, + 'duplicated_from' => $source['quotation_number'], + ], "Duplikována nabídka '{$source['quotation_number']}' jako '$newNumber'"); + + successResponse([ + 'id' => $newId, + 'number' => $newNumber, + ], 'Nabídka byla duplikována'); + } catch (PDOException $e) { + $pdo->rollBack(); + $pdo->query("SELECT RELEASE_LOCK('boha_quotation_number')"); + throw $e; + } +} + +function handleInvalidateOffer(PDO $pdo, int $id): void +{ + $stmt = $pdo->prepare('SELECT quotation_number, status, order_id FROM quotations WHERE id = ?'); + $stmt->execute([$id]); + $quotation = $stmt->fetch(); + + if (!$quotation) { + errorResponse('Nabídka nebyla nalezena', 404); + } + + if ($quotation['status'] === 'invalidated') { + errorResponse('Nabídka je již zneplatněna', 400); + } + + if ($quotation['order_id']) { + errorResponse('Nabídku s objednávkou nelze zneplatnit', 400); + } + + $stmt = $pdo->prepare('UPDATE quotations SET status = ?, modified_at = NOW() WHERE id = ?'); + $stmt->execute(['invalidated', $id]); + + AuditLog::logUpdate( + 'offers_quotation', + $id, + ['status' => 'active'], + ['status' => 'invalidated'], + "Zneplatněna nabídka '{$quotation['quotation_number']}'" + ); + + successResponse(null, 'Nabídka byla zneplatněna'); +} + +function handleDeleteQuotation(PDO $pdo, int $id): void +{ + $stmt = $pdo->prepare('SELECT quotation_number FROM quotations WHERE id = ?'); + $stmt->execute([$id]); + $quotation = $stmt->fetch(); + + + if (!$quotation) { + errorResponse('Nabídka nebyla nalezena', 404); + } + + $pdo->beginTransaction(); + try { + $stmt = $pdo->prepare('DELETE FROM quotation_items WHERE quotation_id = ?'); + $stmt->execute([$id]); + + $stmt = $pdo->prepare('DELETE FROM scope_sections WHERE quotation_id = ?'); + $stmt->execute([$id]); + + $stmt = $pdo->prepare('DELETE FROM quotations WHERE id = ?'); + $stmt->execute([$id]); + + $pdo->commit(); + + AuditLog::logDelete('offers_quotation', $id, [ + 'quotation_number' => $quotation['quotation_number'], + ], "Smazána nabídka '{$quotation['quotation_number']}'"); + + successResponse(null, 'Nabídka byla smazána'); + } catch (PDOException $e) { + $pdo->rollBack(); + throw $e; + } +} + +// --- Helpers --- + +/** @param list> $items */ +function saveItems(PDO $pdo, int $quotationId, array $items): void +{ + if (empty($items)) { + return; + } + + $stmt = $pdo->prepare(' + INSERT INTO quotation_items ( + quotation_id, description, item_description, quantity, unit, + unit_price, is_included_in_total, position, modified_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW()) + '); + + foreach ($items as $i => $item) { + $stmt->execute([ + $quotationId, + $item['description'] ?? '', + $item['item_description'] ?? '', + $item['quantity'] ?? 1, + $item['unit'] ?? '', + $item['unit_price'] ?? 0, + isset($item['is_included_in_total']) ? ($item['is_included_in_total'] ? 1 : 0) : 1, + $item['position'] ?? ($i + 1), + ]); + } +} + +/** @param list> $sections */ +function saveSections(PDO $pdo, int $quotationId, array $sections): void +{ + if (empty($sections)) { + return; + } + + $stmt = $pdo->prepare(' + INSERT INTO scope_sections ( + quotation_id, title, title_cz, content, position, modified_at + ) VALUES (?, ?, ?, ?, ?, NOW()) + '); + + foreach ($sections as $i => $section) { + $stmt->execute([ + $quotationId, + $section['title'] ?? '', + $section['title_cz'] ?? '', + $section['content'] ?? '', + $section['position'] ?? ($i + 1), + ]); + } +} diff --git a/dist/api/admin/handlers/offers-templates-handlers.php b/dist/api/admin/handlers/offers-templates-handlers.php new file mode 100644 index 0000000..b41e6ca --- /dev/null +++ b/dist/api/admin/handlers/offers-templates-handlers.php @@ -0,0 +1,273 @@ +query( + 'SELECT id, name, description, default_price, category + FROM item_templates ORDER BY category, name' + ); + successResponse(['templates' => $stmt->fetchAll()]); +} + +function handleSaveItemTemplate(PDO $pdo): void +{ + $input = getJsonInput(); + + if (empty($input['name'])) { + errorResponse('Název šablony je povinný'); + } + + $id = isset($input['id']) ? (int)$input['id'] : null; + + if ($id) { + // Update + $stmt = $pdo->prepare(' + UPDATE item_templates SET + name = ?, description = ?, default_price = ?, category = ?, + modified_at = NOW(), sync_version = sync_version + 1 + WHERE id = ? + '); + $stmt->execute([ + $input['name'], + $input['description'] ?? '', + $input['default_price'] ?? 0, + $input['category'] ?? '', + $id, + ]); + successResponse(null, 'Šablona byla aktualizována'); + } else { + // Create + $uuid = sprintf( + '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + random_int(0, 0xffff), + random_int(0, 0xffff), + random_int(0, 0xffff), + random_int(0, 0x0fff) | 0x4000, + random_int(0, 0x3fff) | 0x8000, + random_int(0, 0xffff), + random_int(0, 0xffff), + random_int(0, 0xffff) + ); + + $stmt = $pdo->prepare(' + INSERT INTO item_templates (name, description, default_price, category, uuid, modified_at, sync_version) + VALUES (?, ?, ?, ?, ?, NOW(), 1) + '); + $stmt->execute([ + $input['name'], + $input['description'] ?? '', + $input['default_price'] ?? 0, + $input['category'] ?? '', + $uuid, + ]); + $newId = (int)$pdo->lastInsertId(); + + AuditLog::logCreate( + 'offers_item_template', + (int)$newId, + ['name' => $input['name']], + "Vytvořena šablona položky '{$input['name']}'" + ); + + successResponse(['id' => $newId], 'Šablona byla vytvořena'); + } +} + +function handleDeleteItemTemplate(PDO $pdo, int $id): void +{ + $stmt = $pdo->prepare('SELECT name FROM item_templates WHERE id = ?'); + $stmt->execute([$id]); + $template = $stmt->fetch(); + + if (!$template) { + errorResponse('Šablona nebyla nalezena', 404); + } + + $stmt = $pdo->prepare('DELETE FROM item_templates WHERE id = ?'); + $stmt->execute([$id]); + + + AuditLog::logDelete( + 'offers_item_template', + $id, + ['name' => $template['name']], + "Smazána šablona položky '{$template['name']}'" + ); + + successResponse(null, 'Šablona byla smazána'); +} + +// --- Scope Templates --- + +function handleGetScopeTemplates(PDO $pdo): void +{ + $stmt = $pdo->query( + 'SELECT id, name, title, description FROM scope_templates ORDER BY name' + ); + successResponse(['templates' => $stmt->fetchAll()]); +} + +function handleGetScopeDetail(PDO $pdo, int $id): void +{ + $stmt = $pdo->prepare( + 'SELECT id, name, title, description FROM scope_templates WHERE id = ?' + ); + $stmt->execute([$id]); + $template = $stmt->fetch(); + + if (!$template) { + errorResponse('Šablona nebyla nalezena', 404); + } + + $stmt = $pdo->prepare( + 'SELECT id, scope_template_id, position, title, title_cz, content + FROM scope_template_sections WHERE scope_template_id = ? ORDER BY position' + ); + $stmt->execute([$id]); + $template['sections'] = $stmt->fetchAll(); + + successResponse($template); +} + +function handleSaveScopeTemplate(PDO $pdo): void +{ + $input = getJsonInput(); + + if (empty($input['name'])) { + errorResponse('Název šablony je povinný'); + } + + $id = isset($input['id']) ? (int)$input['id'] : null; + $sections = $input['sections'] ?? []; + + $pdo->beginTransaction(); + try { + if ($id) { + // Update template + $stmt = $pdo->prepare(' + UPDATE scope_templates SET + name = ?, + title = ?, + description = ?, + modified_at = NOW(), + sync_version = sync_version + 1 + WHERE id = ? + '); + $stmt->execute([ + $input['name'], + $input['title'] ?? '', + $input['description'] ?? '', + $id, + ]); + } else { + // Create template + $uuid = sprintf( + '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + random_int(0, 0xffff), + random_int(0, 0xffff), + random_int(0, 0xffff), + random_int(0, 0x0fff) | 0x4000, + random_int(0, 0x3fff) | 0x8000, + random_int(0, 0xffff), + random_int(0, 0xffff), + random_int(0, 0xffff) + ); + + $stmt = $pdo->prepare(' + INSERT INTO scope_templates (name, title, description, uuid, modified_at, sync_version) + VALUES (?, ?, ?, ?, NOW(), 1) + '); + $stmt->execute([ + $input['name'], + $input['title'] ?? '', + $input['description'] ?? '', + $uuid, + ]); + $id = (int)$pdo->lastInsertId(); + } + + // Delete existing sections and re-insert + $stmt = $pdo->prepare('DELETE FROM scope_template_sections WHERE scope_template_id = ?'); + $stmt->execute([$id]); + + $stmt = $pdo->prepare(' + INSERT INTO scope_template_sections + (scope_template_id, title, title_cz, content, position, uuid, modified_at, sync_version) + VALUES (?, ?, ?, ?, ?, ?, NOW(), 1) + '); + + foreach ($sections as $i => $section) { + $sectionUuid = sprintf( + '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + random_int(0, 0xffff), + random_int(0, 0xffff), + random_int(0, 0xffff), + random_int(0, 0x0fff) | 0x4000, + random_int(0, 0x3fff) | 0x8000, + random_int(0, 0xffff), + random_int(0, 0xffff), + random_int(0, 0xffff) + ); + $stmt->execute([ + $id, + $section['title'] ?? '', + $section['title_cz'] ?? '', + $section['content'] ?? '', + $i + 1, + $sectionUuid, + ]); + } + + $pdo->commit(); + + AuditLog::logCreate( + 'offers_scope_template', + $id, + ['name' => $input['name']], + "Uložena šablona rozsahu '{$input['name']}'" + ); + + successResponse(['id' => $id], 'Šablona rozsahu byla uložena'); + } catch (PDOException $e) { + $pdo->rollBack(); + throw $e; + } +} + +function handleDeleteScopeTemplate(PDO $pdo, int $id): void +{ + $stmt = $pdo->prepare('SELECT name FROM scope_templates WHERE id = ?'); + $stmt->execute([$id]); + $template = $stmt->fetch(); + + if (!$template) { + errorResponse('Šablona nebyla nalezena', 404); + } + + $pdo->beginTransaction(); + try { + // Delete sections + $stmt = $pdo->prepare('DELETE FROM scope_template_sections WHERE scope_template_id = ?'); + $stmt->execute([$id]); + + // Delete template + $stmt = $pdo->prepare('DELETE FROM scope_templates WHERE id = ?'); + $stmt->execute([$id]); + + $pdo->commit(); + + AuditLog::logDelete( + 'offers_scope_template', + $id, + ['name' => $template['name']], + "Smazána šablona rozsahu '{$template['name']}'" + ); + + successResponse(null, 'Šablona rozsahu byla smazána'); + } catch (PDOException $e) { + $pdo->rollBack(); + throw $e; + } +} diff --git a/dist/api/admin/handlers/orders-handlers.php b/dist/api/admin/handlers/orders-handlers.php new file mode 100644 index 0000000..bc483c3 --- /dev/null +++ b/dist/api/admin/handlers/orders-handlers.php @@ -0,0 +1,528 @@ + */ +function getValidTransitions(string $currentStatus): array +{ + $map = [ + 'prijata' => ['v_realizaci', 'stornovana'], + 'v_realizaci' => ['dokoncena', 'stornovana'], + 'dokoncena' => [], + 'stornovana' => [], + ]; + return $map[$currentStatus] ?? []; +} + +// --- Number generation --- + +function generateOrderNumber(PDO $pdo): string +{ + return generateSharedNumber($pdo); +} + +// --- Handlers --- + +function handleGetList(PDO $pdo): void +{ + $sortMap = [ + 'OrderNumber' => 'o.order_number', + 'order_number' => 'o.order_number', + 'CreatedAt' => 'o.created_at', + 'created_at' => 'o.created_at', + 'Status' => 'o.status', + 'status' => 'o.status', + 'Currency' => 'o.currency', + 'currency' => 'o.currency', + ]; + + $p = PaginationHelper::parseParams($sortMap); + $where = 'WHERE 1=1'; + $params = []; + + if ($p['search']) { + $where .= ' AND (o.order_number LIKE ? OR q.quotation_number LIKE ? OR q.project_code LIKE ? OR c.name LIKE ?)'; + $searchParam = "%{$p['search']}%"; + $params = [$searchParam, $searchParam, $searchParam, $searchParam]; + } + + $from = "FROM orders o + LEFT JOIN quotations q ON o.quotation_id = q.id + LEFT JOIN customers c ON o.customer_id = c.id"; + + $result = PaginationHelper::paginate( + $pdo, + "SELECT COUNT(*) {$from} {$where}", + "SELECT o.id, o.order_number, o.quotation_id, o.status, o.currency, + o.created_at, o.apply_vat, o.vat_rate, + q.quotation_number, q.project_code, + c.name as customer_name, + (SELECT COALESCE(SUM(CASE WHEN oi.is_included_in_total THEN oi.quantity * oi.unit_price ELSE 0 END), 0) + 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.invoice_number FROM invoices inv WHERE inv.order_id = o.id LIMIT 1) as invoice_number + {$from} {$where} + ORDER BY {$p['sort']} {$p['order']}", + $params, + $p + ); + + successResponse([ + 'orders' => $result['items'], + 'pagination' => $result['pagination'], + ]); +} + +function handleGetDetail(PDO $pdo, int $id): void +{ + // BLOB vynechany - stahuje se pres action=attachment + $stmt = $pdo->prepare(' + SELECT o.id, o.order_number, o.customer_order_number, o.attachment_name, + o.quotation_id, o.customer_id, o.status, o.currency, o.language, + o.vat_rate, o.apply_vat, o.exchange_rate, o.scope_title, o.scope_description, + o.notes, o.created_at, o.modified_at, + q.quotation_number, q.project_code, + c.name as customer_name + FROM orders o + LEFT JOIN quotations q ON o.quotation_id = q.id + LEFT JOIN customers c ON o.customer_id = c.id + WHERE o.id = ? + '); + $stmt->execute([$id]); + $order = $stmt->fetch(); + + if (!$order) { + errorResponse('Objednávka nebyla nalezena', 404); + } + + // Get items + $stmt = $pdo->prepare( + 'SELECT id, order_id, description, item_description, quantity, unit, + unit_price, is_included_in_total, position + FROM order_items WHERE order_id = ? ORDER BY position' + ); + $stmt->execute([$id]); + $order['items'] = $stmt->fetchAll(); + + // Get sections + $stmt = $pdo->prepare( + 'SELECT id, order_id, title, title_cz, content, position + FROM order_sections WHERE order_id = ? ORDER BY position' + ); + $stmt->execute([$id]); + $order['sections'] = $stmt->fetchAll(); + + // Get customer + if ($order['customer_id']) { + $stmt = $pdo->prepare( + 'SELECT id, name, company_id, vat_id, street, city, + postal_code, country, custom_fields + FROM customers WHERE id = ?' + ); + $stmt->execute([$order['customer_id']]); + $order['customer'] = $stmt->fetch(); + } + + // Get linked project + $stmt = $pdo->prepare('SELECT id, project_number, name, status FROM projects WHERE order_id = ?'); + $stmt->execute([$id]); + $order['project'] = $stmt->fetch() ?: null; + + // Get linked invoice + $stmt = $pdo->prepare('SELECT id, invoice_number, status FROM invoices WHERE order_id = ? LIMIT 1'); + $stmt->execute([$id]); + $order['invoice'] = $stmt->fetch() ?: null; + + // Valid transitions + $order['valid_transitions'] = getValidTransitions($order['status']); + + successResponse($order); +} + +function handleGetAttachment(PDO $pdo, int $id): void +{ + $stmt = $pdo->prepare('SELECT attachment_data, attachment_name FROM orders WHERE id = ?'); + $stmt->execute([$id]); + $row = $stmt->fetch(); + + if (!$row || !$row['attachment_data']) { + errorResponse('Příloha nebyla nalezena', 404); + } + + $finfo = new finfo(FILEINFO_MIME_TYPE); + $mime = $finfo->buffer($row['attachment_data']); + if ($mime !== 'application/pdf') { + errorResponse('Příloha není platný PDF soubor', 415); + } + + header_remove('Content-Type'); + header('Content-Type: application/pdf'); + $safeName = preg_replace('/[^a-zA-Z0-9._-]/', '_', basename($row['attachment_name'] ?: 'priloha.pdf')); + header('Content-Disposition: attachment; filename="' . $safeName . '"'); + header('Content-Length: ' . strlen($row['attachment_data'])); + echo $row['attachment_data']; + exit; +} + +function handleCreateOrder(PDO $pdo): void +{ + // Podporuje JSON i FormData (kvuli nahravani prilohy) + $contentType = $_SERVER['CONTENT_TYPE'] ?? ''; + if (str_contains($contentType, 'multipart/form-data')) { + $quotationId = (int)($_POST['quotationId'] ?? 0); + $customerOrderNumber = trim($_POST['customerOrderNumber'] ?? ''); + } else { + $input = getJsonInput(); + $quotationId = (int)($input['quotationId'] ?? 0); + $customerOrderNumber = trim($input['customerOrderNumber'] ?? ''); + } + + if (!$quotationId) { + errorResponse('ID nabídky je povinné'); + } + + if ($customerOrderNumber === '') { + errorResponse('Číslo objednávky zákazníka je povinné'); + } + if (mb_strlen($customerOrderNumber) > 100) { + errorResponse('Číslo objednávky zákazníka je příliš dlouhé (max 100 znaků)'); + } + + // Validace prilohy + $attachmentData = null; + $attachmentName = null; + if (!empty($_FILES['attachment']['tmp_name'])) { + $file = $_FILES['attachment']; + if ($file['error'] !== UPLOAD_ERR_OK) { + errorResponse('Chyba při nahrávání souboru'); + } + $finfo = new finfo(FILEINFO_MIME_TYPE); + $mime = $finfo->file($file['tmp_name']); + if ($mime !== 'application/pdf') { + errorResponse('Příloha musí být ve formátu PDF'); + } + if ($file['size'] > 10 * 1024 * 1024) { + errorResponse('Příloha nesmí být větší než 10 MB'); + } + $attachmentData = file_get_contents($file['tmp_name']); + $attachmentName = preg_replace('/[^a-zA-Z0-9._-]/', '_', basename($file['name'])); + } + + // Verify quotation exists and has no order yet + $stmt = $pdo->prepare( + 'SELECT id, quotation_number, project_code, customer_id, currency, + language, vat_rate, apply_vat, exchange_rate, order_id, + scope_title, scope_description + FROM quotations WHERE id = ?' + ); + $stmt->execute([$quotationId]); + $quotation = $stmt->fetch(); + + if (!$quotation) { + errorResponse('Nabídka nebyla nalezena', 404); + } + + if ($quotation['order_id']) { + errorResponse('Tato nabídka již má objednávku'); + } + + // Get quotation items and sections + $stmt = $pdo->prepare( + 'SELECT description, item_description, quantity, unit, + unit_price, is_included_in_total, position + FROM quotation_items WHERE quotation_id = ? ORDER BY position' + ); + $stmt->execute([$quotationId]); + $quotationItems = $stmt->fetchAll(); + + $stmt = $pdo->prepare( + 'SELECT title, title_cz, content, position + FROM scope_sections WHERE quotation_id = ? ORDER BY position' + ); + $stmt->execute([$quotationId]); + $quotationSections = $stmt->fetchAll(); + + // Lock for concurrent number generation + $locked = $pdo->query("SELECT GET_LOCK('boha_order_number', 5)")->fetchColumn(); + if (!$locked) { + errorResponse('Nepodařilo se získat zámek pro číslo objednávky, zkuste to znovu', 503); + } + + $pdo->beginTransaction(); + try { + $orderNumber = generateOrderNumber($pdo); + + $stmt = $pdo->prepare(" + INSERT INTO orders ( + order_number, customer_order_number, attachment_data, attachment_name, + quotation_id, customer_id, status, + currency, language, vat_rate, apply_vat, exchange_rate, + scope_title, scope_description, created_at, modified_at + ) VALUES (?, ?, ?, ?, ?, ?, 'prijata', ?, ?, ?, ?, ?, ?, ?, NOW(), NOW()) + "); + $stmt->execute([ + $orderNumber, + $customerOrderNumber, + $attachmentData, + $attachmentName, + $quotationId, + $quotation['customer_id'], + $quotation['currency'] ?? 'EUR', + $quotation['language'] ?? 'EN', + $quotation['vat_rate'] ?? 0, + $quotation['apply_vat'] ?? 0, + $quotation['exchange_rate'], + $quotation['scope_title'] ?? '', + $quotation['scope_description'] ?? '', + ]); + $orderId = (int)$pdo->lastInsertId(); + + // Copy items + if (!empty($quotationItems)) { + $itemStmt = $pdo->prepare(' + INSERT INTO order_items ( + order_id, description, item_description, quantity, unit, + unit_price, is_included_in_total, position, modified_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW()) + '); + foreach ($quotationItems as $item) { + $itemStmt->execute([ + $orderId, + $item['description'] ?? '', + $item['item_description'] ?? '', + $item['quantity'] ?? 1, + $item['unit'] ?? '', + $item['unit_price'] ?? 0, + $item['is_included_in_total'] ?? 1, + $item['position'] ?? 0, + ]); + } + } + + // Copy sections + if (!empty($quotationSections)) { + $sectionStmt = $pdo->prepare(' + INSERT INTO order_sections ( + order_id, title, title_cz, content, position, modified_at + ) VALUES (?, ?, ?, ?, ?, NOW()) + '); + foreach ($quotationSections as $section) { + $sectionStmt->execute([ + $orderId, + $section['title'] ?? '', + $section['title_cz'] ?? '', + $section['content'] ?? '', + $section['position'] ?? 0, + ]); + } + } + + // Create project with same number + $projectName = $quotation['project_code'] ?: ($quotation['customer_name'] ?? 'Projekt ' . $orderNumber); + // Need customer name + if (!$quotation['project_code'] && $quotation['customer_id']) { + $custStmt = $pdo->prepare('SELECT name FROM customers WHERE id = ?'); + $custStmt->execute([$quotation['customer_id']]); + $custName = $custStmt->fetchColumn(); + if ($custName) { + $projectName = $custName; + } + } + + $stmt = $pdo->prepare(" + INSERT INTO projects ( + project_number, name, customer_id, quotation_id, order_id, + status, start_date, created_at, modified_at + ) VALUES (?, ?, ?, ?, ?, 'aktivni', CURDATE(), NOW(), NOW()) + "); + $stmt->execute([ + $orderNumber, + $projectName, + $quotation['customer_id'], + $quotationId, + $orderId, + ]); + $projectId = (int)$pdo->lastInsertId(); + + // Update quotation with back-reference + $stmt = $pdo->prepare('UPDATE quotations SET order_id = ?, modified_at = NOW() WHERE id = ?'); + $stmt->execute([$orderId, $quotationId]); + + + $pdo->commit(); + $pdo->query("SELECT RELEASE_LOCK('boha_order_number')"); + + AuditLog::logCreate('orders_order', $orderId, [ + 'order_number' => $orderNumber, + 'quotation_number' => $quotation['quotation_number'], + 'project_id' => $projectId, + ], "Vytvořena objednávka '$orderNumber' z nabídky '{$quotation['quotation_number']}'"); + + successResponse([ + 'order_id' => $orderId, + 'order_number' => $orderNumber, + 'project_id' => $projectId, + 'project_number' => $orderNumber, + ], 'Objednávka byla vytvořena'); + } catch (PDOException $e) { + $pdo->rollBack(); + $pdo->query("SELECT RELEASE_LOCK('boha_order_number')"); + throw $e; + } +} + +function handleUpdateOrder(PDO $pdo, int $id): void +{ + $stmt = $pdo->prepare( + 'SELECT id, order_number, status, notes FROM orders WHERE id = ?' + ); + $stmt->execute([$id]); + $order = $stmt->fetch(); + + if (!$order) { + errorResponse('Objednávka nebyla nalezena', 404); + } + + $input = getJsonInput(); + $newStatus = $input['status'] ?? null; + $notes = $input['notes'] ?? null; + $newOrderNumber = isset($input['order_number']) ? trim($input['order_number']) : null; + + // Delkove limity + if ($notes !== null && mb_strlen($notes) > 5000) { + errorResponse('Poznámky jsou příliš dlouhé (max 5000 znaků)'); + } + if ($newOrderNumber !== null && mb_strlen($newOrderNumber) > 50) { + errorResponse('Číslo objednávky je příliš dlouhé (max 50 znaků)'); + } + + // Validate status transition + if ($newStatus && $newStatus !== $order['status']) { + $valid = getValidTransitions($order['status']); + if (!in_array($newStatus, $valid)) { + errorResponse("Neplatný přechod stavu z '{$order['status']}' na '$newStatus'"); + } + } + + // Validate order number uniqueness + if ($newOrderNumber !== null && $newOrderNumber !== $order['order_number']) { + if (empty($newOrderNumber)) { + errorResponse('Číslo objednávky nesmí být prázdné'); + } + $stmt = $pdo->prepare('SELECT id FROM orders WHERE order_number = ? AND id != ?'); + $stmt->execute([$newOrderNumber, $id]); + if ($stmt->fetch()) { + errorResponse('Toto číslo objednávky již existuje'); + } + } + + $pdo->beginTransaction(); + try { + $updates = []; + $params = []; + + if ($newOrderNumber !== null && $newOrderNumber !== $order['order_number']) { + $updates[] = 'order_number = ?'; + $params[] = $newOrderNumber; + + // Sync project number + $stmt = $pdo->prepare('UPDATE projects SET project_number = ?, modified_at = NOW() WHERE order_id = ?'); + $stmt->execute([$newOrderNumber, $id]); + } + if ($newStatus !== null) { + $updates[] = 'status = ?'; + $params[] = $newStatus; + } + if ($notes !== null) { + $updates[] = 'notes = ?'; + $params[] = $notes; + } + + if (!empty($updates)) { + $updates[] = 'modified_at = NOW()'; + $params[] = $id; + $sql = 'UPDATE orders SET ' . implode(', ', $updates) . ' WHERE id = ?'; + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + } + + // Sync project status with order status + if ($newStatus && $newStatus !== $order['status']) { + $projectStatus = null; + if ($newStatus === 'stornovana') { + $projectStatus = 'zruseny'; + } elseif ($newStatus === 'dokoncena') { + $projectStatus = 'dokonceny'; + } elseif ($newStatus === 'v_realizaci') { + $projectStatus = 'aktivni'; + } + + if ($projectStatus) { + $stmt = $pdo->prepare('UPDATE projects SET status = ?, modified_at = NOW() WHERE order_id = ?'); + $stmt->execute([$projectStatus, $id]); + } + } + + $pdo->commit(); + + AuditLog::logUpdate( + 'orders_order', + $id, + ['status' => $order['status'], 'notes' => $order['notes']], + ['status' => $newStatus ?? $order['status'], 'notes' => $notes ?? $order['notes']], + "Upravena objednávka '{$order['order_number']}'" + ); + + successResponse(null, 'Objednávka byla aktualizována'); + } catch (PDOException $e) { + $pdo->rollBack(); + throw $e; + } +} + +function handleDeleteOrder(PDO $pdo, int $id): void +{ + $stmt = $pdo->prepare( + 'SELECT id, order_number, quotation_id FROM orders WHERE id = ?' + ); + $stmt->execute([$id]); + $order = $stmt->fetch(); + + if (!$order) { + errorResponse('Objednávka nebyla nalezena', 404); + } + + $pdo->beginTransaction(); + try { + // Delete project linked to this order + $stmt = $pdo->prepare('DELETE FROM projects WHERE order_id = ?'); + $stmt->execute([$id]); + + // Delete order items and sections + $stmt = $pdo->prepare('DELETE FROM order_items WHERE order_id = ?'); + $stmt->execute([$id]); + + $stmt = $pdo->prepare('DELETE FROM order_sections WHERE order_id = ?'); + $stmt->execute([$id]); + + // Delete order + $stmt = $pdo->prepare('DELETE FROM orders WHERE id = ?'); + $stmt->execute([$id]); + + // Remove back-reference from quotation + $stmt = $pdo->prepare('UPDATE quotations SET order_id = NULL, modified_at = NOW() WHERE order_id = ?'); + $stmt->execute([$id]); + + $pdo->commit(); + + AuditLog::logDelete('orders_order', $id, [ + 'order_number' => $order['order_number'], + 'quotation_id' => $order['quotation_id'], + ], "Smazána objednávka '{$order['order_number']}'"); + + successResponse(null, 'Objednávka byla smazána'); + } catch (PDOException $e) { + $pdo->rollBack(); + throw $e; + } +} diff --git a/dist/api/admin/handlers/projects-handlers.php b/dist/api/admin/handlers/projects-handlers.php new file mode 100644 index 0000000..4d986ec --- /dev/null +++ b/dist/api/admin/handlers/projects-handlers.php @@ -0,0 +1,406 @@ + $number]); +} + +function handleCreateProject(PDO $pdo): void +{ + $input = getJsonInput(); + + $name = trim($input['name'] ?? ''); + if (!$name) { + errorResponse('Název projektu je povinný'); + } + if (mb_strlen($name) > 255) { + errorResponse('Název projektu je příliš dlouhý (max 255 znaků)'); + } + + $customerId = isset($input['customer_id']) ? (int)$input['customer_id'] : null; + if (!$customerId) { + errorResponse('Zákazník je povinný'); + } + + // Verify customer exists + $stmt = $pdo->prepare('SELECT id FROM customers WHERE id = ?'); + $stmt->execute([$customerId]); + if (!$stmt->fetch()) { + errorResponse('Zákazník nebyl nalezen', 404); + } + + $startDate = $input['start_date'] ?? date('Y-m-d'); + if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate)) { + errorResponse('Neplatný formát data zahájení'); + } + + $projectNumber = trim($input['project_number'] ?? ''); + if ($projectNumber && mb_strlen($projectNumber) > 50) { + errorResponse('Číslo projektu je příliš dlouhé (max 50 znaků)'); + } + + // Lock for concurrent number generation + $locked = $pdo->query("SELECT GET_LOCK('boha_project_number', 5)")->fetchColumn(); + if (!$locked) { + errorResponse('Nepodařilo se získat zámek pro číslo projektu, zkuste to znovu', 503); + } + + $pdo->beginTransaction(); + try { + // Generate or validate number + if (!$projectNumber) { + $projectNumber = generateProjectNumber($pdo); + } else { + // Validate uniqueness against both tables + $stmt = $pdo->prepare('SELECT id FROM orders WHERE order_number = ?'); + $stmt->execute([$projectNumber]); + if ($stmt->fetch()) { + $pdo->rollBack(); + $pdo->query("SELECT RELEASE_LOCK('boha_project_number')"); + errorResponse('Číslo projektu je již použito jako číslo objednávky'); + } + + $stmt = $pdo->prepare('SELECT id FROM projects WHERE project_number = ?'); + $stmt->execute([$projectNumber]); + if ($stmt->fetch()) { + $pdo->rollBack(); + $pdo->query("SELECT RELEASE_LOCK('boha_project_number')"); + errorResponse('Číslo projektu je již použito'); + } + } + + $stmt = $pdo->prepare(" + INSERT INTO projects ( + project_number, name, customer_id, + status, start_date, created_at, modified_at + ) VALUES (?, ?, ?, 'aktivni', ?, NOW(), NOW()) + "); + $stmt->execute([ + $projectNumber, + $name, + $customerId, + $startDate, + ]); + $projectId = (int)$pdo->lastInsertId(); + + + $pdo->commit(); + $pdo->query("SELECT RELEASE_LOCK('boha_project_number')"); + + AuditLog::logCreate('projects_project', $projectId, [ + 'project_number' => $projectNumber, + 'name' => $name, + 'customer_id' => $customerId, + ], "Ručně vytvořen projekt '$projectNumber'"); + + successResponse([ + 'project_id' => $projectId, + 'project_number' => $projectNumber, + ], 'Projekt byl vytvořen'); + } catch (PDOException $e) { + $pdo->rollBack(); + $pdo->query("SELECT RELEASE_LOCK('boha_project_number')"); + throw $e; + } +} + +function handleDeleteProject(PDO $pdo, int $id): void +{ + $stmt = $pdo->prepare( + 'SELECT id, project_number, name, order_id, status FROM projects WHERE id = ?' + ); + $stmt->execute([$id]); + $project = $stmt->fetch(); + + if (!$project) { + errorResponse('Projekt nebyl nalezen', 404); + } + + // Only manually created projects (without order_id) can be deleted + if (!empty($project['order_id'])) { + errorResponse('Projekt propojený s objednávkou nelze smazat. Smažte objednávku.', 400); + } + + $pdo->beginTransaction(); + try { + // Delete project notes + $stmt = $pdo->prepare('DELETE FROM project_notes WHERE project_id = ?'); + $stmt->execute([$id]); + + // Delete project + $stmt = $pdo->prepare('DELETE FROM projects WHERE id = ?'); + $stmt->execute([$id]); + + $pdo->commit(); + + AuditLog::logUpdate( + 'projects_project', + $id, + ['status' => $project['status']], + ['status' => 'deleted'], + "Smazán ruční projekt '{$project['project_number']}'" + ); + + successResponse(null, 'Projekt byl smazán'); + } catch (PDOException $e) { + $pdo->rollBack(); + throw $e; + } +} + +function handleGetList(PDO $pdo): void +{ + $sortMap = [ + 'ProjectNumber' => 'p.project_number', + 'project_number' => 'p.project_number', + 'Name' => 'p.name', + 'name' => 'p.name', + 'Status' => 'p.status', + 'status' => 'p.status', + 'StartDate' => 'p.start_date', + 'start_date' => 'p.start_date', + 'EndDate' => 'p.end_date', + 'end_date' => 'p.end_date', + 'CreatedAt' => 'p.created_at', + 'created_at' => 'p.created_at', + ]; + + $p = PaginationHelper::parseParams($sortMap); + $where = 'WHERE 1=1'; + $params = []; + + if ($p['search']) { + $where .= ' AND (p.project_number LIKE ? OR p.name LIKE ? OR c.name LIKE ?)'; + $searchParam = "%{$p['search']}%"; + $params = [$searchParam, $searchParam, $searchParam]; + } + + $from = "FROM projects p + LEFT JOIN customers c ON p.customer_id = c.id + LEFT JOIN orders o ON p.order_id = o.id"; + + $result = PaginationHelper::paginate( + $pdo, + "SELECT COUNT(*) {$from} {$where}", + "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, + c.name as customer_name, + o.order_number + {$from} {$where} + ORDER BY {$p['sort']} {$p['order']}", + $params, + $p + ); + + successResponse([ + 'projects' => $result['items'], + 'pagination' => $result['pagination'], + ]); +} + +function handleGetDetail(PDO $pdo, int $id): void +{ + $stmt = $pdo->prepare(' + SELECT p.id, p.project_number, p.name, p.customer_id, + p.quotation_id, p.order_id, p.status, + p.start_date, p.end_date, p.notes, + p.created_at, p.modified_at, + c.name as customer_name, + o.order_number, o.status as order_status, + q.quotation_number + FROM projects p + LEFT JOIN customers c ON p.customer_id = c.id + LEFT JOIN orders o ON p.order_id = o.id + LEFT JOIN quotations q ON p.quotation_id = q.id + WHERE p.id = ? + '); + $stmt->execute([$id]); + $project = $stmt->fetch(); + + if (!$project) { + errorResponse('Projekt nebyl nalezen', 404); + } + + successResponse($project); +} + +function handleUpdateProject(PDO $pdo, int $id): void +{ + $stmt = $pdo->prepare( + 'SELECT id, project_number, name, status, start_date, end_date, notes + FROM projects WHERE id = ?' + ); + $stmt->execute([$id]); + $project = $stmt->fetch(); + + if (!$project) { + errorResponse('Projekt nebyl nalezen', 404); + } + + $input = getJsonInput(); + + // Validace statusu + if (isset($input['status'])) { + $validStatuses = ['aktivni', 'dokonceny', 'zruseny']; + if (!in_array($input['status'], $validStatuses)) { + errorResponse('Neplatný stav projektu'); + } + } + + // Validace dat + if ( + isset($input['start_date']) + && $input['start_date'] !== null // @phpstan-ignore notIdentical.alwaysTrue + && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $input['start_date']) + ) { + errorResponse('Neplatný formát data zahájení'); + } + if ( + isset($input['end_date']) + && $input['end_date'] !== null // @phpstan-ignore notIdentical.alwaysTrue + && $input['end_date'] !== '' + && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $input['end_date']) + ) { + errorResponse('Neplatný formát data ukončení'); + } + + // Delkove limity + $name = $input['name'] ?? $project['name']; + if (mb_strlen($name) > 255) { + errorResponse('Název projektu je příliš dlouhý (max 255 znaků)'); + } + $notes = $input['notes'] ?? $project['notes']; + if ($notes !== null && mb_strlen($notes) > 5000) { + errorResponse('Poznámky jsou příliš dlouhé (max 5000 znaků)'); + } + + $pdo->beginTransaction(); + try { + $stmt = $pdo->prepare(' + UPDATE projects SET + name = ?, + status = ?, + start_date = ?, + end_date = ?, + notes = ?, + modified_at = NOW() + WHERE id = ? + '); + $stmt->execute([ + $name, + $input['status'] ?? $project['status'], + $input['start_date'] ?? $project['start_date'], + $input['end_date'] ?? $project['end_date'], + $notes, + $id, + ]); + + $pdo->commit(); + + AuditLog::logUpdate( + 'projects_project', + $id, + ['name' => $project['name'], 'status' => $project['status']], + ['name' => $input['name'] ?? $project['name'], 'status' => $input['status'] ?? $project['status']], + "Upraven projekt '{$project['project_number']}'" + ); + + successResponse(null, 'Projekt byl aktualizován'); + } catch (PDOException $e) { + $pdo->rollBack(); + throw $e; + } +} + +function handleGetNotes(PDO $pdo, int $projectId): void +{ + // Verify project exists + $stmt = $pdo->prepare('SELECT id FROM projects WHERE id = ?'); + $stmt->execute([$projectId]); + if (!$stmt->fetch()) { + errorResponse('Projekt nebyl nalezen', 404); + } + + $stmt = $pdo->prepare(' + SELECT id, project_id, user_id, user_name, content, created_at + FROM project_notes + WHERE project_id = ? + ORDER BY created_at DESC + '); + $stmt->execute([$projectId]); + $notes = $stmt->fetchAll(); + + successResponse(['notes' => $notes]); +} + +/** @param array $authData */ +function handleAddNote(PDO $pdo, int $projectId, array $authData): void +{ + // Verify project exists + $stmt = $pdo->prepare('SELECT id FROM projects WHERE id = ?'); + $stmt->execute([$projectId]); + if (!$stmt->fetch()) { + errorResponse('Projekt nebyl nalezen', 404); + } + + $input = getJsonInput(); + $content = trim($input['content'] ?? ''); + + if (!$content) { + errorResponse('Text poznámky je povinný'); + } + + if (mb_strlen($content) > 5000) { + errorResponse('Poznámka je příliš dlouhá (max 5000 znaků)'); + } + + $userName = $authData['user']['full_name'] ?? $authData['user']['username'] ?? 'Neznámý'; + + $stmt = $pdo->prepare(' + INSERT INTO project_notes (project_id, user_id, user_name, content, created_at) + VALUES (?, ?, ?, ?, NOW()) + '); + $stmt->execute([$projectId, $authData['user_id'], $userName, $content]); + + $noteId = (int)$pdo->lastInsertId(); + + // Fetch the new note + $stmt = $pdo->prepare( + 'SELECT id, project_id, user_id, user_name, content, created_at FROM project_notes WHERE id = ?' + ); + $stmt->execute([$noteId]); + $note = $stmt->fetch(); + + successResponse(['note' => $note], 'Poznámka byla přidána'); +} + +/** @param array $authData */ +function handleDeleteNote(PDO $pdo, int $noteId, array $authData): void +{ + // Only admins can delete notes + $isAdmin = $authData['user']['is_admin'] ?? false; + if (!$isAdmin) { + errorResponse('Pouze administrátoři mohou mazat poznámky', 403); + } + + $stmt = $pdo->prepare('SELECT id, project_id, content FROM project_notes WHERE id = ?'); + $stmt->execute([$noteId]); + $note = $stmt->fetch(); + + if (!$note) { + errorResponse('Poznámka nebyla nalezena', 404); + } + + $stmt = $pdo->prepare('DELETE FROM project_notes WHERE id = ?'); + $stmt->execute([$noteId]); + + successResponse(null, 'Poznámka byla smazána'); +} diff --git a/dist/api/admin/handlers/received-invoices-handlers.php b/dist/api/admin/handlers/received-invoices-handlers.php new file mode 100644 index 0000000..c142d7d --- /dev/null +++ b/dist/api/admin/handlers/received-invoices-handlers.php @@ -0,0 +1,511 @@ + */ +function getAllowedMimes(): array +{ + return ['application/pdf', 'image/jpeg', 'image/png']; +} + +// --- Stats --- + +function handleGetStats(PDO $pdo): void +{ + $month = max(1, min(12, (int) ($_GET['month'] ?? (int) date('n')))); + $year = max(2020, min(2099, (int) ($_GET['year'] ?? (int) date('Y')))); + + $monthStart = sprintf('%04d-%02d-01', $year, $month); + $monthEnd = date('Y-m-t', strtotime($monthStart)); + + // Celkem v měsíci (issue_date) + $stmt = $pdo->prepare(' + SELECT currency, SUM(amount) as total, SUM(vat_amount) as vat_total, COUNT(*) as cnt + FROM received_invoices + WHERE issue_date BETWEEN ? AND ? + GROUP BY currency + '); + $stmt->execute([$monthStart, $monthEnd]); + $monthRows = $stmt->fetchAll(); + + $totalAmounts = []; + $vatAmounts = []; + $czkItems = []; + $vatCzkItems = []; + $monthCount = 0; + + foreach ($monthRows as $r) { + $totalAmounts[$r['currency']] = round((float) $r['total'], 2); + $vatAmounts[$r['currency']] = round((float) $r['vat_total'], 2); + $monthCount += (int) $r['cnt']; + $czkItems[] = [ + 'amount' => round((float) $r['total'], 2), + 'currency' => $r['currency'], + 'date' => $monthStart, + ]; + $vatCzkItems[] = [ + 'amount' => round((float) $r['vat_total'], 2), + 'currency' => $r['currency'], + 'date' => $monthStart, + ]; + } + + $totalArr = []; + foreach ($totalAmounts as $cur => $amt) { + $totalArr[] = ['amount' => $amt, 'currency' => $cur]; + } + $vatArr = []; + foreach ($vatAmounts as $cur => $amt) { + $vatArr[] = ['amount' => $amt, 'currency' => $cur]; + } + + // Neuhrazeno celkově + $stmt = $pdo->prepare(' + SELECT currency, SUM(amount) as total, COUNT(*) as cnt + FROM received_invoices WHERE status = ? + GROUP BY currency + '); + $stmt->execute(['unpaid']); + $unpaidRows = $stmt->fetchAll(); + + $unpaidAmounts = []; + $unpaidCzkItems = []; + $unpaidCount = 0; + foreach ($unpaidRows as $r) { + $unpaidAmounts[] = ['amount' => round((float) $r['total'], 2), 'currency' => $r['currency']]; + $unpaidCount += (int) $r['cnt']; + $unpaidCzkItems[] = [ + 'amount' => round((float) $r['total'], 2), + 'currency' => $r['currency'], + 'date' => date('Y-m-d'), + ]; + } + + $cnb = CnbRates::getInstance(); + + successResponse([ + 'total_month' => $totalArr, + 'total_month_czk' => $cnb->sumToCzk($czkItems), + 'vat_month' => $vatArr, + 'vat_month_czk' => $cnb->sumToCzk($vatCzkItems), + 'unpaid' => $unpaidAmounts, + 'unpaid_czk' => $cnb->sumToCzk($unpaidCzkItems), + 'unpaid_count' => $unpaidCount, + 'month_count' => $monthCount, + 'month' => $month, + 'year' => $year, + ]); +} + +// --- List --- + +function handleGetList(PDO $pdo): void +{ + $month = max(1, min(12, (int) ($_GET['month'] ?? (int) date('n')))); + $year = max(2020, min(2099, (int) ($_GET['year'] ?? (int) date('Y')))); + $search = trim($_GET['search'] ?? ''); + $sort = $_GET['sort'] ?? 'created_at'; + $order = strtoupper($_GET['order'] ?? 'DESC') === 'ASC' ? 'ASC' : 'DESC'; + + $sortMap = [ + 'supplier_name' => 'supplier_name', + 'invoice_number' => 'invoice_number', + 'status' => 'status', + 'issue_date' => 'issue_date', + 'due_date' => 'due_date', + 'amount' => 'amount', + 'created_at' => 'created_at', + ]; + if (!isset($sortMap[$sort])) { + errorResponse('Neplatný parametr řazení', 400); + } + $sortCol = $sortMap[$sort]; + + $where = 'WHERE month = ? AND year = ?'; + $params = [$month, $year]; + + if ($search) { + $search = mb_substr($search, 0, 100); + $where .= ' AND (supplier_name LIKE ? OR invoice_number LIKE ?)'; + $searchParam = "%{$search}%"; + $params[] = $searchParam; + $params[] = $searchParam; + } + + $sql = " + SELECT id, supplier_name, invoice_number, description, + amount, currency, vat_rate, vat_amount, + issue_date, due_date, paid_date, status, + file_name, file_mime, file_size, notes, + created_at, modified_at + FROM received_invoices + $where + ORDER BY $sortCol $order + "; + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + $invoices = $stmt->fetchAll(); + + successResponse(['invoices' => $invoices]); +} + +// --- Detail --- + +function handleGetDetail(PDO $pdo, int $id): void +{ + $stmt = $pdo->prepare(' + SELECT id, supplier_name, invoice_number, description, + amount, currency, vat_rate, vat_amount, + issue_date, due_date, paid_date, status, + file_name, file_mime, file_size, notes, + uploaded_by, created_at, modified_at + FROM received_invoices WHERE id = ? + '); + $stmt->execute([$id]); + $invoice = $stmt->fetch(); + + if (!$invoice) { + errorResponse('Přijatá faktura nebyla nalezena', 404); + } + + successResponse($invoice); +} + +// --- File streaming --- + +function handleGetFile(PDO $pdo, int $id): void +{ + $stmt = $pdo->prepare('SELECT file_data, file_name, file_mime, file_size FROM received_invoices WHERE id = ?'); + $stmt->execute([$id]); + $row = $stmt->fetch(); + + if (!$row || !$row['file_data']) { + errorResponse('Soubor nebyl nalezen', 404); + } + + $safeFilename = preg_replace('/[^a-zA-Z0-9._-]/', '_', basename($row['file_name'])); + header('Content-Type: ' . $row['file_mime']); + header('Content-Disposition: inline; filename="' . $safeFilename . '"'); + header('Content-Length: ' . $row['file_size']); + header_remove('X-Content-Type-Options'); + echo $row['file_data']; + exit(); +} + +// --- Bulk upload --- + +/** @param array $authData */ +function handleBulkUpload(PDO $pdo, array $authData): void +{ + $invoicesJson = $_POST['invoices'] ?? '[]'; + $invoicesMeta = json_decode($invoicesJson, true); + + if (!is_array($invoicesMeta)) { + errorResponse('Neplatná metadata'); + } + if (count($invoicesMeta) === 0) { + errorResponse('Žádné faktury k nahrání'); + } + if (count($invoicesMeta) > 20) { + errorResponse('Maximálně 20 faktur najednou'); + } + + $files = $_FILES['files'] ?? []; + $fileCount = is_array($files['tmp_name'] ?? null) ? count($files['tmp_name']) : 0; + + if ($fileCount !== count($invoicesMeta)) { + errorResponse('Počet souborů neodpovídá počtu metadat'); + } + + $allowedMimes = getAllowedMimes(); + $validCurrencies = ['CZK', 'EUR', 'USD', 'GBP']; + $validVatRates = [0, 10, 12, 15, 21]; + + $pdo->beginTransaction(); + try { + $created = []; + $stmt = $pdo->prepare(' + INSERT INTO received_invoices ( + month, year, supplier_name, invoice_number, description, + amount, currency, vat_rate, vat_amount, + issue_date, due_date, status, + file_data, file_name, file_mime, file_size, + notes, uploaded_by + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + '); + + for ($i = 0; $i < $fileCount; $i++) { + $meta = $invoicesMeta[$i]; + $tmpName = $files['tmp_name'][$i]; + $fileError = $files['error'][$i]; + $fileSize = $files['size'][$i]; + $fileName = $files['name'][$i]; + + if ($fileError !== UPLOAD_ERR_OK) { + errorResponse("Chyba při nahrávání souboru #" . ($i + 1)); + } + if ($fileSize > 10 * 1024 * 1024) { + errorResponse("Soubor #" . ($i + 1) . " je větší než 10 MB"); + } + + $finfo = new finfo(FILEINFO_MIME_TYPE); + $mime = $finfo->file($tmpName); + if (!in_array($mime, $allowedMimes)) { + errorResponse("Soubor #" . ($i + 1) . ": nepodporovaný formát (povoleno: PDF, JPEG, PNG)"); + } + + $supplierName = trim($meta['supplier_name'] ?? ''); + if ($supplierName === '') { + errorResponse("Faktura #" . ($i + 1) . ": dodavatel je povinný"); + } + if (mb_strlen($supplierName) > 255) { + errorResponse("Faktura #" . ($i + 1) . ": název dodavatele je příliš dlouhý"); + } + + $amount = (float) ($meta['amount'] ?? 0); + if ($amount <= 0) { + errorResponse("Faktura #" . ($i + 1) . ": částka musí být větší než 0"); + } + + $currency = trim($meta['currency'] ?? 'CZK'); + if (!in_array($currency, $validCurrencies)) { + errorResponse("Faktura #" . ($i + 1) . ": neplatná měna"); + } + + $vatRate = (float) ($meta['vat_rate'] ?? 21); + if (!in_array((int) $vatRate, $validVatRates)) { + errorResponse("Faktura #" . ($i + 1) . ": neplatná sazba DPH"); + } + + $vatAmount = round($amount * $vatRate / 100, 2); + $invoiceNumber = trim($meta['invoice_number'] ?? ''); + $description = trim($meta['description'] ?? ''); + $issueDate = trim($meta['issue_date'] ?? ''); + $dueDate = trim($meta['due_date'] ?? ''); + $notes = trim($meta['notes'] ?? ''); + + // Validace dat + if ($issueDate && (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $issueDate) || !strtotime($issueDate))) { + errorResponse("Faktura #" . ($i + 1) . ": neplatný formát data vystavení"); + } + if ($dueDate && (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dueDate) || !strtotime($dueDate))) { + errorResponse("Faktura #" . ($i + 1) . ": neplatný formát data splatnosti"); + } + + // Délkové limity + if (mb_strlen($invoiceNumber) > 100) { + errorResponse("Faktura #" . ($i + 1) . ": číslo faktury je příliš dlouhé"); + } + if (mb_strlen($description) > 500) { + errorResponse("Faktura #" . ($i + 1) . ": popis je příliš dlouhý"); + } + if (mb_strlen($notes) > 5000) { + errorResponse("Faktura #" . ($i + 1) . ": poznámka je příliš dlouhá"); + } + + // Určit month/year z issue_date nebo aktuální + if ($issueDate) { + $dt = new DateTime($issueDate); + $month = (int) $dt->format('n'); + $year = (int) $dt->format('Y'); + } else { + $month = (int) date('n'); + $year = (int) date('Y'); + } + + $fileData = file_get_contents($tmpName); + $safeName = preg_replace('/[^a-zA-Z0-9._-]/', '_', basename($fileName)); + + $stmt->execute([ + $month, + $year, + $supplierName, + $invoiceNumber ?: null, + $description ?: null, + $amount, + $currency, + $vatRate, + $vatAmount, + $issueDate ?: null, + $dueDate ?: null, + 'unpaid', + $fileData, + $safeName, + $mime, + $fileSize, + $notes ?: null, + $authData['user_id'], + ]); + + $created[] = (int) $pdo->lastInsertId(); + } + + $pdo->commit(); + + AuditLog::logCreate('received_invoices', $created[0], [ + 'count' => count($created), + 'ids' => $created, + ], 'Nahráno ' . count($created) . ' přijatých faktur'); + + successResponse(['ids' => $created], 'Faktury byly nahrány'); + } catch (PDOException $e) { + $pdo->rollBack(); + throw $e; + } +} + +// --- Update --- + +function handleUpdateReceivedInvoice(PDO $pdo, int $id): void +{ + $stmt = $pdo->prepare( + 'SELECT id, supplier_name, invoice_number, description, + amount, currency, vat_rate, vat_amount, + issue_date, due_date, paid_date, status, notes + FROM received_invoices WHERE id = ?' + ); + $stmt->execute([$id]); + $invoice = $stmt->fetch(); + + if (!$invoice) { + errorResponse('Přijatá faktura nebyla nalezena', 404); + } + + $input = getJsonInput(); + + $updates = []; + $params = []; + + $stringFields = [ + 'supplier_name' => 255, + 'invoice_number' => 100, + 'description' => 500, + 'notes' => 5000, + ]; + foreach ($stringFields as $field => $maxLen) { + if (array_key_exists($field, $input)) { + $val = trim((string) $input[$field]); + if ($field === 'supplier_name' && $val === '') { + errorResponse('Dodavatel je povinný'); + } + if (mb_strlen($val) > $maxLen) { + errorResponse("Pole $field je příliš dlouhé (max $maxLen znaků)"); + } + $updates[] = "$field = ?"; + $params[] = $val ?: null; + } + } + + if (array_key_exists('amount', $input)) { + $amount = (float) $input['amount']; + if ($amount <= 0) { + errorResponse('Částka musí být větší než 0'); + } + $updates[] = 'amount = ?'; + $params[] = $amount; + } + + if (array_key_exists('currency', $input)) { + if (!in_array($input['currency'], ['CZK', 'EUR', 'USD', 'GBP'])) { + errorResponse('Neplatná měna'); + } + $updates[] = 'currency = ?'; + $params[] = $input['currency']; + } + + if (array_key_exists('vat_rate', $input)) { + $vatRate = (float) $input['vat_rate']; + if (!in_array((int) $vatRate, [0, 10, 12, 15, 21])) { + errorResponse('Neplatná sazba DPH'); + } + $updates[] = 'vat_rate = ?'; + $params[] = $vatRate; + + $amount = (float) ($input['amount'] ?? $invoice['amount']); + $updates[] = 'vat_amount = ?'; + $params[] = round($amount * $vatRate / 100, 2); + } elseif (array_key_exists('amount', $input)) { + $vatRate = (float) ($input['vat_rate'] ?? $invoice['vat_rate']); + $updates[] = 'vat_amount = ?'; + $params[] = round((float) $input['amount'] * $vatRate / 100, 2); + } + + foreach (['issue_date', 'due_date'] as $dateField) { + if (array_key_exists($dateField, $input)) { + $val = trim((string) $input[$dateField]); + if ($val && (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $val) || !strtotime($val))) { + errorResponse("Neplatný formát data: $dateField"); + } + $updates[] = "$dateField = ?"; + $params[] = $val ?: null; + } + } + + // Aktualizace month/year pokud se změní issue_date + if (array_key_exists('issue_date', $input) && $input['issue_date']) { + $dt = new DateTime($input['issue_date']); + $updates[] = 'month = ?'; + $params[] = (int) $dt->format('n'); + $updates[] = 'year = ?'; + $params[] = (int) $dt->format('Y'); + } + + // Změna stavu - pouze unpaid -> paid (jednosmerny prechod) + if (array_key_exists('status', $input)) { + $newStatus = $input['status']; + if (!in_array($newStatus, ['unpaid', 'paid'])) { + errorResponse('Neplatný stav'); + } + if ($invoice['status'] === 'paid' && $newStatus !== 'paid') { + errorResponse('Uhrazenou fakturu nelze vrátit do stavu neuhrazená'); + } + if ($newStatus !== $invoice['status']) { + $updates[] = 'status = ?'; + $params[] = $newStatus; + if ($newStatus === 'paid') { + $updates[] = 'paid_date = CURDATE()'; + } + } + } + + if (empty($updates)) { + errorResponse('Žádné změny k uložení'); + } + + $updates[] = 'modified_at = NOW()'; + $params[] = $id; + $sql = 'UPDATE received_invoices SET ' . implode(', ', $updates) . ' WHERE id = ?'; + $pdo->prepare($sql)->execute($params); + + AuditLog::logUpdate( + 'received_invoices', + $id, + ['status' => $invoice['status']], + ['status' => $input['status'] ?? $invoice['status']], + "Aktualizována přijatá faktura #{$id}" + ); + + successResponse(null, 'Faktura byla aktualizována'); +} + +// --- Delete --- + +function handleDeleteReceivedInvoice(PDO $pdo, int $id): void +{ + $stmt = $pdo->prepare('SELECT id, supplier_name, invoice_number FROM received_invoices WHERE id = ?'); + $stmt->execute([$id]); + $invoice = $stmt->fetch(); + + if (!$invoice) { + errorResponse('Přijatá faktura nebyla nalezena', 404); + } + + $pdo->prepare('DELETE FROM received_invoices WHERE id = ?')->execute([$id]); + + AuditLog::logDelete('received_invoices', $id, [ + 'supplier_name' => $invoice['supplier_name'], + 'invoice_number' => $invoice['invoice_number'], + ], "Smazána přijatá faktura #{$id}"); + + successResponse(null, 'Faktura byla smazána'); +} diff --git a/dist/api/admin/handlers/roles-handlers.php b/dist/api/admin/handlers/roles-handlers.php new file mode 100644 index 0000000..b9b8188 --- /dev/null +++ b/dist/api/admin/handlers/roles-handlers.php @@ -0,0 +1,242 @@ +query(' + SELECT r.id, r.name, r.display_name, r.description, r.created_at, + COUNT(u.id) as user_count + FROM roles r + LEFT JOIN users u ON u.role_id = r.id + GROUP BY r.id + ORDER BY r.id + '); + $roles = $stmt->fetchAll(); + + // Batch fetch all role-permission mappings in one query (was N+1) + $stmt = $pdo->query(' + SELECT rp.role_id, p.name + FROM role_permissions rp + JOIN permissions p ON p.id = rp.permission_id + '); + $allRolePerms = $stmt->fetchAll(); + + // Group permissions by role_id + $permsByRole = []; + foreach ($allRolePerms as $rp) { + $permsByRole[$rp['role_id']][] = $rp['name']; + } + + foreach ($roles as &$role) { + $role['permissions'] = $permsByRole[$role['id']] ?? []; + $role['permission_count'] = count($role['permissions']); + } + unset($role); + + // Get all available permissions grouped by module + $stmt = $pdo->query('SELECT id, name, display_name, description FROM permissions ORDER BY id'); + $allPermissions = $stmt->fetchAll(); + + $grouped = []; + foreach ($allPermissions as $perm) { + $parts = explode('.', $perm['name'], 2); + $module = $parts[0]; + if (!isset($grouped[$module])) { + $grouped[$module] = []; + } + $grouped[$module][] = $perm; + } + + successResponse([ + 'roles' => $roles, + 'permissions' => $allPermissions, + 'permission_groups' => $grouped, + ]); +} + +/** + * POST - Create new role + */ +function handleCreateRole(PDO $pdo): void +{ + $input = getJsonInput(); + + $name = trim($input['name'] ?? ''); + $displayName = trim($input['display_name'] ?? ''); + $description = trim($input['description'] ?? ''); + $permissions = $input['permissions'] ?? []; + + if (!$name) { + errorResponse('Název role je povinný'); + } + + if (!$displayName) { + errorResponse('Zobrazovaný název je povinný'); + } + + // Validate name format (slug) + if (!preg_match('/^[a-z0-9_-]+$/', $name)) { + errorResponse('Název role může obsahovat pouze malá písmena, čísla, pomlčky a podtržítka'); + } + + // Check uniqueness + $stmt = $pdo->prepare('SELECT id FROM roles WHERE name = ?'); + $stmt->execute([$name]); + if ($stmt->fetch()) { + errorResponse('Role s tímto názvem již existuje'); + } + + $pdo->beginTransaction(); + + try { + // Create role + $stmt = $pdo->prepare(' + INSERT INTO roles (name, display_name, description) + VALUES (?, ?, ?) + '); + $stmt->execute([$name, $displayName, $description ?: null]); + $newRoleId = (int)$pdo->lastInsertId(); + + // Assign permissions + if (!empty($permissions)) { + $stmt = $pdo->prepare(' + INSERT INTO role_permissions (role_id, permission_id) + SELECT ?, id FROM permissions WHERE name = ? + '); + foreach ($permissions as $permName) { + $stmt->execute([$newRoleId, $permName]); + } + } + + $pdo->commit(); + + AuditLog::logCreate('role', $newRoleId, [ + 'name' => $name, + 'display_name' => $displayName, + 'permissions' => $permissions, + ], "Vytvořena role '$displayName'"); + + successResponse(['id' => $newRoleId], 'Role byla vytvořena'); + } catch (PDOException $e) { + $pdo->rollBack(); + throw $e; + } +} + +/** + * PUT - Update role + */ +function handleUpdateRole(PDO $pdo, int $roleId): void +{ + // Get existing role + $stmt = $pdo->prepare( + 'SELECT id, name, display_name, description FROM roles WHERE id = ?' + ); + $stmt->execute([$roleId]); + $role = $stmt->fetch(); + + if (!$role) { + errorResponse('Role nebyla nalezena', 404); + } + + // Block editing admin role name + if ($role['name'] === 'admin') { + errorResponse('Roli administrátora nelze upravovat'); + } + + $input = getJsonInput(); + + $displayName = trim($input['display_name'] ?? $role['display_name']); + $description = trim($input['description'] ?? $role['description'] ?? ''); + $permissions = $input['permissions'] ?? null; + + if (!$displayName) { + errorResponse('Zobrazovaný název je povinný'); + } + + $pdo->beginTransaction(); + + try { + // Update role + $stmt = $pdo->prepare(' + UPDATE roles SET display_name = ?, description = ? + WHERE id = ? + '); + $stmt->execute([$displayName, $description ?: null, $roleId]); + + // Update permissions if provided + if ($permissions !== null) { + // Remove existing permissions + $stmt = $pdo->prepare('DELETE FROM role_permissions WHERE role_id = ?'); + $stmt->execute([$roleId]); + + // Add new permissions + if (!empty($permissions)) { + $stmt = $pdo->prepare(' + INSERT INTO role_permissions (role_id, permission_id) + SELECT ?, id FROM permissions WHERE name = ? + '); + foreach ($permissions as $permName) { + $stmt->execute([$roleId, $permName]); + } + } + } + + $pdo->commit(); + + AuditLog::logUpdate('role', $roleId, [ + 'display_name' => $role['display_name'], + ], [ + 'display_name' => $displayName, + 'permissions' => $permissions, + ], "Upravena role '$displayName'"); + + successResponse(null, 'Role byla aktualizována'); + } catch (PDOException $e) { + $pdo->rollBack(); + throw $e; + } +} + +/** + * DELETE - Delete role + */ +function handleDeleteRole(PDO $pdo, int $roleId): void +{ + $stmt = $pdo->prepare( + 'SELECT id, name, display_name, description FROM roles WHERE id = ?' + ); + $stmt->execute([$roleId]); + $role = $stmt->fetch(); + + if (!$role) { + errorResponse('Role nebyla nalezena', 404); + } + + // Block deleting admin role + if ($role['name'] === 'admin') { + errorResponse('Roli administrátora nelze smazat'); + } + + // Check if role has users + $stmt = $pdo->prepare('SELECT COUNT(*) FROM users WHERE role_id = ?'); + $stmt->execute([$roleId]); + $userCount = $stmt->fetchColumn(); + + if ($userCount > 0) { + errorResponse("Nelze smazat roli s {$userCount} přiřazenými uživateli. Nejprve změňte roli těmto uživatelům."); + } + + // Delete role (cascade deletes role_permissions) + $stmt = $pdo->prepare('DELETE FROM roles WHERE id = ?'); + $stmt->execute([$roleId]); + + AuditLog::logDelete('role', $roleId, $role, "Smazána role '{$role['display_name']}'"); + + successResponse(null, 'Role byla smazána'); +} diff --git a/dist/api/admin/handlers/session-handlers.php b/dist/api/admin/handlers/session-handlers.php new file mode 100644 index 0000000..303454a --- /dev/null +++ b/dist/api/admin/handlers/session-handlers.php @@ -0,0 +1,21 @@ + */ +function get2FAInfo(PDO $pdo, int $userId): array +{ + try { + $stmt = $pdo->prepare("SELECT totp_enabled FROM users WHERE id = ?"); + $stmt->execute([$userId]); + $row = $stmt->fetch(); + + $r2fa = $pdo->query("SELECT require_2fa FROM company_settings LIMIT 1"); + return [ + 'totp_enabled' => (bool) ($row['totp_enabled'] ?? false), + 'require_2fa' => (bool) $r2fa->fetchColumn(), + ]; + } catch (PDOException $e) { + return ['totp_enabled' => false, 'require_2fa' => false]; + } +} diff --git a/dist/api/admin/handlers/sessions-handlers.php b/dist/api/admin/handlers/sessions-handlers.php new file mode 100644 index 0000000..6fc7623 --- /dev/null +++ b/dist/api/admin/handlers/sessions-handlers.php @@ -0,0 +1,180 @@ +prepare( + 'DELETE FROM refresh_tokens WHERE user_id = ? AND (expires_at < NOW()' + . ' OR (replaced_at IS NOT NULL AND replaced_at < DATE_SUB(NOW(), INTERVAL ' + . JWTAuth::getGracePeriod() . ' SECOND)))' + ); + $stmt->execute([$userId]); + + // Jen aktivní sessions (nereplacované) + $stmt = $pdo->prepare(' + SELECT + id, + ip_address, + user_agent, + created_at, + expires_at, + token_hash + FROM refresh_tokens + WHERE user_id = ? AND replaced_at IS NULL + ORDER BY created_at DESC + '); + $stmt->execute([$userId]); + $sessions = $stmt->fetchAll(); + + // Process sessions to add is_current flag and parse user agent + $processedSessions = array_map(function ($session) use ($currentTokenHash) { + return [ + 'id' => (int) $session['id'], + 'ip_address' => $session['ip_address'], + 'user_agent' => $session['user_agent'], + 'device_info' => parseUserAgent($session['user_agent']), + 'created_at' => $session['created_at'], + 'expires_at' => $session['expires_at'], + 'is_current' => $currentTokenHash && $session['token_hash'] === $currentTokenHash, + ]; + }, $sessions); + + successResponse([ + 'sessions' => $processedSessions, + 'total' => count($processedSessions), + ]); +} + +/** + * DELETE - Delete a specific session + */ +function handleDeleteSession(PDO $pdo, int $sessionId, int $userId, ?string $currentTokenHash): void +{ + // Verify the session belongs to the current user + $stmt = $pdo->prepare('SELECT token_hash FROM refresh_tokens WHERE id = ? AND user_id = ?'); + $stmt->execute([$sessionId, $userId]); + $session = $stmt->fetch(); + + if (!$session) { + errorResponse('Relace nebyla nalezena', 404); + } + + // Check if trying to delete current session + if ($currentTokenHash && $session['token_hash'] === $currentTokenHash) { + // Check if force parameter is set + $input = getJsonInput(); + if (!($input['force'] ?? false)) { + errorResponse('Nelze smazat aktuální relaci. Použijte tlačítko odhlášení.', 400); + } + } + + // Delete the session + $stmt = $pdo->prepare('DELETE FROM refresh_tokens WHERE id = ? AND user_id = ?'); + $stmt->execute([$sessionId, $userId]); + + successResponse(null, 'Relace byla úspěšně ukončena'); +} + +/** + * DELETE - Delete all sessions except current + */ +function handleDeleteAllSessions(PDO $pdo, int $userId, ?string $currentTokenHash): void +{ + if (!$currentTokenHash) { + $stmt = $pdo->prepare('DELETE FROM refresh_tokens WHERE user_id = ?'); + $stmt->execute([$userId]); + $deleted = $stmt->rowCount(); + } else { + // Ponechat aktuální session, smazat ostatní (včetně replaced) + $stmt = $pdo->prepare('DELETE FROM refresh_tokens WHERE user_id = ? AND token_hash != ?'); + $stmt->execute([$userId, $currentTokenHash]); + $deleted = $stmt->rowCount(); + } + + successResponse([ + 'deleted' => $deleted, + ], $deleted > 0 ? 'Ostatní relace byly úspěšně ukončeny' : 'Žádné další relace k ukončení'); +} + +/** + * Parse user agent string to extract device/browser info + * + * @return array{browser: string, os: string} + */ +function parseUserAgent(?string $userAgent): array +{ + if (empty($userAgent)) { + return [ + 'browser' => 'Neznámý prohlížeč', + 'os' => 'Neznámý systém', + 'device' => 'Neznámé zařízení', + 'icon' => 'device', + ]; + } + + $browser = 'Neznámý prohlížeč'; + $os = 'Neznámý systém'; + $device = 'desktop'; + $icon = 'desktop'; + + // Detect browser + if (preg_match('/Edg(e|A|iOS)?\/[\d.]+/i', $userAgent)) { + $browser = 'Microsoft Edge'; + } elseif (preg_match('/OPR\/[\d.]+|Opera/i', $userAgent)) { + $browser = 'Opera'; + } elseif (preg_match('/Chrome\/[\d.]+/i', $userAgent) && !preg_match('/Chromium/i', $userAgent)) { + $browser = 'Google Chrome'; + } elseif (preg_match('/Firefox\/[\d.]+/i', $userAgent)) { + $browser = 'Mozilla Firefox'; + } elseif (preg_match('/Safari\/[\d.]+/i', $userAgent) && !preg_match('/Chrome/i', $userAgent)) { + $browser = 'Safari'; + } elseif (preg_match('/MSIE|Trident/i', $userAgent)) { + $browser = 'Internet Explorer'; + } + + // Detect OS + if (preg_match('/Windows NT 10/i', $userAgent)) { + $os = 'Windows 10/11'; + } elseif (preg_match('/Windows NT 6\.3/i', $userAgent)) { + $os = 'Windows 8.1'; + } elseif (preg_match('/Windows NT 6\.2/i', $userAgent)) { + $os = 'Windows 8'; + } elseif (preg_match('/Windows NT 6\.1/i', $userAgent)) { + $os = 'Windows 7'; + } elseif (preg_match('/Windows/i', $userAgent)) { + $os = 'Windows'; + } elseif (preg_match('/Macintosh|Mac OS X/i', $userAgent)) { + $os = 'macOS'; + } elseif (preg_match('/Linux/i', $userAgent) && !preg_match('/Android/i', $userAgent)) { + $os = 'Linux'; + } elseif (preg_match('/iPhone/i', $userAgent)) { + $os = 'iOS'; + $device = 'mobile'; + $icon = 'smartphone'; + } elseif (preg_match('/iPad/i', $userAgent)) { + $os = 'iPadOS'; + $device = 'tablet'; + $icon = 'tablet'; + } elseif (preg_match('/Android/i', $userAgent)) { + $os = 'Android'; + if (preg_match('/Mobile/i', $userAgent)) { + $device = 'mobile'; + $icon = 'smartphone'; + } else { + $device = 'tablet'; + $icon = 'tablet'; + } + } + + return [ + 'browser' => $browser, + 'os' => $os, + 'device' => $device, + 'icon' => $icon, + ]; +} diff --git a/dist/api/admin/handlers/totp-handlers.php b/dist/api/admin/handlers/totp-handlers.php new file mode 100644 index 0000000..3189ad4 --- /dev/null +++ b/dist/api/admin/handlers/totp-handlers.php @@ -0,0 +1,426 @@ +prepare('SELECT totp_enabled FROM users WHERE id = ?'); + $stmt->execute([$userId]); + $user = $stmt->fetch(); + + successResponse([ + 'totp_enabled' => (bool) ($user['totp_enabled'] ?? false), + ]); +} + +/** POST ?action=setup - vygenerovat secret + QR URI (jeste neaktivuje 2FA) */ +function handleSetup(PDO $pdo, TwoFactorAuth $tfa): void +{ + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + errorResponse('Metoda není povolena', 405); + } + + $authData = JWTAuth::requireAuth(); + AuditLog::setUser($authData['user_id'], $authData['user']['username'] ?? 'unknown'); + $userId = $authData['user_id']; + + $stmt = $pdo->prepare('SELECT totp_enabled, username, email FROM users WHERE id = ?'); + $stmt->execute([$userId]); + $user = $stmt->fetch(); + + if ($user['totp_enabled']) { + errorResponse('2FA je již aktivní. Nejdříve ji deaktivujte.'); + } + + $secret = $tfa->createSecret(); + + $stmt = $pdo->prepare('UPDATE users SET totp_secret = ? WHERE id = ?'); + $stmt->execute([Encryption::encrypt($secret), $userId]); + + $label = $user['email'] ?: $user['username']; + $qrUri = $tfa->getQRText($label, $secret); + + successResponse([ + 'secret' => $secret, + 'qr_uri' => $qrUri, + ]); +} + +/** POST ?action=enable { "code": "123456" } */ +function handleEnable(PDO $pdo, TwoFactorAuth $tfa): void +{ + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + errorResponse('Metoda není povolena', 405); + } + + $authData = JWTAuth::requireAuth(); + AuditLog::setUser($authData['user_id'], $authData['user']['username'] ?? 'unknown'); + $userId = $authData['user_id']; + $input = getJsonInput(); + $code = trim($input['code'] ?? ''); + + if (empty($code)) { + errorResponse('Ověřovací kód je povinný'); + } + + $stmt = $pdo->prepare('SELECT totp_secret, totp_enabled FROM users WHERE id = ?'); + $stmt->execute([$userId]); + $user = $stmt->fetch(); + + if (!$user['totp_secret']) { + errorResponse('Nejprve vygenerujte tajný klíč (setup)'); + } + + if ($user['totp_enabled']) { + errorResponse('2FA je již aktivní'); + } + + $decryptedSecret = decryptTotpSecret($user['totp_secret']); + if (!$tfa->verifyCode($decryptedSecret, $code)) { + errorResponse('Neplatný ověřovací kód. Zkontrolujte čas na telefonu.'); + } + + $backupCodes = generateBackupCodes(); + $hashedCodes = array_map(fn ($c) => password_hash($c, PASSWORD_BCRYPT, ['cost' => 10]), $backupCodes); + + $stmt = $pdo->prepare('UPDATE users SET totp_enabled = 1, totp_backup_codes = ? WHERE id = ?'); + $stmt->execute([json_encode($hashedCodes), $userId]); + + AuditLog::logUpdate('user', $userId, ['totp_enabled' => 0], ['totp_enabled' => 1], 'Uživatel aktivoval 2FA'); + + successResponse([ + 'backup_codes' => $backupCodes, + ], '2FA bylo úspěšně aktivováno'); +} + +/** POST ?action=disable { "code": "123456" } */ +function handleDisable(PDO $pdo, TwoFactorAuth $tfa): void +{ + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + errorResponse('Metoda není povolena', 405); + } + + $authData = JWTAuth::requireAuth(); + AuditLog::setUser($authData['user_id'], $authData['user']['username'] ?? 'unknown'); + $userId = $authData['user_id']; + $input = getJsonInput(); + $code = trim($input['code'] ?? ''); + + if (empty($code)) { + errorResponse('Ověřovací kód je povinný'); + } + + $stmt = $pdo->prepare('SELECT totp_secret, totp_enabled FROM users WHERE id = ?'); + $stmt->execute([$userId]); + $user = $stmt->fetch(); + + if (!$user['totp_enabled']) { + errorResponse('2FA není aktivní'); + } + + $decryptedSecret = decryptTotpSecret($user['totp_secret']); + if (!$tfa->verifyCode($decryptedSecret, $code)) { + errorResponse('Neplatný ověřovací kód'); + } + $stmt = $pdo->prepare( + 'UPDATE users SET totp_enabled = 0, totp_secret = NULL, + totp_backup_codes = NULL WHERE id = ?' + ); + $stmt->execute([$userId]); + + AuditLog::logUpdate('user', $userId, ['totp_enabled' => 1], ['totp_enabled' => 0], 'Uživatel deaktivoval 2FA'); + + successResponse(null, '2FA bylo deaktivováno'); +} + +/** + * POST ?action=verify - overeni TOTP kodu pri loginu (pre-auth) + * Body: { "login_token": "...", "code": "123456", "remember": false } + */ +function handleVerify(PDO $pdo, TwoFactorAuth $tfa): void +{ + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + errorResponse('Metoda není povolena', 405); + } + + $rateLimiter = new RateLimiter(); + $rateLimiter->setFailClosed(); + $rateLimiter->enforce('totp_2fa', 5); + + $input = getJsonInput(); + $loginToken = $input['login_token'] ?? ''; + $code = trim($input['code'] ?? ''); + $remember = (bool) ($input['remember'] ?? false); + + if (empty($loginToken) || empty($code)) { + errorResponse('Přihlašovací token a ověřovací kód jsou povinné'); + } + + $tokenData = verifyLoginToken($pdo, $loginToken); + if (!$tokenData) { + errorResponse('Neplatný nebo expirovaný přihlašovací token. Přihlaste se znovu.', 401); + } + + $userId = $tokenData['user_id']; + + $stmt = $pdo->prepare(' + SELECT u.id, u.username, u.email, u.first_name, u.last_name, + u.role_id, u.is_active, u.totp_secret, u.totp_enabled, + r.name as role_name, r.display_name as role_display_name + FROM users u + LEFT JOIN roles r ON u.role_id = r.id + WHERE u.id = ? AND u.totp_enabled = 1 + '); + $stmt->execute([$userId]); + $user = $stmt->fetch(); + + if (!$user) { + errorResponse('Uživatel nenalezen nebo 2FA není aktivní', 401); + } + + $decryptedSecret = decryptTotpSecret($user['totp_secret']); + if (!$tfa->verifyCode($decryptedSecret, $code, 1)) { + errorResponse('Neplatný ověřovací kód'); + } + + deleteLoginToken($pdo, $loginToken); + completeLogin($pdo, $user, $remember); +} + +/** POST ?action=backup_verify { "login_token": "...", "code": "XXXXXXXX", "remember": false } */ +function handleBackupVerify(PDO $pdo): void +{ + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + errorResponse('Metoda není povolena', 405); + } + + $rateLimiter = new RateLimiter(); + $rateLimiter->setFailClosed(); + $rateLimiter->enforce('totp_2fa', 5); + + $input = getJsonInput(); + $loginToken = $input['login_token'] ?? ''; + $code = strtoupper(trim($input['code'] ?? '')); + $remember = (bool) ($input['remember'] ?? false); + + if (empty($loginToken) || empty($code)) { + errorResponse('Přihlašovací token a záložní kód jsou povinné'); + } + + $tokenData = verifyLoginToken($pdo, $loginToken); + if (!$tokenData) { + errorResponse('Neplatný nebo expirovaný přihlašovací token. Přihlaste se znovu.', 401); + } + + $userId = $tokenData['user_id']; + + $stmt = $pdo->prepare(' + SELECT u.id, u.username, u.email, u.first_name, u.last_name, + u.role_id, u.is_active, u.totp_enabled, u.totp_backup_codes, + r.name as role_name, r.display_name as role_display_name + FROM users u + LEFT JOIN roles r ON u.role_id = r.id + WHERE u.id = ? AND u.totp_enabled = 1 + '); + $stmt->execute([$userId]); + $user = $stmt->fetch(); + + if (!$user || !$user['totp_backup_codes']) { + errorResponse('Uživatel nenalezen nebo nemá záložní kódy', 401); + } + + $hashedCodes = json_decode($user['totp_backup_codes'], true); + $matched = false; + $remainingCodes = []; + + foreach ($hashedCodes as $hashed) { + if (!$matched && password_verify($code, $hashed)) { + $matched = true; + } else { + $remainingCodes[] = $hashed; + } + } + + if (!$matched) { + errorResponse('Neplatný záložní kód'); + } + + $stmt = $pdo->prepare('UPDATE users SET totp_backup_codes = ? WHERE id = ?'); + $stmt->execute([json_encode($remainingCodes), $userId]); + + deleteLoginToken($pdo, $loginToken); + completeLogin($pdo, $user, $remember); +} + +/** GET ?action=get_required (admin only) */ +function handleGetRequired(PDO $pdo): void +{ + $authData = JWTAuth::requireAuth(); + AuditLog::setUser($authData['user_id'], $authData['user']['username'] ?? 'unknown'); + requirePermission($authData, 'settings.security'); + + $stmt = $pdo->query("SELECT require_2fa FROM company_settings LIMIT 1"); + + successResponse([ + 'require_2fa' => (bool) $stmt->fetchColumn(), + ]); +} + +/** POST ?action=set_required { "required": true/false } (admin only) */ +function handleSetRequired(PDO $pdo): void +{ + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + errorResponse('Metoda není povolena', 405); + } + + $authData = JWTAuth::requireAuth(); + AuditLog::setUser($authData['user_id'], $authData['user']['username'] ?? 'unknown'); + requirePermission($authData, 'settings.security'); + + $input = getJsonInput(); + $required = (bool) ($input['required'] ?? false); + + $stmt = $pdo->prepare("UPDATE company_settings SET require_2fa = ? LIMIT 1"); + $stmt->execute([$required ? 1 : 0]); + + successResponse([ + 'require_2fa' => $required, + ], $required ? '2FA je nyní povinná pro všechny uživatele' : '2FA již není povinná'); +} + +// --- Helper functions --- + +/** Desifrovat TOTP secret z DB (zpetne kompatibilni s plaintextem pred migraci) */ +function decryptTotpSecret(string $value): string +{ + if (Encryption::isEncrypted($value)) { + return Encryption::decrypt($value); + } + return $value; +} + +/** + * Generovat 8 nahodnych backup kodu + * + * @return list + */ +function generateBackupCodes(int $count = 8): array +{ + $codes = []; + for ($i = 0; $i < $count; $i++) { + $codes[] = strtoupper(bin2hex(random_bytes(4))); // 8-char hex + } + return $codes; +} + +/** Docasny login token pro 2FA (5 min) */ +function createLoginToken(PDO $pdo, int $userId): string +{ + $token = bin2hex(random_bytes(32)); + $hashedToken = hash('sha256', $token); + $expiresAt = date('Y-m-d H:i:s', time() + 300); // 5 minutes + + $stmt = $pdo->prepare('DELETE FROM totp_login_tokens WHERE user_id = ? OR expires_at < NOW()'); + $stmt->execute([$userId]); + + $stmt = $pdo->prepare(' + INSERT INTO totp_login_tokens (user_id, token_hash, expires_at) + VALUES (?, ?, ?) + '); + $stmt->execute([$userId, $hashedToken, $expiresAt]); + + return $token; +} + +/** + * Overit login token + * + * @return array|null + */ +function verifyLoginToken(PDO $pdo, string $token): ?array +{ + $hashedToken = hash('sha256', $token); + + $stmt = $pdo->prepare(' + SELECT id, user_id, token_hash, expires_at, created_at + FROM totp_login_tokens + WHERE token_hash = ? AND expires_at > NOW() + '); + $stmt->execute([$hashedToken]); + return $stmt->fetch() ?: null; +} + +/** Smazat login token po pouziti */ +function deleteLoginToken(PDO $pdo, string $token): void +{ + $hashedToken = hash('sha256', $token); + $stmt = $pdo->prepare('DELETE FROM totp_login_tokens WHERE token_hash = ?'); + $stmt->execute([$hashedToken]); +} + +/** + * Dokoncit login po uspesnem 2FA - vydat JWT + refresh token + * + * @param array $user + */ +function completeLogin(PDO $pdo, array $user, bool $remember): void +{ + $stmt = $pdo->prepare(' + UPDATE users SET failed_login_attempts = 0, locked_until = NULL, last_login = NOW() + WHERE id = ? + '); + $stmt->execute([$user['id']]); + + $userData = [ + 'id' => $user['id'], + 'username' => $user['username'], + 'email' => $user['email'], + 'first_name' => $user['first_name'], + 'last_name' => $user['last_name'], + 'role' => $user['role_name'] ?? null, + 'role_display' => $user['role_display_name'] ?? $user['role_name'] ?? null, + 'is_admin' => ($user['role_name'] ?? '') === 'admin', + ]; + + $accessToken = JWTAuth::generateAccessToken($userData); + JWTAuth::generateRefreshToken($user['id'], $remember); + + AuditLog::logLogin($user['id'], $user['username']); + + $stmt = $pdo->query("SELECT require_2fa FROM company_settings LIMIT 1"); + $require2FA = (bool) $stmt->fetchColumn(); + + successResponse([ + 'access_token' => $accessToken, + 'expires_in' => JWTAuth::getAccessTokenExpiry(), + 'user' => [ + 'id' => $userData['id'], + 'username' => $userData['username'], + 'email' => $userData['email'], + 'full_name' => trim($userData['first_name'] . ' ' . $userData['last_name']), + 'role' => $userData['role'], + 'role_display' => $userData['role_display'], + 'is_admin' => $userData['is_admin'], + 'permissions' => JWTAuth::getUserPermissions($user['id']), + 'totp_enabled' => true, + 'require_2fa' => $require2FA, + ], + ], 'Přihlášení úspěšné'); +} diff --git a/dist/api/admin/handlers/trips-handlers.php b/dist/api/admin/handlers/trips-handlers.php new file mode 100644 index 0000000..c9671fb --- /dev/null +++ b/dist/api/admin/handlers/trips-handlers.php @@ -0,0 +1,685 @@ +prepare(' + SELECT COALESCE( + (SELECT MAX(end_km) FROM trips WHERE vehicle_id = ?), + (SELECT initial_km FROM vehicles WHERE id = ?), + 0 + ) as last_km + '); + $stmt->execute([$vehicleId, $vehicleId]); + $result = $stmt->fetch(); + + return $result ? (int)$result['last_km'] : 0; +} + +function formatKm(int $km): string +{ + return number_format($km, 0, ',', ' ') . ' km'; +} + +// ============================================================================ +// GET Handlers +// ============================================================================ + +/** + * GET - Current month trips (filtered to current user) + */ +function handleGetCurrent(PDO $pdo, int $userId): void +{ + $month = validateMonth(); + $vehicleId = isset($_GET['vehicle_id']) ? (int)$_GET['vehicle_id'] : null; + $startDate = "{$month}-01"; + $endDate = date('Y-m-t', strtotime($startDate)); + + $sql = " + SELECT t.id, t.vehicle_id, t.user_id, t.trip_date, t.start_km, + t.end_km, t.distance, t.route_from, t.route_to, + t.is_business, t.notes, t.created_at, + v.spz, v.name as vehicle_name, v.brand, v.model, + CONCAT(u.first_name, ' ', u.last_name) as driver_name + FROM trips t + JOIN vehicles v ON t.vehicle_id = v.id + JOIN users u ON t.user_id = u.id + WHERE t.trip_date BETWEEN ? AND ? + AND t.user_id = ? + "; + $params = [$startDate, $endDate, $userId]; + + if ($vehicleId) { + $sql .= ' AND t.vehicle_id = ?'; + $params[] = $vehicleId; + } + + $sql .= ' ORDER BY t.trip_date DESC, t.start_km DESC'; + + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + $trips = $stmt->fetchAll(); + + // Get active vehicles for selection + $stmt = $pdo->query('SELECT id, spz, name, brand, model FROM vehicles WHERE is_active = 1 ORDER BY name'); + $vehicles = $stmt->fetchAll(); + + // Calculate totals + $totalDistance = 0; + $businessDistance = 0; + $privateDistance = 0; + + foreach ($trips as $trip) { + $totalDistance += $trip['distance']; + if ($trip['is_business']) { + $businessDistance += $trip['distance']; + } else { + $privateDistance += $trip['distance']; + } + } + + successResponse([ + 'trips' => $trips, + 'vehicles' => $vehicles, + 'month' => $month, + 'totals' => [ + 'total' => $totalDistance, + 'business' => $businessDistance, + 'private' => $privateDistance, + 'count' => count($trips), + ], + ]); +} + +/** + * GET - Trip history with filters (filtered to current user) + */ +function handleGetHistory(PDO $pdo, int $userId): void +{ + $month = validateMonth(); + $vehicleId = isset($_GET['vehicle_id']) ? (int)$_GET['vehicle_id'] : null; + + $startDate = "{$month}-01"; + $endDate = date('Y-m-t', strtotime($startDate)); + + $sql = " + SELECT t.id, t.vehicle_id, t.user_id, t.trip_date, t.start_km, + t.end_km, t.distance, t.route_from, t.route_to, + t.is_business, t.notes, t.created_at, + v.spz, v.name as vehicle_name, v.brand, v.model, + CONCAT(u.first_name, ' ', u.last_name) as driver_name + FROM trips t + JOIN vehicles v ON t.vehicle_id = v.id + JOIN users u ON t.user_id = u.id + WHERE t.trip_date BETWEEN ? AND ? + AND t.user_id = ? + "; + $params = [$startDate, $endDate, $userId]; + + if ($vehicleId) { + $sql .= ' AND t.vehicle_id = ?'; + $params[] = $vehicleId; + } + + $sql .= ' ORDER BY t.trip_date DESC, t.start_km DESC'; + + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + $trips = $stmt->fetchAll(); + + // Get vehicles for filter + $stmt = $pdo->query('SELECT id, spz, name FROM vehicles WHERE is_active = 1 ORDER BY name'); + $vehicles = $stmt->fetchAll(); + + // Calculate totals + $totalDistance = 0; + $businessDistance = 0; + + foreach ($trips as $trip) { + $totalDistance += $trip['distance']; + if ($trip['is_business']) { + $businessDistance += $trip['distance']; + } + } + + successResponse([ + 'trips' => $trips, + 'vehicles' => $vehicles, + 'month' => $month, + 'totals' => [ + 'total' => $totalDistance, + 'business' => $businessDistance, + 'count' => count($trips), + ], + ]); +} + +/** + * GET - Admin view of all trips + */ +function handleGetAdmin(PDO $pdo): void +{ + $dateFrom = $_GET['date_from'] ?? null; + $dateTo = $_GET['date_to'] ?? null; + $vehicleId = isset($_GET['vehicle_id']) ? (int)$_GET['vehicle_id'] : null; + $filterUserId = isset($_GET['user_id']) ? (int)$_GET['user_id'] : null; + + // Default to current month if no dates provided + if (!$dateFrom || !$dateTo) { + $month = date('Y-m'); + $startDate = "{$month}-01"; + $endDate = date('Y-m-t', strtotime($startDate)); + } else { + if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateFrom) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateTo)) { + errorResponse('Neplatný formát data (očekáváno YYYY-MM-DD)'); + } + $startDate = $dateFrom; + $endDate = $dateTo; + } + + $sql = " + SELECT t.id, t.vehicle_id, t.user_id, t.trip_date, t.start_km, + t.end_km, t.distance, t.route_from, t.route_to, + t.is_business, t.notes, t.created_at, + v.spz, v.name as vehicle_name, + CONCAT(u.first_name, ' ', u.last_name) as driver_name + FROM trips t + JOIN vehicles v ON t.vehicle_id = v.id + JOIN users u ON t.user_id = u.id + WHERE t.trip_date BETWEEN ? AND ? + "; + $params = [$startDate, $endDate]; + + if ($vehicleId) { + $sql .= ' AND t.vehicle_id = ?'; + $params[] = $vehicleId; + } + + if ($filterUserId) { + $sql .= ' AND t.user_id = ?'; + $params[] = $filterUserId; + } + + $sql .= ' ORDER BY t.trip_date DESC, t.start_km DESC'; + + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + $trips = $stmt->fetchAll(); + + // Get vehicles for filter + $stmt = $pdo->query('SELECT id, spz, name FROM vehicles ORDER BY name'); + $vehicles = $stmt->fetchAll(); + + // Get users for filter + $stmt = $pdo->query( + "SELECT id, CONCAT(first_name, ' ', last_name) as name FROM users WHERE is_active = 1 ORDER BY last_name" + ); + $users = $stmt->fetchAll(); + + // Calculate totals + $totalDistance = 0; + $businessDistance = 0; + + foreach ($trips as $trip) { + $totalDistance += $trip['distance']; + if ($trip['is_business']) { + $businessDistance += $trip['distance']; + } + } + + successResponse([ + 'trips' => $trips, + 'vehicles' => $vehicles, + 'users' => $users, + 'date_from' => $startDate, + 'date_to' => $endDate, + 'totals' => [ + 'total' => $totalDistance, + 'business' => $businessDistance, + 'count' => count($trips), + ], + ]); +} + +/** + * GET - All vehicles (admin) + */ +function handleGetVehicles(PDO $pdo): void +{ + $stmt = $pdo->query(' + SELECT v.id, v.spz, v.name, v.brand, v.model, + v.initial_km, v.actual_km, v.is_active, + v.created_at, v.updated_at, + COUNT(t.id) as trip_count, + COALESCE(MAX(t.end_km), v.initial_km) as current_km + FROM vehicles v + LEFT JOIN trips t ON t.vehicle_id = v.id + GROUP BY v.id + ORDER BY v.is_active DESC, v.name + '); + $vehicles = $stmt->fetchAll(); + + successResponse(['vehicles' => $vehicles]); +} + +/** + * GET - Active vehicles for selection + */ +function handleGetActiveVehicles(PDO $pdo): void +{ + $stmt = $pdo->query(' + SELECT v.id, v.spz, v.name, v.brand, v.model, + COALESCE(MAX(t.end_km), v.initial_km) as current_km + FROM vehicles v + LEFT JOIN trips t ON t.vehicle_id = v.id + WHERE v.is_active = 1 + GROUP BY v.id + ORDER BY v.name + '); + $vehicles = $stmt->fetchAll(); + + successResponse(['vehicles' => $vehicles]); +} + +/** + * GET - Last km for vehicle + */ +function handleGetLastKm(PDO $pdo): void +{ + $vehicleId = (int)($_GET['vehicle_id'] ?? 0); + if (!$vehicleId) { + errorResponse('Vehicle ID je povinné'); + } + + $lastKm = getLastKmForVehicle($pdo, $vehicleId); + successResponse(['last_km' => $lastKm]); +} + +// ============================================================================ +// POST Handlers +// ============================================================================ + +/** + * POST - Create trip + */ +function handleCreateTrip(PDO $pdo, int $userId): void +{ + $input = getJsonInput(); + + $vehicleId = (int)($input['vehicle_id'] ?? 0); + $tripDate = $input['trip_date'] ?? ''; + $startKm = (int)($input['start_km'] ?? 0); + $endKm = (int)($input['end_km'] ?? 0); + $routeFrom = trim($input['route_from'] ?? ''); + $routeTo = trim($input['route_to'] ?? ''); + $isBusiness = (int)($input['is_business'] ?? 1); + $notes = trim($input['notes'] ?? ''); + + // Validation + if (!$vehicleId) { + errorResponse('Vyberte vozidlo'); + } + if (!$tripDate) { + errorResponse('Datum jízdy je povinné'); + } + if (!$startKm) { + errorResponse('Počáteční stav km je povinný'); + } + if (!$endKm) { + errorResponse('Konečný stav km je povinný'); + } + if (!$routeFrom) { + errorResponse('Místo odjezdu je povinné'); + } + if (!$routeTo) { + errorResponse('Místo příjezdu je povinné'); + } + if ($endKm <= $startKm) { + errorResponse('Konečný stav km musí být větší než počáteční'); + } + + // Check vehicle exists + $stmt = $pdo->prepare('SELECT id FROM vehicles WHERE id = ? AND is_active = 1'); + $stmt->execute([$vehicleId]); + if (!$stmt->fetch()) { + errorResponse('Vozidlo neexistuje nebo není aktivní'); + } + + $stmt = $pdo->prepare(' + INSERT INTO trips (vehicle_id, user_id, trip_date, start_km, end_km, route_from, route_to, is_business, notes) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + '); + $stmt->execute([ + $vehicleId, $userId, $tripDate, $startKm, $endKm, + $routeFrom, $routeTo, $isBusiness, $notes ?: null, + ]); + + $newId = (int)$pdo->lastInsertId(); + AuditLog::logCreate('trips', $newId, $input, 'Vytvořen záznam jízdy'); + + successResponse(['id' => $newId], 'Jízda byla zaznamenána'); +} + +/** + * POST - Create/update vehicle (admin) + */ +function handleVehicle(PDO $pdo): void +{ + $input = getJsonInput(); + + $id = (int)($input['id'] ?? 0); + $spz = strtoupper(trim($input['spz'] ?? '')); + $name = trim($input['name'] ?? ''); + $brand = trim($input['brand'] ?? ''); + $model = trim($input['model'] ?? ''); + $initialKm = (int)($input['initial_km'] ?? 0); + $isActive = isset($input['is_active']) ? (int)$input['is_active'] : 1; + + if (!$spz) { + errorResponse('SPZ je povinná'); + } + if (!$name) { + errorResponse('Název je povinný'); + } + + if ($id) { + // Update + $stmt = $pdo->prepare(' + UPDATE vehicles + SET spz = ?, name = ?, brand = ?, model = ?, initial_km = ?, is_active = ? + WHERE id = ? + '); + $stmt->execute([$spz, $name, $brand ?: null, $model ?: null, $initialKm, $isActive, $id]); + + AuditLog::logUpdate('vehicles', $id, [], $input, 'Upraveno vozidlo'); + successResponse(null, 'Vozidlo bylo aktualizováno'); + } else { + // Create + $stmt = $pdo->prepare(' + INSERT INTO vehicles (spz, name, brand, model, initial_km, is_active) + VALUES (?, ?, ?, ?, ?, ?) + '); + + try { + $stmt->execute([$spz, $name, $brand ?: null, $model ?: null, $initialKm, $isActive]); + $newId = (int)$pdo->lastInsertId(); + + AuditLog::logCreate('vehicles', $newId, $input, 'Vytvořeno vozidlo'); + successResponse(['id' => $newId], 'Vozidlo bylo vytvořeno'); + } catch (PDOException $e) { + if ($e->getCode() == 23000) { + errorResponse('Vozidlo s touto SPZ již existuje'); + } + throw $e; + } + } +} + +// ============================================================================ +// PUT Handler +// ============================================================================ + +/** + * PUT - Update trip + * + * @param array $authData + */ +function handleUpdateTrip(PDO $pdo, int $id, int $userId, array $authData): void +{ + $stmt = $pdo->prepare( + 'SELECT id, vehicle_id, user_id, trip_date, start_km, end_km, + route_from, route_to, is_business, notes + FROM trips WHERE id = ?' + ); + $stmt->execute([$id]); + $trip = $stmt->fetch(); + + if (!$trip) { + errorResponse('Záznam nebyl nalezen', 404); + } + + // Check permission - own trips or trips.admin + if ($trip['user_id'] !== $userId && !hasPermission($authData, 'trips.admin')) { + errorResponse('Nemáte oprávnění upravit tento záznam', 403); + } + + $input = getJsonInput(); + + $vehicleId = (int)($input['vehicle_id'] ?? $trip['vehicle_id']); + $tripDate = $input['trip_date'] ?? $trip['trip_date']; + $startKm = (int)($input['start_km'] ?? $trip['start_km']); + $endKm = (int)($input['end_km'] ?? $trip['end_km']); + $routeFrom = trim($input['route_from'] ?? $trip['route_from']); + $routeTo = trim($input['route_to'] ?? $trip['route_to']); + $isBusiness = isset($input['is_business']) ? (int)$input['is_business'] : $trip['is_business']; + $notes = trim($input['notes'] ?? $trip['notes'] ?? ''); + + if ($endKm <= $startKm) { + errorResponse('Konečný stav km musí být větší než počáteční'); + } + + $stmt = $pdo->prepare(' + UPDATE trips + SET vehicle_id = ?, trip_date = ?, start_km = ?, end_km = ?, + route_from = ?, route_to = ?, is_business = ?, notes = ? + WHERE id = ? + '); + $stmt->execute([$vehicleId, $tripDate, $startKm, $endKm, $routeFrom, $routeTo, $isBusiness, $notes ?: null, $id]); + + AuditLog::logUpdate('trips', $id, $trip, $input, 'Upraven záznam jízdy'); + + successResponse(null, 'Záznam byl aktualizován'); +} + +// ============================================================================ +// DELETE Handlers +// ============================================================================ + +/** + * DELETE - Delete trip + * + * @param array $authData + */ +function handleDeleteTrip(PDO $pdo, int $id, int $userId, array $authData): void +{ + $stmt = $pdo->prepare( + 'SELECT id, vehicle_id, user_id, trip_date, start_km, end_km, + route_from, route_to, is_business, notes + FROM trips WHERE id = ?' + ); + $stmt->execute([$id]); + $trip = $stmt->fetch(); + + if (!$trip) { + errorResponse('Záznam nebyl nalezen', 404); + } + + // Check permission - own trips or trips.admin + if ($trip['user_id'] !== $userId && !hasPermission($authData, 'trips.admin')) { + errorResponse('Nemáte oprávnění smazat tento záznam', 403); + } + + $stmt = $pdo->prepare('DELETE FROM trips WHERE id = ?'); + $stmt->execute([$id]); + + AuditLog::logDelete('trips', $id, $trip, 'Smazán záznam jízdy'); + + successResponse(null, 'Záznam byl smazán'); +} + +/** + * DELETE - Delete vehicle (admin) + */ +function handleDeleteVehicle(PDO $pdo, int $id): void +{ + if (!$id) { + errorResponse('ID je povinné'); + } + + $stmt = $pdo->prepare( + 'SELECT id, spz, name, brand, model, is_active FROM vehicles WHERE id = ?' + ); + $stmt->execute([$id]); + $vehicle = $stmt->fetch(); + + if (!$vehicle) { + errorResponse('Vozidlo nebylo nalezeno', 404); + } + + // Check if vehicle has trips + $stmt = $pdo->prepare('SELECT COUNT(*) FROM trips WHERE vehicle_id = ?'); + $stmt->execute([$id]); + $tripCount = $stmt->fetchColumn(); + + if ($tripCount > 0) { + errorResponse( + "Nelze smazat vozidlo s {$tripCount} záznamy jízd. Nejprve smažte záznamy jízd nebo deaktivujte vozidlo." + ); + } + + $stmt = $pdo->prepare('DELETE FROM vehicles WHERE id = ?'); + $stmt->execute([$id]); + + AuditLog::logDelete('vehicles', $id, $vehicle, 'Smazáno vozidlo'); + + successResponse(null, 'Vozidlo bylo smazáno'); +} + +// ============================================================================ +// Print Handler +// ============================================================================ + +/** + * Format date range for display + */ +function formatPeriodName(string $startDate, string $endDate): string +{ + $start = new DateTime($startDate); + $end = new DateTime($endDate); + + // If same month + if ($start->format('Y-m') === $end->format('Y-m')) { + return getCzechMonthName((int)$start->format('n')) . ' ' . $start->format('Y'); + } + + // If same year + if ($start->format('Y') === $end->format('Y')) { + return $start->format('j.n.') . ' - ' . $end->format('j.n.Y'); + } + + // Different years + return $start->format('j.n.Y') . ' - ' . $end->format('j.n.Y'); +} + +/** + * GET - Print data for trips (admin) + */ +function handleGetPrint(PDO $pdo): void +{ + $dateFrom = $_GET['date_from'] ?? null; + $dateTo = $_GET['date_to'] ?? null; + $vehicleId = isset($_GET['vehicle_id']) && $_GET['vehicle_id'] !== '' ? (int)$_GET['vehicle_id'] : null; + $filterUserId = isset($_GET['user_id']) && $_GET['user_id'] !== '' ? (int)$_GET['user_id'] : null; + + // Default to current month if no dates provided + if (!$dateFrom || !$dateTo) { + $startDate = date('Y-m-01'); + $endDate = date('Y-m-t'); + } else { + if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateFrom) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateTo)) { + errorResponse('Neplatný formát data (očekáváno YYYY-MM-DD)'); + } + $startDate = $dateFrom; + $endDate = $dateTo; + } + + $sql = " + SELECT t.id, t.vehicle_id, t.user_id, t.trip_date, t.start_km, + t.end_km, t.distance, t.route_from, t.route_to, + t.is_business, t.notes, t.created_at, + v.spz, v.name as vehicle_name, v.brand, v.model, + CONCAT(u.first_name, ' ', u.last_name) as driver_name + FROM trips t + JOIN vehicles v ON t.vehicle_id = v.id + JOIN users u ON t.user_id = u.id + WHERE t.trip_date BETWEEN ? AND ? + "; + $params = [$startDate, $endDate]; + + if ($vehicleId) { + $sql .= ' AND t.vehicle_id = ?'; + $params[] = $vehicleId; + } + + if ($filterUserId) { + $sql .= ' AND t.user_id = ?'; + $params[] = $filterUserId; + } + + $sql .= ' ORDER BY t.trip_date ASC, t.start_km ASC'; + + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + $trips = $stmt->fetchAll(); + + // Get vehicles for filter + $stmt = $pdo->query('SELECT id, spz, name FROM vehicles ORDER BY name'); + $vehicles = $stmt->fetchAll(); + + // Get users for filter + $stmt = $pdo->query( + "SELECT id, CONCAT(first_name, ' ', last_name) as name FROM users WHERE is_active = 1 ORDER BY last_name" + ); + $users = $stmt->fetchAll(); + + // Calculate totals + $totalDistance = 0; + $businessDistance = 0; + $privateDistance = 0; + + foreach ($trips as $trip) { + $totalDistance += $trip['distance']; + if ($trip['is_business']) { + $businessDistance += $trip['distance']; + } else { + $privateDistance += $trip['distance']; + } + } + + // Get selected vehicle/user names for header + $selectedVehicleName = ''; + if ($vehicleId) { + $stmt = $pdo->prepare("SELECT CONCAT(spz, ' - ', name) as name FROM vehicles WHERE id = ?"); + $stmt->execute([$vehicleId]); + $v = $stmt->fetch(); + $selectedVehicleName = $v ? $v['name'] : ''; + } + + $selectedUserName = ''; + if ($filterUserId) { + $stmt = $pdo->prepare("SELECT CONCAT(first_name, ' ', last_name) as name FROM users WHERE id = ?"); + $stmt->execute([$filterUserId]); + $u = $stmt->fetch(); + $selectedUserName = $u ? $u['name'] : ''; + } + + successResponse([ + 'trips' => $trips, + 'vehicles' => $vehicles, + 'users' => $users, + 'date_from' => $startDate, + 'date_to' => $endDate, + 'period_name' => formatPeriodName($startDate, $endDate), + 'selected_vehicle' => $vehicleId, + 'selected_vehicle_name' => $selectedVehicleName, + 'selected_user' => $filterUserId, + 'selected_user_name' => $selectedUserName, + 'totals' => [ + 'total' => $totalDistance, + 'business' => $businessDistance, + 'private' => $privateDistance, + 'count' => count($trips), + ], + ]); +} diff --git a/dist/api/admin/handlers/users-handlers.php b/dist/api/admin/handlers/users-handlers.php new file mode 100644 index 0000000..a9b1104 --- /dev/null +++ b/dist/api/admin/handlers/users-handlers.php @@ -0,0 +1,277 @@ +query(' + SELECT + u.id, + u.username, + u.email, + u.first_name, + u.last_name, + u.role_id, + u.is_active, + u.last_login, + u.created_at, + r.name as role_name, + r.display_name as role_display_name + FROM users u + LEFT JOIN roles r ON u.role_id = r.id + ORDER BY u.created_at DESC + '); + $users = $stmt->fetchAll(); + + // Get roles for dropdown + $stmt = $pdo->query('SELECT id, name, display_name FROM roles ORDER BY id'); + $roles = $stmt->fetchAll(); + + successResponse([ + 'users' => $users, + 'roles' => $roles, + ]); +} + +/** + * POST - Create new user + * + * @param array $authData + */ +function handleCreateUser(PDO $pdo, array $authData): void +{ + $input = getJsonInput(); + + // Validate required fields + $requiredFields = [ + 'username' => 'Uživatelské jméno', + 'email' => 'E-mail', + 'password' => 'Heslo', + 'first_name' => 'Jméno', + 'last_name' => 'Příjmení', + 'role_id' => 'Role', + ]; + foreach ($requiredFields as $field => $label) { + if (empty($input[$field])) { + errorResponse("$label je povinné"); + } + } + + $username = sanitize($input['username']); + $email = sanitize($input['email']); + $password = $input['password']; + $firstName = sanitize($input['first_name']); + $lastName = sanitize($input['last_name']); + $roleId = (int) $input['role_id']; + $isActive = isset($input['is_active']) ? ($input['is_active'] ? 1 : 0) : 1; + + // Non-admin nesmí přiřadit admin roli + if (!($authData['user']['is_admin'] ?? false)) { + $stmt = $pdo->prepare('SELECT name FROM roles WHERE id = ?'); + $stmt->execute([$roleId]); + $targetRole = $stmt->fetch(); + if ($targetRole && $targetRole['name'] === 'admin') { + errorResponse('Nemáte oprávnění přiřadit roli administrátora', 403); + } + } + + // Validate email format + if (!isValidEmail($email)) { + errorResponse('Neplatný formát e-mailu'); + } + + // Validate password length + if (strlen($password) < 8) { + errorResponse('Heslo musí mít alespoň 8 znaků'); + } + + // Check username uniqueness + $stmt = $pdo->prepare('SELECT id FROM users WHERE username = ?'); + $stmt->execute([$username]); + if ($stmt->fetch()) { + errorResponse('Uživatelské jméno již existuje'); + } + + // Check email uniqueness + $stmt = $pdo->prepare('SELECT id FROM users WHERE email = ?'); + $stmt->execute([$email]); + if ($stmt->fetch()) { + errorResponse('E-mail již existuje'); + } + + // Validate role exists + $stmt = $pdo->prepare('SELECT id FROM roles WHERE id = ?'); + $stmt->execute([$roleId]); + if (!$stmt->fetch()) { + errorResponse('Neplatná role'); + } + + // Hash password + $passwordHash = password_hash($password, PASSWORD_BCRYPT, ['cost' => BCRYPT_COST]); + + // Insert user + $stmt = $pdo->prepare(' + INSERT INTO users (username, email, password_hash, first_name, last_name, role_id, is_active) + VALUES (?, ?, ?, ?, ?, ?, ?) + '); + $stmt->execute([$username, $email, $passwordHash, $firstName, $lastName, $roleId, $isActive]); + + $newUserId = (int)$pdo->lastInsertId(); + + // Audit log + AuditLog::logCreate('user', $newUserId, [ + 'username' => $username, + 'email' => $email, + 'first_name' => $firstName, + 'last_name' => $lastName, + 'role_id' => $roleId, + 'is_active' => $isActive, + ], "Vytvořen uživatel '$username'"); + + successResponse(['id' => $newUserId], 'Uživatel byl úspěšně vytvořen'); +} + +/** + * PUT - Update user + * + * @param array $authData + */ +function handleUpdateUser(PDO $pdo, int $userId, int $currentUserId, array $authData): void +{ + // Get existing user + $stmt = $pdo->prepare(' + SELECT id, username, email, first_name, last_name, role_id, is_active, + last_login, created_at + FROM users WHERE id = ? + '); + $stmt->execute([$userId]); + $existingUser = $stmt->fetch(); + + if (!$existingUser) { + errorResponse('Uživatel nebyl nalezen', 404); + } + + $input = getJsonInput(); + + $username = isset($input['username']) ? sanitize($input['username']) : $existingUser['username']; + $email = isset($input['email']) ? sanitize($input['email']) : $existingUser['email']; + $firstName = isset($input['first_name']) ? sanitize($input['first_name']) : $existingUser['first_name']; + $lastName = isset($input['last_name']) ? sanitize($input['last_name']) : $existingUser['last_name']; + $roleId = isset($input['role_id']) ? (int) $input['role_id'] : $existingUser['role_id']; + $isActive = isset($input['is_active']) ? ($input['is_active'] ? 1 : 0) : $existingUser['is_active']; + + // Validate email format + if (!isValidEmail($email)) { + errorResponse('Neplatný formát e-mailu'); + } + + // Check username uniqueness (excluding current user) + $stmt = $pdo->prepare('SELECT id FROM users WHERE username = ? AND id != ?'); + $stmt->execute([$username, $userId]); + if ($stmt->fetch()) { + errorResponse('Uživatelské jméno již existuje'); + } + + // Check email uniqueness (excluding current user) + $stmt = $pdo->prepare('SELECT id FROM users WHERE email = ? AND id != ?'); + $stmt->execute([$email, $userId]); + if ($stmt->fetch()) { + errorResponse('E-mail již existuje'); + } + + // Validate role exists + $stmt = $pdo->prepare('SELECT id, name FROM roles WHERE id = ?'); + $stmt->execute([$roleId]); + $targetRole = $stmt->fetch(); + if (!$targetRole) { + errorResponse('Neplatná role'); + } + + // Non-admin nesmí přiřadit admin roli + if (!($authData['user']['is_admin'] ?? false) && $targetRole['name'] === 'admin') { + errorResponse('Nemáte oprávnění přiřadit roli administrátora', 403); + } + + // Update user + if (!empty($input['password'])) { + // Validate password length + if (strlen($input['password']) < 8) { + errorResponse('Heslo musí mít alespoň 8 znaků'); + } + + $passwordHash = password_hash($input['password'], PASSWORD_BCRYPT, ['cost' => BCRYPT_COST]); + + $stmt = $pdo->prepare(' + UPDATE users + SET username = ?, email = ?, password_hash = ?, + first_name = ?, last_name = ?, role_id = ?, + is_active = ?, password_changed_at = NOW() + WHERE id = ? + '); + $stmt->execute([$username, $email, $passwordHash, $firstName, $lastName, $roleId, $isActive, $userId]); + } else { + $stmt = $pdo->prepare(' + UPDATE users + SET username = ?, email = ?, first_name = ?, last_name = ?, role_id = ?, is_active = ? + WHERE id = ? + '); + $stmt->execute([$username, $email, $firstName, $lastName, $roleId, $isActive, $userId]); + } + + // Note: With JWT, user data is in the token - no session to update + + // Audit log + AuditLog::logUpdate('user', $userId, [ + 'username' => $existingUser['username'], + 'email' => $existingUser['email'], + 'first_name' => $existingUser['first_name'], + 'last_name' => $existingUser['last_name'], + 'role_id' => $existingUser['role_id'], + 'is_active' => $existingUser['is_active'], + ], [ + 'username' => $username, + 'email' => $email, + 'first_name' => $firstName, + 'last_name' => $lastName, + 'role_id' => $roleId, + 'is_active' => $isActive, + ], "Upraven uživatel '$username'"); + + successResponse(null, 'Uživatel byl úspěšně aktualizován'); +} + +/** + * DELETE - Delete user + */ +function handleDeleteUser(PDO $pdo, int $userId, int $currentUserId): void +{ + // Prevent self-deletion + if ($userId === $currentUserId) { + errorResponse('Nemůžete smazat svůj vlastní účet'); + } + + // Get user for audit log + $stmt = $pdo->prepare('SELECT username FROM users WHERE id = ?'); + $stmt->execute([$userId]); + $user = $stmt->fetch(); + + if (!$user) { + errorResponse('Uživatel nebyl nalezen', 404); + } + + // Delete related records first (refresh tokens for JWT auth) + $stmt = $pdo->prepare('DELETE FROM refresh_tokens WHERE user_id = ?'); + $stmt->execute([$userId]); + + // Delete user + $stmt = $pdo->prepare('DELETE FROM users WHERE id = ?'); + $stmt->execute([$userId]); + + // Audit log + AuditLog::logDelete('user', $userId, ['username' => $user['username']], "Smazán uživatel '{$user['username']}'"); + + successResponse(null, 'Uživatel byl úspěšně smazán'); +} diff --git a/dist/api/admin/invoices-pdf.php b/dist/api/admin/invoices-pdf.php new file mode 100644 index 0000000..0ee8c5f --- /dev/null +++ b/dist/api/admin/invoices-pdf.php @@ -0,0 +1,1113 @@ +prepare( + 'SELECT id, invoice_number, order_id, customer_id, status, currency, + vat_rate, apply_vat, payment_method, constant_symbol, + bank_name, bank_swift, bank_iban, bank_account, + issue_date, due_date, tax_date, paid_date, + issued_by, notes + FROM invoices WHERE id = ?' + ); + $stmt->execute([$id]); + $invoice = $stmt->fetch(); + if (!$invoice) { + header('Content-Type: application/json; charset=utf-8'); + errorResponse('Faktura nebyla nalezena', 404); + } + + // Polozky + $stmt = $pdo->prepare( + 'SELECT id, invoice_id, description, quantity, unit, unit_price, vat_rate, position + FROM invoice_items WHERE invoice_id = ? ORDER BY position' + ); + $stmt->execute([$id]); + $items = $stmt->fetchAll(); + + // Zakaznik + $customer = null; + if ($invoice['customer_id']) { + $stmt = $pdo->prepare( + 'SELECT id, name, street, city, postal_code, country, + company_id, vat_id, custom_fields + FROM customers WHERE id = ?' + ); + $stmt->execute([$invoice['customer_id']]); + $customer = $stmt->fetch(); + } + + // Firemni udaje + $stmt = $pdo->query( + 'SELECT id, company_name, company_id, vat_id, street, city, + postal_code, country, custom_fields, logo_data, + default_currency, default_vat_rate + FROM company_settings LIMIT 1' + ); + $settings = $stmt->fetch(); + + // Logo + $logoBase64 = ''; + $logoMime = 'image/png'; + if (!empty($settings['logo_data'])) { + $logoBase64 = base64_encode($settings['logo_data']); + $finfo = new finfo(FILEINFO_MIME_TYPE); + $detected = $finfo->buffer($settings['logo_data']); + $allowedMimes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp']; + if ($detected && in_array($detected, $allowedMimes)) { + $logoMime = $detected; + } + } + + // Helpery + $esc = function ($str) { + return htmlspecialchars($str ?? '', ENT_QUOTES, 'UTF-8'); + }; + + $formatNum = function ($number, $decimals = 2) { + return number_format((float)$number, $decimals, ',', "\xC2\xA0"); + }; + + $formatDate = function ($dateStr) { + if (!$dateStr) { + return ''; + } + $d = strtotime($dateStr); + return $d !== false ? date('d.m.Y', $d) : $dateStr; + }; + + // Preklady statickych textu + $translations = [ + 'cs' => [ + 'title' => 'Faktura', + 'heading' => 'FAKTURA - DAŇOVÝ DOKLAD č.', + 'supplier' => 'Dodavatel', + 'customer' => 'Odběratel', + 'bank' => 'Banka:', + 'swift' => 'SWIFT:', + 'iban' => 'IBAN:', + 'account_no' => 'Číslo účtu:', + 'var_symbol' => 'Variabilní s.:', + 'const_symbol' => 'Konstantní s.:', + 'order_no' => 'Objednávka č.:', + 'issue_date' => 'Datum vystavení:', + 'due_date' => 'Datum splatnosti:', + 'tax_date' => 'Datum uskutečnění plnění:', + 'payment_method' => 'Forma úhrady:', + 'billing' => 'Fakturujeme Vám za:', + 'col_no' => 'Č.', + 'col_desc' => 'Popis', + 'col_qty' => 'Množství', + 'col_unit_price' => 'Jedn. cena', + 'col_price' => 'Cena', + 'col_vat_pct' => '%DPH', + 'col_vat' => 'DPH', + 'col_total' => 'Celkem', + 'subtotal' => 'Mezisoučet:', + 'vat_label' => 'DPH', + 'total' => 'Celkem k úhradě', + 'amounts_in' => 'Částky jsou uvedeny v', + 'notes' => 'Poznámky', + 'issued_by' => 'Vystavil:', + 'notice' => 'Dovolujeme si Vás upozornit, že v případě nedodržení data splatnosti' + . ' uvedeného na faktuře Vám budeme účtovat úrok z prodlení v dohodnuté, resp.' + . ' zákonné výši a smluvní pokutu (byla-li sjednána).', + 'vat_recap' => 'Rekapitulace DPH v Kč:', + 'vat_base' => 'Základ v Kč', + 'vat_rate' => 'Sazba', + 'vat_amount' => 'DPH v Kč', + 'vat_with_total' => 'Celkem s DPH v Kč', + 'received_by' => 'Převzal:', + 'stamp' => 'Razítko:', + 'ico' => 'IČ: ', + 'dic' => 'DIČ: ', + ], + 'en' => [ + 'title' => 'Invoice', + 'heading' => 'INVOICE - TAX DOCUMENT No.', + 'supplier' => 'Supplier', + 'customer' => 'Customer', + 'bank' => 'Bank:', + 'swift' => 'SWIFT:', + 'iban' => 'IBAN:', + 'account_no' => 'Account No.:', + 'var_symbol' => 'Variable symbol:', + 'const_symbol' => 'Constant symbol:', + 'order_no' => 'Order No.:', + 'issue_date' => 'Issue date:', + 'due_date' => 'Due date:', + 'tax_date' => 'Tax point date:', + 'payment_method' => 'Payment method:', + 'billing' => 'We invoice you for:', + 'col_no' => 'No.', + 'col_desc' => 'Description', + 'col_qty' => 'Quantity', + 'col_unit_price' => 'Unit price', + 'col_price' => 'Price', + 'col_vat_pct' => 'VAT%', + 'col_vat' => 'VAT', + 'col_total' => 'Total', + 'subtotal' => 'Subtotal:', + 'vat_label' => 'VAT', + 'total' => 'Total to pay', + 'amounts_in' => 'Amounts are in', + 'notes' => 'Notes', + 'issued_by' => 'Issued by:', + 'notice' => 'Please note that in case of late payment, we will charge default interest' + . ' at the agreed or statutory rate and a contractual penalty (if agreed).', + 'vat_recap' => 'VAT recapitulation in CZK:', + 'vat_base' => 'Tax base in CZK', + 'vat_rate' => 'Rate', + 'vat_amount' => 'VAT in CZK', + 'vat_with_total' => 'Total incl. VAT in CZK', + 'received_by' => 'Received by:', + 'stamp' => 'Stamp:', + 'ico' => 'Reg. No.: ', + 'dic' => 'Tax ID: ', + ], + ]; + $t = $translations[$lang]; + + $currency = $invoice['currency'] ?? 'CZK'; + $applyVat = !empty($invoice['apply_vat']); + + // Vypocty + $vatSummary = []; + $subtotal = 0; + + foreach ($items as $item) { + $lineSubtotal = (float)$item['quantity'] * (float)$item['unit_price']; + $subtotal += $lineSubtotal; + $rate = (float)$item['vat_rate']; + + if (!isset($vatSummary[(string)$rate])) { + $vatSummary[(string)$rate] = ['base' => 0, 'vat' => 0]; + } + $vatSummary[(string)$rate]['base'] += $lineSubtotal; + if ($applyVat) { + $vatSummary[(string)$rate]['vat'] += $lineSubtotal * $rate / 100; + } + } + + $totalVat = 0; + foreach ($vatSummary as $data) { + $totalVat += $data['vat']; + } + $totalToPay = $subtotal + $totalVat; + + // Rekapitulace DPH - vzdy v CZK (cesky danovy doklad) + $isForeign = strtoupper($currency) !== 'CZK'; + $cnbRate = 1.0; + if ($isForeign) { + $cnb = CnbRates::getInstance(); + $issueDate = $invoice['issue_date'] ?? ''; + // Kurz CNB k datu vystaveni + $cnbRate = $cnb->toCzk(1.0, $currency, $issueDate); + } + + $vatRates = [21, 12, 0]; + $vatRecap = []; + foreach ($vatRates as $rate) { + $key = (string)(float)$rate; + $base = $vatSummary[$key]['base'] ?? 0; + $vat = $vatSummary[$key]['vat'] ?? 0; + $vatRecap[] = [ + 'rate' => $rate, + 'base' => round($base * $cnbRate, 2), + 'vat' => round($vat * $cnbRate, 2), + 'total' => round(($base + $vat) * $cnbRate, 2), + ]; + } + + // QR kod - SPAYD + $spaydParts = [ + 'SPD*1.0', + 'ACC:' . str_replace(' ', '', $invoice['bank_iban']), + 'AM:' . number_format($totalToPay, 2, '.', ''), + 'CC:' . $currency, + 'X-VS:' . $invoice['invoice_number'], + 'X-KS:' . ($invoice['constant_symbol'] ?: '0308'), + 'MSG:' . $t['title'] . ' ' . $invoice['invoice_number'], + ]; + $spaydString = implode('*', $spaydParts); + + $qrSvg = ''; + try { + $qrOptions = new QROptions([ + 'outputType' => QRCode::OUTPUT_MARKUP_SVG, + 'eccLevel' => QRCode::ECC_M, + 'scale' => 3, + 'addQuietzone' => true, + 'svgUseCssProperties' => false, + ]); + $qrCode = new QRCode($qrOptions); + $qrSvg = $qrCode->render($spaydString); + } catch (Exception $e) { + error_log('QR code generation failed: ' . $e->getMessage()); + } + + // Sanitizace HTML z Rich editoru + $cleanQuillHtml = function (string $html): string { + $allowedTags = '


    1. ' + . '

      ';
      +        $html = strip_tags($html, $allowedTags);
      +        // Odstranit event handlery (quoted i unquoted hodnoty)
      +        $html = preg_replace('/\s+on\w+\s*=\s*["\'][^"\']*["\']/i', '', $html);
      +        $html = preg_replace('/\s+on\w+\s*=\s*[^\s>]*/i', '', $html);
      +        // Odstranit javascript: v href a jinych atributech
      +        $html = preg_replace('/href\s*=\s*["\']?\s*javascript\s*:[^"\'>\s]*/i', 'href="#"', $html);
      +        $html = preg_replace('/\s+javascript\s*:/i', '', $html);
      +        $html = preg_replace_callback(
      +            '/(<[^>]*>)|( )/u',
      +            function ($m) {
      +                if (!empty($m[1])) {
      +                    return $m[1];
      +                }
      +                return ' ';
      +            },
      +            $html
      +        );
      +        $prev = null;
      +        while ($prev !== $html) {
      +            $prev = $html;
      +            $html = preg_replace_callback(
      +                '/]*)>(.*?)<\/span>\s*/su',
      +                fn ($m) => '' . $m[2],
      +                $html
      +            );
      +        }
      +        return $html;
      +    };
      +
      +    $indentCSS = '';
      +    for ($n = 1; $n <= 9; $n++) {
      +        $pad = $n * 3;
      +        $liPad = $n * 3 + 1.5;
      +        $indentCSS .= "  .ql-indent-{$n} { padding-left: {$pad}em; }\n";
      +        $indentCSS .= "  li.ql-indent-{$n} { padding-left: {$liPad}em; }\n";
      +    }
      +
      +    // Logo img
      +    $logoImg = '';
      +    if ($logoBase64) {
      +        $logoImg = '';
      +    }
      +
      +    // Polozky HTML
      +    $itemsHtml = '';
      +    foreach ($items as $i => $item) {
      +        $lineSubtotal = (float)$item['quantity'] * (float)$item['unit_price'];
      +        $vatRate = (float)$item['vat_rate'];
      +        $lineVat = $applyVat ? $lineSubtotal * $vatRate / 100 : 0;
      +        $lineTotal = $lineSubtotal + $lineVat;
      +        $rowNum = $i + 1;
      +
      +        $itemsHtml .= '
      +            ' . $rowNum . '
      +            ' . $esc($item['description']) . '
      +            ' . $formatNum(
      +                $item['quantity'],
      +                (floor((float)$item['quantity']) == (float)$item['quantity'] ? 0 : 2)
      +            ) . '
      +            ' . $formatNum($item['unit_price']) . '
      +            ' . $formatNum($lineSubtotal) . '
      +            ' . ($applyVat ? intval($vatRate) : 0) . '%
      +            ' . $formatNum($lineVat) . '
      +            ' . $formatNum($lineTotal) . '
      +        ';
      +    }
      +
      +    // DPH rekapitulace HTML
      +    $vatRecapHtml = '';
      +    foreach ($vatRecap as $vr) {
      +        $vatRecapHtml .= '
      +            ' . $formatNum($vr['base']) . '
      +            ' . intval($vr['rate']) . '%
      +            ' . $formatNum($vr['vat']) . '
      +            ' . $formatNum($vr['total']) . '
      +        ';
      +    }
      +
      +    $invoiceNumber = $esc($invoice['invoice_number']);
      +
      +    // Odberatel
      +    $custName = $customer ? $esc($customer['name']) : '';
      +    $custStreet = $customer ? $esc($customer['street'] ?? '') : '';
      +    $custCity = $customer ? trim(($customer['postal_code'] ?? '') . ' ' . ($customer['city'] ?? '')) : '';
      +    $custCountry = $customer ? $esc($customer['country'] ?? '') : '';
      +    $custIco = $customer ? $esc($customer['company_id'] ?? '') : '';
      +    $custDic = $customer ? $esc($customer['vat_id'] ?? '') : '';
      +
      +    // Dodavatel
      +    $suppName = $esc($settings['company_name'] ?? '');
      +    $suppStreet = $esc($settings['street'] ?? '');
      +    $suppCity = trim(($settings['postal_code'] ?? '') . ' ' . ($settings['city'] ?? ''));
      +    $suppCountry = $esc($settings['country'] ?? 'Česká republika');
      +    $suppIco = $esc($settings['company_id'] ?? '');
      +    $suppDic = $esc($settings['vat_id'] ?? '');
      +    $suppEmail = $esc($settings['Email'] ?? '');
      +    $suppWeb = $esc($settings['Website'] ?? '');
      +
      +    // Sestaveni adresovych radku (stejne jako v nabidkach)
      +    $buildAddressLines = function ($entity, $isSupplier) use ($t) {
      +        if (!$entity) {
      +            return ['name' => '', 'lines' => []];
      +        }
      +
      +        $nameKey = $isSupplier ? 'company_name' : 'name';
      +        $name = $entity[$nameKey] ?? '';
      +
      +        $cfData = [];
      +        $fieldOrder = null;
      +        $raw = $entity['custom_fields'] ?? null;
      +        if ($raw) {
      +            $parsed = is_string($raw) ? json_decode($raw, true) : $raw;
      +            if (isset($parsed['fields'])) {
      +                $cfData = $parsed['fields'] ?? [];
      +                $fieldOrder = $parsed['field_order'] ?? $parsed['fieldOrder'] ?? null;
      +            } elseif (is_array($parsed) && isset($parsed[0])) {
      +                $cfData = $parsed;
      +            }
      +        }
      +        // Zpetna kompatibilita - stare DB zaznamy maji PascalCase klice
      +        if (is_array($fieldOrder)) {
      +            $legacyMap = [
      +                'Name' => 'name', 'CompanyName' => 'company_name',
      +                'Street' => 'street', 'CityPostal' => 'city_postal',
      +                'Country' => 'country', 'CompanyId' => 'company_id', 'VatId' => 'vat_id',
      +            ];
      +            $fieldOrder = array_map(fn ($k) => $legacyMap[$k] ?? $k, $fieldOrder);
      +        }
      +
      +        $fieldMap = [];
      +        if ($name) {
      +            $fieldMap[$nameKey] = $name;
      +        }
      +        if (!empty($entity['street'])) {
      +            $fieldMap['street'] = $entity['street'];
      +        }
      +        $cityParts = array_filter([$entity['city'] ?? '', $entity['postal_code'] ?? '']);
      +        $cityPostal = trim(implode(' ', $cityParts));
      +        if ($cityPostal) {
      +            $fieldMap['city_postal'] = $cityPostal;
      +        }
      +        if (!empty($entity['country'])) {
      +            $fieldMap['country'] = $entity['country'];
      +        }
      +        if (!empty($entity['company_id'])) {
      +            $fieldMap['company_id'] = $t['ico'] . $entity['company_id'];
      +        }
      +        if (!empty($entity['vat_id'])) {
      +            $fieldMap['vat_id'] = $t['dic'] . $entity['vat_id'];
      +        }
      +
      +        foreach ($cfData as $i => $cf) {
      +            $cfName = trim($cf['name'] ?? '');
      +            $cfValue = trim($cf['value'] ?? '');
      +            $showLabel = $cf['showLabel'] ?? true;
      +            if ($cfValue) {
      +                $fieldMap["custom_{$i}"] = ($showLabel && $cfName) ? "{$cfName}: {$cfValue}" : $cfValue;
      +            }
      +        }
      +
      +        $lines = [];
      +        if (is_array($fieldOrder) && count($fieldOrder)) {
      +            foreach ($fieldOrder as $key) {
      +                if ($key === $nameKey) {
      +                    continue;
      +                }
      +                if (isset($fieldMap[$key])) {
      +                    $lines[] = $fieldMap[$key];
      +                }
      +            }
      +            foreach ($fieldMap as $key => $line) {
      +                if ($key === $nameKey) {
      +                    continue;
      +                }
      +                if (!in_array($key, $fieldOrder)) {
      +                    $lines[] = $line;
      +                }
      +            }
      +        } else {
      +            foreach ($fieldMap as $key => $line) {
      +                if ($key === $nameKey) {
      +                    continue;
      +                }
      +                $lines[] = $line;
      +            }
      +        }
      +
      +        return ['name' => $name, 'lines' => $lines];
      +    };
      +
      +    $supp = $buildAddressLines($settings, true);
      +    $cust = $buildAddressLines($customer, false);
      +
      +    $suppLinesHtml = '';
      +    foreach ($supp['lines'] as $line) {
      +        $suppLinesHtml .= '
      ' . $esc($line) . '
      '; + } + $custLinesHtml = ''; + foreach ($cust['lines'] as $line) { + $custLinesHtml .= '
      ' . $esc($line) . '
      '; + } + + // Objednavka + $orderNumber = ''; + $orderDate = ''; + if ($invoice['order_id']) { + $stmtOrder = $pdo->prepare('SELECT order_number, customer_order_number, created_at FROM orders WHERE id = ?'); + $stmtOrder->execute([$invoice['order_id']]); + $orderRow = $stmtOrder->fetch(); + if ($orderRow) { + $orderNumber = $esc($orderRow['customer_order_number'] ?: $orderRow['order_number']); + $orderDate = $formatDate($orderRow['created_at'] ?? ''); + } + } + + $html = ' + + + +' . $esc($t['title']) . ' ' . $invoiceNumber . ' + + + + +
      +
      + + +
      +
      + ' . ($logoImg ? '
      ' . $logoImg . '
      ' : '') . ' +
      +
      ' . $esc($t['heading']) . ' ' . $invoiceNumber . '
      +
      + +
      + + + +
      +
      +
      ' . $esc($t['supplier']) . '
      +
      ' . $esc($supp['name']) . '
      + ' . $suppLinesHtml . ' +
      +
      +
      ' . $esc($t['customer']) . '
      +
      ' . $esc($cust['name']) . '
      + ' . $custLinesHtml . ' +
      +
      + + +
      +
      +
      + ' . $esc($t['bank']) . ' ' . $esc($invoice['bank_name']) . '
      + ' . $esc($t['swift']) . ' ' . $esc($invoice['bank_swift']) . '
      + ' . $esc($t['iban']) . ' ' . $esc($invoice['bank_iban']) . '
      + ' . $esc($t['account_no']) . ' ' . $esc($invoice['bank_account']) . ' +
      +
      + ' . $esc($t['var_symbol']) . ' ' . $invoiceNumber . ' +     ' . $esc($t['const_symbol']) + . ' ' . $esc($invoice['constant_symbol']) . '
      + ' . ($orderNumber ? $esc($t['order_no']) . ' ' . $orderNumber : '') . ' +
      +
      +
      +
      +
      ' . $esc($t['issue_date']) . ' ' + . $esc($formatDate($invoice['issue_date'])) . '
      +
      ' . $esc($t['due_date']) . ' ' + . $esc($formatDate($invoice['due_date'])) . '
      +
      ' . $esc($t['tax_date']) . ' ' + . $esc($formatDate($invoice['tax_date'])) . '
      +
      ' . $esc($t['payment_method']) . ' ' + . $esc($invoice['payment_method']) . '
      +
      +
      +
      + + +
      ' . $esc($t['billing']) . '
      + + + + + + + + + + + + + + + ' . $itemsHtml . ' + +
      ' . $esc($t['col_no']) . '' . $esc($t['col_desc']) . '' . $esc($t['col_qty']) . '' . $esc($t['col_unit_price']) . '' . $esc($t['col_price']) . '' . $esc($t['col_vat_pct']) . '' . $esc($t['col_vat']) . '' . $esc($t['col_total']) . '
      + + +
      +
      +
      +
      + ' . $esc($t['subtotal']) . ' + ' . $formatNum($subtotal) . ' ' . $esc($currency) . ' +
      '; + + // DPH radky + if ($applyVat) { + foreach ($vatSummary as $rate => $data) { + if ($data['vat'] > 0) { + $html .= ' +
      + ' . $esc($t['vat_label']) . ' ' . intval((float)$rate) . '%: + ' . $formatNum($data['vat']) . ' ' . $esc($currency) . ' +
      '; + } + } + } + + $html .= ' +
      +
      + ' . $esc($t['total']) . ' + ' . $formatNum($totalToPay) . ' ' . $esc($currency) . ' +
      +
      ' . $esc($t['amounts_in']) . ' ' . $esc($currency) . '
      +
      +
      + + ' . (!empty(trim(strip_tags($invoice['notes'] ?? ''))) ? ' + +
      +
      ' . $esc($t['notes']) . '
      +
      ' . $cleanQuillHtml($invoice['notes']) . '
      +
      + ' : '') . ' + +
      + +
      + + +'; + + header('Content-Type: text/html; charset=utf-8'); + echo $html; + exit(); +} catch (PDOException $e) { + error_log('Invoices PDF API error: ' . $e->getMessage()); + header('Content-Type: application/json; charset=utf-8'); + if (DEBUG_MODE) { + errorResponse('Chyba databáze: ' . $e->getMessage(), 500); + } else { + errorResponse('Chyba generování PDF', 500); + } +} catch (Exception $e) { + error_log('Invoices PDF generation error: ' . $e->getMessage()); + header('Content-Type: application/json; charset=utf-8'); + if (DEBUG_MODE) { + errorResponse('Chyba PDF: ' . $e->getMessage(), 500); + } else { + errorResponse('Chyba generování PDF', 500); + } +} diff --git a/dist/api/admin/invoices.php b/dist/api/admin/invoices.php new file mode 100644 index 0000000..e2d9abe --- /dev/null +++ b/dist/api/admin/invoices.php @@ -0,0 +1,110 @@ +exec("UPDATE invoices SET status = 'overdue' WHERE status = 'issued' AND due_date < CURDATE()"); + } + + switch ($method) { + case 'GET': + requirePermission($authData, 'invoices.view'); + switch ($action) { + case 'detail': + if (!$id) { + errorResponse('ID faktury je povinné'); + } + handleGetDetail($pdo, $id); + break; + case 'next_number': + requirePermission($authData, 'invoices.create'); + handleGetNextNumber($pdo); + break; + case 'order_data': + requirePermission($authData, 'invoices.create'); + if (!$id) { + errorResponse('ID objednávky je povinné'); + } + handleGetOrderData($pdo, $id); + break; + case 'stats': + requirePermission($authData, 'invoices.view'); + handleGetStats($pdo); + break; + default: + handleGetList($pdo); + } + break; + + case 'POST': + requirePermission($authData, 'invoices.create'); + handleCreateInvoice($pdo, $authData); + break; + + case 'PUT': + requirePermission($authData, 'invoices.edit'); + if (!$id) { + errorResponse('ID faktury je povinné'); + } + handleUpdateInvoice($pdo, $id); + break; + + case 'DELETE': + requirePermission($authData, 'invoices.delete'); + if (!$id) { + errorResponse('ID faktury je povinné'); + } + handleDeleteInvoice($pdo, $id); + break; + + default: + errorResponse('Metoda není povolena', 405); + } +} catch (PDOException $e) { + error_log('Invoices API error: ' . $e->getMessage()); + if (DEBUG_MODE) { + errorResponse('Chyba databáze: ' . $e->getMessage(), 500); + } else { + errorResponse('Chyba databáze', 500); + } +} + +// --- Status transitions --- + +/** @return list */ diff --git a/dist/api/admin/leave-requests.php b/dist/api/admin/leave-requests.php new file mode 100644 index 0000000..acb35f4 --- /dev/null +++ b/dist/api/admin/leave-requests.php @@ -0,0 +1,80 @@ +getMessage()); + errorResponse('Chyba databáze', 500); +} + +// ============================================================================ +// Helper Functions +// ============================================================================ diff --git a/dist/api/admin/login.php b/dist/api/admin/login.php new file mode 100644 index 0000000..d54a231 --- /dev/null +++ b/dist/api/admin/login.php @@ -0,0 +1,180 @@ +setFailClosed(); +$rateLimiter->enforce('login', 10); + +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + errorResponse('Metoda není povolena', 405); +} + +$input = getJsonInput(); + +$username = trim($input['username'] ?? ''); +$password = $input['password'] ?? ''; +$remember = (bool) ($input['remember'] ?? false); + +if (empty($username)) { + errorResponse('Uživatelské jméno je povinné'); +} + +if (empty($password)) { + errorResponse('Heslo je povinné'); +} + +try { + $pdo = db(); + + $stmt = $pdo->prepare(' + SELECT u.id, u.username, u.email, u.password_hash, u.first_name, u.last_name, + u.role_id, u.failed_login_attempts, u.locked_until, u.is_active, u.totp_enabled, + r.name as role_name, r.display_name as role_display_name + FROM users u + LEFT JOIN roles r ON u.role_id = r.id + WHERE u.username = ? OR u.email = ? + '); + $stmt->execute([$username, $username]); + $user = $stmt->fetch(); + + if (!$user) { + AuditLog::logLoginFailed($username, 'invalid_credentials'); + errorResponse('Neplatné uživatelské jméno nebo heslo', 401); + } + + if (!$user['is_active']) { + AuditLog::logLoginFailed($username, 'account_deactivated'); + errorResponse('Neplatné uživatelské jméno nebo heslo', 401); + } + + if ($user['locked_until'] && strtotime($user['locked_until']) > time()) { + AuditLog::logLoginFailed($username, 'account_locked'); + errorResponse('Neplatné uživatelské jméno nebo heslo', 401); + } + + if (!password_verify($password, $user['password_hash'])) { + $attempts = $user['failed_login_attempts'] + 1; + $lockUntil = null; + + if ($attempts >= MAX_LOGIN_ATTEMPTS) { + $lockUntil = date('Y-m-d H:i:s', time() + (LOCKOUT_MINUTES * 60)); + $attempts = 0; // Reset after lockout + } + + $stmt = $pdo->prepare(' + UPDATE users SET failed_login_attempts = ?, locked_until = ? + WHERE id = ? + '); + $stmt->execute([$attempts, $lockUntil, $user['id']]); + + AuditLog::logLoginFailed($username, 'invalid_credentials'); + errorResponse('Neplatné uživatelské jméno nebo heslo', 401); + } + + $role = ['name' => $user['role_name'], 'display_name' => $user['role_display_name']]; + + // 2FA - neresit failed_attempts, az po overeni + if ($user['totp_enabled']) { + $loginToken = bin2hex(random_bytes(32)); + $hashedLoginToken = hash('sha256', $loginToken); + $loginTokenExpiry = date('Y-m-d H:i:s', time() + 300); + + $stmt = $pdo->prepare('DELETE FROM totp_login_tokens WHERE user_id = ? OR expires_at < NOW()'); + $stmt->execute([$user['id']]); + + $stmt = $pdo->prepare(' + INSERT INTO totp_login_tokens (user_id, token_hash, expires_at) + VALUES (?, ?, ?) + '); + $stmt->execute([$user['id'], $hashedLoginToken, $loginTokenExpiry]); + + successResponse([ + 'requires_2fa' => true, + 'login_token' => $loginToken, + ]); + } + + // Bez 2FA - reset failed attempts a pokracovat + $stmt = $pdo->prepare(' + UPDATE users SET failed_login_attempts = 0, locked_until = NULL, last_login = NOW() + WHERE id = ? + '); + $stmt->execute([$user['id']]); + + $userData = [ + 'id' => $user['id'], + 'username' => $user['username'], + 'email' => $user['email'], + 'first_name' => $user['first_name'], + 'last_name' => $user['last_name'], + 'role' => $role['name'] ?? null, + 'role_display' => $role['display_name'] ?? $role['name'] ?? null, + 'is_admin' => ($role['name'] ?? '') === 'admin', + ]; + + $accessToken = JWTAuth::generateAccessToken($userData); + JWTAuth::generateRefreshToken($user['id'], $remember); + AuditLog::logLogin($user['id'], $user['username']); + $require2FA = false; + try { + $stmt = $pdo->query("SELECT require_2fa FROM company_settings LIMIT 1"); + $require2FA = (bool) $stmt->fetchColumn(); + } catch (PDOException $e) { + } + + $permissions = JWTAuth::getUserPermissions($user['id']); + + successResponse([ + 'access_token' => $accessToken, + 'expires_in' => JWTAuth::getAccessTokenExpiry(), + 'user' => [ + 'id' => $userData['id'], + 'username' => $userData['username'], + 'email' => $userData['email'], + 'full_name' => trim($userData['first_name'] . ' ' . $userData['last_name']), + 'role' => $userData['role'], + 'role_display' => $userData['role_display'], + 'is_admin' => $userData['is_admin'], + 'permissions' => $permissions, + 'totp_enabled' => false, + 'require_2fa' => $require2FA, + ], + ], 'Přihlášení úspěšné'); +} catch (PDOException $e) { + error_log('Login PDO error: ' . $e->getMessage()); + errorResponse('Došlo k systémové chybě. Zkuste to prosím později.', 500); +} catch (Exception $e) { + error_log('Login error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine()); + errorResponse('Došlo k systémové chybě. Zkuste to prosím později.', 500); +} diff --git a/dist/api/admin/logout.php b/dist/api/admin/logout.php new file mode 100644 index 0000000..f93420c --- /dev/null +++ b/dist/api/admin/logout.php @@ -0,0 +1,51 @@ +enforce('logout', 30); + +// Only accept POST +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + errorResponse('Metoda není povolena', 405); +} + +// Get user from access token if available (for audit logging) +$authData = JWTAuth::optionalAuth(); + +// Log logout before revoking tokens +if ($authData) { + AuditLog::logLogout($authData['user_id'], $authData['user']['username'] ?? 'unknown'); +} + +// Revoke refresh token (from cookie) +$refreshToken = $_COOKIE['refresh_token'] ?? null; +if ($refreshToken) { + JWTAuth::revokeRefreshToken($refreshToken); +} + +successResponse(null, 'Odhlášení úspěšné'); diff --git a/dist/api/admin/offers-pdf.php b/dist/api/admin/offers-pdf.php new file mode 100644 index 0000000..9698276 --- /dev/null +++ b/dist/api/admin/offers-pdf.php @@ -0,0 +1,879 @@ +prepare( + 'SELECT id, quotation_number, project_code, customer_id, created_at, + valid_until, currency, language, vat_rate, apply_vat, + exchange_rate, scope_title, scope_description + FROM quotations WHERE id = ?' + ); + $stmt->execute([$id]); + $quotation = $stmt->fetch(); + if (!$quotation) { + header('Content-Type: application/json; charset=utf-8'); + errorResponse('Nabídka nebyla nalezena', 404); + } + + $customer = null; + if ($quotation['customer_id']) { + $stmt = $pdo->prepare( + 'SELECT id, name, street, city, postal_code, country, + company_id, vat_id, custom_fields + FROM customers WHERE id = ?' + ); + $stmt->execute([$quotation['customer_id']]); + $customer = $stmt->fetch(); + } + + $stmt = $pdo->prepare( + 'SELECT id, quotation_id, position, description, item_description, + quantity, unit, unit_price, is_included_in_total + FROM quotation_items WHERE quotation_id = ? ORDER BY position' + ); + $stmt->execute([$id]); + $items = $stmt->fetchAll(); + + $stmt = $pdo->prepare( + 'SELECT id, quotation_id, position, title, title_cz, content + FROM scope_sections WHERE quotation_id = ? ORDER BY position' + ); + $stmt->execute([$id]); + $sections = $stmt->fetchAll(); + + $stmt = $pdo->query( + 'SELECT id, company_name, company_id, vat_id, street, city, + postal_code, country, custom_fields, logo_data, + quotation_prefix, default_currency, default_vat_rate + FROM company_settings LIMIT 1' + ); + $settings = $stmt->fetch(); + + $logoBase64 = ''; + $logoMime = 'image/png'; + if (!empty($settings['logo_data'])) { + $logoBase64 = base64_encode($settings['logo_data']); + $finfo = new finfo(FILEINFO_MIME_TYPE); + $detected = $finfo->buffer($settings['logo_data']); + $allowedMimes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp']; + if ($detected && in_array($detected, $allowedMimes)) { + $logoMime = $detected; + } + } + + // --------------------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------------------- + $isCzech = ($quotation['language'] ?? 'EN') === 'CZ'; + $currency = $quotation['currency'] ?? 'EUR'; + + $translations = [ + 'title' => ['EN' => 'PRICE QUOTATION', 'CZ' => 'CENOVÁ NABÍDKA'], + 'scope_title' => ['EN' => 'SCOPE OF THE PROJECT', 'CZ' => 'ROZSAH PROJEKTU'], + 'valid_until' => ['EN' => 'Valid until', 'CZ' => 'Platnost do'], + 'customer' => ['EN' => 'Customer', 'CZ' => 'Zákazník'], + 'supplier' => ['EN' => 'Supplier', 'CZ' => 'Dodavatel'], + 'no' => ['EN' => 'N.', 'CZ' => 'Č.'], + 'description' => ['EN' => 'Description', 'CZ' => 'Popis'], + 'qty' => ['EN' => 'Qty', 'CZ' => 'Mn.'], + 'unit_price' => ['EN' => 'Unit Price', 'CZ' => 'Jedn. cena'], + 'included' => ['EN' => 'Included', 'CZ' => 'Zahrnuto'], + 'total' => ['EN' => 'Total', 'CZ' => 'Celkem'], + 'subtotal' => ['EN' => 'Subtotal', 'CZ' => 'Mezisoučet'], + 'vat' => ['EN' => 'VAT', 'CZ' => 'DPH'], + 'total_to_pay' => ['EN' => 'Total to pay', 'CZ' => 'Celkem k úhradě'], + 'exchange_rate' => ['EN' => 'Exchange rate', 'CZ' => 'Směnný kurz'], + 'ico' => ['EN' => 'ID', 'CZ' => 'IČO'], + 'dic' => ['EN' => 'VAT ID', 'CZ' => 'DIČ'], + 'yes' => ['EN' => 'Yes', 'CZ' => 'Ano'], + 'no_val' => ['EN' => 'No', 'CZ' => 'Ne'], + 'page' => ['EN' => 'Page', 'CZ' => 'Strana'], + 'of' => ['EN' => 'of', 'CZ' => 'z'], + ]; + + $lang = $isCzech ? 'CZ' : 'EN'; + $t = function ($key) use ($translations, $lang) { + return $translations[$key][$lang] ?? $key; + }; + + $formatNum = function ($number, $decimals, $decSep = ',', $thousandSep = "\xC2\xA0") { + $fixed = number_format(abs($number), $decimals, '.', ''); + $parts = explode('.', $fixed); + $intPart = preg_replace('/\B(?=(\d{3})+(?!\d))/', $thousandSep, $parts[0]); + $result = isset($parts[1]) ? $intPart . $decSep . $parts[1] : $intPart; + return $number < 0 ? '-' . $result : $result; + }; + + $formatCurrency = function ($amount) use ($currency, $formatNum) { + $n = floatval($amount); + switch ($currency) { + case 'EUR': + return $formatNum($n, 2, ',', "\xC2\xA0") . ' €'; + case 'USD': + return '$' . $formatNum($n, 2, '.', ','); + case 'CZK': + return $formatNum($n, 2, ',', "\xC2\xA0") . ' Kč'; + case 'GBP': + return '£' . $formatNum($n, 2, '.', ','); + default: + return $formatNum($n, 2, ',', "\xC2\xA0") . ' ' . $currency; + } + }; + + $formatDate = function ($dateStr) { + if (!$dateStr) { + return ''; + } + $d = strtotime($dateStr); + if ($d === false) { + return $dateStr; + } + return date('d.m.Y', $d); + }; + + $esc = function ($str) { + return htmlspecialchars($str ?? '', ENT_QUOTES, 'UTF-8'); + }; + + $buildAddressLines = function ($entity, $isSupplier) use ($t) { + if (!$entity) { + return ['name' => '', 'lines' => []]; + } + + $nameKey = $isSupplier ? 'company_name' : 'name'; + $name = $entity[$nameKey] ?? ''; + + $cfData = []; + $fieldOrder = null; + $raw = $entity['custom_fields'] ?? null; + if ($raw) { + $parsed = is_string($raw) ? json_decode($raw, true) : $raw; + if (isset($parsed['fields'])) { + $cfData = $parsed['fields'] ?? []; + $fieldOrder = $parsed['field_order'] ?? $parsed['fieldOrder'] ?? null; + } elseif (is_array($parsed) && isset($parsed[0])) { + $cfData = $parsed; + } + } + // Zpetna kompatibilita - stare DB zaznamy maji PascalCase klice + if (is_array($fieldOrder)) { + $legacyMap = [ + 'Name' => 'name', 'CompanyName' => 'company_name', + 'Street' => 'street', 'CityPostal' => 'city_postal', + 'Country' => 'country', 'CompanyId' => 'company_id', 'VatId' => 'vat_id', + ]; + $fieldOrder = array_map(fn ($k) => $legacyMap[$k] ?? $k, $fieldOrder); + } + + $fieldMap = []; + if ($name) { + $fieldMap[$nameKey] = $name; + } + if (!empty($entity['street'])) { + $fieldMap['street'] = $entity['street']; + } + $cityParts = array_filter([$entity['city'] ?? '', $entity['postal_code'] ?? '']); + $cityPostal = trim(implode(' ', $cityParts)); + if ($cityPostal) { + $fieldMap['city_postal'] = $cityPostal; + } + if (!empty($entity['country'])) { + $fieldMap['country'] = $entity['country']; + } + if (!empty($entity['company_id'])) { + $fieldMap['company_id'] = $t('ico') . ': ' . $entity['company_id']; + } + if (!empty($entity['vat_id'])) { + $fieldMap['vat_id'] = $t('dic') . ': ' . $entity['vat_id']; + } + + foreach ($cfData as $i => $cf) { + $cfName = trim($cf['name'] ?? ''); + $cfValue = trim($cf['value'] ?? ''); + $showLabel = $cf['showLabel'] ?? true; + if ($cfValue) { + $fieldMap["custom_{$i}"] = ($showLabel && $cfName) ? "{$cfName}: {$cfValue}" : $cfValue; + } + } + + $lines = []; + if (is_array($fieldOrder) && count($fieldOrder)) { + foreach ($fieldOrder as $key) { + if ($key === $nameKey) { + continue; + } + if (isset($fieldMap[$key])) { + $lines[] = $fieldMap[$key]; + } + } + foreach ($fieldMap as $key => $line) { + if ($key === $nameKey) { + continue; + } + if (!in_array($key, $fieldOrder)) { + $lines[] = $line; + } + } + } else { + foreach ($fieldMap as $key => $line) { + if ($key === $nameKey) { + continue; + } + $lines[] = $line; + } + } + + return ['name' => $name, 'lines' => $lines]; + }; + + $logoImg = ''; + if ($logoBase64) { + $logoImg = ''; + } + + $subtotal = 0; + foreach ($items as $item) { + $included = $item['is_included_in_total'] ?? 1; + if ($included) { + $subtotal += (floatval($item['quantity']) ?: 0) * (floatval($item['unit_price']) ?: 0); + } + } + + $applyVat = !empty($quotation['apply_vat']); + $vatRate = floatval($quotation['vat_rate'] ?? 21) ?: 21; + $vatAmount = $applyVat ? $subtotal * ($vatRate / 100) : 0; + $totalToPay = $subtotal + $vatAmount; + $exchangeRate = floatval($quotation['exchange_rate'] ?? 0); + $vatMode = $applyVat ? 'standard' : 'exempt'; + + $hasScopeContent = false; + foreach ($sections as $s) { + if (trim($s['content'] ?? '') || trim($s['title'] ?? '')) { + $hasScopeContent = true; + break; + } + } + + $sectionTitle = function ($section) use ($isCzech) { + if ($isCzech && !empty(trim($section['title_cz'] ?? ''))) { + return $section['title_cz']; + } + return $section['title'] ?? ''; + }; + + /** + * Merge adjacent tags with identical attributes produced by Quill. + * Quill sometimes splits a single word across multiple elements + * when cursor operations occur mid-word, creating spurious line-break + * opportunities in the PDF renderer. + */ + $cleanQuillHtml = function (string $html): string { + // Sanitizace - povolit jen bezpecne HTML tagy z Quill editoru + $allowedTags = '


        1. ' + . '

          ';
          +        $html = strip_tags($html, $allowedTags);
          +
          +        // Odstranit event handlery (quoted i unquoted hodnoty)
          +        $html = preg_replace('/\s+on\w+\s*=\s*["\'][^"\']*["\']/i', '', $html);
          +        $html = preg_replace('/\s+on\w+\s*=\s*[^\s>]*/i', '', $html);
          +        // Odstranit javascript: v href a jinych atributech
          +        $html = preg_replace('/href\s*=\s*["\']?\s*javascript\s*:[^"\'>\s]*/i', 'href="#"', $html);
          +        $html = preg_replace('/\s+javascript\s*:/i', '', $html);
          +
          +        // Replace   with regular spaces in text content (not inside tags)
          +        $html = preg_replace_callback(
          +            '/(<[^>]*>)|( )/u',
          +            function ($m) {
          +                if (!empty($m[1])) {
          +                    return $m[1];
          +                }
          +                return ' ';
          +            },
          +            $html
          +        );
          +
          +        // Merge adjacent spans with the same attributes
          +        $prev = null;
          +        while ($prev !== $html) {
          +            $prev = $html;
          +            $html = preg_replace_callback(
          +                '/]*)>(.*?)<\/span>\s*/su',
          +                fn ($m) => '' . $m[2],
          +                $html
          +            );
          +        }
          +        return $html;
          +    };
          +
          +    $cust = $buildAddressLines($customer, false);
          +    $supp = $buildAddressLines($settings, true);
          +
          +    $indentCSS = '';
          +    for ($n = 1; $n <= 9; $n++) {
          +        $pad = $n * 3;
          +        $liPad = $n * 3 + 1.5;
          +        $indentCSS .= "  .ql-indent-{$n} { padding-left: {$pad}em; }\n";
          +        $indentCSS .= "  li.ql-indent-{$n} { padding-left: {$liPad}em; }\n";
          +    }
          +
          +    $itemsHtml = '';
          +    foreach ($items as $i => $item) {
          +        $lineTotal = (floatval($item['quantity']) ?: 0) * (floatval($item['unit_price']) ?: 0);
          +        $subDesc = $item['item_description'] ?? '';
          +        $rowNum = $i + 1;
          +        $evenClass = ($i % 2 === 1) ? ' class="even"' : '';
          +        $itemsHtml .= '
          +            ' . $rowNum . '
          +            ' . $esc($item['description'] ?? '')
          +                . ($subDesc ? '
          ' . $esc($subDesc) . '
          ' : '') . ' + ' . $formatNum(floatval($item['quantity']) ?: 1, 0) + . (!empty(trim($item['unit'] ?? '')) ? ' / ' . $esc(trim($item['unit'])) : '') . ' + ' . $formatCurrency($item['unit_price'] ?? 0) . ' + ' . $formatCurrency($lineTotal) . ' + '; + } + + $totalsHtml = ''; + if ($vatMode === 'standard') { + $totalsHtml .= '
          +
          + ' . $esc($t('subtotal')) . ': + ' . $formatCurrency($subtotal) . ' +
          +
          + ' . $esc($t('vat')) . ' (' . intval($vatRate) . '%): + ' . $formatCurrency($vatAmount) . ' +
          +
          '; + } + $totalsHtml .= '
          + ' . $esc($t('total_to_pay')) . ' + ' . $formatCurrency($totalToPay) . ' +
          '; + if ($exchangeRate > 0) { + $totalsHtml .= '
          ' + . $esc($t('exchange_rate')) . ': ' . $formatNum($exchangeRate, 4) . '
          '; + } + + $scopeHtml = ''; + if ($hasScopeContent) { + $scopeHtml .= '
          '; + $scopeHtml .= ' +
          '; + + foreach ($sections as $section) { + $title = $sectionTitle($section); + $content = trim($section['content'] ?? ''); + if (!$title && !$content) { + continue; + } + $scopeHtml .= '
          '; + if ($title) { + $scopeHtml .= '
          ' . $esc($title) . '
          '; + } + if ($content) { + $scopeHtml .= '
          ' . $cleanQuillHtml($content) . '
          '; + } + $scopeHtml .= '
          '; + } + $scopeHtml .= '
          '; + } + + $custLinesHtml = ''; + foreach ($cust['lines'] as $line) { + $custLinesHtml .= '
          ' . $esc($line) . '
          '; + } + + $suppLinesHtml = ''; + foreach ($supp['lines'] as $line) { + $suppLinesHtml .= '
          ' . $esc($line) . '
          '; + } + + $pageLabel = $t('page'); + $ofLabel = $t('of'); + $quotationNumber = $esc($quotation['quotation_number'] ?? ''); + + // --------------------------------------------------------------------------- + // Build final HTML + // --------------------------------------------------------------------------- + $html = ' + + + +' . $quotationNumber . ' + + + + + + + +
          + + + + + + + +
          +
          ' . $logoImg . '
          +
          +
          + +
          + +
          +
          +
          ' . $esc($t('customer')) . '
          +
          ' . $esc($cust['name']) . '
          + ' . $custLinesHtml . ' +
          +
          +
          ' . $esc($t('supplier')) . '
          +
          ' . $esc($supp['name']) . '
          + ' . $suppLinesHtml . ' +
          +
          + + + + + + + + + + + + + ' . $itemsHtml . ' + +
          ' . $esc($t('no')) . '' . $esc($t('description')) . '' . $esc($t('qty')) . '' . $esc($t('unit_price')) . '' . $esc($t('total')) . '
          + +
          +
          + ' . $totalsHtml . ' +
          +
          +
          +
          +
          + + ' . $scopeHtml . ' + + +'; + + header('Content-Type: text/html; charset=utf-8'); + echo $html; + exit(); +} catch (PDOException $e) { + error_log('Offers PDF API error: ' . $e->getMessage()); + header('Content-Type: application/json; charset=utf-8'); + if (DEBUG_MODE) { + errorResponse('Chyba databáze: ' . $e->getMessage(), 500); + } else { + errorResponse('Chyba generování PDF', 500); + } +} catch (Exception $e) { + error_log('Offers PDF generation error: ' . $e->getMessage()); + header('Content-Type: application/json; charset=utf-8'); + if (DEBUG_MODE) { + errorResponse('Chyba PDF: ' . $e->getMessage(), 500); + } else { + errorResponse('Chyba generování PDF', 500); + } +} diff --git a/dist/api/admin/offers-templates.php b/dist/api/admin/offers-templates.php new file mode 100644 index 0000000..b27b0f8 --- /dev/null +++ b/dist/api/admin/offers-templates.php @@ -0,0 +1,101 @@ +getMessage()); + if (DEBUG_MODE) { + errorResponse('Chyba databáze: ' . $e->getMessage(), 500); + } else { + errorResponse('Chyba databáze', 500); + } +} + +// --- Item Templates --- diff --git a/dist/api/admin/offers.php b/dist/api/admin/offers.php new file mode 100644 index 0000000..3a9ce7d --- /dev/null +++ b/dist/api/admin/offers.php @@ -0,0 +1,96 @@ +getMessage()); + if (DEBUG_MODE) { + errorResponse('Chyba databáze: ' . $e->getMessage(), 500); + } else { + errorResponse('Chyba databáze', 500); + } +} diff --git a/dist/api/admin/orders.php b/dist/api/admin/orders.php new file mode 100644 index 0000000..62a2039 --- /dev/null +++ b/dist/api/admin/orders.php @@ -0,0 +1,92 @@ +getMessage()); + if (DEBUG_MODE) { + errorResponse('Chyba databáze: ' . $e->getMessage(), 500); + } else { + errorResponse('Chyba databáze', 500); + } +} + +// --- Valid status transitions --- + +/** @return list */ diff --git a/dist/api/admin/profile.php b/dist/api/admin/profile.php new file mode 100644 index 0000000..91ceaa2 --- /dev/null +++ b/dist/api/admin/profile.php @@ -0,0 +1,117 @@ +prepare(' + SELECT id, username, email, first_name, last_name, role_id, is_active, + last_login, created_at + FROM users WHERE id = ? + '); + $stmt->execute([$userId]); + $existingUser = $stmt->fetch(); + + if (!$existingUser) { + errorResponse('Uživatel nebyl nalezen', 404); + } + + $input = getJsonInput(); + + $username = isset($input['username']) ? sanitize($input['username']) : $existingUser['username']; + $email = isset($input['email']) ? sanitize($input['email']) : $existingUser['email']; + $firstName = isset($input['first_name']) ? sanitize($input['first_name']) : $existingUser['first_name']; + $lastName = isset($input['last_name']) ? sanitize($input['last_name']) : $existingUser['last_name']; + + // Validate email format + if (!isValidEmail($email)) { + errorResponse('Neplatný formát e-mailu'); + } + + // Check username uniqueness (excluding current user) + $stmt = $pdo->prepare('SELECT id FROM users WHERE username = ? AND id != ?'); + $stmt->execute([$username, $userId]); + if ($stmt->fetch()) { + errorResponse('Uživatelské jméno již existuje'); + } + + // Check email uniqueness (excluding current user) + $stmt = $pdo->prepare('SELECT id FROM users WHERE email = ? AND id != ?'); + $stmt->execute([$email, $userId]); + if ($stmt->fetch()) { + errorResponse('E-mail již existuje'); + } + + // Update user + if (!empty($input['password'])) { + // Validate password length + if (strlen($input['password']) < 8) { + errorResponse('Heslo musí mít alespoň 8 znaků'); + } + + $passwordHash = password_hash($input['password'], PASSWORD_BCRYPT, ['cost' => BCRYPT_COST]); + + $stmt = $pdo->prepare(' + UPDATE users + SET username = ?, email = ?, password_hash = ?, first_name = ?, last_name = ?, password_changed_at = NOW() + WHERE id = ? + '); + $stmt->execute([$username, $email, $passwordHash, $firstName, $lastName, $userId]); + } else { + $stmt = $pdo->prepare(' + UPDATE users + SET username = ?, email = ?, first_name = ?, last_name = ? + WHERE id = ? + '); + $stmt->execute([$username, $email, $firstName, $lastName, $userId]); + } + + // Audit log + AuditLog::logUpdate('user', $userId, [ + 'username' => $existingUser['username'], + 'email' => $existingUser['email'], + 'first_name' => $existingUser['first_name'], + 'last_name' => $existingUser['last_name'], + ], [ + 'username' => $username, + 'email' => $email, + 'first_name' => $firstName, + 'last_name' => $lastName, + ], 'Uživatel aktualizoval svůj profil'); + + successResponse(null, 'Profil byl úspěšně aktualizován'); +} catch (PDOException $e) { + error_log('Profile API error: ' . $e->getMessage()); + errorResponse('Chyba databáze', 500); +} diff --git a/dist/api/admin/projects.php b/dist/api/admin/projects.php new file mode 100644 index 0000000..0fb8de4 --- /dev/null +++ b/dist/api/admin/projects.php @@ -0,0 +1,115 @@ +getMessage()); + if (DEBUG_MODE) { + errorResponse('Chyba databáze: ' . $e->getMessage(), 500); + } else { + errorResponse('Chyba databáze', 500); + } +} + +// --- Number generation --- diff --git a/dist/api/admin/received-invoices.php b/dist/api/admin/received-invoices.php new file mode 100644 index 0000000..a5aac2f --- /dev/null +++ b/dist/api/admin/received-invoices.php @@ -0,0 +1,97 @@ +getMessage()); + if (DEBUG_MODE) { + errorResponse('Chyba databáze: ' . $e->getMessage(), 500); + } else { + errorResponse('Chyba databáze', 500); + } +} + +// --- Allowed MIME types --- + +/** @return list */ diff --git a/dist/api/admin/refresh.php b/dist/api/admin/refresh.php new file mode 100644 index 0000000..1355dc4 --- /dev/null +++ b/dist/api/admin/refresh.php @@ -0,0 +1,59 @@ +enforce('refresh', 30); + +// Check for refresh token in cookie +if (!isset($_COOKIE['refresh_token'])) { + errorResponse('No refresh token', 401); +} + +// Attempt to refresh tokens +$result = JWTAuth::refreshTokens(); + +if (!$result) { + errorResponse('Invalid or expired refresh token', 401); +} + +// Add 2FA info to user data +try { + $pdo = db(); + $stmt = $pdo->prepare('SELECT totp_enabled FROM users WHERE id = ?'); + $stmt->execute([$result['user']['id']]); + $u = $stmt->fetch(); + $result['user']['totp_enabled'] = (bool) ($u['totp_enabled'] ?? false); + + $stmt = $pdo->query("SELECT require_2fa FROM company_settings LIMIT 1"); + $result['user']['require_2fa'] = (bool) $stmt->fetchColumn(); +} catch (PDOException $e) { + $result['user']['totp_enabled'] = false; + $result['user']['require_2fa'] = false; +} + +successResponse([ + 'access_token' => $result['access_token'], + 'expires_in' => $result['expires_in'], + 'user' => $result['user'], +], 'Token refreshed'); diff --git a/dist/api/admin/roles.php b/dist/api/admin/roles.php new file mode 100644 index 0000000..cd44259 --- /dev/null +++ b/dist/api/admin/roles.php @@ -0,0 +1,67 @@ +getMessage()); + errorResponse('Chyba databáze', 500); +} diff --git a/dist/api/admin/session.php b/dist/api/admin/session.php new file mode 100644 index 0000000..012e94a --- /dev/null +++ b/dist/api/admin/session.php @@ -0,0 +1,94 @@ +enforce('session', 200); + +// Cleanup expired refresh tokenu (0.1% sance) +if (rand(1, 1000) === 1) { + try { + JWTAuth::cleanupExpiredTokens(); + } catch (Exception $e) { + } +} + +if (!in_array($_SERVER['REQUEST_METHOD'], ['GET', 'POST'])) { + errorResponse('Metoda není povolena', 405); +} + +$authData = JWTAuth::optionalAuth(); + +if ($authData) { + $userData = $authData['user']; + $userData['permissions'] = JWTAuth::getUserPermissions($authData['user_id']); + + $twoFA = get2FAInfo(db(), $authData['user_id']); + $userData['totp_enabled'] = $twoFA['totp_enabled']; + $userData['require_2fa'] = $twoFA['require_2fa']; + + successResponse([ + 'authenticated' => true, + 'user' => $userData, + 'access_token' => null, + 'expires_in' => null, + ]); +} + +$refreshToken = $_COOKIE['refresh_token'] ?? null; + +if ($refreshToken) { + $result = JWTAuth::refreshTokens(); + + if ($result) { + $twoFA = get2FAInfo(db(), $result['user']['id']); + $result['user']['totp_enabled'] = $twoFA['totp_enabled']; + $result['user']['require_2fa'] = $twoFA['require_2fa']; + + successResponse([ + 'authenticated' => true, + 'user' => $result['user'], + 'access_token' => $result['access_token'], + 'expires_in' => $result['expires_in'], + ]); + } +} + +successResponse([ + 'authenticated' => false, + 'user' => null, + 'access_token' => null, + 'expires_in' => null, +]); diff --git a/dist/api/admin/sessions.php b/dist/api/admin/sessions.php new file mode 100644 index 0000000..c58a6c1 --- /dev/null +++ b/dist/api/admin/sessions.php @@ -0,0 +1,67 @@ +getMessage()); + if (DEBUG_MODE) { + errorResponse('Chyba databáze: ' . $e->getMessage(), 500); + } else { + errorResponse('Chyba databáze', 500); + } +} diff --git a/dist/api/admin/totp.php b/dist/api/admin/totp.php new file mode 100644 index 0000000..8c732d6 --- /dev/null +++ b/dist/api/admin/totp.php @@ -0,0 +1,72 @@ +getMessage()); + errorResponse('Chyba databáze', 500); +} catch (Exception $e) { + error_log('TOTP error: ' . $e->getMessage()); + errorResponse('Došlo k chybě', 500); +} diff --git a/dist/api/admin/trips.php b/dist/api/admin/trips.php new file mode 100644 index 0000000..fb62153 --- /dev/null +++ b/dist/api/admin/trips.php @@ -0,0 +1,132 @@ +getMessage()); + errorResponse('Chyba databáze', 500); +} + +// ============================================================================ +// Helper Functions +// ============================================================================ diff --git a/dist/api/admin/users.php b/dist/api/admin/users.php new file mode 100644 index 0000000..4edbea5 --- /dev/null +++ b/dist/api/admin/users.php @@ -0,0 +1,73 @@ +getMessage()); + if (DEBUG_MODE) { + errorResponse('Chyba databáze: ' . $e->getMessage(), 500); + } else { + errorResponse('Chyba databáze', 500); + } +} diff --git a/dist/api/cleanup.php b/dist/api/cleanup.php new file mode 100644 index 0000000..f500e08 --- /dev/null +++ b/dist/api/cleanup.php @@ -0,0 +1,58 @@ + $rateLimitMaxAge) { + if (unlink($file)) { + $deleted++; + } else { + $errors++; + } + } + } + } +} + +echo "Rate limits: smazano {$deleted} souboru" . ($errors > 0 ? " ({$errors} chyb)" : '') . "\n"; + +// Audit log zaznamy starsi 90 dni +try { + $pdo = db(); + $stmt = $pdo->prepare( + 'DELETE FROM audit_logs WHERE created_at < DATE_SUB(NOW(), INTERVAL ? DAY)' + ); + $stmt->execute([$auditLogMaxDays]); + $auditDeleted = $stmt->rowCount(); + echo "Audit log: smazano {$auditDeleted} zaznamu starsich {$auditLogMaxDays} dni\n"; +} catch (PDOException $e) { + echo "Audit log: chyba - {$e->getMessage()}\n"; +} diff --git a/dist/api/config.php b/dist/api/config.php new file mode 100644 index 0000000..c7166e3 --- /dev/null +++ b/dist/api/config.php @@ -0,0 +1,25 @@ +prepare($sql); + $stmt->execute($params); + $records = $stmt->fetchAll(); + + enrichRecordsWithProjectLogs($pdo, $records); + + $stmt = $pdo->query( + "SELECT id, CONCAT(first_name, ' ', last_name) as name + FROM users WHERE is_active = 1 ORDER BY last_name" + ); + $users = $stmt->fetchAll(); + + $userTotals = calculateUserTotals($records); + $leaveBalances = getLeaveBalancesBatch( + $pdo, + array_keys($userTotals), + $year + ); + + $monthNum = (int)substr($month, 5, 2); + addFundDataToUserTotals($pdo, $userTotals, $year, $monthNum); + + successResponse([ + 'records' => $records, + 'users' => $users, + 'month' => $month, + 'user_totals' => $userTotals, + 'leave_balances' => $leaveBalances, + ]); +} + +function handleGetBalances(PDO $pdo): void +{ + $year = (int)($_GET['year'] ?? date('Y')); + + $stmt = $pdo->query( + "SELECT id, CONCAT(first_name, ' ', last_name) as name + FROM users WHERE is_active = 1 ORDER BY last_name" + ); + $users = $stmt->fetchAll(); + + $userIds = array_column($users, 'id'); + $batchBalances = getLeaveBalancesBatch($pdo, $userIds, $year); + + $balances = []; + foreach ($users as $user) { + $balances[$user['id']] = array_merge( + ['name' => $user['name']], + $batchBalances[$user['id']] + ); + } + + successResponse([ + 'users' => $users, + 'balances' => $balances, + 'year' => $year, + ]); +} + +function handleGetWorkFund(PDO $pdo): void +{ + $year = (int)($_GET['year'] ?? date('Y')); + $currentYear = (int)date('Y'); + $currentMonth = (int)date('m'); + + $maxMonth = ($year < $currentYear) ? 12 : (($year === $currentYear) ? $currentMonth : 0); + + if ($maxMonth === 0) { + successResponse(['months' => [], 'holidays' => [], 'year' => $year]); + return; + } + + $stmt = $pdo->query( + "SELECT id, CONCAT(first_name, ' ', last_name) as name + FROM users WHERE is_active = 1 ORDER BY last_name" + ); + $users = $stmt->fetchAll(); + + $startDate = sprintf('%04d-01-01', $year); + $endDate = sprintf('%04d-%02d-%02d', $year, $maxMonth, cal_days_in_month(CAL_GREGORIAN, $maxMonth, $year)); + + $stmt = $pdo->prepare( + 'SELECT id, user_id, shift_date, arrival_time, break_start, break_end, + departure_time, notes, project_id, leave_type, leave_hours + FROM attendance WHERE shift_date BETWEEN ? AND ? ORDER BY shift_date' + ); + $stmt->execute([$startDate, $endDate]); + $allRecords = $stmt->fetchAll(); + + $monthUserData = []; + foreach ($allRecords as $rec) { + $m = (int)date('m', strtotime($rec['shift_date'])); + $uid = $rec['user_id']; + if (!isset($monthUserData[$m][$uid])) { + $monthUserData[$m][$uid] = ['minutes' => 0, 'vacation' => 0, 'sick' => 0, 'holiday' => 0, 'unpaid' => 0]; + } + $lt = $rec['leave_type'] ?? 'work'; + $lh = (float)($rec['leave_hours'] ?? 0); + if ($lt === 'work') { + if ($rec['departure_time']) { + $monthUserData[$m][$uid]['minutes'] += calculateWorkMinutes($rec); + } + } elseif ($lt === 'vacation') { + $monthUserData[$m][$uid]['vacation'] += $lh; + } elseif ($lt === 'sick') { + $monthUserData[$m][$uid]['sick'] += $lh; + } elseif ($lt === 'holiday') { + $monthUserData[$m][$uid]['holiday'] += $lh; + } elseif ($lt === 'unpaid') { + $monthUserData[$m][$uid]['unpaid'] += $lh; + } + } + + $months = []; + for ($m = 1; $m <= $maxMonth; $m++) { + $fund = CzechHolidays::getMonthlyWorkFund($year, $m); + $businessDays = CzechHolidays::getBusinessDaysInMonth($year, $m); + $monthName = getCzechMonthName($m); + + $userStats = []; + foreach ($users as $user) { + $uid = $user['id']; + $ud = $monthUserData[$m][$uid] ?? [ + 'minutes' => 0, 'vacation' => 0, 'sick' => 0, + 'holiday' => 0, 'unpaid' => 0, + ]; + $worked = round($ud['minutes'] / 60, 1); + $leave = $ud['vacation'] + $ud['sick']; + $covered = $worked + $leave; + $missing = max(0, round($fund - $covered, 1)); + $overtime = max(0, round($covered - $fund, 1)); + + $userStats[$uid] = [ + 'name' => $user['name'], + 'worked' => $worked, + 'vacation' => $ud['vacation'], + 'sick' => $ud['sick'], + 'holiday' => $ud['holiday'], + 'unpaid' => $ud['unpaid'], + 'leave' => $leave, + 'covered' => $covered, + 'missing' => $missing, + 'overtime' => $overtime, + ]; + } + + $months[$m] = [ + 'month' => $m, + 'month_name' => $monthName, + 'fund' => $fund, + 'business_days' => $businessDays, + 'users' => $userStats, + ]; + } + + $userIds = array_column($users, 'id'); + $batchBalances = getLeaveBalancesBatch($pdo, $userIds, $year); + $balances = []; + foreach ($users as $user) { + $balances[$user['id']] = array_merge( + ['name' => $user['name']], + $batchBalances[$user['id']] + ); + } + + $holidays = CzechHolidays::getHolidays($year); + + successResponse([ + 'months' => $months, + 'balances' => $balances, + 'holidays' => $holidays, + 'users' => $users, + 'year' => $year, + ]); +} + +function handleGetLocation(PDO $pdo, int $recordId): void +{ + $stmt = $pdo->prepare(" + SELECT a.id, a.user_id, a.shift_date, a.arrival_time, + a.arrival_lat, a.arrival_lng, a.arrival_accuracy, a.arrival_address, + a.break_start, a.break_end, a.departure_time, + a.departure_lat, a.departure_lng, a.departure_accuracy, + a.departure_address, a.notes, a.project_id, + a.leave_type, a.leave_hours, a.created_at, + CONCAT(u.first_name, ' ', u.last_name) as user_name + FROM attendance a + JOIN users u ON a.user_id = u.id + WHERE a.id = ? + "); + $stmt->execute([$recordId]); + $record = $stmt->fetch(); + + if (!$record) { + errorResponse('Záznam nebyl nalezen', 404); + } + + successResponse(['record' => $record]); +} + +function handleGetUsers(PDO $pdo): void +{ + $stmt = $pdo->query( + "SELECT id, CONCAT(first_name, ' ', last_name) as name + FROM users WHERE is_active = 1 ORDER BY last_name" + ); + $users = $stmt->fetchAll(); + successResponse(['users' => $users]); +} + +function handleCreateAttendance(PDO $pdo): void +{ + $input = getJsonInput(); + + $userId = (int)($input['user_id'] ?? 0); + $shiftDate = $input['shift_date'] ?? ''; + $leaveType = $input['leave_type'] ?? 'work'; + $leaveHours = $input['leave_hours'] ?? null; + $notes = $input['notes'] ?? null; + + if (!$userId || !$shiftDate) { + errorResponse('Vyplňte zaměstnance a datum směny'); + } + + if ($leaveType !== 'work') { + $leaveHours = $leaveHours ?: 8; + $pdo->beginTransaction(); + try { + $stmt = $pdo->prepare(' + INSERT INTO attendance (user_id, shift_date, leave_type, leave_hours, notes) + VALUES (?, ?, ?, ?, ?) + '); + $stmt->execute([$userId, $shiftDate, $leaveType, $leaveHours, $notes]); + + updateLeaveBalance($pdo, $userId, $shiftDate, $leaveType, (float)$leaveHours); + $pdo->commit(); + } catch (\Throwable $e) { + $pdo->rollBack(); + throw $e; + } + } else { + $arrivalDate = $input['arrival_date'] ?? $shiftDate; + $arrivalTime = $input['arrival_time'] ?? null; + $breakStartDate = $input['break_start_date'] ?? null; + $breakStartTime = $input['break_start_time'] ?? null; + $breakEndDate = $input['break_end_date'] ?? null; + $breakEndTime = $input['break_end_time'] ?? null; + $departureDate = $input['departure_date'] ?? null; + $departureTime = $input['departure_time'] ?? null; + /** @var mixed $rawProjectId */ + $rawProjectId = $input['project_id'] ?? null; + $projectId = isset($input['project_id']) && $rawProjectId !== '' && $rawProjectId !== null + ? (int)$rawProjectId + : null; + + $arrival = $arrivalTime ? "{$arrivalDate} {$arrivalTime}:00" : null; + $breakStart = ($breakStartDate && $breakStartTime) ? "{$breakStartDate} {$breakStartTime}:00" : null; + $breakEnd = ($breakEndDate && $breakEndTime) ? "{$breakEndDate} {$breakEndTime}:00" : null; + $departure = ($departureDate && $departureTime) ? "{$departureDate} {$departureTime}:00" : null; + + $stmt = $pdo->prepare(" + INSERT INTO attendance + (user_id, shift_date, arrival_time, break_start, + break_end, departure_time, leave_type, notes, project_id) + VALUES (?, ?, ?, ?, ?, ?, 'work', ?, ?) + "); + $stmt->execute([$userId, $shiftDate, $arrival, $breakStart, $breakEnd, $departure, $notes, $projectId]); + } + + $newId = (int)$pdo->lastInsertId(); + + $projectLogs = $input['project_logs'] ?? []; + if (!empty($projectLogs) && $leaveType === 'work') { + $logStmt = $pdo->prepare( + 'INSERT INTO attendance_project_logs + (attendance_id, project_id, hours, minutes) + VALUES (?, ?, ?, ?)' + ); + foreach ($projectLogs as $log) { + $pid = (int)($log['project_id'] ?? 0); + if (!$pid) { + continue; + } + $h = (int)($log['hours'] ?? 0); + $m = (int)($log['minutes'] ?? 0); + if ($h === 0 && $m === 0) { + continue; + } + $logStmt->execute([$newId, $pid, $h, $m]); + } + } + + AuditLog::logCreate('attendance', $newId, $input, 'Admin vytvořil záznam docházky'); + + successResponse(['id' => $newId], 'Záznam byl vytvořen'); +} + +function handleBulkAttendance(PDO $pdo): void +{ + $input = getJsonInput(); + + $monthStr = $input['month'] ?? ''; + $userIds = $input['user_ids'] ?? []; + $arrivalTime = trim($input['arrival_time'] ?? '08:00'); + $departureTime = trim($input['departure_time'] ?? '16:30'); + $breakStartTime = trim($input['break_start_time'] ?? '12:00'); + $breakEndTime = trim($input['break_end_time'] ?? '12:30'); + + if (!$monthStr || !preg_match('/^\d{4}-\d{2}$/', $monthStr)) { + errorResponse('Měsíc je povinný (formát YYYY-MM)'); + } + + if (empty($userIds) || !is_array($userIds)) { + errorResponse('Vyberte alespoň jednoho zaměstnance'); + } + + $year = (int)substr($monthStr, 0, 4); + $month = (int)substr($monthStr, 5, 2); + + $holidays = CzechHolidays::getHolidays($year); + $daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year); + + $inserted = 0; + $skipped = 0; + + // Batch fetch existing records (eliminates N*M queries) + $dateFrom = sprintf('%04d-%02d-01', $year, $month); + $dateTo = sprintf('%04d-%02d-%02d', $year, $month, $daysInMonth); + $userIdInts = array_map('intval', $userIds); + $placeholders = implode(',', array_fill(0, count($userIdInts), '?')); + $existStmt = $pdo->prepare(" + SELECT user_id, shift_date FROM attendance + WHERE user_id IN ($placeholders) AND shift_date BETWEEN ? AND ? + "); + $existParams = array_merge($userIdInts, [$dateFrom, $dateTo]); + $existStmt->execute($existParams); + $existingRecords = []; + foreach ($existStmt->fetchAll() as $row) { + $existingRecords[$row['user_id'] . ':' . $row['shift_date']] = true; + } + + $holidayStmt = $pdo->prepare(" + INSERT INTO attendance (user_id, shift_date, leave_type, leave_hours, notes) + VALUES (?, ?, 'holiday', 8, 'Státní svátek') + "); + $workStmt = $pdo->prepare(' + INSERT INTO attendance (user_id, shift_date, arrival_time, departure_time, break_start, break_end) + VALUES (?, ?, ?, ?, ?, ?) + '); + + foreach ($userIdInts as $userId) { + for ($day = 1; $day <= $daysInMonth; $day++) { + $date = sprintf('%04d-%02d-%02d', $year, $month, $day); + $dayOfWeek = (int)date('N', strtotime($date)); + + if ($dayOfWeek > 5) { + continue; + } + + $isHoliday = in_array($date, $holidays, true); + + if (isset($existingRecords[$userId . ':' . $date])) { + $skipped++; + continue; + } + + if ($isHoliday) { + $holidayStmt->execute([$userId, $date]); + } else { + $arrival = $date . ' ' . $arrivalTime . ':00'; + $departure = $date . ' ' . $departureTime . ':00'; + $breakStart = $date . ' ' . $breakStartTime . ':00'; + $breakEnd = $date . ' ' . $breakEndTime . ':00'; + $workStmt->execute([$userId, $date, $arrival, $departure, $breakStart, $breakEnd]); + } + $inserted++; + } + } + + AuditLog::logCreate('attendance', 0, [ + 'month' => $monthStr, + 'user_ids' => $userIds, + 'inserted' => $inserted, + 'skipped' => $skipped, + ], "Admin hromadně vytvořil $inserted záznamů docházky pro měsíc $monthStr"); + + $msg = "Vytvořeno $inserted záznamů"; + if ($skipped > 0) { + $msg .= " ($skipped přeskočeno — již existují)"; + } + + successResponse(['inserted' => $inserted, 'skipped' => $skipped], $msg); +} + +function handleUpdateBalance(PDO $pdo): void +{ + $input = getJsonInput(); + + $userId = (int)($input['user_id'] ?? 0); + $year = (int)($input['year'] ?? date('Y')); + $actionType = $input['action_type'] ?? 'edit'; + + if (!$userId) { + errorResponse('ID uživatele je povinné'); + } + + $stmt = $pdo->prepare('SELECT id FROM leave_balances WHERE user_id = ? AND year = ?'); + $stmt->execute([$userId, $year]); + $exists = $stmt->fetch(); + + if ($actionType === 'reset') { + if ($exists) { + $stmt = $pdo->prepare( + 'UPDATE leave_balances + SET vacation_used = 0, sick_used = 0 + WHERE user_id = ? AND year = ?' + ); + $stmt->execute([$userId, $year]); + } + successResponse(null, 'Bilance byla resetována'); + } else { + $vacationTotal = (float)($input['vacation_total'] ?? 160); + $vacationUsed = (float)($input['vacation_used'] ?? 0); + $sickUsed = (float)($input['sick_used'] ?? 0); + + if ($exists) { + $stmt = $pdo->prepare( + 'UPDATE leave_balances + SET vacation_total = ?, vacation_used = ?, sick_used = ? + WHERE user_id = ? AND year = ?' + ); + $stmt->execute([$vacationTotal, $vacationUsed, $sickUsed, $userId, $year]); + } else { + $stmt = $pdo->prepare( + 'INSERT INTO leave_balances + (user_id, year, vacation_total, vacation_used, sick_used) + VALUES (?, ?, ?, ?, ?)' + ); + $stmt->execute([$userId, $year, $vacationTotal, $vacationUsed, $sickUsed]); + } + successResponse(null, 'Bilance byla aktualizována'); + } +} + +function handleUpdateAttendance(PDO $pdo, int $recordId): void +{ + $stmt = $pdo->prepare( + 'SELECT id, user_id, shift_date, arrival_time, break_start, break_end, + departure_time, notes, project_id, leave_type, leave_hours + FROM attendance WHERE id = ?' + ); + $stmt->execute([$recordId]); + $record = $stmt->fetch(); + + if (!$record) { + errorResponse('Záznam nebyl nalezen', 404); + } + + $input = getJsonInput(); + + $shiftDate = $input['shift_date'] ?? $record['shift_date']; + $leaveType = $input['leave_type'] ?? 'work'; + $leaveHours = $input['leave_hours'] ?? null; + $notes = $input['notes'] ?? null; + + $oldLeaveType = $record['leave_type'] ?? 'work'; + $oldLeaveHours = $record['leave_hours'] ?? 0; + + $pdo->beginTransaction(); + try { + if ($leaveType !== 'work') { + $leaveHours = $leaveHours ?: 8; + $stmt = $pdo->prepare(' + UPDATE attendance + SET shift_date = ?, leave_type = ?, leave_hours = ?, + arrival_time = NULL, break_start = NULL, + break_end = NULL, departure_time = NULL, notes = ? + WHERE id = ? + '); + $stmt->execute([$shiftDate, $leaveType, $leaveHours, $notes, $recordId]); + + if ($oldLeaveType !== 'work' && $oldLeaveHours > 0) { + updateLeaveBalance( + $pdo, + (int)$record['user_id'], + $record['shift_date'], + $oldLeaveType, + -(float)$oldLeaveHours + ); + } + updateLeaveBalance( + $pdo, + (int)$record['user_id'], + $shiftDate, + $leaveType, + (float)$leaveHours + ); + } else { + $arrivalDate = $input['arrival_date'] ?? $shiftDate; + $arrivalTime = $input['arrival_time'] ?? null; + $breakStartDate = $input['break_start_date'] ?? null; + $breakStartTime = $input['break_start_time'] ?? null; + $breakEndDate = $input['break_end_date'] ?? null; + $breakEndTime = $input['break_end_time'] ?? null; + $departureDate = $input['departure_date'] ?? null; + $departureTime = $input['departure_time'] ?? null; + /** @var mixed $rawProjectId */ + $rawProjectId = $input['project_id'] ?? null; + $projectId = isset($input['project_id']) && $rawProjectId !== '' && $rawProjectId !== null + ? (int)$rawProjectId + : null; + + $arrival = $arrivalTime ? "{$arrivalDate} {$arrivalTime}:00" : null; + $breakStart = $breakStartTime ? "{$breakStartDate} {$breakStartTime}:00" : null; + $breakEnd = $breakEndTime ? "{$breakEndDate} {$breakEndTime}:00" : null; + $departure = $departureTime ? "{$departureDate} {$departureTime}:00" : null; + + $stmt = $pdo->prepare(" + UPDATE attendance + SET shift_date = ?, arrival_time = ?, break_start = ?, + break_end = ?, departure_time = ?, + leave_type = 'work', leave_hours = NULL, + notes = ?, project_id = ? + WHERE id = ? + "); + $stmt->execute([$shiftDate, $arrival, $breakStart, $breakEnd, $departure, $notes, $projectId, $recordId]); + + if ($oldLeaveType !== 'work' && $oldLeaveHours > 0) { + updateLeaveBalance( + $pdo, + (int)$record['user_id'], + $record['shift_date'], + $oldLeaveType, + -(float)$oldLeaveHours + ); + } + } + $pdo->commit(); + } catch (\Throwable $e) { + $pdo->rollBack(); + throw $e; + } + + $projectLogs = $input['project_logs'] ?? null; + if ($projectLogs !== null) { + $stmt = $pdo->prepare('DELETE FROM attendance_project_logs WHERE attendance_id = ?'); + $stmt->execute([$recordId]); + + if (!empty($projectLogs) && ($input['leave_type'] ?? 'work') === 'work') { + $logStmt = $pdo->prepare( + 'INSERT INTO attendance_project_logs + (attendance_id, project_id, hours, minutes) + VALUES (?, ?, ?, ?)' + ); + foreach ($projectLogs as $log) { + $pid = (int)($log['project_id'] ?? 0); + if (!$pid) { + continue; + } + $h = (int)($log['hours'] ?? 0); + $m = (int)($log['minutes'] ?? 0); + if ($h === 0 && $m === 0) { + continue; + } + $logStmt->execute([$recordId, $pid, $h, $m]); + } + } + } + + AuditLog::logUpdate('attendance', $recordId, $record, $input, 'Admin upravil záznam docházky'); + + successResponse(null, 'Záznam byl aktualizován'); +} + +function handleDeleteAttendance(PDO $pdo, int $recordId): void +{ + $stmt = $pdo->prepare( + 'SELECT id, user_id, shift_date, leave_type, leave_hours + FROM attendance WHERE id = ?' + ); + $stmt->execute([$recordId]); + $record = $stmt->fetch(); + + if (!$record) { + errorResponse('Záznam nebyl nalezen', 404); + } + + $leaveType = $record['leave_type'] ?? 'work'; + $leaveHours = $record['leave_hours'] ?? 0; + if ($leaveType !== 'work' && $leaveHours > 0) { + updateLeaveBalance($pdo, (int)$record['user_id'], $record['shift_date'], $leaveType, -(float)$leaveHours); + } + + $stmt = $pdo->prepare('DELETE FROM attendance_project_logs WHERE attendance_id = ?'); + $stmt->execute([$recordId]); + + $stmt = $pdo->prepare('DELETE FROM attendance WHERE id = ?'); + $stmt->execute([$recordId]); + + AuditLog::logDelete('attendance', $recordId, $record, 'Admin smazal záznam docházky'); + + successResponse(null, 'Záznam byl smazán'); +} + +function handleGetProjectReport(PDO $pdo): void +{ + $yearParam = $_GET['year'] ?? null; + $monthParam = $_GET['month'] ?? null; + + if ($yearParam) { + $yearInt = (int)$yearParam; + $currentYear = (int)date('Y'); + $currentMonth = (int)date('m'); + $maxMonth = ($yearInt < $currentYear) ? 12 : (($yearInt === $currentYear) ? $currentMonth : 0); + + if ($maxMonth === 0) { + successResponse(['months' => [], 'year' => $yearInt]); + return; + } + + $startDate = sprintf('%04d-01-01', $yearInt); + $lastDay = cal_days_in_month(CAL_GREGORIAN, $maxMonth, $yearInt); + $endDate = sprintf('%04d-%02d-%02d', $yearInt, $maxMonth, $lastDay); + + $stmt = $pdo->prepare(" + SELECT a.user_id, a.id as attendance_id, a.shift_date, + a.arrival_time, a.departure_time, + a.break_start, a.break_end, + CONCAT(u.first_name, ' ', u.last_name) as user_name + FROM attendance a + JOIN users u ON a.user_id = u.id + WHERE a.shift_date BETWEEN ? AND ? + AND a.departure_time IS NOT NULL + AND (a.leave_type IS NULL OR a.leave_type = 'work') + ORDER BY u.last_name ASC + "); + $stmt->execute([$startDate, $endDate]); + $workRecords = $stmt->fetchAll(); + + $totalWork = []; + $attendanceIds = []; + foreach ($workRecords as $rec) { + $m = (int)date('m', strtotime($rec['shift_date'])); + $uid = $rec['user_id']; + $attendanceIds[] = $rec['attendance_id']; + if (!isset($totalWork[$m][$uid])) { + $totalWork[$m][$uid] = ['name' => $rec['user_name'], 'minutes' => 0]; + } + $totalWork[$m][$uid]['minutes'] += calculateWorkMinutes($rec); + } + + $loggedMinutes = []; + $monthData = []; + $projectIds = []; + + if (!empty($attendanceIds)) { + $placeholders = implode(',', array_fill(0, count($attendanceIds), '?')); + $stmt = $pdo->prepare(" + SELECT pl.project_id, pl.started_at, pl.ended_at, pl.hours, pl.minutes AS mins, a.user_id, a.shift_date + FROM attendance_project_logs pl + JOIN attendance a ON pl.attendance_id = a.id + WHERE pl.attendance_id IN ($placeholders) + AND (pl.hours IS NOT NULL OR pl.ended_at IS NOT NULL) + "); + $stmt->execute($attendanceIds); + $logs = $stmt->fetchAll(); + + foreach ($logs as $log) { + $m = (int)date('m', strtotime($log['shift_date'])); + $pid = (int)$log['project_id']; + $uid = $log['user_id']; + $projectIds[$pid] = true; + if ($log['hours'] !== null) { + $minutes = (int)$log['hours'] * 60 + (int)$log['mins']; + } else { + $minutes = max(0, (strtotime($log['ended_at']) - strtotime($log['started_at'])) / 60); + } + + if (!isset($monthData[$m][$pid][$uid])) { + $monthData[$m][$pid][$uid] = ['minutes' => 0]; + } + $monthData[$m][$pid][$uid]['minutes'] += $minutes; + + if (!isset($loggedMinutes[$m][$uid])) { + $loggedMinutes[$m][$uid] = 0; + } + $loggedMinutes[$m][$uid] += $minutes; + } + } + + // "Bez projektu" = total work - logged + foreach ($totalWork as $m => $users) { + foreach ($users as $uid => $ud) { + $logged = $loggedMinutes[$m][$uid] ?? 0; + $unlogged = $ud['minutes'] - $logged; + if ($unlogged > 1) { + if (!isset($monthData[$m][0][$uid])) { + $monthData[$m][0][$uid] = ['minutes' => 0]; + } + $monthData[$m][0][$uid]['minutes'] += $unlogged; + } + } + } + + $projectMap = []; + if (!empty($projectIds)) { + try { + $offersPdo = db(); + $ids = array_keys($projectIds); + $placeholders = implode(',', array_fill(0, count($ids), '?')); + $stmt2 = $offersPdo->prepare( + "SELECT id, project_number, name + FROM projects WHERE id IN ($placeholders)" + ); + $stmt2->execute($ids); + foreach ($stmt2->fetchAll() as $p) { + $projectMap[$p['id']] = $p; + } + } catch (\Exception $e) { + error_log('Failed to fetch project names for yearly report: ' . $e->getMessage()); + } + } + + $userNames = []; + foreach ($totalWork as $m => $users) { + foreach ($users as $uid => $ud) { + $userNames[$uid] = $ud['name']; + } + } + + $months = []; + for ($m = 1; $m <= $maxMonth; $m++) { + $projects = []; + if (isset($monthData[$m])) { + foreach ($monthData[$m] as $pid => $usersData) { + $proj = $pid ? ($projectMap[$pid] ?? null) : null; + $users = []; + $projectTotal = 0; + foreach ($usersData as $uid => $ud) { + $hours = round($ud['minutes'] / 60, 1); + $projectTotal += $hours; + $users[] = [ + 'user_id' => $uid, + 'user_name' => $userNames[$uid] ?? "User #$uid", + 'hours' => $hours, + ]; + } + usort($users, fn ($a, $b) => $b['hours'] <=> $a['hours']); + $projects[] = [ + 'project_id' => $pid ?: null, + 'project_number' => $proj ? $proj['project_number'] : null, + 'project_name' => $proj ? $proj['name'] : null, + 'hours' => round($projectTotal, 1), + 'users' => $users, + ]; + } + usort($projects, fn ($a, $b) => $b['hours'] <=> $a['hours']); + } + $months[$m] = [ + 'month' => $m, + 'month_name' => getCzechMonthName($m), + 'projects' => $projects, + ]; + } + + successResponse(['months' => $months, 'year' => $yearInt]); + return; + } + + // Single month mode + $month = $monthParam ?? date('Y-m'); + $startDate = "{$month}-01"; + $endDate = date('Y-m-t', strtotime($startDate)); + + $stmt = $pdo->prepare(" + SELECT a.user_id, a.id as attendance_id, a.arrival_time, a.departure_time, a.break_start, a.break_end, + CONCAT(u.first_name, ' ', u.last_name) as user_name + FROM attendance a + JOIN users u ON a.user_id = u.id + WHERE a.shift_date BETWEEN ? AND ? + AND a.departure_time IS NOT NULL + AND (a.leave_type IS NULL OR a.leave_type = 'work') + ORDER BY u.last_name ASC + "); + $stmt->execute([$startDate, $endDate]); + $workRecords = $stmt->fetchAll(); + + $userTotalMinutes = []; + $userNames = []; + $attendanceIds = []; + foreach ($workRecords as $rec) { + $uid = $rec['user_id']; + $attendanceIds[] = $rec['attendance_id']; + $userNames[$uid] = $rec['user_name']; + if (!isset($userTotalMinutes[$uid])) { + $userTotalMinutes[$uid] = 0; + } + $userTotalMinutes[$uid] += calculateWorkMinutes($rec); + } + + $aggregated = []; + $projectIds = []; + $userLoggedMinutes = []; + + if (!empty($attendanceIds)) { + $placeholders = implode(',', array_fill(0, count($attendanceIds), '?')); + $stmt = $pdo->prepare(" + SELECT pl.project_id, pl.started_at, pl.ended_at, pl.hours, pl.minutes AS mins, a.user_id + FROM attendance_project_logs pl + JOIN attendance a ON pl.attendance_id = a.id + WHERE pl.attendance_id IN ($placeholders) + AND (pl.hours IS NOT NULL OR pl.ended_at IS NOT NULL) + "); + $stmt->execute($attendanceIds); + $logs = $stmt->fetchAll(); + + foreach ($logs as $log) { + $uid = $log['user_id']; + $pid = (int)$log['project_id']; + $key = "{$uid}_{$pid}"; + $projectIds[$pid] = true; + if ($log['hours'] !== null) { + $minutes = (int)$log['hours'] * 60 + (int)$log['mins']; + } else { + $minutes = max(0, (strtotime($log['ended_at']) - strtotime($log['started_at'])) / 60); + } + + if (!isset($aggregated[$key])) { + $aggregated[$key] = ['user_id' => $uid, 'project_id' => $pid, 'minutes' => 0]; + } + $aggregated[$key]['minutes'] += $minutes; + + if (!isset($userLoggedMinutes[$uid])) { + $userLoggedMinutes[$uid] = 0; + } + $userLoggedMinutes[$uid] += $minutes; + } + } + + // "Bez projektu" per user + foreach ($userTotalMinutes as $uid => $total) { + $logged = $userLoggedMinutes[$uid] ?? 0; + $unlogged = $total - $logged; + if ($unlogged > 1) { + $key = "{$uid}_0"; + if (!isset($aggregated[$key])) { + $aggregated[$key] = ['user_id' => $uid, 'project_id' => 0, 'minutes' => 0]; + } + $aggregated[$key]['minutes'] += $unlogged; + } + } + + $projectMap = []; + if (!empty($projectIds)) { + try { + $offersPdo = db(); + $ids = array_keys($projectIds); + $placeholders = implode(',', array_fill(0, count($ids), '?')); + $stmt = $offersPdo->prepare("SELECT id, project_number, name FROM projects WHERE id IN ($placeholders)"); + $stmt->execute($ids); + foreach ($stmt->fetchAll() as $p) { + $projectMap[$p['id']] = $p; + } + } catch (\Exception $e) { + error_log('Failed to fetch project names for report: ' . $e->getMessage()); + } + } + + $report = []; + foreach ($aggregated as $item) { + $pid = $item['project_id']; + $proj = $pid ? ($projectMap[$pid] ?? null) : null; + $report[] = [ + 'user_id' => $item['user_id'], + 'user_name' => $userNames[$item['user_id']] ?? "User #{$item['user_id']}", + 'project_id' => $pid ?: null, + 'project_number' => $proj ? $proj['project_number'] : null, + 'project_name' => $proj ? $proj['name'] : null, + 'hours' => round($item['minutes'] / 60, 2), + ]; + } + + successResponse([ + 'report' => $report, + 'month' => $month, + ]); +} + +function handleGetPrint(PDO $pdo): void +{ + $month = validateMonth(); + $filterUserId = isset($_GET['user_id']) && $_GET['user_id'] !== '' ? (int)$_GET['user_id'] : null; + + $year = (int)substr($month, 0, 4); + $monthNum = (int)substr($month, 5, 2); + + $startDate = "{$month}-01"; + $endDate = date('Y-m-t', strtotime($startDate)); + + $stmt = $pdo->query( + "SELECT id, CONCAT(first_name, ' ', last_name) as name + FROM users WHERE is_active = 1 ORDER BY last_name" + ); + $users = $stmt->fetchAll(); + + $sql = " + SELECT a.id, a.user_id, a.shift_date, a.arrival_time, a.arrival_address, + a.break_start, a.break_end, a.departure_time, a.departure_address, + a.notes, a.project_id, a.leave_type, a.leave_hours, a.created_at, + CONCAT(u.first_name, ' ', u.last_name) as user_name + FROM attendance a + JOIN users u ON a.user_id = u.id + WHERE a.shift_date BETWEEN ? AND ? + "; + $params = [$startDate, $endDate]; + + if ($filterUserId) { + $sql .= ' AND a.user_id = ?'; + $params[] = $filterUserId; + } + + $sql .= ' ORDER BY u.last_name ASC, a.shift_date ASC'; + + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + $records = $stmt->fetchAll(); + + enrichRecordsWithProjectLogs($pdo, $records); + + $userTotals = calculateUserTotals($records, true); + $leaveBalances = getLeaveBalancesBatch($pdo, array_keys($userTotals), $year); + addFundDataToUserTotals($pdo, $userTotals, $year, $monthNum); + + $selectedUserName = ''; + if ($filterUserId) { + $stmt = $pdo->prepare("SELECT CONCAT(first_name, ' ', last_name) as name FROM users WHERE id = ?"); + $stmt->execute([$filterUserId]); + $user = $stmt->fetch(); + $selectedUserName = $user ? $user['name'] : ''; + } + + $fund = CzechHolidays::getMonthlyWorkFund($year, $monthNum); + + successResponse([ + 'user_totals' => $userTotals, + 'leave_balances' => $leaveBalances, + 'users' => $users, + 'month' => $month, + 'month_name' => getCzechMonthName($monthNum) . ' ' . $year, + 'selected_user' => $filterUserId, + 'selected_user_name' => $selectedUserName, + 'year' => $year, + 'fund' => $fund, + ]); +} diff --git a/dist/api/includes/AttendanceHelpers.php b/dist/api/includes/AttendanceHelpers.php new file mode 100644 index 0000000..1a4f770 --- /dev/null +++ b/dist/api/includes/AttendanceHelpers.php @@ -0,0 +1,386 @@ +query("SELECT NOW() AS now, CURDATE() AS today, YEAR(NOW()) AS y, MONTH(NOW()) AS m")->fetch(); + return [ + 'now' => $row['now'], + 'today' => $row['today'], + 'year' => (int)$row['y'], + 'month' => (int)$row['m'], + ]; +} + +function roundUpTo15Minutes(string $datetime): string +{ + $timestamp = strtotime($datetime); + $minutes = (int)date('i', $timestamp); + + $remainder = $minutes % 15; + + if ($remainder === 0) { + $roundedMinutes = $minutes; + } else { + $roundedMinutes = $minutes + (15 - $remainder); + } + + $baseTime = strtotime(date('Y-m-d H:00:00', $timestamp)); + return date('Y-m-d H:i:s', $baseTime + ($roundedMinutes * 60)); +} + +function roundDownTo15Minutes(string $datetime): string +{ + $timestamp = strtotime($datetime); + $minutes = (int)date('i', $timestamp); + + $remainder = $minutes % 15; + $roundedMinutes = $minutes - $remainder; + + $baseTime = strtotime(date('Y-m-d H:00:00', $timestamp)); + return date('Y-m-d H:i:s', $baseTime + ($roundedMinutes * 60)); +} + +function roundToNearest10Minutes(string $datetime): string +{ + $timestamp = strtotime($datetime); + $minutes = (int)date('i', $timestamp); + + $remainder = $minutes % 10; + + if ($remainder < 5) { + $roundedMinutes = $minutes - $remainder; + } else { + $roundedMinutes = $minutes + (10 - $remainder); + } + + $baseTime = strtotime(date('Y-m-d H:00:00', $timestamp)); + return date('Y-m-d H:i:s', $baseTime + ($roundedMinutes * 60)); +} + +/** + * @param array $record + */ +function calculateWorkMinutes(array $record): int +{ + if (!$record['arrival_time'] || !$record['departure_time']) { + return 0; + } + + $arrival = strtotime($record['arrival_time']); + $departure = strtotime($record['departure_time']); + $totalMinutes = ($departure - $arrival) / 60; + + if ($record['break_start'] && $record['break_end']) { + $breakStart = strtotime($record['break_start']); + $breakEnd = strtotime($record['break_end']); + $breakMinutes = ($breakEnd - $breakStart) / 60; + $totalMinutes -= $breakMinutes; + } + + return max(0, (int)$totalMinutes); +} + +/** + * @return array{vacation_total: float, vacation_used: float, vacation_remaining: float, sick_used: float} + */ +function getLeaveBalance(PDO $pdo, int $userId, ?int $year = null): array +{ + $year = $year ?: (int)date('Y'); + + $stmt = $pdo->prepare( + 'SELECT vacation_total, vacation_used, sick_used FROM leave_balances WHERE user_id = ? AND year = ?' + ); + $stmt->execute([$userId, $year]); + $balance = $stmt->fetch(); + + if (!$balance) { + return [ + 'vacation_total' => 160, + 'vacation_used' => 0, + 'vacation_remaining' => 160, + 'sick_used' => 0, + ]; + } + + return [ + 'vacation_total' => (float)$balance['vacation_total'], + 'vacation_used' => (float)$balance['vacation_used'], + 'vacation_remaining' => (float)$balance['vacation_total'] - (float)$balance['vacation_used'], + 'sick_used' => (float)$balance['sick_used'], + ]; +} + +/** + * Batch get leave balances for multiple users (eliminates N+1 queries) + * + * @param array $userIds + * @return array + */ +function getLeaveBalancesBatch(PDO $pdo, array $userIds, ?int $year = null): array +{ + $year = $year ?: (int)date('Y'); + $result = []; + + $default = [ + 'vacation_total' => 160, + 'vacation_used' => 0, + 'vacation_remaining' => 160, + 'sick_used' => 0, + ]; + + if (empty($userIds)) { + return $result; + } + + $placeholders = implode(',', array_fill(0, count($userIds), '?')); + $stmt = $pdo->prepare(" + SELECT user_id, vacation_total, vacation_used, sick_used + FROM leave_balances + WHERE user_id IN ($placeholders) AND year = ? + "); + $params = array_values($userIds); + $params[] = $year; + $stmt->execute($params); + $rows = $stmt->fetchAll(); + + $balanceMap = []; + foreach ($rows as $row) { + $balanceMap[$row['user_id']] = [ + 'vacation_total' => (float)$row['vacation_total'], + 'vacation_used' => (float)$row['vacation_used'], + 'vacation_remaining' => (float)$row['vacation_total'] - (float)$row['vacation_used'], + 'sick_used' => (float)$row['sick_used'], + ]; + } + + foreach ($userIds as $uid) { + $result[$uid] = $balanceMap[$uid] ?? $default; + } + + return $result; +} + +function updateLeaveBalance(PDO $pdo, int $userId, string $date, string $leaveType, float $hours): void +{ + if ($leaveType === 'work' || $leaveType === 'holiday' || $leaveType === 'unpaid') { + return; + } + + $year = (int)date('Y', strtotime($date)); + + $stmt = $pdo->prepare('SELECT id FROM leave_balances WHERE user_id = ? AND year = ?'); + $stmt->execute([$userId, $year]); + $balance = $stmt->fetch(); + + if (!$balance) { + $stmt = $pdo->prepare( + 'INSERT INTO leave_balances (user_id, year, vacation_total, vacation_used, sick_used) + VALUES (?, ?, 160, 0, 0)' + ); + $stmt->execute([$userId, $year]); + } + + if ($leaveType === 'vacation') { + $stmt = $pdo->prepare( + 'UPDATE leave_balances SET vacation_used = vacation_used + ? WHERE user_id = ? AND year = ?' + ); + $stmt->execute([$hours, $userId, $year]); + } elseif ($leaveType === 'sick') { + $stmt = $pdo->prepare('UPDATE leave_balances SET sick_used = sick_used + ? WHERE user_id = ? AND year = ?'); + $stmt->execute([$hours, $userId, $year]); + } +} + +function getCzechMonthName(int $month): string +{ + $months = [ + 1 => 'Leden', 2 => 'Únor', 3 => 'Březen', 4 => 'Duben', + 5 => 'Květen', 6 => 'Červen', 7 => 'Červenec', 8 => 'Srpen', + 9 => 'Září', 10 => 'Říjen', 11 => 'Listopad', 12 => 'Prosinec', + ]; + return $months[$month] ?? ''; +} + +function getCzechDayName(int $dayOfWeek): string +{ + $days = [ + 0 => 'neděle', 1 => 'pondělí', 2 => 'úterý', 3 => 'středa', + 4 => 'čtvrtek', 5 => 'pátek', 6 => 'sobota', + ]; + return $days[$dayOfWeek] ?? ''; +} + +/** + * Enrich attendance records with project logs and project names (in-place) + * + * @param array> $records + */ +function enrichRecordsWithProjectLogs(PDO $pdo, array &$records): void +{ + $recordIds = array_column($records, 'id'); + $recordProjectLogs = []; + if (!empty($recordIds)) { + $placeholders = implode(',', array_fill(0, count($recordIds), '?')); + $stmt = $pdo->prepare( + "SELECT id, attendance_id, project_id, started_at, ended_at, hours, minutes + FROM attendance_project_logs + WHERE attendance_id IN ($placeholders) ORDER BY started_at ASC" + ); + $stmt->execute($recordIds); + foreach ($stmt->fetchAll() as $log) { + $recordProjectLogs[$log['attendance_id']][] = $log; + } + } + + $projectIds = []; + foreach ($records as $rec) { + if ($rec['project_id']) { + $projectIds[$rec['project_id']] = $rec['project_id']; + } + } + foreach ($recordProjectLogs as $logs) { + foreach ($logs as $l) { + $projectIds[$l['project_id']] = $l['project_id']; + } + } + $projectNameMap = fetchProjectNames($projectIds); + + foreach ($records as &$rec) { + $rec['project_name'] = $rec['project_id'] ? ($projectNameMap[$rec['project_id']] ?? null) : null; + $logs = $recordProjectLogs[$rec['id']] ?? []; + foreach ($logs as &$l) { + $l['project_name'] = $projectNameMap[$l['project_id']] ?? null; + } + unset($l); + $rec['project_logs'] = $logs; + } + unset($rec); +} + +/** + * Calculate per-user totals from records array + * + * @param list> $records + * @return array> + */ +function calculateUserTotals(array $records, bool $includeRecords = false): array +{ + $userTotals = []; + foreach ($records as $record) { + $uid = $record['user_id']; + if (!isset($userTotals[$uid])) { + $userTotals[$uid] = [ + 'name' => $record['user_name'], + 'minutes' => 0, + 'working' => false, + 'vacation_hours' => 0, + 'sick_hours' => 0, + 'holiday_hours' => 0, + 'unpaid_hours' => 0, + ]; + if ($includeRecords) { + $userTotals[$uid]['records'] = []; + } + } + + $leaveType = $record['leave_type'] ?? 'work'; + $leaveHours = (float)($record['leave_hours'] ?? 0); + + if ($leaveType === 'vacation') { + $userTotals[$uid]['vacation_hours'] += $leaveHours; + } elseif ($leaveType === 'sick') { + $userTotals[$uid]['sick_hours'] += $leaveHours; + } elseif ($leaveType === 'holiday') { + $userTotals[$uid]['holiday_hours'] += $leaveHours; + } elseif ($leaveType === 'unpaid') { + $userTotals[$uid]['unpaid_hours'] += $leaveHours; + } else { + $userTotals[$uid]['minutes'] += calculateWorkMinutes($record); + } + + if ($includeRecords) { + $userTotals[$uid]['records'][] = $record; + } + + if ($record['arrival_time'] && !$record['departure_time']) { + $userTotals[$uid]['working'] = true; + } + } + return $userTotals; +} + +/** + * Add monthly fund data and "working now" status to user totals + * + * @param array> $userTotals + */ +function addFundDataToUserTotals(PDO $pdo, array &$userTotals, int $year, int $monthNum): void +{ + $fund = CzechHolidays::getMonthlyWorkFund($year, $monthNum); + $businessDays = CzechHolidays::getBusinessDaysInMonth($year, $monthNum); + + foreach ($userTotals as $uid => &$ut) { + $workedHours = round($ut['minutes'] / 60, 1); + $leaveHours = $ut['vacation_hours'] + $ut['sick_hours']; + $covered = $workedHours + $leaveHours; + $ut['fund'] = $fund; + $ut['business_days'] = $businessDays; + $ut['worked_hours'] = $workedHours; + $ut['covered'] = $covered; + $ut['missing'] = max(0, round($fund - $covered, 1)); + $ut['overtime'] = max(0, round($covered - $fund, 1)); + } + unset($ut); + + $stmt = $pdo->prepare(" + SELECT DISTINCT user_id FROM attendance + WHERE shift_date = CURDATE() + AND arrival_time IS NOT NULL + AND departure_time IS NULL + AND (leave_type IS NULL OR leave_type = 'work') + "); + $stmt->execute(); + $workingNow = $stmt->fetchAll(PDO::FETCH_COLUMN); + foreach ($workingNow as $uid) { + if (isset($userTotals[$uid])) { + $userTotals[$uid]['working'] = true; + } + } +} + +/** + * Fetch project names from offers DB + * + * @param array $projectIds + * @return array + */ +function fetchProjectNames(array $projectIds): array +{ + if (empty($projectIds)) { + return []; + } + try { + $pdo = db(); + $placeholders = implode(',', array_fill(0, count($projectIds), '?')); + $stmt = $pdo->prepare("SELECT id, project_number, name FROM projects WHERE id IN ($placeholders)"); + $stmt->execute(array_values($projectIds)); + $map = []; + foreach ($stmt->fetchAll() as $p) { + $map[$p['id']] = $p['project_number'] . ' – ' . $p['name']; + } + return $map; + } catch (\Exception $e) { + error_log('Failed to fetch project names: ' . $e->getMessage()); + return []; + } +} diff --git a/dist/api/includes/AuditLog.php b/dist/api/includes/AuditLog.php new file mode 100644 index 0000000..5c955b4 --- /dev/null +++ b/dist/api/includes/AuditLog.php @@ -0,0 +1,560 @@ +|null $oldValues Previous values (for updates) + * @param array|null $newValues New values (for updates/creates) + */ + public static function log( + string $action, + ?string $entityType = null, + ?int $entityId = null, + ?string $description = null, + ?array $oldValues = null, + ?array $newValues = null + ): void { + try { + $pdo = db(); + + $userId = self::$currentUserId; + $username = self::$currentUsername; + + $stmt = $pdo->prepare(' + INSERT INTO audit_logs ( + user_id, + username, + user_ip, + action, + entity_type, + entity_id, + description, + old_values, + new_values, + user_agent, + session_id + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + '); + + $stmt->execute([ + $userId, + $username, + getClientIp(), + $action, + $entityType, + $entityId, + $description, + $oldValues ? json_encode($oldValues, JSON_UNESCAPED_UNICODE) : null, + $newValues ? json_encode($newValues, JSON_UNESCAPED_UNICODE) : null, + substr($_SERVER['HTTP_USER_AGENT'] ?? 'unknown', 0, 500), + session_id() ?: null, + ]); + } catch (PDOException $e) { + error_log('AuditLog error: ' . $e->getMessage()); + } + } + + /** + * Log successful login + * + * @param int $userId User ID + * @param string $username Username + */ + public static function logLogin(int $userId, string $username): void + { + try { + $pdo = db(); + + $stmt = $pdo->prepare(' + INSERT INTO audit_logs ( + user_id, + username, + user_ip, + action, + entity_type, + entity_id, + description, + user_agent, + session_id + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + '); + + $stmt->execute([ + $userId, + $username, + getClientIp(), + self::ACTION_LOGIN, + 'user', + $userId, + "Přihlášení uživatele '$username'", + substr($_SERVER['HTTP_USER_AGENT'] ?? 'unknown', 0, 500), + session_id() ?: null, + ]); + } catch (PDOException $e) { + error_log('AuditLog login error: ' . $e->getMessage()); + } + } + + /** + * Log failed login attempt + * + * @param string $username Attempted username + * @param string $reason Failure reason + */ + public static function logLoginFailed(string $username, string $reason = 'invalid_credentials'): void + { + try { + $pdo = db(); + + $stmt = $pdo->prepare(' + INSERT INTO audit_logs ( + username, + user_ip, + action, + entity_type, + description, + user_agent, + session_id + ) VALUES (?, ?, ?, ?, ?, ?, ?) + '); + + $stmt->execute([ + $username, + getClientIp(), + self::ACTION_LOGIN_FAILED, + 'user', + "Neúspěšné přihlášení '$username': $reason", + substr($_SERVER['HTTP_USER_AGENT'] ?? 'unknown', 0, 500), + session_id() ?: null, + ]); + } catch (PDOException $e) { + error_log('AuditLog login failed error: ' . $e->getMessage()); + } + } + + /** + * Log logout + * + * @param int|null $userId User ID (optional, for JWT-based auth) + * @param string|null $username Username (optional, for JWT-based auth) + */ + public static function logLogout(?int $userId = null, ?string $username = null): void + { + if ($userId === null) { + return; + } + + try { + $pdo = db(); + + $stmt = $pdo->prepare(' + INSERT INTO audit_logs ( + user_id, + username, + user_ip, + action, + entity_type, + entity_id, + description, + user_agent + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + '); + + $stmt->execute([ + $userId, + $username, + getClientIp(), + self::ACTION_LOGOUT, + 'user', + $userId, + "Odhlášení uživatele '{$username}'", + substr($_SERVER['HTTP_USER_AGENT'] ?? 'unknown', 0, 500), + ]); + } catch (PDOException $e) { + error_log('AuditLog logout error: ' . $e->getMessage()); + } + } + + /** + * Log entity creation + * + * @param string $entityType Entity type + * @param int $entityId Entity ID + * @param array $data Created data + * @param string|null $description Optional description + */ + public static function logCreate( + string $entityType, + int $entityId, + array $data, + ?string $description = null + ): void { + // Remove sensitive fields from logged data + $safeData = self::sanitizeData($data); + + self::log( + self::ACTION_CREATE, + $entityType, + $entityId, + $description ?? "Vytvořen $entityType #$entityId", + null, + $safeData + ); + } + + /** + * Log entity update + * + * @param string $entityType Entity type + * @param int $entityId Entity ID + * @param array $oldData Old values + * @param array $newData New values + * @param string|null $description Optional description + */ + public static function logUpdate( + string $entityType, + int $entityId, + array $oldData, + array $newData, + ?string $description = null + ): void { + // Only log changed fields + $changes = self::getChanges($oldData, $newData); + + if (empty($changes['old']) && empty($changes['new'])) { + return; // No actual changes + } + + self::log( + self::ACTION_UPDATE, + $entityType, + $entityId, + $description ?? "Upraven $entityType #$entityId", + $changes['old'], + $changes['new'] + ); + } + + /** + * Log entity deletion + * + * @param string $entityType Entity type + * @param int $entityId Entity ID + * @param array|null $data Deleted entity data + * @param string|null $description Optional description + */ + public static function logDelete( + string $entityType, + int $entityId, + ?array $data = null, + ?string $description = null + ): void { + $safeData = $data ? self::sanitizeData($data) : null; + + self::log( + self::ACTION_DELETE, + $entityType, + $entityId, + $description ?? "Smazán $entityType #$entityId", + $safeData, + null + ); + } + + /** + * Log access denied + * + * @param string $resource Resource that was denied + * @param string|null $permission Required permission + */ + public static function logAccessDenied(string $resource, ?string $permission = null): void + { + $description = "Přístup odepřen k '$resource'"; + if ($permission) { + $description .= " (vyžaduje: $permission)"; + } + + self::log( + self::ACTION_ACCESS_DENIED, + null, + null, + $description + ); + } + + /** + * Get changes between old and new data + * + * @param array $oldData Old values + * @param array $newData New values + * @return array{old: array, new: array} + */ + private static function getChanges(array $oldData, array $newData): array + { + $oldData = self::sanitizeData($oldData); + $newData = self::sanitizeData($newData); + + $changedOld = []; + $changedNew = []; + + // Find changed fields + foreach ($newData as $key => $newValue) { + $oldValue = $oldData[$key] ?? null; + + if ($oldValue !== $newValue) { + $changedOld[$key] = $oldValue; + $changedNew[$key] = $newValue; + } + } + + // Find removed fields + foreach ($oldData as $key => $oldValue) { + if (!array_key_exists($key, $newData)) { + $changedOld[$key] = $oldValue; + $changedNew[$key] = null; + } + } + + return ['old' => $changedOld, 'new' => $changedNew]; + } + + /** + * Remove sensitive fields from data before logging + * + * @param array $data Data to sanitize + * @return array Sanitized data + */ + private static function sanitizeData(array $data): array + { + $sensitiveFields = [ + 'password', + 'password_hash', + 'token', + 'token_hash', + 'secret', + 'api_key', + 'private_key', + 'csrf_token', + ]; + + foreach ($sensitiveFields as $field) { + if (isset($data[$field])) { + $data[$field] = '[REDACTED]'; + } + } + + return $data; + } + + /** + * Get audit logs with filtering and pagination + * + * @param array $filters Filter options + * @param int $page Page number (1-based) + * @param int $perPage Items per page + * @return array{logs: list>, total: int, pages: int, page: int, per_page: int} + */ + public static function getLogs(array $filters = [], int $page = 1, int $perPage = 50): array + { + try { + $pdo = db(); + + $where = []; + $params = []; + + // Apply filters + if (!empty($filters['user_id'])) { + $where[] = 'user_id = ?'; + $params[] = $filters['user_id']; + } + + if (!empty($filters['username'])) { + $where[] = 'username LIKE ?'; + $params[] = '%' . $filters['username'] . '%'; + } + + if (!empty($filters['action'])) { + $where[] = 'action = ?'; + $params[] = $filters['action']; + } + + if (!empty($filters['entity_type'])) { + $where[] = 'entity_type = ?'; + $params[] = $filters['entity_type']; + } + + if (!empty($filters['ip'])) { + $where[] = 'user_ip LIKE ?'; + $params[] = '%' . $filters['ip'] . '%'; + } + + if (!empty($filters['date_from'])) { + $where[] = 'created_at >= ?'; + $params[] = $filters['date_from'] . ' 00:00:00'; + } + + if (!empty($filters['date_to'])) { + $where[] = 'created_at <= ?'; + $params[] = $filters['date_to'] . ' 23:59:59'; + } + + if (!empty($filters['search'])) { + $where[] = '(description LIKE ? OR username LIKE ?)'; + $searchTerm = '%' . $filters['search'] . '%'; + $params[] = $searchTerm; + $params[] = $searchTerm; + } + + $whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : ''; + + // Count total + $countSql = "SELECT COUNT(*) FROM audit_logs $whereClause"; + $stmt = $pdo->prepare($countSql); + $stmt->execute($params); + $total = (int) $stmt->fetchColumn(); + + // Calculate pagination + $pages = max(1, ceil($total / $perPage)); + $page = max(1, min($page, $pages)); + $offset = ($page - 1) * $perPage; + + // Get logs + $sql = " + SELECT id, user_id, username, user_ip, action, + entity_type, entity_id, description, + old_values, new_values, created_at + FROM audit_logs + $whereClause + ORDER BY created_at DESC + LIMIT $perPage OFFSET $offset + "; + + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + $logs = $stmt->fetchAll(); + + // Parse JSON fields + foreach ($logs as &$log) { + $log['old_values'] = $log['old_values'] ? json_decode($log['old_values'], true) : null; + $log['new_values'] = $log['new_values'] ? json_decode($log['new_values'], true) : null; + } + + return [ + 'logs' => $logs, + 'total' => $total, + 'pages' => $pages, + 'page' => $page, + 'per_page' => $perPage, + ]; + } catch (PDOException $e) { + error_log('AuditLog getLogs error: ' . $e->getMessage()); + return ['logs' => [], 'total' => 0, 'pages' => 0, 'page' => 1, 'per_page' => $perPage]; + } + } + + /** + * Get recent activity for a user + * + * @param int $userId User ID + * @param int $limit Number of records + * @return list> Recent logs + */ + public static function getUserActivity(int $userId, int $limit = 10): array + { + try { + $pdo = db(); + + $stmt = $pdo->prepare(' + SELECT id, user_id, username, user_ip, action, + entity_type, entity_id, description, + old_values, new_values, created_at + FROM audit_logs + WHERE user_id = ? + ORDER BY created_at DESC + LIMIT ? + '); + $stmt->execute([$userId, $limit]); + + return $stmt->fetchAll(); + } catch (PDOException $e) { + error_log('AuditLog getUserActivity error: ' . $e->getMessage()); + return []; + } + } + + /** + * Get entity history + * + * @param string $entityType Entity type + * @param int $entityId Entity ID + * @return list> Audit log history + */ + public static function getEntityHistory(string $entityType, int $entityId): array + { + try { + $pdo = db(); + + $stmt = $pdo->prepare(' + SELECT id, user_id, username, user_ip, action, + entity_type, entity_id, description, + old_values, new_values, created_at + FROM audit_logs + WHERE entity_type = ? AND entity_id = ? + ORDER BY created_at DESC + '); + $stmt->execute([$entityType, $entityId]); + + $logs = $stmt->fetchAll(); + + foreach ($logs as &$log) { + $log['old_values'] = $log['old_values'] ? json_decode($log['old_values'], true) : null; + $log['new_values'] = $log['new_values'] ? json_decode($log['new_values'], true) : null; + } + + return $logs; + } catch (PDOException $e) { + error_log('AuditLog getEntityHistory error: ' . $e->getMessage()); + return []; + } + } +} diff --git a/dist/api/includes/CnbRates.php b/dist/api/includes/CnbRates.php new file mode 100644 index 0000000..a468520 --- /dev/null +++ b/dist/api/includes/CnbRates.php @@ -0,0 +1,205 @@ +toCzk(1000.0, 'EUR', '2026-03-01'); + */ + +declare(strict_types=1); + +class CnbRates +{ + private const API_URL = 'https://api.cnb.cz/cnbapi/exrates/daily'; + private const CACHE_FILE = 'cnb_rates_cache.json'; + + // Kurzy starsi nez dnesek se nemeni, cachujem navzdy. + // Dnesni kurz cachujeme na 6 hodin (muze se behem dne aktualizovat). + private const TODAY_CACHE_TTL = 21600; + + /** + * In-memory cache: date => currency => {rate, amount} + * @var array> + */ + private array $ratesByDate = []; + + private static ?CnbRates $instance = null; + + public static function getInstance(): self + { + if (self::$instance === null) { + self::$instance = new self(); + } + return self::$instance; + } + + private function __construct() + { + $this->loadCache(); + } + + /** + * Prevede castku na CZK dle kurzu platneho k danemu datu. + */ + public function toCzk(float $amount, string $currency, string $date = ''): float + { + if ($currency === 'CZK') { + return $amount; + } + + $rates = $this->getRatesForDate($date ?: date('Y-m-d')); + if (!isset($rates[$currency])) { + return $amount; + } + + $info = $rates[$currency]; + return $amount * ($info['rate'] / $info['amount']); + } + + /** + * Secte pole [{amount, currency, date?}] do jedne CZK castky. + * Kazda polozka muze mit vlastni datum pro kurz. + * + * @param array $items + */ + public function sumToCzk(array $items): float + { + $total = 0.0; + foreach ($items as $item) { + $total += $this->toCzk( + (float) $item['amount'], + (string) $item['currency'], + (string) ($item['date'] ?? '') + ); + } + return round($total, 2); + } + + /** + * @return array + */ + private function getRatesForDate(string $date): array + { + if (isset($this->ratesByDate[$date])) { + return $this->ratesByDate[$date]; + } + + $rates = $this->fetchFromApi($date); + if ($rates !== []) { + $this->ratesByDate[$date] = $rates; + $this->saveCache(); + } + + return $rates; + } + + // --- Cache --- + + private function getCachePath(): string + { + return sys_get_temp_dir() . DIRECTORY_SEPARATOR . self::CACHE_FILE; + } + + private function loadCache(): void + { + $path = $this->getCachePath(); + if (!file_exists($path)) { + return; + } + + $content = file_get_contents($path); + if ($content === false) { + return; + } + + $data = json_decode($content, true); + if (!is_array($data)) { + return; + } + + $today = date('Y-m-d'); + foreach ($data as $date => $entry) { + if (!is_array($entry) || !isset($entry['rates'], $entry['fetched_at'])) { + continue; + } + // Dnesni kurz expiruje po TTL, starsi zustavaji navzdy + if ($date === $today) { + $age = time() - (int) $entry['fetched_at']; + if ($age > self::TODAY_CACHE_TTL) { + continue; + } + } + $this->ratesByDate[$date] = $entry['rates']; + } + } + + private function saveCache(): void + { + $path = $this->getCachePath(); + + // Nacist existujici cache a mergovat + $existing = []; + if (file_exists($path)) { + $content = file_get_contents($path); + if ($content !== false) { + $decoded = json_decode($content, true); + if (is_array($decoded)) { + $existing = $decoded; + } + } + } + + $now = time(); + foreach ($this->ratesByDate as $date => $rates) { + // Neprepisuj existujici pokud uz tam je (zachovej fetched_at) + if (!isset($existing[$date])) { + $existing[$date] = [ + 'rates' => $rates, + 'fetched_at' => $now, + ]; + } + } + + $json = json_encode($existing, JSON_THROW_ON_ERROR); + file_put_contents($path, $json, LOCK_EX); + } + + // --- API --- + + /** + * @return array + */ + private function fetchFromApi(string $date): array + { + $url = self::API_URL . '?lang=EN&date=' . urlencode($date); + + $context = stream_context_create([ + 'http' => ['timeout' => 5], + ]); + + $response = @file_get_contents($url, false, $context); + if ($response === false) { + return []; + } + + $data = json_decode($response, true); + if (!is_array($data) || !isset($data['rates'])) { + return []; + } + + $rates = []; + foreach ($data['rates'] as $entry) { + if (!isset($entry['currencyCode'], $entry['rate'], $entry['amount'])) { + continue; + } + $rates[$entry['currencyCode']] = [ + 'rate' => (float) $entry['rate'], + 'amount' => (int) $entry['amount'], + ]; + } + + return $rates; + } +} diff --git a/dist/api/includes/CzechHolidays.php b/dist/api/includes/CzechHolidays.php new file mode 100644 index 0000000..1620c3f --- /dev/null +++ b/dist/api/includes/CzechHolidays.php @@ -0,0 +1,117 @@ +> Static cache for holidays by year */ + private static array $holidayCache = []; + + /** + * Get all Czech public holidays for a given year. + * Returns array of 'Y-m-d' strings (11 fixed + 2 Easter-based). + * Results are cached per-request to avoid recalculation. + * + * @return list + */ + public static function getHolidays(int $year): array + { + if (isset(self::$holidayCache[$year])) { + return self::$holidayCache[$year]; + } + // Fixed holidays + $holidays = [ + sprintf('%04d-01-01', $year), // Den obnovy samostatného českého státu + sprintf('%04d-05-01', $year), // Svátek práce + sprintf('%04d-05-08', $year), // Den vítězství + sprintf('%04d-07-05', $year), // Den slovanských věrozvěstů Cyrila a Metoděje + sprintf('%04d-07-06', $year), // Den upálení mistra Jana Husa + sprintf('%04d-09-28', $year), // Den české státnosti + sprintf('%04d-10-28', $year), // Den vzniku samostatného československého státu + sprintf('%04d-11-17', $year), // Den boje za svobodu a demokracii + sprintf('%04d-12-24', $year), // Štědrý den + sprintf('%04d-12-25', $year), // 1. svátek vánoční + sprintf('%04d-12-26', $year), // 2. svátek vánoční + ]; + + // Easter-based holidays (Anonymous Gregorian algorithm) + $easterSunday = self::getEasterSunday($year); + $goodFriday = date('Y-m-d', strtotime($easterSunday . ' -2 days')); + $easterMonday = date('Y-m-d', strtotime($easterSunday . ' +1 day')); + $holidays[] = $goodFriday; // Velký pátek + $holidays[] = $easterMonday; // Velikonoční pondělí + + sort($holidays); + self::$holidayCache[$year] = $holidays; + return $holidays; + } + + /** + * Check if a date is a Czech public holiday. + */ + public static function isHoliday(string $date): bool + { + $year = (int)date('Y', strtotime($date)); + $formatted = date('Y-m-d', strtotime($date)); + return in_array($formatted, self::getHolidays($year), true); + } + + /** + * Get number of business days (Mon-Fri, excluding holidays) in a month. + */ + public static function getBusinessDaysInMonth(int $year, int $month): int + { + $holidays = self::getHolidays($year); + $daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year); + $businessDays = 0; + + for ($day = 1; $day <= $daysInMonth; $day++) { + $date = sprintf('%04d-%02d-%02d', $year, $month, $day); + $dayOfWeek = (int)date('N', strtotime($date)); // 1=Mon, 7=Sun + if ($dayOfWeek <= 5 && !in_array($date, $holidays, true)) { + $businessDays++; + } + } + + return $businessDays; + } + + /** + * Get monthly work fund in hours (business days × 8). + */ + public static function getMonthlyWorkFund(int $year, int $month): float + { + return self::getBusinessDaysInMonth($year, $month) * 8.0; + } + + /** + * Calculate Easter Sunday date using the Anonymous Gregorian algorithm. + * Returns 'Y-m-d' string. + */ + private static function getEasterSunday(int $year): string + { + $a = $year % 19; + $b = intdiv($year, 100); + $c = $year % 100; + $d = intdiv($b, 4); + $e = $b % 4; + $f = intdiv($b + 8, 25); + $g = intdiv($b - $f + 1, 3); + $h = (19 * $a + $b - $d - $g + 15) % 30; + $i = intdiv($c, 4); + $k = $c % 4; + $l = (32 + 2 * $e + 2 * $i - $h - $k) % 7; + $m = intdiv($a + 11 * $h + 22 * $l, 451); + $month = intdiv($h + $l - 7 * $m + 114, 31); + $day = (($h + $l - 7 * $m + 114) % 31) + 1; + + return sprintf('%04d-%02d-%02d', $year, $month, $day); + } +} diff --git a/dist/api/includes/Encryption.php b/dist/api/includes/Encryption.php new file mode 100644 index 0000000..01d6b7b --- /dev/null +++ b/dist/api/includes/Encryption.php @@ -0,0 +1,98 @@ + self::NONCE_LENGTH + self::TAG_LENGTH; + } +} diff --git a/dist/api/includes/JWTAuth.php b/dist/api/includes/JWTAuth.php new file mode 100644 index 0000000..c5f6d77 --- /dev/null +++ b/dist/api/includes/JWTAuth.php @@ -0,0 +1,663 @@ + $userData + */ + public static function generateAccessToken(array $userData): string + { + $issuedAt = time(); + $expiry = $issuedAt + self::getAccessTokenExpiry(); + + $payload = [ + 'iss' => 'boha-automation', // Issuer + 'iat' => $issuedAt, // Issued at + 'exp' => $expiry, // Expiry + 'type' => 'access', // Token type + 'sub' => $userData['id'], // Subject (user ID) + 'user' => [ + 'id' => $userData['id'], + 'username' => $userData['username'], + 'email' => $userData['email'], + 'full_name' => trim(($userData['first_name'] ?? '') . ' ' . ($userData['last_name'] ?? '')), + 'role' => $userData['role'] ?? null, + 'role_display' => $userData['role_display'] ?? $userData['role'] ?? null, + 'is_admin' => $userData['is_admin'] ?? ($userData['role'] === 'admin'), + ], + ]; + + return JWT::encode($payload, self::getSecretKey(), self::ALGORITHM); + } + + /** + * Generate a refresh token (stored in httpOnly cookie) + * + * @param int $userId User ID + * @param bool $remember If true: 30 day persistent cookie. If false: session cookie (1 hour DB expiry) + */ + public static function generateRefreshToken(int $userId, bool $remember = false): string + { + $token = bin2hex(random_bytes(32)); // 64 character random string + $hashedToken = hash('sha256', $token); + + // Calculate expiry based on remember me + if ($remember) { + $dbExpiry = time() + (self::getRefreshTokenExpiryDays() * 86400); // 30 days default + $cookieExpiry = $dbExpiry; // Persistent cookie + } else { + $dbExpiry = time() + self::getRefreshTokenExpirySession(); // 1 hour default + $cookieExpiry = 0; // Session cookie (deleted on browser close) + } + + $expiresAt = date('Y-m-d H:i:s', $dbExpiry); + + try { + $pdo = db(); + + // Pročistit replaced tokeny (po grace period uz nepotřebné) + $stmt = $pdo->prepare( + 'DELETE FROM refresh_tokens WHERE user_id = ? AND replaced_at IS NOT NULL' + . ' AND replaced_at < DATE_SUB(NOW(), INTERVAL ' . self::ROTATION_GRACE_PERIOD . ' SECOND)' + ); + $stmt->execute([$userId]); + + // Limit aktivních sessions per user (max 5 devices) + $stmt = $pdo->prepare( + 'SELECT COUNT(*) FROM refresh_tokens WHERE user_id = ? AND replaced_at IS NULL' + ); + $stmt->execute([$userId]); + $count = $stmt->fetchColumn(); + + if ($count >= 5) { + $stmt = $pdo->prepare(' + DELETE FROM refresh_tokens + WHERE user_id = ? AND replaced_at IS NULL + ORDER BY created_at ASC + LIMIT 1 + '); + $stmt->execute([$userId]); + } + + // Store new refresh token + $stmt = $pdo->prepare(' + INSERT INTO refresh_tokens (user_id, token_hash, expires_at, ip_address, user_agent, remember_me) + VALUES (?, ?, ?, ?, ?, ?) + '); + $stmt->execute([ + $userId, + $hashedToken, + $expiresAt, + getClientIp(), + substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255), + $remember ? 1 : 0, + ]); + + // Set httpOnly cookie + $secure = !DEBUG_MODE || (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on'); + + setcookie('refresh_token', $token, [ + 'expires' => $cookieExpiry, + 'path' => '/api/', + 'domain' => '', + 'secure' => $secure, + 'httponly' => true, + 'samesite' => 'Strict', + ]); + + return $token; + } catch (PDOException $e) { + error_log('JWTAuth refresh token error: ' . $e->getMessage()); + throw new Exception('Failed to create refresh token'); + } + } + + /** + * Verify and decode an access token + * + * @return array{user_id: mixed, user: array}|null + */ + public static function verifyAccessToken(string $token): ?array + { + try { + $decoded = JWT::decode($token, new Key(self::getSecretKey(), self::ALGORITHM)); + $payload = (array) $decoded; + + // Verify it's an access token + if (($payload['type'] ?? '') !== 'access') { + return null; + } + + return [ + 'user_id' => $payload['sub'], + 'user' => (array) $payload['user'], + ]; + } catch (ExpiredException $e) { + // Token expired - client should use refresh token + return null; + } catch (Exception $e) { + error_log('JWT verification error: ' . $e->getMessage()); + return null; + } + } + + /** + * Verify refresh token and return user data if valid + * Returns array with 'user' data and 'remember_me' flag + * Deletes expired tokens from database when found + * + * @return array{user: array, remember_me: bool, in_grace_period?: bool}|null + */ + public static function verifyRefreshToken(?string $token = null): ?array + { + // Get token from cookie if not provided + if ($token === null) { + $token = $_COOKIE['refresh_token'] ?? null; + } + + if (empty($token)) { + return null; + } + + try { + $pdo = db(); + $hashedToken = hash('sha256', $token); + + // First check if token exists (regardless of expiry) + $stmt = $pdo->prepare(' + SELECT rt.id, rt.user_id, rt.token_hash, rt.expires_at, + rt.replaced_at, rt.remember_me, + u.id as user_id, u.username, u.email, + u.first_name, u.last_name, u.is_active, + r.name as role_name, r.display_name as role_display_name + FROM refresh_tokens rt + JOIN users u ON rt.user_id = u.id + LEFT JOIN roles r ON u.role_id = r.id + WHERE rt.token_hash = ? + '); + $stmt->execute([$hashedToken]); + $data = $stmt->fetch(); + + if (!$data) { + self::clearRefreshCookie(); + return null; + } + + // Token byl rotovan - zkontrolovat grace period + if ($data['replaced_at'] !== null) { + $replacedAt = strtotime($data['replaced_at']); + if ((time() - $replacedAt) <= self::ROTATION_GRACE_PERIOD) { + // Grace period - token jeste plati (souběžny request) + if (!$data['is_active']) { + return null; + } + return [ + 'user' => [ + 'id' => $data['user_id'], + 'username' => $data['username'], + 'email' => $data['email'], + 'first_name' => $data['first_name'], + 'last_name' => $data['last_name'], + 'role' => $data['role_name'], + 'role_display' => $data['role_display_name'] ?? $data['role_name'], + 'is_admin' => $data['role_name'] === 'admin', + 'permissions' => self::getUserPermissions($data['user_id']), + ], + 'remember_me' => (bool) ($data['remember_me'] ?? false), + 'in_grace_period' => true, + ]; + } + + // Po grace period - stary token uz neni platny, smazat jen tento token + $uid = $data['user_id']; + error_log("Refresh token reuse after grace period for user {$uid}"); + $stmt = $pdo->prepare('DELETE FROM refresh_tokens WHERE token_hash = ?'); + $stmt->execute([$hashedToken]); + self::clearRefreshCookie(); + return null; + } + + // Check if token is expired + if (strtotime($data['expires_at']) < time()) { + $stmt = $pdo->prepare('DELETE FROM refresh_tokens WHERE token_hash = ?'); + $stmt->execute([$hashedToken]); + self::clearRefreshCookie(); + return null; + } + + // Check user is still active + if (!$data['is_active']) { + self::revokeRefreshToken($token); + return null; + } + + return [ + 'user' => [ + 'id' => $data['user_id'], + 'username' => $data['username'], + 'email' => $data['email'], + 'first_name' => $data['first_name'], + 'last_name' => $data['last_name'], + 'role' => $data['role_name'], + 'role_display' => $data['role_display_name'] ?? $data['role_name'], + 'is_admin' => $data['role_name'] === 'admin', + 'permissions' => self::getUserPermissions($data['user_id']), + ], + 'remember_me' => (bool) ($data['remember_me'] ?? false), + ]; + } catch (PDOException $e) { + error_log('JWTAuth verify refresh error: ' . $e->getMessage()); + return null; + } + } + + /** Grace period pro rotovane tokeny (sekundy) */ + private const ROTATION_GRACE_PERIOD = 30; + + public static function getGracePeriod(): int + { + return self::ROTATION_GRACE_PERIOD; + } + + /** + * Refresh tokens - issue new access token + rotate refresh token + * Grace period 30s pro souběžné requesty + * + * @return array{access_token: string, user: array, expires_in: int}|null + */ + public static function refreshTokens(): ?array + { + $token = $_COOKIE['refresh_token'] ?? null; + + $tokenData = self::verifyRefreshToken($token); + if (!$tokenData) { + return null; + } + + try { + $userData = $tokenData['user']; + $accessToken = self::generateAccessToken($userData); + + // Rotace: pokud token nebyl jiz nahrazen (grace period request), rotovat + if (!($tokenData['in_grace_period'] ?? false)) { + self::rotateRefreshToken( + $token, + $userData['id'], + (bool) $tokenData['remember_me'] + ); + } + + return [ + 'access_token' => $accessToken, + 'user' => [ + 'id' => $userData['id'], + 'username' => $userData['username'], + 'email' => $userData['email'], + 'full_name' => trim(($userData['first_name'] ?? '') . ' ' . ($userData['last_name'] ?? '')), + 'role' => $userData['role'], + 'role_display' => $userData['role_display'], + 'is_admin' => $userData['is_admin'], + 'permissions' => $userData['permissions'] ?? self::getUserPermissions($userData['id']), + ], + 'expires_in' => self::getAccessTokenExpiry(), + ]; + } catch (Exception $e) { + error_log('JWTAuth refresh error: ' . $e->getMessage()); + return null; + } + } + + /** + * Rotace refresh tokenu - vygeneruje novy, stary oznaci jako replaced + */ + private static function rotateRefreshToken(string $oldToken, int $userId, bool $remember): void + { + $pdo = db(); + $oldHash = hash('sha256', $oldToken); + + $newToken = bin2hex(random_bytes(32)); + $newHash = hash('sha256', $newToken); + + if ($remember) { + $dbExpiry = time() + (self::getRefreshTokenExpiryDays() * 86400); + $cookieExpiry = $dbExpiry; + } else { + $dbExpiry = time() + self::getRefreshTokenExpirySession(); + $cookieExpiry = 0; + } + + $expiresAt = date('Y-m-d H:i:s', $dbExpiry); + + // Oznacit stary token jako replaced (atomicky - race condition ochrana) + $stmt = $pdo->prepare(' + UPDATE refresh_tokens SET replaced_at = NOW(), replaced_by_hash = ? + WHERE token_hash = ? AND replaced_at IS NULL + '); + $stmt->execute([$newHash, $oldHash]); + + // Jiny request uz token rotoval - nepokracovat + if ($stmt->rowCount() === 0) { + return; + } + + // Procistit drive replaced tokeny (az po uspesne rotaci, respektovat grace period) + $pdo->prepare( + 'DELETE FROM refresh_tokens WHERE user_id = ? AND replaced_at IS NOT NULL AND token_hash != ?' + . ' AND replaced_at < DATE_SUB(NOW(), INTERVAL ' . self::ROTATION_GRACE_PERIOD . ' SECOND)' + )->execute([$userId, $oldHash]); + + // Vlozit novy token + $stmt = $pdo->prepare(' + INSERT INTO refresh_tokens (user_id, token_hash, expires_at, ip_address, user_agent, remember_me) + VALUES (?, ?, ?, ?, ?, ?) + '); + $stmt->execute([ + $userId, + $newHash, + $expiresAt, + getClientIp(), + substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255), + $remember ? 1 : 0, + ]); + + // Novy cookie + $secure = !DEBUG_MODE || (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on'); + setcookie('refresh_token', $newToken, [ + 'expires' => $cookieExpiry, + 'path' => '/api/', + 'domain' => '', + 'secure' => $secure, + 'httponly' => true, + 'samesite' => 'Strict', + ]); + } + + /** + * Revoke a specific refresh token + */ + public static function revokeRefreshToken(string $token): bool + { + try { + $pdo = db(); + $hashedToken = hash('sha256', $token); + + $stmt = $pdo->prepare('DELETE FROM refresh_tokens WHERE token_hash = ?'); + $stmt->execute([$hashedToken]); + + self::clearRefreshCookie(); + + return true; + } catch (PDOException $e) { + error_log('JWTAuth revoke error: ' . $e->getMessage()); + return false; + } + } + + /** + * Revoke all refresh tokens for a user (logout from all devices) + */ + public static function revokeAllUserTokens(int $userId): bool + { + try { + $pdo = db(); + $stmt = $pdo->prepare('DELETE FROM refresh_tokens WHERE user_id = ?'); + $stmt->execute([$userId]); + + self::clearRefreshCookie(); + + return true; + } catch (PDOException $e) { + error_log('JWTAuth revoke all error: ' . $e->getMessage()); + return false; + } + } + + /** + * Clear the refresh token cookie + */ + private static function clearRefreshCookie(): void + { + $secure = !DEBUG_MODE || (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on'); + setcookie('refresh_token', '', [ + 'expires' => time() - 3600, + 'path' => '/api/', + 'domain' => '', + 'secure' => $secure, + 'httponly' => true, + 'samesite' => 'Strict', + ]); + unset($_COOKIE['refresh_token']); + } + + /** + * Get access token from Authorization header + */ + public static function getTokenFromHeader(): ?string + { + $headers = getallheaders(); + $authHeader = $headers['Authorization'] ?? $headers['authorization'] ?? ''; + + if (preg_match('/Bearer\s+(.+)$/i', $authHeader, $matches)) { + return $matches[1]; + } + + return null; + } + + /** + * Middleware: Require valid access token + * Also verifies refresh token still exists in database (session not revoked) + * Extends session expiry only when less than 50% of time remaining (smart extend) + * + * @return array + */ + public static function requireAuth(): array + { + $token = self::getTokenFromHeader(); + + if (!$token) { + errorResponse('Access token required', 401); + } + + $payload = self::verifyAccessToken($token); + + if (!$payload) { + errorResponse('Invalid or expired token', 401); + } + + // Verify refresh token exists + smart extend in a single query + $refreshToken = $_COOKIE['refresh_token'] ?? null; + if ($refreshToken) { + $hashedToken = hash('sha256', $refreshToken); + try { + $pdo = db(); + // Verify session - tolerovat replaced tokeny v grace period + $stmt = $pdo->prepare(' + SELECT id, remember_me, expires_at, replaced_at + FROM refresh_tokens + WHERE token_hash = ? AND expires_at > NOW() + '); + $stmt->execute([$hashedToken]); + $tokenData = $stmt->fetch(); + + if (!$tokenData) { + self::clearRefreshCookie(); + errorResponse('Session revoked', 401); + } + + // Replaced token v grace period - jen validovat, neextendovat + if ($tokenData['replaced_at'] !== null) { + $replacedAt = strtotime($tokenData['replaced_at']); + if ((time() - $replacedAt) > self::ROTATION_GRACE_PERIOD) { + self::clearRefreshCookie(); + errorResponse('Session revoked', 401); + } + // V grace period - skip extend, access token jeste plati + return $payload; + } + + // Smart extend: only UPDATE when less than 50% of session time remaining + $expiresAt = strtotime($tokenData['expires_at']); + $now = time(); + $remaining = $expiresAt - $now; + + if ($tokenData['remember_me']) { + $totalWindow = self::getRefreshTokenExpiryDays() * 86400; + } else { + $totalWindow = self::getRefreshTokenExpirySession(); + } + + // Only extend if less than 50% remaining + if ($remaining < ($totalWindow * 0.5)) { + $newExpiry = date('Y-m-d H:i:s', $now + $totalWindow); + $stmt = $pdo->prepare('UPDATE refresh_tokens SET expires_at = ? WHERE id = ?'); + $stmt->execute([$newExpiry, $tokenData['id']]); + + // Refresh cookie expiry for remember-me sessions + if ($tokenData['remember_me']) { + $secure = !DEBUG_MODE || (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on'); + setcookie('refresh_token', $refreshToken, [ + 'expires' => $now + $totalWindow, + 'path' => '/api/', + 'domain' => '', + 'secure' => $secure, + 'httponly' => true, + 'samesite' => 'Strict', + ]); + } + } + } catch (PDOException $e) { + error_log('JWTAuth session check error: ' . $e->getMessage()); + } + } + + return $payload; + } + + /** + * Middleware: Optional auth - returns user data if valid token, null otherwise + * + * @return array|null + */ + public static function optionalAuth(): ?array + { + $token = self::getTokenFromHeader(); + + if (!$token) { + return null; + } + + return self::verifyAccessToken($token); + } + + /** + * Get permission names for a user + * Admin role returns all permissions. + * + * @return list + */ + public static function getUserPermissions(int $userId): array + { + return getUserPermissions($userId); + } + + /** + * Cleanup expired and replaced refresh tokens + */ + public static function cleanupExpiredTokens(): int + { + try { + $pdo = db(); + $stmt = $pdo->prepare( + 'DELETE FROM refresh_tokens WHERE expires_at < NOW()' + . ' OR (replaced_at IS NOT NULL AND replaced_at < DATE_SUB(NOW(), INTERVAL ' + . self::ROTATION_GRACE_PERIOD . ' SECOND))' + ); + $stmt->execute(); + return $stmt->rowCount(); + } catch (PDOException $e) { + error_log('JWTAuth cleanup error: ' . $e->getMessage()); + return 0; + } + } +} diff --git a/dist/api/includes/LeaveNotification.php b/dist/api/includes/LeaveNotification.php new file mode 100644 index 0000000..763439a --- /dev/null +++ b/dist/api/includes/LeaveNotification.php @@ -0,0 +1,90 @@ + */ + private static array $leaveTypeLabels = [ + 'vacation' => 'Dovolená', + 'sick' => 'Nemocenská', + 'unpaid' => 'Neplacené volno', + ]; + + /** + * Send notification about a new leave request + * + * @param array $request + */ + public static function notifyNewRequest(array $request, string $employeeName): void + { + $notifyEmail = env('LEAVE_NOTIFY_EMAIL', ''); + if (!$notifyEmail) { + return; + } + + $leaveType = self::$leaveTypeLabels[$request['leave_type']] ?? $request['leave_type']; + $dateFrom = date('d.m.Y', strtotime($request['date_from'])); + $dateTo = date('d.m.Y', strtotime($request['date_to'])); + $notes = $request['notes'] ?? ''; + + $subject = "Nová žádost o nepřítomnost - $employeeName ($leaveType)"; + + $html = " + + +

          Nová žádost o nepřítomnost

          + + + + + + + + + + + + + + + + + " + . ($notes ? " + + + + ' : '') . " +
          + Zaměstnanec:" + . htmlspecialchars($employeeName) . "
          Typ:" . htmlspecialchars($leaveType) . "
          Období:$dateFrom – $dateTo
          Pracovní dny:" + . "{$request['total_days']} dní ({$request['total_hours']} hodin)
          Poznámka:" . htmlspecialchars($notes) . '
          + " . (env('APP_URL', '') ? " +

          + + Přejít ke schvalování + +

          " : "") . " +
          +

          + Tato zpráva byla automaticky vygenerována systémem.
          + Datum: " . date('d.m.Y H:i:s') . ' +

          + + '; + + $sent = Mailer::send($notifyEmail, $subject, $html); + if (!$sent) { + error_log("LeaveNotification: Failed to send new request notification to $notifyEmail"); + } + } +} diff --git a/dist/api/includes/Mailer.php b/dist/api/includes/Mailer.php new file mode 100644 index 0000000..2584d47 --- /dev/null +++ b/dist/api/includes/Mailer.php @@ -0,0 +1,45 @@ +\r\n"; + if ($replyTo) { + $headers .= "Reply-To: $replyTo\r\n"; + } + + $sent = mail($to, $encodedSubject, $htmlBody, $headers); + + if (!$sent) { + error_log("Mailer error: mail() failed for recipient $to"); + } + + return $sent; + } +} diff --git a/dist/api/includes/PaginationHelper.php b/dist/api/includes/PaginationHelper.php new file mode 100644 index 0000000..34fc886 --- /dev/null +++ b/dist/api/includes/PaginationHelper.php @@ -0,0 +1,84 @@ + $sortMap + * @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{total: int, page: int, per_page: int, total_pages: int}} + */ + 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, + ], + ]; + } +} diff --git a/dist/api/includes/RateLimiter.php b/dist/api/includes/RateLimiter.php new file mode 100644 index 0000000..ad8e5c7 --- /dev/null +++ b/dist/api/includes/RateLimiter.php @@ -0,0 +1,220 @@ +storagePath = $storagePath + ?? (defined('RATE_LIMIT_STORAGE_PATH') + ? RATE_LIMIT_STORAGE_PATH + : __DIR__ . '/../rate_limits'); + + // Only check directory once per process (static flag) + if (!self::$dirVerified) { + if (!is_dir($this->storagePath)) { + mkdir($this->storagePath, 0755, true); + } + self::$dirVerified = true; + } + + // Cleanup old files very rarely (0.1% of requests instead of 1%) + if (rand(1, 1000) === 1) { + $this->cleanup(); + } + } + + /** + * Check if the request should be rate limited + * + * Uses exclusive file locking for the entire read-check-increment-write cycle + * to prevent race conditions under concurrent requests. + * + * @param string $endpoint Endpoint identifier (e.g., 'login', 'session') + * @param int|null $limit Custom limit for this endpoint (requests per minute) + * @return bool True if request is allowed, false if rate limited + */ + /** @var bool Fail-closed: blokuj request pri chybe FS (pro kriticke endpointy) */ + private bool $failClosed = false; + + public function setFailClosed(bool $failClosed = true): self + { + $this->failClosed = $failClosed; + return $this; + } + + public function check(string $endpoint, ?int $limit = null): bool + { + $limit = $limit ?? $this->defaultLimit; + $ip = $this->getClientIp(); + $key = $this->getKey($ip, $endpoint); + $file = $this->storagePath . '/' . $key . '.json'; + $now = time(); + + // Open file with exclusive lock for atomic read-check-increment-write + $fp = @fopen($file, 'c+'); + if (!$fp) { + return !$this->failClosed; + } + + if (!flock($fp, LOCK_EX)) { + fclose($fp); + return !$this->failClosed; + } + + // Read current data under lock + $content = stream_get_contents($fp); + $data = $content ? json_decode($content, true) : null; + + if (is_array($data) && $data['window_start'] > ($now - $this->windowSeconds)) { + // Same window - check count + if ($data['count'] >= $limit) { + flock($fp, LOCK_UN); + fclose($fp); + return false; // Rate limited + } + $data['count']++; + } else { + // New window - reset counter + $data = ['window_start' => $now, 'count' => 1]; + } + + // Write updated data + ftruncate($fp, 0); + rewind($fp); + fwrite($fp, json_encode($data)); + fflush($fp); + + flock($fp, LOCK_UN); + fclose($fp); + + return true; + } + + /** + * Enforce rate limit and return 429 response if exceeded + * + * @param string $endpoint Endpoint identifier + * @param int|null $limit Custom limit for this endpoint + */ + public function enforce(string $endpoint, ?int $limit = null): void + { + if (!$this->check($endpoint, $limit)) { + $this->sendRateLimitResponse(); + } + } + + /** + * Send 429 Too Many Requests response and exit + */ + private function sendRateLimitResponse(): void + { + http_response_code(429); + header('Content-Type: application/json; charset=utf-8'); + header('Retry-After: ' . $this->windowSeconds); + + echo json_encode([ + 'success' => false, + 'error' => 'Prilis mnoho pozadavku. Zkuste to prosim pozdeji.', + 'retry_after' => $this->windowSeconds, + ], JSON_UNESCAPED_UNICODE); + + exit(); + } + + /** + * Get client IP address + * + * @return string IP address + */ + private function getClientIp(): string + { + // Use the global helper if available + if (function_exists('getClientIp')) { + return getClientIp(); + } + + // Fallback: use only REMOTE_ADDR (cannot be spoofed) + return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; + } + + /** + * Generate storage key for IP and endpoint + * + * @param string $ip Client IP + * @param string $endpoint Endpoint identifier + * @return string Storage key (filename-safe) + */ + private function getKey(string $ip, string $endpoint): string + { + // Create a safe filename from IP and endpoint + return md5($ip . ':' . $endpoint); + } + + /** + * Cleanup expired rate limit files + * + * Removes files older than the time window to prevent disk space issues + */ + private function cleanup(): void + { + if (!is_dir($this->storagePath)) { + return; + } + + $files = glob($this->storagePath . '/*.json'); + $expireTime = time() - ($this->windowSeconds * 2); // Keep for 2x window to be safe + + foreach ($files as $file) { + if (filemtime($file) < $expireTime) { + @unlink($file); + } + } + } + + /** + * Clear all rate limit data (useful for testing) + */ + public function clearAll(): void + { + if (!is_dir($this->storagePath)) { + return; + } + + $files = glob($this->storagePath . '/*.json'); + foreach ($files as $file) { + @unlink($file); + } + } +} diff --git a/dist/api/includes/Validator.php b/dist/api/includes/Validator.php new file mode 100644 index 0000000..bc9cc09 --- /dev/null +++ b/dist/api/includes/Validator.php @@ -0,0 +1,139 @@ +required('name')->string('name', 1, 255); + * $v->required('email')->email('email'); + * $v->int('amount', 0, 1000000); + * $v->in('status', ['active', 'inactive']); + * if ($v->fails()) errorResponse($v->firstError()); + */ + +declare(strict_types=1); + +class Validator +{ + /** @var array */ + private array $data; + + /** @var array */ + private array $errors = []; + + /** @param array $data */ + public function __construct(array $data) + { + $this->data = $data; + } + + public function required(string $field, string $label = ''): self + { + $value = $this->data[$field] ?? null; + if ($value === null || $value === '') { + $this->errors[$field] = ($label ?: $field) . ' je povinné pole'; + } + return $this; + } + + public function string(string $field, int $min = 0, int $max = 0, string $label = ''): self + { + $value = $this->data[$field] ?? null; + if ($value === null || $value === '') { + return $this; + } + if (!is_string($value)) { + $this->errors[$field] = ($label ?: $field) . ' musí být text'; + return $this; + } + $len = mb_strlen($value); + if ($min > 0 && $len < $min) { + $this->errors[$field] = ($label ?: $field) . " musí mít alespoň {$min} znaků"; + } elseif ($max > 0 && $len > $max) { + $this->errors[$field] = ($label ?: $field) . " nesmí překročit {$max} znaků"; + } + return $this; + } + + public function int(string $field, ?int $min = null, ?int $max = null, string $label = ''): self + { + $value = $this->data[$field] ?? null; + if ($value === null || $value === '') { + return $this; + } + if (!is_numeric($value)) { + $this->errors[$field] = ($label ?: $field) . ' musí být číslo'; + return $this; + } + $intVal = (int) $value; + if ($min !== null && $intVal < $min) { + $this->errors[$field] = ($label ?: $field) . " musí být alespoň {$min}"; + } elseif ($max !== null && $intVal > $max) { + $this->errors[$field] = ($label ?: $field) . " nesmí překročit {$max}"; + } + return $this; + } + + public function email(string $field, string $label = ''): self + { + $value = $this->data[$field] ?? null; + if ($value === null || $value === '') { + return $this; + } + if (!is_string($value) || !filter_var($value, FILTER_VALIDATE_EMAIL)) { + $this->errors[$field] = ($label ?: $field) . ' musí být platný e-mail'; + } + return $this; + } + + /** + * @param list $allowed + */ + public function in(string $field, array $allowed, string $label = ''): self + { + $value = $this->data[$field] ?? null; + if ($value === null || $value === '') { + return $this; + } + if (!in_array($value, $allowed, true)) { + $this->errors[$field] = ($label ?: $field) . ' má neplatnou hodnotu'; + } + return $this; + } + + public function numeric(string $field, ?float $min = null, ?float $max = null, string $label = ''): self + { + $value = $this->data[$field] ?? null; + if ($value === null || $value === '') { + return $this; + } + if (!is_numeric($value)) { + $this->errors[$field] = ($label ?: $field) . ' musí být číslo'; + return $this; + } + $numVal = (float) $value; + if ($min !== null && $numVal < $min) { + $this->errors[$field] = ($label ?: $field) . " musí být alespoň {$min}"; + } elseif ($max !== null && $numVal > $max) { + $this->errors[$field] = ($label ?: $field) . " nesmí překročit {$max}"; + } + return $this; + } + + public function fails(): bool + { + return count($this->errors) > 0; + } + + public function firstError(): string + { + return reset($this->errors) ?: ''; + } + + /** @return array */ + public function errors(): array + { + return $this->errors; + } +} diff --git a/dist/api/includes/constants.php b/dist/api/includes/constants.php new file mode 100644 index 0000000..8b88867 --- /dev/null +++ b/dist/api/includes/constants.php @@ -0,0 +1,38 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES ' . DB_CHARSET, + ]; + + try { + $pdo = new PDO($dsn, DB_USER, DB_PASS, $options); + } catch (PDOException $e) { + if (DEBUG_MODE) { + throw $e; + } + error_log('Database connection failed: ' . $e->getMessage()); + throw new PDOException('Database connection failed'); + } + } + + return $pdo; +} + +/** + * Set CORS headers for API responses + */ +function setCorsHeaders(): void +{ + $origin = $_SERVER['HTTP_ORIGIN'] ?? ''; + + if (in_array($origin, CORS_ALLOWED_ORIGINS)) { + header("Access-Control-Allow-Origin: $origin"); + header('Access-Control-Allow-Credentials: true'); + } elseif (DEBUG_MODE && str_starts_with($origin, 'http://127.0.0.1:')) { + header("Access-Control-Allow-Origin: $origin"); + header('Access-Control-Allow-Credentials: true'); + } + // Neznamy origin = zadny CORS header + header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS'); + header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With'); + header('Access-Control-Max-Age: 86400'); + + // Handle preflight requests + if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { + http_response_code(200); + exit(); + } +} + +/** + * Send JSON response and exit + * + * @param mixed $data Data to send + * @param int $statusCode HTTP status code + */ +function jsonResponse($data, int $statusCode = 200): void +{ + http_response_code($statusCode); + header('Content-Type: application/json; charset=utf-8'); + echo json_encode($data, JSON_UNESCAPED_UNICODE); + exit(); +} + +/** + * Send error response + * + * @param string $message Error message + * @param int $statusCode HTTP status code + */ +function errorResponse(string $message, int $statusCode = 400): void +{ + jsonResponse(['success' => false, 'error' => $message], $statusCode); +} + +/** + * Send success response + * + * @param mixed $data Data to include + * @param string $message Optional message + */ +function successResponse($data = null, string $message = ''): void +{ + $response = ['success' => true]; + if ($message) { + $response['message'] = $message; + } + if ($data !== null) { + $response['data'] = $data; + } + jsonResponse($response); +} + +/** + * Get JSON request body + * + * @return array Decoded JSON data + */ +function getJsonInput(): array +{ + $input = file_get_contents('php://input'); + $data = json_decode($input, true); + return is_array($data) ? $data : []; +} + +/** + * Sanitize string input + * + * @param string $input Input string + * @return string Sanitized string + */ +function sanitize(string $input): string +{ + return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8'); +} + +/** + * Validate email format + * + * @param string $email Email to validate + * @return bool True if valid + */ +function isValidEmail(string $email): bool +{ + return filter_var($email, FILTER_VALIDATE_EMAIL) !== false; +} + +/** + * Validate and sanitize month parameter (YYYY-MM format) + */ +function validateMonth(string $param = 'month'): string +{ + $month = $_GET[$param] ?? date('Y-m'); + if (!preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $month)) { + $month = date('Y-m'); + } + return $month; +} + +/** + * Get client IP address + * + * Uses only REMOTE_ADDR which cannot be spoofed (TCP connection IP). + * If you add a reverse proxy (Cloudflare, Nginx, etc.) in the future, + * update this function to trust specific proxy headers only from known proxy IPs. + * + * @return string IP address + */ +function getClientIp(): string +{ + return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; +} + +/** + * Set security headers for API responses + * + * Sets standard security headers to protect against common web vulnerabilities: + * - X-Content-Type-Options: Prevents MIME type sniffing + * - X-Frame-Options: Prevents clickjacking attacks + * - X-XSS-Protection: Enables browser XSS filter + * - Referrer-Policy: Controls referrer information sent with requests + * + * Note: Content-Security-Policy is not set here as it may interfere with the React frontend + */ +function setSecurityHeaders(): void +{ + header('X-Content-Type-Options: nosniff'); + header('X-Frame-Options: DENY'); + header('Referrer-Policy: strict-origin-when-cross-origin'); + if (!DEBUG_MODE && isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') { + header('Strict-Transport-Security: max-age=31536000; includeSubDomains'); + } +} + +/** + * Set no-cache headers + * + * Prevents browser caching for sensitive endpoints + */ +function setNoCacheHeaders(): void +{ + header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); + header('Cache-Control: post-check=0, pre-check=0', false); + header('Pragma: no-cache'); +} + +/** + * Sdilene generovani cisel pro objednavky a projekty (spolecny ciselny prostor) + */ +function generateSharedNumber(PDO $pdo): string +{ + $yy = date('y'); + + $settings = $pdo->query('SELECT order_type_code FROM company_settings LIMIT 1')->fetch(); + $typeCode = ($settings && !empty($settings['order_type_code'])) ? $settings['order_type_code'] : '71'; + + $prefix = $yy . $typeCode; + $prefixLen = strlen($prefix); + $likePattern = $prefix . '%'; + + $stmt = $pdo->prepare(' + SELECT COALESCE(MAX(seq), 0) FROM ( + SELECT CAST(SUBSTRING(order_number, ? + 1) AS UNSIGNED) AS seq + FROM orders WHERE order_number LIKE ? + UNION ALL + SELECT CAST(SUBSTRING(project_number, ? + 1) AS UNSIGNED) AS seq + FROM projects WHERE project_number LIKE ? + ) combined + '); + $stmt->execute([$prefixLen, $likePattern, $prefixLen, $likePattern]); + $max = (int) $stmt->fetchColumn(); + + return sprintf('%s%s%04d', $yy, $typeCode, $max + 1); +} + +/** + * Get permissions for a user by their ID + * Cached per-request via static variable + * + * @return list + */ +function getUserPermissions(int $userId): array +{ + static $cache = []; + + if (isset($cache[$userId])) { + return $cache[$userId]; + } + + try { + $pdo = db(); + + $stmt = $pdo->prepare(' + SELECT r.name FROM users u + JOIN roles r ON u.role_id = r.id + WHERE u.id = ? + '); + $stmt->execute([$userId]); + $role = $stmt->fetch(); + + if ($role && $role['name'] === 'admin') { + $stmt = $pdo->query('SELECT name FROM permissions'); + $cache[$userId] = $stmt->fetchAll(PDO::FETCH_COLUMN); + return $cache[$userId]; + } + + $stmt = $pdo->prepare(' + SELECT p.name + FROM permissions p + JOIN role_permissions rp ON p.id = rp.permission_id + JOIN users u ON u.role_id = rp.role_id + WHERE u.id = ? + '); + $stmt->execute([$userId]); + $cache[$userId] = $stmt->fetchAll(PDO::FETCH_COLUMN); + return $cache[$userId]; + } catch (PDOException $e) { + error_log('getUserPermissions error: ' . $e->getMessage()); + return []; + } +} + +/** + * Require a specific permission, return 403 if denied + * + * @param array $authData + */ +function requirePermission(array $authData, string $permission): void +{ + if ($authData['user']['is_admin'] ?? false) { + return; + } + + $permissions = getUserPermissions($authData['user_id']); + if (!in_array($permission, $permissions)) { + errorResponse('Přístup odepřen. Nemáte potřebná oprávnění.', 403); + } +} + +/** + * Check if user has a specific permission (returns bool) + * + * @param array $authData + */ +function hasPermission(array $authData, string $permission): bool +{ + if ($authData['user']['is_admin'] ?? false) { + return true; + } + + $permissions = getUserPermissions($authData['user_id']); + return in_array($permission, $permissions); +} diff --git a/dist/api/rate_limits/.htaccess b/dist/api/rate_limits/.htaccess new file mode 100644 index 0000000..3a42882 --- /dev/null +++ b/dist/api/rate_limits/.htaccess @@ -0,0 +1 @@ +Deny from all diff --git a/dist/api/rate_limits/8d5c008a5d9ce3db19bbd930d2b86dd1.json b/dist/api/rate_limits/8d5c008a5d9ce3db19bbd930d2b86dd1.json new file mode 100644 index 0000000..da87883 --- /dev/null +++ b/dist/api/rate_limits/8d5c008a5d9ce3db19bbd930d2b86dd1.json @@ -0,0 +1 @@ +{"window_start":1773343550,"count":1} \ No newline at end of file diff --git a/dist/api/rate_limits/92f2e2a3520918dc3a4f54cfed15fbfe.json b/dist/api/rate_limits/92f2e2a3520918dc3a4f54cfed15fbfe.json new file mode 100644 index 0000000..8ddf211 --- /dev/null +++ b/dist/api/rate_limits/92f2e2a3520918dc3a4f54cfed15fbfe.json @@ -0,0 +1 @@ +{"window_start":1773345124,"count":12} \ No newline at end of file diff --git a/dist/api/rate_limits/95bc5d544df53a813f55a3a2ae270497.json b/dist/api/rate_limits/95bc5d544df53a813f55a3a2ae270497.json new file mode 100644 index 0000000..6dad71b --- /dev/null +++ b/dist/api/rate_limits/95bc5d544df53a813f55a3a2ae270497.json @@ -0,0 +1 @@ +{"window_start":1773344714,"count":1} \ No newline at end of file diff --git a/dist/api/rate_limits/ddb677adee83b940eedb4fd82821f581.json b/dist/api/rate_limits/ddb677adee83b940eedb4fd82821f581.json new file mode 100644 index 0000000..3f95f1b --- /dev/null +++ b/dist/api/rate_limits/ddb677adee83b940eedb4fd82821f581.json @@ -0,0 +1 @@ +{"window_start":1773343540,"count":1} \ No newline at end of file diff --git a/dist/apple-touch-icon.png b/dist/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1d135b0d20c8fce66e8e71be6856d6c1becf5472 GIT binary patch literal 10312 zcma)iWl&tvvNZ&PhrtO1hTy?HxVsZbaA$B04uiY9`veHV-Q5BN8{93pOK^R|z4g^s z^?tn{GjnR6Q$2g1?%iv3uN|(UB>fJJ7!3{%?wzcRgevT}?)8I$1p5{oMPi48!-A8Q z5dG>7f8=c$r#t7edUI7K$N`7MFB^v_;}lPqNI@5#1WHd}l8Y*Imt{=F8IUh{RQXYW zV_lt8Ks8LqkDr`evL!7?L8`M8`?XG!c_2ZNDTd0rNTwW-I}Q;w0505{)Q~kW-Q&6A zVa$690i&y1O3AG=61>X0%fGwgbGmAOvfNP#hIR!r*>&==R&6bAY!te2X3D>;oSoIg z$_XvlFh49pp?l&9<{=>|$EiZ}-n#6RbXHoknBMEvlWEbON1P$U4Vdqkyj4sdzQtB= zBd7mfu_tSk92wpP!kYzhG2p^S( zDuy+@rEW>27v)f6Y2jL8 zTXr9dOkJA{;dIvzBNZ{T?ACJrhae4U~{oT9oM|1u4)-x#{rYA#Q&n+rq+)~ogsaH2F z&DuI_m6MgB24ANt6D=CwSQoTQ>7PZ?cUHIn>aP8oO)$&x-D~@n?_4Qu$ z-Jbka$!bs>U-M_B5IKn{u-a7n?4RH@I4g+0yFHXeg=Tu71zA50h_#E>X)tvF=N6XO zn3h@Ox$l9xGA*81OxLDM+m4gxOChC_3VU!Fcy`crx@MQ2)}=VACg8;z51r=X_5{a* zYai4XR#X0&v$e^j>de6&tplic2y{nxP-X%{D*fWAFO=A4S_Or@WEvKV<}X!S?ecES>z6;4MTst5^E0QAHCNEc~qBB==?=-v5DtM7(@SkVM{39 zf34OdBaM6;N}ifZ6z1S`9$AZ`Lse9IU|_HO3e|DWX=&Cx{n1f@K8BvwO11Z4?3=pnv>;SU@Z$*qp=Coy$%KVN zdk$vB1aU!8wB34e+w^@sfd?RInMqyV{wy$g*twmd->zC$9TwykwLmO4L81r6jY56L zTZV6*lL-xHi%04Q!{y+QSAHm?rrJW4wsHMdem;DA3&bwP;JkO>u4TQS)C0ss;UmwW z?4!?Z$@d9|KoM;{_hFA>S%Dv1VK*0=Hwr5Vwu=aOp^AIh*ck{JB~Kw|Bd9As-3$y| z2d>~`!+J^ZAn5iA!~0E{W%j>XJw-^E?IlOx_=sh3qm+$F+JxvCa8jYpVeArXUKLXqPW-3NqkkG<@EYTyUb-DG!&I?3QzyMK?agW{Y4$Cl62AHV-Tf4Tg74#Q zJ&}lmDgiFMn?Q@Kb0S-x0DE_tKCPv>(8G+tCQJ8UN!a`GM$2_7T0fSlDVt;+|NR@9 z#9nT59x66nP;Jvar>e16szKyN1*dO$*1t@(RFpcGrygd^VP2YO+1uY6N$BmsAkPgG zx*zir7*p@v?jDSCw`8rcAzaq_D@ne9MJsYGSy-C6{e35c$Bk)f-nUh!1->@vwC$DD zO7WDNL7(w^`FK#Tse7eqdtDW1DYb3fv47irP2l30VRlnkzZw4Ju7u(2nPF~o<&Vei zcIsb)J>s<*GcNshFTA<1b%uwBE+w)mj<(JUN={jfE6ytIf(|nWtT_Oe{KS#lQ#i%s z`&RNgTlwt|sq>W*wU#xtuFI~GZDsu(qOPTxR)x)HG&bh5hZFIOHNlc}rIPD8EQPSNexylN*g-#LnVF)~cBS9F)#t-wCIfRX zskQosHdA`a_4S4%edLa5YbJCjHY`z!(wg5WQHfOrjmT#WZjs)_^xI>0WsOrQWqNS2As zv#O+%tkFvGNts1I(%11^(mgwcB4-rroMFXRn2EWfXc5~KwgKO0`G%UGb%}soa>HG7 zCMYOCYMVSc3wQCo){Z-pLFEks&5aJTcA$pkEb!o1 zl*q-*O~wtEFNFV{Csv?>KG!|yR*N6!GZC8!!2`h(`8TO`P_Q2czq7lFNc}~e+!(5D zR20d+K9e*v5lPoOPma5gnNo+ti*3ynVLwr(FIh`WlQTh|Ng0imiS5}vhOYfF3>H&N zS9SZN6A`_QQHXNtnB;>*LJ0Xh8mK`Rj`T?wgpLHXy_lr>+9A3X>Gx8{F>Q8NXu5@Z z1kPXft;CAHj|HJ*vyu+x$;|fbP^N6$1x5XwHf;3x<^m3CLf!u ztzp1K(P`F?=H9tzPXor|Z$2LrZ%Kh7ZlK|-HaJ)OCtBi;dC~OjRd)g@1BWMt^O!bnuTFBH)n zu)vk7&8}yQ^o~5nay+>kLXT3=2Z?#1cfO_FHQwkB#$bW@Q`~`^0kJ&o3k!F})L7`; z%wE7`e^3so-lRAS%w^QL`IBfx2PE>2e-P5gM8*{L4M7)i=rGf0aZc4|c>Wm}ERH=+ zaLlk34mpsUSK~inq76tFkaPa>QrwuQKuHosi@St@={) zJ*y)I@xaVu=*;a><0onj_8*XAd^GKt(UJA_>93Irp?U}65z&guo4-IcpI9j;icP

          2;6TgF1CRiFcU@1IP3M4Kb{dv1^_OcXoznAj*C)N+$N;0ITQ-vP zobEc=^3Q|Q8QSy=L>+ZZlLPEKb9Itq##C={`MM+tkm_u?mzN?}F>`;krb2=H>e#3W z9~P-(o&}KEe`xX39$79VW@K}d@ z6>96v-MGGdy9k>5)4|b0Lpd;ynn0;n*-V?Ym|~}B0nh<%LBkOnsHwOnCbVS# z8{jJa3WIc`bxW~|>;B>ga_3qgnY3o_p*m<*E z@4JLzm#ymwZ3-!=7-OnrxxF|E0#)pj&**xDHdIv{fEC$2fyv1NOePclIcwBGD$0!d zDhZ%vO;kFkJ^Qt11WlXFa`ax=0y$ZB9Dzu z!oSB*qS3TXjwzlz?r@qr$fjD_(3FKdKb0sqZFUlXNsOMDqBLqgwJ_B$Rn!N;${77% zela=d+D}JYIvIoNFy$jJcS;*>RmvskXBJ5C1%mbMgVnhG^8M9L^z=dsPjZqyEaQn> zi;Hjf%{R?%gE{|^e2~G!uSf?D4|1VVzexW1WG3VuNQEn z@GHwYw*U|R8)F{NXy7`xjeI@&Mm9445vr_!p=PB(Qeb_%V{QLX=TsjdwvzM z0lBO%o~zXMcNwL5{21hn4wX;RM$+vO-1c15tg`M|UudPNa>S8MO6fFX_ z;gC@ia6c1OBmNi}(cGh_e3;D9V`8|!chGXGlFX=)*JAl=_ui_Ls?w-kI4`7eq{ov#wpNX)Wu70KyIAL1f`abn!a266>X8!J*oQr0CI8 zHozswI*V8vxss$Rri9&q+il#ZGQPQYzNkcPA!z7@12hf1-wHWgCEp6}-#ky_yU*bt zp6jHR_u9gM|Ifz8C8x6De&L@6(7dF(pMLM>eM(jG-m%4BHm9KvmfN%E-$y}`# zHdKn_^QY|3rfk#^$gDR<4vmsh0_o3!UaQlywK{tS<_HvjPy>l+1c2cM+Ws z-r1oRepDtP2y;+EG6zGf-?bFQk$Q^gjJJU?OzgjqM&*J=sV3JE>#B^Ao9BorUdmOe0IkgSJ!z^ zUzF^Q=NoP7tpp~m1x!839zwNJg26B|JxRaweIlC~lN;nKo+lwe?KO%e&~-)&b-Bst z35|1qB5@fgP(PY`-*7_a}5a17*R5L5i@1Br08)c+s8k1f@=5_b0%h zFK`%w6mQ;kKO@|pV)q$-DaWiCIBm#JQZPzB>Brvu^hlXAE*x^5_O(QAM zV^dN}qk`rky@A95=vw3fJ$BuWF*j*$WSLz*#Z8*r#)WLAF|x07o<2>=>QO|dL0P&` z5QR%LaK6X{;KRjj1S{z`h*P4a|A5N!6idMP1a+KIC??>}t40hlcqKqI7>}6^(r))T zzO&Yi1vtA}E8Gf6swi1e+?$0E(8IN1Is^&4tB?*=w ziK)j>Or1eINrsKVW=(eo^8anhOY7|UCZii(x6_Nr$8v?bY4dNS!%9t|rXXvavCn+| zG;DJzS$q*K_=wto!SzaV&_Vpc3OS=xFgk@!y=y$gK|`KcR92&&2&>kZi*#c0PHf(kNz)clbPVX}?9td62%Bidq= zQe}mR<7ml9HgRc@%NgBMa*VF(x7j%3bv7Fb`j|Nr-4jdXQv)Ikvjz_L^u@+wd=He^ z{mP<46s1Pes2{Y@DJf~4-6OJehr)7xWfQ+A;{B}@M~}k`V+=MSk>N|F+!2IanN61c zpX{(8m0B%`@(=eUCz|_mV&GdQkFWnpt4k0kI~t7i8XN@}8xr72Ex?OMCvu(TH6(pC zM^*5Mr6uuw6z@tDqSCia!O3e z0)UAsenDGG^-g7Mx(}a{(%s`e>>w`%`0lIib|2p?V>%qB}o!NJ#z7h9X@Ubna;L5xuhwErvP2`VNc#c3Ug<4Dk{YmZ}( z17Wek{HctPu_w3Rv^WzKGQf|tLQ_N9LVSESl$319egxgvX!`NIIF;&p{976brT}pW z)yTRg444YXyfo~8f1HyB_L2$RzZar^92?a+(^$&wjlCLNBx6=g4)#93=S@+j z*krWS5wx+mn}{3^4V|(PEGU#2JEmG$2?_}%-u{9Bz^~~8rEq!umzV=Tu5r`~Yrd>_ zBRkwoEs5oH=L(b7Nb+rsH`w&&3fANMGCviYH*DmP@}K8FAy=Q?QqlYwH_WTI{S}A* z2E=cj6b!2vs_G6!v0IQ+Er03@vj>?e+ovxZ4KUMk#v$n<08-0rb+-7G?k(Ux_F`rW zEa9+w@Ff1F4(&{TTCPTYyzLV8GCvhsZgmsR5%P3?p8FF$U>)ydkceTx$M}o9=@uMQ zdKtG(aqM;bVdpoWVoM^_vH&&5j#DxWZ%iAL$UF#BIevkU?DGe~g^T+Id*Y{|Ci9q> z>Lp_q}zaBdl(wwVd1zVh&A=5(SXzqa|zvV*=uoRU>{ycrAg@ z&DJscd}}w2ZOfyr2+Eb5z~tYCuaia8QEarZ-lq$gCfAd98U$SfqKb-sn2vuV!v*FU;vj z-!u#&``i;h(hj?@ba!g3RvL6*aQ&Zb71cp_;1rtU$PG*g``MF;)1k&3|NMTvGG%TuXa1eWo+esg}({mf`aV%u` zd#a58O32wE`s@N1+51V^W;3 zf*ej2Ti@tS+ceLQ72Fr>jAC~O%BFlPH(YJ;bl6^CQ4^5)|FE$^7vL;3RDcX7J~P18 zo1H`V1vyzx)_|#vz%*pjnRSmy(TwBY5F8)?f{nrwtiqsd6Q!!4N<;}HtTf&>Ut^qS z*QT3}3I9TCR8o^Dfq4+uBMjWewLkI_qAO$1q`V)hW44Q!ra8)UR_m*u2V#Il7IU@6 zg47wll`pF6APu33cI=du%8U&W(;Fo!3}YmEgaX$UEEXUZI@27@gTK?M-1O~a-_}gu zJrADMs{3U{QjezvQT-y3q1_IY?D#$@jgVktp8fIQt5_&iO`P0T9R3fy08FHmBLcFX zXf2ym$X21Sdxu`^bYoO>ckB}BAt$Wpw?5+9**4tur-n@x-jSIghk|yF-nEC9j)N73 zQ4WWY2nkFra%ewAvigX~b*)|Z;KgIUHvj1it_-f{z%AP}M;(8$TAxW4(&FzU#rO^L zaxB<_0W^fNbQGz7aO|SWus&fkgV1UGlE^6J!%ChC=p~A9@j}8D6cW5G2bg$674+qn zgn7furDN8$n7FUQ=br;a(VXWYuu7EIBpq4U`t8t1iM=z!p15RWLiR@}=++b}ojn_X zO1`aQBd-dJFOz}`x!wV}K`jBsJ;`-3^)NjfA%T$7qs2hd&CN4u-FDjSH>9q=P>J!Y&OfWaHy~j{OkHO2M<$f=q^K_c{0asJXa0fn z#oU1de9o<7H}){Mf**&pa9~tVDaRHyUEoIRmCXbvz`6;BJqtA667=q-C0M;6U5khR_Sh4|eit|RdZhOjlId%#i#^Zz)W~_tA76FE}eR zb5f~`0)YoU6b+V0K(`U2sRljq9Lmf@Y}EIt>nA1yRkGGQ_SVJU*G>d2T~{+Z;q$YU zD)eM602b)TeUOh5enYiPYFgOs7wx>s6na8=d34)Lv!CxtnhhEE{%lcQwX1q9j)nZ!9;5e`l!XvYm+#w7a8!Ts1it=TI)2=zk+b3t z=HdXStWHsL)VB6dRtH^=3RWU&^wlM0=si#sra6$kmK8EMNg>XvLYg&XEkUX@JEJZ+ zRP3-eO&F}r=F-9XvP~^~{tp2c_x45Tiu3uhn)d9@yuk?8cI~7av2}?&F&4`N;$Zg% zG7Y2AU}$;BywWzRSK4+4i$4Ylm3jYQ8!r`cYdqWcf!M7QaZNEBz#fP zo`G&JtKMe>%c(iun7((H>W{g!jqARVUAdkYmR_RrQK!}+J{M<3x7dxsr5lA(3ywD= zt!Yr=EkpHBX@($JXY$CLDK07ty3xHW9$bbcYJ?0+$)Dc(v9XXTbcOE|-TwOnrlFbY zINzlBhp;IAoRflOFmt`M7S@!D=l=ql!o4K}2n7h=4+S<^Pi@{FmM@*#@c5D4G(=y#WCFd^IM$i1Qi>CG$iiQ@a1+r70m- zb8fT%*t{XD69`yKHTWZ5BYy`)0FZ}=-H;Covgdy<3dqx_im%@KuGTAtq1DG@-5HffTchw*!2{xfq9*#H3o{lm&jMx?rAtSGaHmMMOr^~0@FHLR-~&s zo_SwY4<)y0rPdpmCgMugeSg1u3A^H5`NDs^3e!%Oyb+b%p7h?AqOJr7t`0To;`G(>N1ideW{rD()!(SD4!xGoX4rd)Z#YD{`=bsfv!*0l~*?w|`s^etU)1`xq-R zx2rR^)zP`2lQy>F1O5`!m6_f(4{l#}2>&VYkpJKZkQMTj%qdT};_6*WI*+h(7KJKU znECJJ)>OpoRnBS!vbq(y%uG>m#tyR<7s6CZk)>-to)KB>mb6c3gZuL@OpUhEzdFv| z-uS(6hV$r>;6m7>dR~r(^Va^e2s#{<;mgK5tc z@w@iTP58#@eVi6d%3aJy4yb*I2;hg^v;CJTCQP@igXivShLlshw5fDZS_T#o{5W>< z2~9{FZ(ngE2vzU$(xKBYI>J728ny)21NiR^^Ycr?-Yt7QK~i5XkK&y=f1c77wl95= zdDSZWml1sI*r~_tW8BPwty5ZS8E8Ims1-@$k}{h+5^C=yz7BCi06A zbTRSaD5inBtMjDyFD^ssD-qY%2eC;dWCtxxo?XL6EL;r{MQb0?1>bYJjT$A%$9T01 z3UVwa?hy7pD)v003M6X3hJ_}lAS<;`j64wewouAMDoEy;=g9*V;TWkk6n(h1zmF#92Tw^Y_cDwE} zZnw6b+OV$|cz(_i1jp=AOf72h&ETTyp=86~3r71YU&m5CmxanOm%_rQ&0Wwz!rF|9 zV|_?nX9Pj{P_=KS`ShO>Rh^^X`FZ;iGLj)Xcwkf1-;=P|1U6;&bI)~$FYEB_k2grj z=lS{4H@BO_j=QrSsK-fx$bgu{Xt3#8{{EAX<*C<5`fBiN*rBf={3U50xrONOMgHA- zLdGQF**1?+lC{Y8IvR|B0w^gH`U0SRJ{W%H2cEm)q+jL;3y>wuEXq-!rzn(m4F{PF zs@h97%tI^oaB!(~u~Gc&BgDqV^<)YEqI^L*)E4Qu@p^J2%hNBY`O22-LSt%bD);k^ zUugR@K4EaqPoXu8Y~}0;<$>;@q2AcUOV^y6U-bW?cXyQwTIA zyddT$kzov{Lv+w)M|gN|Y$lj*fLFDs5}mqEhs~%cmV~#exDt??o!(7L?F%8iF>&lX z%yLy_asgyX0@BK)`>B0ZUN|^79t+I2JEuR0I1RHo*^7?mphBDi2kHtNbB|Ue#=OK2 zv&QMqEODOk&y$W!a14UJ!hFbM3P_{ei4evzKe6D|36tkov*GmQwp16&<4|tSgv_^H z*`Q<-U-YJ)pZDHi0&dn6FHGKae)32PT4TI7-kycIRPDM;!C@QQbXokn1qifm_x%7| zIu}|Ac*yAAaQmrGhWdJhsOvjUXkVma zLgg?V>cmD0f@KP=BwlJl`jw~fZY1>LV@7Ud&CicKh zMMV%~wzkqwx6<0?zIf}|a6+ZIzbpUToP^~H+-I^q2UM(my|*?vNpfl-Z3ch*w-ds1A#=#B=#cZrgQl*Nt7 jgMdyc*agxn;3L3IdL!N&;0SxbM&M*6l_V;}3=2&&n<=4?"dny":"dnů"}function ue(n){return n.overtime>0?"linear-gradient(135deg, var(--warning), #d97706)":n.covered>=n.fund?"linear-gradient(135deg, var(--success), #059669)":"var(--gradient)"}function ge(){const n=ie(),{hasPermission:D}=de(),[J,G]=l.useState(!0),[N,v]=l.useState(!1),[i,V]=l.useState({ongoing_shift:null,today_shifts:[],date:"",leave_balance:{vacation_total:160,vacation_used:0,vacation_remaining:160,sick_used:0},monthly_fund:null}),[z,b]=l.useState(!1),[d,_]=l.useState({leave_type:"vacation",date_from:new Date().toISOString().split("T")[0],date_to:new Date().toISOString().split("T")[0],notes:""}),[M,$]=l.useState(!1),[B,L]=l.useState(""),[j,U]=l.useState([]),[Y,A]=l.useState(!1),[F,K]=l.useState([]),[f,Q]=l.useState(null),[I,T]=l.useState({show:!1,action:null}),k=l.useRef(null);l.useEffect(()=>()=>{k.current&&k.current.abort()},[]);const g=l.useCallback(async()=>{try{const a=await p(`${x}/attendance.php`);if(a.status===401)return;const t=await a.json();t.success&&(V(t.data),L(t.data.ongoing_shift?.notes||""),K(t.data.project_logs||[]),Q(t.data.active_project_id||null))}catch{n.error("Nepodařilo se načíst data")}finally{G(!1)}},[n]);if(l.useEffect(()=>{g()},[g]),l.useEffect(()=>{(async()=>{try{const s=await(await p(`${x}/attendance.php?action=projects`)).json();s.success&&U(s.data.projects||[])}catch{}})()},[]),le(z),!D("attendance.record"))return e.jsx(me,{});const R=a=>{if(v(!0),!navigator.geolocation){n.warning("GPS není dostupná"),O(a,{});return}navigator.geolocation.getCurrentPosition(t=>{const{latitude:s,longitude:r,accuracy:m}=t.coords;O(a,{latitude:s,longitude:r,accuracy:m,address:""}),k.current&&k.current.abort();const c=new AbortController;k.current=c,fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${s}&lon=${r}&zoom=18&addressdetails=1`,{headers:{"Accept-Language":"cs"},signal:c.signal}).then(o=>o.json()).then(o=>{o.display_name&&p(`${x}/attendance.php?action=update_address`,{method:"POST",body:JSON.stringify({latitude:s,longitude:r,address:o.display_name,punch_action:a})}).catch(()=>{})}).catch(()=>{})},t=>{let s="Nepodařilo se získat polohu";t.code===t.PERMISSION_DENIED?s="Přístup k poloze byl zamítnut":t.code===t.TIMEOUT&&(s="Vypršel časový limit"),n.error(s),T({show:!0,action:a})},{enableHighAccuracy:!0,timeout:1e4,maximumAge:6e4})},O=async(a,t={})=>{try{const s=await p(`${x}/attendance.php`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({punch_action:a,...t})});if(s.status===401)return;const r=await s.json();v(!1),r.success?(await g(),setTimeout(()=>{n.success(r.message)},300)):n.error(r.error)}catch{v(!1),n.error("Chyba připojení")}},X=async()=>{v(!0);try{const a=await p(`${x}/attendance.php`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({punch_action:"break_start"})});if(a.status===401)return;const t=await a.json();t.success?(await g(),n.success(t.message)):n.error(t.error)}catch{n.error("Chyba připojení")}finally{v(!1)}},ee=async()=>{try{const a=await p(`${x}/attendance.php?action=notes`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({notes:B})});if(a.status===401)return;const t=await a.json();t.success?n.success("Poznámka byla uložena"):n.error(t.error)}catch{n.error("Chyba připojení")}},ae=async a=>{A(!0);try{const t=await p(`${x}/attendance.php?action=switch_project`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project_id:a||null})});if(t.status===401)return;const s=await t.json();s.success?(await g(),n.success(s.message)):n.error(s.error)}catch{n.error("Chyba připojení")}finally{A(!1)}},S=(a,t)=>{if(!a||!t)return 0;const s=new Date(a),r=new Date(t);if(r{$(!0);try{const a=await p(`${x}/leave-requests.php`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(d)});if(a.status===401)return;const t=await a.json();t.success?(b(!1),await g(),await new Promise(s=>setTimeout(s,300)),n.success(t.message),_({leave_type:"vacation",date_from:new Date().toISOString().split("T")[0],date_to:new Date().toISOString().split("T")[0],notes:""})):n.error(t.error)}catch{n.error("Chyba připojení")}finally{$(!1)}};if(J)return e.jsxs("div",{className:"admin-skeleton",style:{padding:0,gap:"1.5rem"},children:[e.jsx("div",{className:"admin-skeleton-row",style:{justifyContent:"space-between"},children:e.jsxs("div",{children:[e.jsx("div",{className:"admin-skeleton-line h-8",style:{width:"200px",marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line",style:{width:"140px"}})]})}),e.jsxs("div",{style:{display:"flex",gap:"1.5rem"},children:[e.jsx("div",{className:"admin-card",style:{flex:2},children:e.jsxs("div",{className:"admin-skeleton",style:{gap:"1.25rem"},children:[e.jsx("div",{className:"admin-skeleton-line h-8",style:{width:"120px",marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line h-10",style:{width:"180px"}}),e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsxs("div",{style:{flex:1},children:[e.jsx("div",{className:"admin-skeleton-line w-1/3",style:{marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line w-1/4",style:{height:"10px"}})]}),e.jsxs("div",{style:{flex:1},children:[e.jsx("div",{className:"admin-skeleton-line w-1/3",style:{marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line w-1/4",style:{height:"10px"}})]})]}),e.jsx("div",{className:"admin-skeleton-line h-10",style:{width:"100%",borderRadius:"8px"}})]})}),e.jsxs("div",{style:{flex:1,display:"flex",flexDirection:"column",gap:"1rem"},children:[e.jsx("div",{className:"admin-card",children:e.jsxs("div",{className:"admin-skeleton",style:{gap:"1rem"},children:[e.jsx("div",{className:"admin-skeleton-line w-1/3",style:{marginBottom:"0.25rem"}}),e.jsx("div",{className:"admin-skeleton-line h-8",style:{width:"80px"}}),e.jsx("div",{className:"admin-skeleton-line",style:{width:"100%",height:"6px",borderRadius:"3px"}})]})}),e.jsx("div",{className:"admin-card",children:e.jsxs("div",{className:"admin-skeleton",style:{gap:"1rem"},children:[e.jsx("div",{className:"admin-skeleton-line w-1/3",style:{marginBottom:"0.25rem"}}),e.jsx("div",{className:"admin-skeleton-line h-8",style:{width:"80px"}}),e.jsx("div",{className:"admin-skeleton-line",style:{width:"100%",height:"6px",borderRadius:"3px"}})]})})]})]})]});const{ongoing_shift:u,today_shifts:se,leave_balance:y}=i,q=u&&!u.departure_time,W=se.filter(a=>a.departure_time),E=Math.floor(y.vacation_remaining/8),H=y.vacation_remaining%8;return e.jsxs("div",{children:[e.jsx(w.div,{className:"admin-page-header",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4},children:e.jsxs("div",{children:[e.jsx("h1",{className:"admin-page-title",children:"Docházka"}),e.jsx("p",{className:"admin-page-subtitle",children:new Date().toLocaleDateString("cs-CZ",{weekday:"long",day:"numeric",month:"long",year:"numeric"})})]})}),e.jsxs("div",{className:"attendance-layout",children:[e.jsxs(w.div,{className:"attendance-main",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.1},children:[e.jsxs("div",{className:"attendance-clock-card",children:[e.jsxs("div",{className:"attendance-clock-header",children:[e.jsx("div",{className:"attendance-clock-status",children:q?e.jsxs(e.Fragment,{children:[e.jsx("span",{className:"attendance-status-dot active"}),e.jsx("span",{children:"Pracuji"})]}):e.jsxs(e.Fragment,{children:[e.jsx("span",{className:"attendance-status-dot"}),e.jsx("span",{children:"Nepracuji"})]})}),e.jsx("div",{className:"attendance-clock-time",children:new Date().toLocaleTimeString("cs-CZ",{hour:"2-digit",minute:"2-digit"})})]}),q?e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"attendance-shift-info",children:e.jsxs("div",{className:"attendance-shift-row",children:[e.jsxs("div",{className:"attendance-shift-item",children:[e.jsx("span",{className:"attendance-shift-label",children:"Příchod"}),e.jsx("span",{className:"attendance-shift-value success",children:h(u.arrival_time)})]}),e.jsxs("div",{className:"attendance-shift-item",children:[e.jsx("span",{className:"attendance-shift-label",children:"Pauza"}),e.jsx("span",{className:`attendance-shift-value ${u.break_start?"success":""}`,children:u.break_start?`${h(u.break_start)} - ${h(u.break_end)}`:"—"})]}),e.jsxs("div",{className:"attendance-shift-item",children:[e.jsx("span",{className:"attendance-shift-label",children:"Odchod"}),e.jsx("span",{className:"attendance-shift-value",children:"—"})]})]})}),j.length>0&&e.jsxs("div",{className:"attendance-project-section",children:[e.jsxs("div",{className:"attendance-project-header",children:[e.jsx("span",{className:"attendance-shift-label",children:"Projekt"}),f?e.jsx("span",{className:"admin-badge admin-badge-wrap",style:{fontSize:"0.8125rem"},children:j.find(a=>String(a.id)===String(f))?`${j.find(a=>String(a.id)===String(f)).project_number} – ${j.find(a=>String(a.id)===String(f)).name}`:`Projekt #${f}`}):e.jsx("span",{className:"text-muted",style:{fontSize:"0.8125rem"},children:"Žádný"})]}),e.jsxs("select",{value:f||"",onChange:a=>ae(a.target.value||null),disabled:Y,className:"admin-form-select",style:{fontSize:"0.875rem"},children:[e.jsx("option",{value:"",children:"— Bez projektu —"}),j.map(a=>e.jsxs("option",{value:a.id,children:[a.project_number," – ",a.name]},a.id))]}),F.length>0&&e.jsx("div",{className:"attendance-project-logs",children:F.map((a,t)=>{const s=new Date(a.started_at),r=a.ended_at?new Date(a.ended_at):new Date,m=Math.floor((r-s)/6e4),c=Math.floor(m/60),o=m%60;return e.jsxs("div",{className:"attendance-project-log-item",children:[e.jsx("span",{className:"attendance-project-log-name",children:a.project_name||`Projekt #${a.project_id}`}),e.jsxs("span",{className:"attendance-project-log-time",children:[h(a.started_at)," – ",a.ended_at?h(a.ended_at):"nyní"]}),e.jsxs("span",{className:"attendance-project-log-duration",children:[c,":",String(o).padStart(2,"0")," h"]})]},a.id||t)})})]}),e.jsxs("div",{className:"attendance-clock-actions",children:[!u.break_start&&e.jsx("button",{onClick:X,disabled:N,className:"admin-btn admin-btn-secondary",style:{width:"100%"},children:"Pauza (30 min)"}),e.jsx("button",{onClick:()=>R("departure"),disabled:N,className:"admin-btn admin-btn-primary",style:{width:"100%"},children:N?"Zpracovávám...":"Odchod"}),e.jsx("button",{onClick:()=>b(!0),className:"admin-btn admin-btn-secondary",style:{width:"100%"},children:"Žádost o nepřítomnost"})]}),e.jsxs("div",{className:"attendance-notes",children:[e.jsx("label",{className:"attendance-notes-label",children:"Poznámka ke směně"}),e.jsx("textarea",{value:B,onChange:a=>L(a.target.value),placeholder:"Co jste dělali během směny...",className:"admin-form-textarea",rows:3}),e.jsx("div",{style:{marginTop:"0.5rem"},children:e.jsx("button",{onClick:ee,className:"admin-btn admin-btn-secondary admin-btn-sm",children:"Uložit poznámku"})})]})]}):e.jsxs("div",{className:"attendance-clock-actions",children:[e.jsx("button",{onClick:()=>R("arrival"),disabled:N,className:"admin-btn admin-btn-primary",style:{width:"100%"},children:N?"Zpracovávám...":"Příchod"}),e.jsx("button",{onClick:()=>b(!0),className:"admin-btn admin-btn-secondary",style:{width:"100%"},children:"Žádost o nepřítomnost"})]})]}),W.length>0&&e.jsxs("div",{className:"admin-card",style:{marginTop:"1.5rem"},children:[e.jsx("div",{className:"admin-card-header",children:e.jsx("h2",{className:"admin-card-title",children:"Dnešní dokončené směny"})}),e.jsx("div",{className:"admin-card-body",children:e.jsx("div",{className:"admin-table-responsive",children:e.jsxs("table",{className:"admin-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Příchod"}),e.jsx("th",{children:"Pauza"}),e.jsx("th",{children:"Odchod"}),e.jsx("th",{children:"Odpracováno"}),j.length>0&&e.jsx("th",{children:"Projekty"})]})}),e.jsx("tbody",{children:W.map(a=>{const t=a.project_logs||[];return e.jsxs("tr",{children:[e.jsx("td",{className:"admin-mono",children:h(a.arrival_time)}),e.jsx("td",{className:"admin-mono",children:a.break_start&&a.break_end?`${h(a.break_start)} - ${h(a.break_end)}`:"—"}),e.jsx("td",{className:"admin-mono",children:h(a.departure_time)}),e.jsx("td",{className:"admin-mono",children:ce(oe(a),!0)}),j.length>0&&e.jsx("td",{children:t.length>0?e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"0.25rem"},children:t.map((s,r)=>{const m=s.ended_at?Math.floor((new Date(s.ended_at)-new Date(s.started_at))/6e4):0,c=Math.floor(m/60),o=m%60;return e.jsxs("span",{style:{fontSize:"12px"},children:[s.project_name||`#${s.project_id}`," (",c,":",String(o).padStart(2,"0"),"h)"]},s.id||r)})}):"—"})]},a.id)})})]})})})]})]}),e.jsxs(w.div,{className:"attendance-sidebar",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.1},children:[e.jsxs("div",{className:"attendance-balance-card",children:[e.jsxs("h3",{className:"attendance-balance-title",children:["Dovolená ",new Date().getFullYear()]}),e.jsxs("div",{className:"attendance-balance-value",children:[e.jsx("span",{className:"attendance-balance-number",children:E}),e.jsxs("span",{className:"attendance-balance-unit",children:[he(E),H>0&&` ${H}h`]})]}),e.jsxs("div",{className:"attendance-balance-detail",children:[e.jsxs("span",{children:["Celkem: ",y.vacation_total,"h"]}),e.jsxs("span",{children:["Čerpáno: ",y.vacation_used,"h"]})]}),e.jsx("div",{className:"attendance-balance-bar",children:e.jsx("div",{className:"attendance-balance-progress",style:{width:`${y.vacation_remaining/y.vacation_total*100}%`}})})]}),i.monthly_fund&&e.jsxs("div",{className:"admin-stat-card",style:{flexDirection:"column",alignItems:"stretch"},children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"1rem"},children:[e.jsx("div",{className:"admin-stat-icon info",children:e.jsxs("svg",{width:"24",height:"24",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("rect",{x:"3",y:"4",width:"18",height:"18",rx:"2",ry:"2"}),e.jsx("line",{x1:"16",y1:"2",x2:"16",y2:"6"}),e.jsx("line",{x1:"8",y1:"2",x2:"8",y2:"6"}),e.jsx("line",{x1:"3",y1:"10",x2:"21",y2:"10"})]})}),e.jsxs("div",{className:"admin-stat-content",children:[e.jsx("span",{className:"admin-stat-label",children:i.monthly_fund.month_name}),e.jsxs("span",{className:"admin-stat-value",children:[i.monthly_fund.worked,"h / ",i.monthly_fund.fund,"h"]})]})]}),e.jsxs("div",{style:{marginTop:"0.75rem"},children:[e.jsxs("div",{className:"text-secondary",style:{display:"flex",justifyContent:"space-between",fontSize:"0.8125rem",marginBottom:"0.5rem"},children:[e.jsxs("span",{children:["Odpracováno: ",i.monthly_fund.worked,"h"]}),i.monthly_fund.overtime>0?e.jsxs("span",{className:"text-warning fw-600",children:["Přesčas: +",i.monthly_fund.overtime,"h"]}):e.jsxs("span",{children:["Zbývá: ",i.monthly_fund.remaining,"h"]})]}),e.jsx("div",{className:"attendance-balance-bar",children:e.jsx("div",{className:"attendance-balance-progress",style:{width:`${Math.min(100,i.monthly_fund.covered/i.monthly_fund.fund*100)}%`,background:ue(i.monthly_fund)}})}),i.monthly_fund.leave_hours>0&&e.jsxs("div",{className:"text-muted",style:{fontSize:"0.75rem",marginTop:"0.375rem"},children:["Pokryto: ",i.monthly_fund.covered,"h (práce ",i.monthly_fund.worked,"h",i.monthly_fund.vacation_hours>0&&` + dovolená ${i.monthly_fund.vacation_hours}h`,i.monthly_fund.sick_hours>0&&` + nemoc ${i.monthly_fund.sick_hours}h`,i.monthly_fund.holiday_hours>0&&` + svátek ${i.monthly_fund.holiday_hours}h`,i.monthly_fund.unpaid_hours>0&&` + neplacené ${i.monthly_fund.unpaid_hours}h`,")"]})]})]}),e.jsxs("div",{className:"admin-stat-card",children:[e.jsx("div",{className:"admin-stat-icon danger",children:e.jsx("svg",{width:"24",height:"24",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:e.jsx("path",{d:"M22 12h-4l-3 9L9 3l-3 9H2"})})}),e.jsxs("div",{className:"admin-stat-content",children:[e.jsxs("span",{className:"admin-stat-label",children:["Nemoc ",new Date().getFullYear()]}),e.jsxs("span",{className:"admin-stat-value",children:[y.sick_used,"h čerpáno"]})]})]}),e.jsxs("div",{className:"attendance-quick-links",children:[e.jsx("h4",{className:"attendance-quick-title",children:"Rychlé odkazy"}),e.jsxs(C,{to:"/attendance/requests",className:"attendance-quick-link",children:[e.jsxs("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M9 11l3 3L22 4"}),e.jsx("path",{d:"M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"})]}),e.jsx("span",{children:"Moje žádosti"}),e.jsx("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:e.jsx("path",{d:"M9 18l6-6-6-6"})})]}),e.jsxs(C,{to:"/attendance/history",className:"attendance-quick-link",children:[e.jsxs("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M3 3v18h18"}),e.jsx("path",{d:"M18.7 8l-5.1 5.2-2.8-2.7L7 14.3"})]}),e.jsx("span",{children:"Historie docházky"}),e.jsx("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:e.jsx("path",{d:"M9 18l6-6-6-6"})})]}),D("attendance.admin")&&e.jsxs(C,{to:"/attendance/admin",className:"attendance-quick-link",children:[e.jsxs("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"}),e.jsx("circle",{cx:"9",cy:"7",r:"4"}),e.jsx("path",{d:"M23 21v-2a4 4 0 0 0-3-3.87"}),e.jsx("path",{d:"M16 3.13a4 4 0 0 1 0 7.75"})]}),e.jsx("span",{children:"Správa docházky"}),e.jsx("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:e.jsx("path",{d:"M9 18l6-6-6-6"})})]}),D("attendance.balances")&&e.jsxs(C,{to:"/attendance/balances",className:"attendance-quick-link",children:[e.jsx("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:e.jsx("path",{d:"M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"})}),e.jsx("span",{children:"Správa bilancí"}),e.jsx("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:e.jsx("path",{d:"M9 18l6-6-6-6"})})]})]})]})]}),e.jsx(ne,{children:z&&e.jsxs(w.div,{className:"admin-modal-overlay",initial:{opacity:0},animate:{opacity:1},exit:{opacity:0},transition:{duration:.2},children:[e.jsx("div",{className:"admin-modal-backdrop",onClick:()=>b(!1)}),e.jsxs(w.div,{className:"admin-modal",initial:{opacity:0,scale:.95,y:20},animate:{opacity:1,scale:1,y:0},exit:{opacity:0,scale:.95,y:20},transition:{duration:.2},children:[e.jsx("div",{className:"admin-modal-header",children:e.jsx("h2",{className:"admin-modal-title",children:"Žádost o nepřítomnost"})}),e.jsx("div",{className:"admin-modal-body",children:e.jsxs("div",{className:"admin-form",children:[e.jsx(P,{label:"Typ nepřítomnosti",children:e.jsxs("select",{value:d.leave_type,onChange:a=>_({...d,leave_type:a.target.value}),className:"admin-form-select",children:[e.jsx("option",{value:"vacation",children:"Dovolená"}),e.jsx("option",{value:"sick",children:"Nemoc"}),e.jsx("option",{value:"unpaid",children:"Neplacené volno"})]})}),e.jsxs("div",{style:{display:"grid",gridTemplateColumns:"1fr 1fr",gap:"1rem"},children:[e.jsx(P,{label:"Od",children:e.jsx(Z,{mode:"date",value:d.date_from,onChange:a=>{_(t=>({...t,date_from:a,date_to:t.date_to_({...d,date_to:a})})})]}),d.date_from&&d.date_to&&e.jsx("div",{className:"admin-form-group",children:e.jsxs("div",{style:{display:"flex",gap:"1.5rem",padding:"0.75rem 1rem",background:"var(--bg-tertiary)",borderRadius:"var(--border-radius)",fontSize:"0.875rem"},children:[e.jsxs("span",{children:[e.jsx("strong",{children:S(d.date_from,d.date_to)})," ",(()=>{const a=S(d.date_from,d.date_to);return a===1?"pracovní den":a>=2&&a<=4?"pracovní dny":"pracovních dnů"})()]}),e.jsxs("span",{className:"text-muted",children:[S(d.date_from,d.date_to)*8," hodin"]})]})}),e.jsx(P,{label:"Poznámka",children:e.jsx("textarea",{value:d.notes,onChange:a=>_({...d,notes:a.target.value}),placeholder:"Volitelná poznámka...",className:"admin-form-textarea",rows:2})})]})}),e.jsxs("div",{className:"admin-modal-footer",children:[e.jsx("button",{type:"button",onClick:()=>b(!1),className:"admin-btn admin-btn-secondary",disabled:M,children:"Zrušit"}),e.jsx("button",{type:"button",onClick:te,disabled:M||S(d.date_from,d.date_to)===0,className:"admin-btn admin-btn-primary",children:M?"Odesílám...":"Odeslat žádost"})]})]})]})}),e.jsx(re,{isOpen:I.show,onClose:()=>{T({show:!1,action:null}),v(!1)},onConfirm:()=>{T({show:!1,action:null}),O(I.action,{})},title:"GPS nedostupná",message:"Nepodařilo se získat polohu. Chcete pokračovat bez GPS?",confirmText:"Pokračovat",cancelText:"Zrušit",type:"warning"})]})}export{ge as default}; diff --git a/dist/assets/AttendanceAdmin-CN6S51Mm.js b/dist/assets/AttendanceAdmin-CN6S51Mm.js new file mode 100644 index 0000000..58d802b --- /dev/null +++ b/dist/assets/AttendanceAdmin-CN6S51Mm.js @@ -0,0 +1,125 @@ +import{j as e,A as pe,m as T}from"./vendor-animation-0s3FMHwK.js";import{b as Z,A as j,c as L,a as we,u as Se,F as ce,C as Ce}from"./index-BBlIrj2z.js";import{F as ze}from"./Forbidden-D25jV3Oq.js";import{b as le,j as ue,k as xe,c as $e,g as ge,d as ve,e as me,a as Q,f as oe,l as J,m as q,h as Me,i as G}from"./attendanceHelpers-D6sLEw0q.js";import{L as Pe,r as v}from"./vendor-react-BVs3cwbi.js";import{a9 as Te}from"./vendor-utils-Dyr8OjFr.js";function Be({show:a,onClose:s,form:d,setForm:n,users:r,onSubmit:m,submitting:l,toggleUser:h,toggleAllUsers:_}){return Z(a),e.jsx(pe,{children:a&&e.jsxs(T.div,{className:"admin-modal-overlay",initial:{opacity:0},animate:{opacity:1},exit:{opacity:0},transition:{duration:.2},children:[e.jsx("div",{className:"admin-modal-backdrop",onClick:()=>!l&&s()}),e.jsxs(T.div,{className:"admin-modal admin-modal-lg",initial:{opacity:0,scale:.95,y:20},animate:{opacity:1,scale:1,y:0},exit:{opacity:0,scale:.95,y:20},transition:{duration:.2},children:[e.jsxs("div",{className:"admin-modal-header",children:[e.jsx("h2",{className:"admin-modal-title",children:"Vyplnit docházku za měsíc"}),e.jsx("p",{style:{color:"var(--text-secondary)",marginTop:"0.25rem",fontSize:"0.875rem"},children:"Vytvoří záznamy pro všechny pracovní dny. Svátky se automaticky označí. Existující záznamy se přeskočí."})]}),e.jsx("div",{className:"admin-modal-body",children:e.jsxs("div",{className:"admin-form",children:[e.jsxs("div",{className:"admin-form-group",children:[e.jsx("label",{className:"admin-form-label",children:"Měsíc"}),e.jsx(j,{mode:"month",value:d.month,onChange:p=>n({...d,month:p})})]}),e.jsxs("div",{className:"admin-form-group",children:[e.jsxs("label",{className:"admin-form-label",children:["Zaměstnanci",e.jsx("button",{type:"button",onClick:_,style:{marginLeft:"0.75rem",background:"none",border:"none",color:"var(--accent-color)",cursor:"pointer",fontSize:"0.8125rem",fontWeight:500,padding:0},children:d.user_ids.length===r.length?"Odznačit vše":"Vybrat vše"})]}),e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"0.375rem",maxHeight:"200px",overflowY:"auto",padding:"0.75rem",background:"var(--bg-tertiary)",borderRadius:"var(--border-radius-sm)",border:"1px solid var(--border-color)"},children:r.map(p=>e.jsxs("label",{className:"admin-form-checkbox",children:[e.jsx("input",{type:"checkbox",checked:d.user_ids.includes(String(p.id)),onChange:()=>h(p.id)}),e.jsx("span",{children:p.name})]},p.id))}),e.jsxs("small",{className:"admin-form-hint",children:["Vybráno: ",d.user_ids.length," z ",r.length]})]}),e.jsxs("div",{className:"admin-form-row",children:[e.jsxs("div",{className:"admin-form-group",children:[e.jsx("label",{className:"admin-form-label",children:"Příchod"}),e.jsx(j,{mode:"time",value:d.arrival_time,onChange:p=>n({...d,arrival_time:p})})]}),e.jsxs("div",{className:"admin-form-group",children:[e.jsx("label",{className:"admin-form-label",children:"Odchod"}),e.jsx(j,{mode:"time",value:d.departure_time,onChange:p=>n({...d,departure_time:p})})]})]}),e.jsxs("div",{className:"admin-form-row",children:[e.jsxs("div",{className:"admin-form-group",children:[e.jsx("label",{className:"admin-form-label",children:"Začátek pauzy"}),e.jsx(j,{mode:"time",value:d.break_start_time,onChange:p=>n({...d,break_start_time:p})})]}),e.jsxs("div",{className:"admin-form-group",children:[e.jsx("label",{className:"admin-form-label",children:"Konec pauzy"}),e.jsx(j,{mode:"time",value:d.break_end_time,onChange:p=>n({...d,break_end_time:p})})]})]})]})}),e.jsxs("div",{className:"admin-modal-footer",children:[e.jsx("button",{type:"button",onClick:s,className:"admin-btn admin-btn-secondary",disabled:l,children:"Zrušit"}),e.jsx("button",{type:"button",onClick:m,className:"admin-btn admin-btn-primary",disabled:l||d.user_ids.length===0,children:l?"Vytvářím záznamy...":"Vyplnit měsíc"})]})]})]})})}let Fe=0;function Le({form:a,projectLogs:s}){const d=ue(a),n=xe(s),r=d-n;if(!s.some(h=>h.project_id)||d<=0)return null;const l=r===0;return e.jsxs("div",{style:{padding:"0.5rem 0.75rem",marginBottom:"0.5rem",borderRadius:"6px",fontSize:"0.8rem",background:l?"var(--success-bg, rgba(34,197,94,0.1))":"var(--danger-bg, rgba(239,68,68,0.1))",color:l?"var(--success-color, #16a34a)":"var(--danger-color, #dc2626)",border:`1px solid ${l?"var(--success-border, rgba(34,197,94,0.3))":"var(--danger-border, rgba(239,68,68,0.3))"}`},children:["Odpracováno: ",Math.floor(d/60),"h ",d%60,"m | Přiřazeno: ",Math.floor(n/60),"h ",n%60,"m | Zbývá: ",Math.floor(Math.abs(r)/60),"h ",Math.abs(r)%60,"m ",r<0?"(překročeno)":""]})}function Oe({log:a,index:s,projectList:d,onUpdate:n,onRemove:r}){return e.jsxs("div",{style:{display:"flex",gap:"0.5rem",alignItems:"center",marginBottom:"0.5rem"},children:[e.jsxs("select",{value:a.project_id,onChange:m=>n(s,"project_id",m.target.value),className:"admin-form-select",style:{flex:3,marginBottom:0},children:[e.jsx("option",{value:"",children:"— Projekt —"}),d.map(m=>e.jsxs("option",{value:m.id,children:[m.project_number," – ",m.name]},m.id))]}),e.jsx("input",{type:"number",min:"0",max:"24",value:a.hours,onChange:m=>n(s,"hours",m.target.value),className:"admin-form-input",style:{width:"60px",marginBottom:0,textAlign:"center"},placeholder:"h"}),e.jsx("span",{style:{fontSize:"0.85rem",color:"var(--text-secondary)"},children:"h"}),e.jsx("input",{type:"number",min:"0",max:"59",value:a.minutes,onChange:m=>n(s,"minutes",m.target.value),className:"admin-form-input",style:{width:"60px",marginBottom:0,textAlign:"center"},placeholder:"m"}),e.jsx("span",{style:{fontSize:"0.85rem",color:"var(--text-secondary)"},children:"m"}),e.jsx("button",{type:"button",onClick:()=>r(s),className:"admin-btn admin-btn-secondary admin-btn-sm",style:{padding:"0.375rem",flexShrink:0},title:"Odebrat",children:e.jsx("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:e.jsx("path",{d:"M18 6L6 18M6 6l12 12"})})})]})}function he({mode:a,show:s,onClose:d,onSubmit:n,form:r,setForm:m,projectLogs:l,setProjectLogs:h,projectList:_,users:p,onShiftDateChange:w,editingRecord:S}){Z(s);const y=a==="create",k=r.leave_type==="work",x=(i,f)=>{m({...r,[i]:f})},E=(i,f,M)=>{const z=[...l];z[i]={...z[i],[f]:M},h(z)},B=i=>{h(l.filter((f,M)=>M!==i))},C=()=>{h([...l,{_key:`log-${++Fe}`,project_id:"",hours:"",minutes:""}])};return e.jsx(pe,{children:s&&e.jsxs(T.div,{className:"admin-modal-overlay",initial:{opacity:0},animate:{opacity:1},exit:{opacity:0},transition:{duration:.2},children:[e.jsx("div",{className:"admin-modal-backdrop",onClick:d}),e.jsxs(T.div,{className:"admin-modal admin-modal-lg",initial:{opacity:0,scale:.95,y:20},animate:{opacity:1,scale:1,y:0},exit:{opacity:0,scale:.95,y:20},transition:{duration:.2},children:[e.jsxs("div",{className:"admin-modal-header",children:[e.jsx("h2",{className:"admin-modal-title",children:y?"Přidat záznam docházky":"Upravit docházku"}),!y&&S&&e.jsxs("p",{style:{color:"var(--text-secondary)",marginTop:"0.25rem"},children:[S.user_name," — ",le(S.shift_date)]})]}),e.jsx("div",{className:"admin-modal-body",children:e.jsxs("div",{className:"admin-form",children:[y?e.jsxs("div",{className:"admin-form-row",children:[e.jsxs("div",{className:"admin-form-group",children:[e.jsx("label",{className:"admin-form-label required",children:"Zaměstnanec"}),e.jsxs("select",{value:r.user_id,onChange:i=>x("user_id",i.target.value),className:"admin-form-select",children:[e.jsx("option",{value:"",children:"Vyberte zaměstnance"}),p.map(i=>e.jsx("option",{value:i.id,children:i.name},i.id))]})]}),e.jsxs("div",{className:"admin-form-group",children:[e.jsx("label",{className:"admin-form-label required",children:"Datum směny"}),e.jsx(j,{mode:"date",value:r.shift_date,onChange:i=>w(i)})]})]}):e.jsxs("div",{className:"admin-form-group",children:[e.jsx("label",{className:"admin-form-label",children:"Datum směny"}),e.jsx(j,{mode:"date",value:r.shift_date,onChange:i=>x("shift_date",i)})]}),e.jsxs("div",{className:"admin-form-group",children:[e.jsx("label",{className:"admin-form-label",children:"Typ záznamu"}),e.jsxs("select",{value:r.leave_type,onChange:i=>x("leave_type",i.target.value),className:"admin-form-select",children:[e.jsx("option",{value:"work",children:"Práce"}),e.jsx("option",{value:"vacation",children:"Dovolená"}),e.jsx("option",{value:"sick",children:"Nemoc"}),e.jsx("option",{value:"holiday",children:"Svátek"}),e.jsx("option",{value:"unpaid",children:"Neplacené volno"})]})]}),!k&&e.jsxs("div",{className:"admin-form-group",children:[e.jsx("label",{className:"admin-form-label",children:"Počet hodin"}),e.jsx("input",{type:"number",inputMode:"decimal",value:r.leave_hours,onChange:i=>x("leave_hours",parseFloat(i.target.value)),min:"0.5",max:"24",step:"0.5",className:"admin-form-input"}),y&&e.jsx("small",{className:"admin-form-hint",children:"8 hodin = celý den"})]}),k&&e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"admin-form-row",children:[e.jsxs("div",{className:"admin-form-group",children:[e.jsx("label",{className:"admin-form-label",children:"Příchod - datum"}),e.jsx(j,{mode:"date",value:r.arrival_date,onChange:i=>x("arrival_date",i)})]}),e.jsxs("div",{className:"admin-form-group",children:[e.jsx("label",{className:"admin-form-label",children:"Příchod - čas"}),e.jsx(j,{mode:"time",value:r.arrival_time,onChange:i=>x("arrival_time",i)})]})]}),e.jsxs("div",{className:"admin-form-row",children:[e.jsxs("div",{className:"admin-form-group",children:[e.jsx("label",{className:"admin-form-label",children:"Začátek pauzy - datum"}),e.jsx(j,{mode:"date",value:r.break_start_date,onChange:i=>x("break_start_date",i)})]}),e.jsxs("div",{className:"admin-form-group",children:[e.jsx("label",{className:"admin-form-label",children:"Začátek pauzy - čas"}),e.jsx(j,{mode:"time",value:r.break_start_time,onChange:i=>x("break_start_time",i)})]})]}),e.jsxs("div",{className:"admin-form-row",children:[e.jsxs("div",{className:"admin-form-group",children:[e.jsx("label",{className:"admin-form-label",children:"Konec pauzy - datum"}),e.jsx(j,{mode:"date",value:r.break_end_date,onChange:i=>x("break_end_date",i)})]}),e.jsxs("div",{className:"admin-form-group",children:[e.jsx("label",{className:"admin-form-label",children:"Konec pauzy - čas"}),e.jsx(j,{mode:"time",value:r.break_end_time,onChange:i=>x("break_end_time",i)})]})]}),e.jsxs("div",{className:"admin-form-row",children:[e.jsxs("div",{className:"admin-form-group",children:[e.jsx("label",{className:"admin-form-label",children:"Odchod - datum"}),e.jsx(j,{mode:"date",value:r.departure_date,onChange:i=>x("departure_date",i)})]}),e.jsxs("div",{className:"admin-form-group",children:[e.jsx("label",{className:"admin-form-label",children:"Odchod - čas"}),e.jsx(j,{mode:"time",value:r.departure_time,onChange:i=>x("departure_time",i)})]})]})]}),k&&_.length>0&&e.jsxs("div",{className:"admin-form-group",children:[e.jsx("label",{className:"admin-form-label",children:"Projekty"}),e.jsx(Le,{form:r,projectLogs:l}),l.map((i,f)=>e.jsx(Oe,{log:i,index:f,projectList:_,onUpdate:E,onRemove:B},i._key||f)),e.jsx("button",{type:"button",onClick:C,className:"admin-btn admin-btn-secondary admin-btn-sm",children:"+ Přidat projekt"})]}),e.jsxs("div",{className:"admin-form-group",children:[e.jsx("label",{className:"admin-form-label",children:"Poznámka"}),e.jsx("textarea",{value:r.notes,onChange:i=>x("notes",i.target.value),className:"admin-form-textarea",rows:3})]})]})}),e.jsxs("div",{className:"admin-modal-footer",children:[e.jsx("button",{type:"button",onClick:d,className:"admin-btn admin-btn-secondary",children:"Zrušit"}),e.jsx("button",{type:"button",onClick:n,className:"admin-btn admin-btn-primary",children:"Uložit"})]})]})]})})}function Ee(a){return a.break_start&&a.break_end?`${oe(a.break_start)} - ${oe(a.break_end)}`:a.break_start?`${oe(a.break_start)} - ?`:"—"}function Ae(a){return a.project_logs&&a.project_logs.length>0?e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"0.125rem"},children:a.project_logs.map((s,d)=>{let n,r,m=!1;if(s.hours!==null&&s.hours!==void 0)n=parseInt(s.hours)||0,r=parseInt(s.minutes)||0;else{m=!s.ended_at;const l=s.ended_at?new Date(s.ended_at):new Date,h=Math.floor((l-new Date(s.started_at))/6e4);n=Math.floor(h/60),r=h%60}return e.jsxs("span",{className:"admin-badge",style:{fontSize:"0.7rem",display:"inline-block",background:m?"var(--accent-light)":void 0},children:[s.project_name||`#${s.project_id}`," (",n,":",String(r).padStart(2,"0"),"h",m?" ▸":"",")"]},s.id||d)})}):a.project_name?e.jsx("span",{className:"admin-badge admin-badge-wrap",style:{fontSize:"0.75rem"},children:a.project_name}):"—"}function We({records:a,onEdit:s,onDelete:d}){return a.length===0?e.jsx("div",{className:"admin-empty-state",children:e.jsx("p",{children:"Za tento měsíc nejsou žádné záznamy."})}):e.jsx("div",{className:"admin-table-responsive",children:e.jsxs("table",{className:"admin-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Datum"}),e.jsx("th",{children:"Zaměstnanec"}),e.jsx("th",{children:"Typ"}),e.jsx("th",{children:"Příchod"}),e.jsx("th",{children:"Pauza"}),e.jsx("th",{children:"Odchod"}),e.jsx("th",{children:"Hodiny"}),e.jsx("th",{children:"Projekt"}),e.jsx("th",{children:"GPS"}),e.jsx("th",{children:"Poznámka"}),e.jsx("th",{children:"Akce"})]})}),e.jsx("tbody",{children:a.map(n=>{const r=n.leave_type||"work",m=r!=="work",l=m?(n.leave_hours||8)*60:$e(n),h=n.arrival_lat&&n.arrival_lng||n.departure_lat&&n.departure_lng;return e.jsxs("tr",{children:[e.jsx("td",{className:"admin-mono",children:le(n.shift_date)}),e.jsx("td",{children:n.user_name}),e.jsx("td",{children:e.jsx("span",{className:`attendance-leave-badge ${ve(r)}`,children:ge(r)})}),e.jsx("td",{className:"admin-mono",children:m?"—":me(n.arrival_time)}),e.jsx("td",{className:"admin-mono",children:m?"—":Ee(n)}),e.jsx("td",{className:"admin-mono",children:m?"—":me(n.departure_time)}),e.jsx("td",{className:"admin-mono",children:l>0?`${Q(l)} h`:"—"}),e.jsx("td",{children:Ae(n)}),e.jsx("td",{children:h?e.jsx(Pe,{to:`/attendance/location/${n.id}`,className:"attendance-gps-link",title:"Zobrazit polohu","aria-label":"Zobrazit polohu",children:"📍"}):"—"}),e.jsx("td",{style:{maxWidth:"100px",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},title:n.notes||"",children:n.notes||""}),e.jsx("td",{children:e.jsxs("div",{className:"admin-table-actions",children:[e.jsx("button",{onClick:()=>s(n),className:"admin-btn-icon",title:"Upravit","aria-label":"Upravit",children:e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"}),e.jsx("path",{d:"M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"})]})}),e.jsx("button",{onClick:()=>d(n),className:"admin-btn-icon danger",title:"Smazat","aria-label":"Smazat",children:e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("polyline",{points:"3 6 5 6 21 6"}),e.jsx("path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"})]})})]})})]},n.id)})})]})})}const O="/api/admin";function Ze(a){return a.overtime>0?`+${a.overtime}h přesčas`:a.missing>0?`−${a.missing}h`:'splněno'}function Re(a){return a.project_logs&&a.project_logs.length>0?a.project_logs.map(s=>{let d,n;if(s.hours!==null&&s.hours!==void 0)d=parseInt(s.hours)||0,n=parseInt(s.minutes)||0;else if(s.started_at&&s.ended_at){const r=Math.max(0,Math.floor((new Date(s.ended_at)-new Date(s.started_at))/6e4));d=Math.floor(r/60),n=r%60}else d=0,n=0;return`

          ${s.project_name||`#${s.project_id}`} (${d}:${String(n).padStart(2,"0")}h)
          `}).join(""):a.project_name||"—"}function Ue(a,s,d){const n=d.leave_balances[a]?Ve(a,s,d):"",r=s.records.map(l=>{const h=l.leave_type||"work",_=h!=="work",p=Me(l),w=Math.floor(p/60),S=p%60,y=_||!l.break_start||!l.break_end?"—":`${G(l.break_start,l.shift_date)} - ${G(l.break_end,l.shift_date)}`;return` + ${le(l.shift_date)} + ${ge(h)} + ${_?"—":G(l.arrival_time,l.shift_date)} + ${y} + ${_?"—":G(l.departure_time,l.shift_date)} + ${p>0?`${w}:${String(S).padStart(2,"0")}`:"—"} + ${Re(l)} + ${l.notes||""} + `}).join(""),m=s.fund!==null?` + Fond měsíce: + ${s.covered}h / ${s.fund}h + ${Ze(s)} + `:"";return`
          +
          +

          ${s.name}

          + Odpracováno: ${Q(s.minutes)} h +
          + ${n} + + + + + + + + + + + + ${r} + + + + + + + ${m} + +
          DatumTypPříchodPauzaOdchodHodinyProjektyPoznámka
          Odpracováno:${Q(s.minutes)} h
          +
          `}function Ve(a,s,d){const n=d.leave_balances[a];let r=`Dovolená ${d.year}: Zbývá ${n.vacation_remaining.toFixed(1)}h z ${n.vacation_total}h`;return s.vacation_hours>0&&(r+=` | Tento měsíc: ${s.vacation_hours}h`),s.sick_hours>0&&(r+=` | Nemoc: ${s.sick_hours}h`),s.holiday_hours>0&&(r+=` | Svátek: ${s.holiday_hours}h`),s.overtime>0&&(r+=` | Přesčas: +${s.overtime}h`),`
          ${r}
          `}function He({alert:a}){const[s,d]=v.useState(!0),[n,r]=v.useState(()=>{const t=new Date;return`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}`}),[m,l]=v.useState(""),[h,_]=v.useState({records:[],users:[],user_totals:{},leave_balances:{}}),[p,w]=v.useState(!1),[S,y]=v.useState(!1),[k,x]=v.useState({month:"",user_ids:[],arrival_time:"08:00",departure_time:"16:30",break_start_time:"12:00",break_end_time:"12:30"}),[E,B]=v.useState(!1),C=new Date().toISOString().split("T")[0],[i,f]=v.useState({user_id:"",shift_date:C,leave_type:"work",leave_hours:8,arrival_date:C,arrival_time:"",break_start_date:C,break_start_time:"",break_end_date:C,break_end_time:"",departure_date:C,departure_time:"",notes:""}),[M,z]=v.useState(!1),[R,X]=v.useState(null),[F,A]=v.useState({shift_date:"",leave_type:"work",leave_hours:8,arrival_date:"",arrival_time:"",break_start_date:"",break_start_time:"",break_end_date:"",break_end_time:"",departure_date:"",departure_time:"",notes:""}),[W,U]=v.useState({show:!1,record:null}),[D,ee]=v.useState([]),[V,H]=v.useState([]),[I,K]=v.useState([]),ae=v.useRef(null);v.useEffect(()=>{(async()=>{try{const o=await(await L(`${O}/attendance.php?action=projects`)).json();o.success&&ee(o.data.projects||[])}catch{}})()},[]);const P=v.useCallback(async(t=!0)=>{t&&d(!0);try{let c=`${O}/attendance.php?action=admin&month=${n}`;m&&(c+=`&user_id=${m}`);const o=await L(c);if(o.status===401)return;const b=await o.json();b.success&&_(b.data)}catch{a.error("Nepodařilo se načíst data")}finally{t&&d(!1)}},[n,m,a]);v.useEffect(()=>{P()},[P]);const te=()=>{const t=new Date().toISOString().split("T")[0];f({user_id:"",shift_date:t,leave_type:"work",leave_hours:8,arrival_date:t,arrival_time:"",break_start_date:t,break_start_time:"",break_end_date:t,break_end_time:"",departure_date:t,departure_time:"",notes:"",project_id:""}),H([]),B(!0)},se=t=>{f({...i,shift_date:t,arrival_date:t,break_start_date:t,break_end_date:t,departure_date:t})},ne=async()=>{if(!i.user_id||!i.shift_date){a.error("Vyplňte zaměstnance a datum směny");return}const t=V.filter(c=>c.project_id);if(!(t.length>0&&i.leave_type==="work"&&!Y(t,i)))try{const c={...i};t.length>0&&i.leave_type==="work"&&(c.project_logs=t);const b=await(await L(`${O}/attendance.php?action=create`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(c)})).json();b.success?(B(!1),await P(!1),await new Promise($=>setTimeout($,300)),a.success(b.message)):a.error(b.error)}catch{a.error("Chyba připojení")}},Y=(t,c)=>{const o=ue(c),b=xe(t);if(o>0&&b!==o){const $=Math.floor(o/60),N=o%60,re=Math.floor(b/60),de=b%60;return a.error(`Součet hodin projektů (${re}h ${de}m) neodpovídá odpracovanému času (${$}h ${N}m)`),!1}return!0},ie=()=>{x({month:n,user_ids:h.users.map(t=>String(t.id)),arrival_time:"08:00",departure_time:"16:30",break_start_time:"12:00",break_end_time:"12:30"}),w(!0)},g=t=>{const c=String(t);x(o=>({...o,user_ids:o.user_ids.includes(c)?o.user_ids.filter(b=>b!==c):[...o.user_ids,c]}))},u=()=>{const t=h.users.map(c=>String(c.id));x(c=>({...c,user_ids:c.user_ids.length===t.length?[]:t}))},be=async()=>{if(!k.month){a.error("Vyberte měsíc");return}if(k.user_ids.length===0){a.error("Vyberte alespoň jednoho zaměstnance");return}y(!0);try{const c=await(await L(`${O}/attendance.php?action=bulk_attendance`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(k)})).json();c.success?(w(!1),await P(!1),await new Promise(o=>setTimeout(o,300)),a.success(c.message)):a.error(c.error)}catch{a.error("Chyba připojení")}finally{y(!1)}},je=t=>{X(t),A({shift_date:t.shift_date,leave_type:t.leave_type||"work",leave_hours:t.leave_hours||8,arrival_date:q(t.arrival_time)||t.shift_date,arrival_time:J(t.arrival_time),break_start_date:q(t.break_start)||t.shift_date,break_start_time:J(t.break_start),break_end_date:q(t.break_end)||t.shift_date,break_end_time:J(t.break_end),departure_date:q(t.departure_time)||t.shift_date,departure_time:J(t.departure_time),notes:t.notes||"",project_id:t.project_id||""});const c=(t.project_logs||[]).map(o=>{if(o.hours!==null&&o.hours!==void 0)return{project_id:String(o.project_id),hours:String(o.hours),minutes:String(o.minutes||0)};if(o.started_at&&o.ended_at){const b=Math.max(0,Math.floor((new Date(o.ended_at)-new Date(o.started_at))/6e4));return{project_id:String(o.project_id),hours:String(Math.floor(b/60)),minutes:String(b%60)}}return{project_id:String(o.project_id),hours:"",minutes:""}});K(c),z(!0)},fe=async()=>{const c=(F.leave_type||"work")==="work"?I.filter(o=>o.project_id):[];if(!(c.length>0&&!Y(c,F)))try{const o={...F};o.project_logs=c;const $=await(await L(`${O}/attendance.php?id=${R.id}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)})).json();$.success?(z(!1),await P(!1),await new Promise(N=>setTimeout(N,300)),a.success($.message)):a.error($.error)}catch{a.error("Chyba připojení")}},_e=async()=>{if(W.record)try{const c=await(await L(`${O}/attendance.php?id=${W.record.id}`,{method:"DELETE",credentials:"include"})).json();c.success?(U({show:!1,record:null}),await P(!1),a.success(c.message)):a.error(c.error)}catch{a.error("Chyba připojení")}},ye=async()=>{try{let t=`${O}/attendance.php?action=print&month=${n}`;m&&(t+=`&user_id=${m}`);const c=await L(t);if(c.status===401)return;const o=await c.json();o.success&&ke(o.data)}catch{a.error("Nepodařilo se připravit tisk")}},ke=t=>{const c=Object.entries(t.user_totals).map(([re,de])=>Ue(re,de,t)).join(""),o=Object.keys(t.user_totals).length===0?'

          Za vybrané období nejsou žádné záznamy.

          ':"",b=t.selected_user_name?`
          Zaměstnanec: ${t.selected_user_name}
          `:"",$=Ie(t,c,o,b),N=window.open("","_blank");N&&(N.document.open(),N.document.write(Te.sanitize($,{WHOLE_DOCUMENT:!0})),N.document.close(),N.onload=()=>N.print())},Ne=Object.keys(h.user_totals).length>0;return{loading:s,month:n,setMonth:r,filterUserId:m,setFilterUserId:l,data:h,hasData:Ne,showBulkModal:p,setShowBulkModal:w,bulkSubmitting:S,bulkForm:k,setBulkForm:x,showCreateModal:E,setShowCreateModal:B,createForm:i,setCreateForm:f,showEditModal:M,setShowEditModal:z,editingRecord:R,editForm:F,setEditForm:A,deleteConfirm:W,setDeleteConfirm:U,projectList:D,createProjectLogs:V,setCreateProjectLogs:H,editProjectLogs:I,setEditProjectLogs:K,printRef:ae,openCreateModal:te,handleCreateShiftDateChange:se,handleCreateSubmit:ne,openBulkModal:ie,toggleBulkUser:g,toggleAllBulkUsers:u,handleBulkSubmit:be,openEditModal:je,handleEditSubmit:fe,handleDelete:_e,handlePrint:ye}}function Ie(a,s,d,n){return` + + + + + Docházka - ${a.month_name} + + + + + + + + +`}function Ke(a){return a.overtime>0?"linear-gradient(135deg, var(--warning), #d97706)":a.covered>=a.fund?"linear-gradient(135deg, var(--success), #059669)":"var(--gradient)"}function De(){const a=we(),{hasPermission:s}=Se(),{loading:d,month:n,setMonth:r,filterUserId:m,setFilterUserId:l,data:h,hasData:_,showBulkModal:p,setShowBulkModal:w,bulkSubmitting:S,bulkForm:y,setBulkForm:k,showCreateModal:x,setShowCreateModal:E,createForm:B,setCreateForm:C,showEditModal:i,setShowEditModal:f,editingRecord:M,editForm:z,setEditForm:R,deleteConfirm:X,setDeleteConfirm:F,projectList:A,createProjectLogs:W,setCreateProjectLogs:U,editProjectLogs:D,setEditProjectLogs:ee,openCreateModal:V,handleCreateShiftDateChange:H,handleCreateSubmit:I,openBulkModal:K,toggleBulkUser:ae,toggleAllBulkUsers:P,handleBulkSubmit:te,openEditModal:se,handleEditSubmit:ne,handleDelete:Y,handlePrint:ie}=He({alert:a});return Z(p),Z(i),Z(x),s("attendance.admin")?d?e.jsxs("div",{className:"admin-skeleton",style:{padding:0,gap:"1.5rem"},children:[e.jsxs("div",{className:"admin-skeleton-row",style:{justifyContent:"space-between"},children:[e.jsx("div",{children:e.jsx("div",{className:"admin-skeleton-line h-8",style:{width:"200px",marginBottom:"0.5rem"}})}),e.jsxs("div",{className:"admin-skeleton-row",style:{gap:"0.5rem"},children:[e.jsx("div",{className:"admin-skeleton-line h-10",style:{width:"120px",borderRadius:"8px"}}),e.jsx("div",{className:"admin-skeleton-line h-10",style:{width:"120px",borderRadius:"8px"}}),e.jsx("div",{className:"admin-skeleton-line h-10",style:{width:"140px",borderRadius:"8px"}})]})]}),e.jsx("div",{className:"admin-card",children:e.jsx("div",{className:"admin-skeleton",style:{gap:"0.75rem",padding:"1rem"},children:e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line h-10",style:{flex:1,borderRadius:"8px"}}),e.jsx("div",{className:"admin-skeleton-line h-10",style:{flex:1,borderRadius:"8px"}})]})})}),e.jsx("div",{className:"admin-grid admin-grid-3",children:[0,1,2].map(g=>e.jsx("div",{className:"admin-card",children:e.jsx("div",{className:"admin-card-body",children:e.jsxs("div",{className:"admin-skeleton",style:{gap:"0.75rem"},children:[e.jsx("div",{className:"admin-skeleton-line w-1/2"}),e.jsx("div",{className:"admin-skeleton-line h-8",style:{width:"80px"}}),e.jsx("div",{className:"admin-skeleton-line w-1/3",style:{height:"10px"}}),e.jsx("div",{className:"admin-skeleton-line w-full",style:{height:"4px"}})]})})},g))}),e.jsx("div",{className:"admin-card",children:e.jsx("div",{className:"admin-skeleton",style:{gap:"1.25rem"},children:[0,1,2,3,4].map(g=>e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line w-1/4"}),e.jsx("div",{className:"admin-skeleton-line w-1/3"}),e.jsx("div",{className:"admin-skeleton-line w-1/4"})]},g))})})]}):e.jsxs("div",{children:[e.jsxs(T.div,{className:"admin-page-header",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4},children:[e.jsx("div",{children:e.jsx("h1",{className:"admin-page-title",children:"Správa docházky"})}),e.jsxs("div",{className:"admin-page-actions",children:[_&&e.jsx(e.Fragment,{children:e.jsxs("button",{onClick:ie,className:"admin-btn admin-btn-secondary",title:"Tisk docházky",children:[e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",style:{marginRight:"0.5rem"},children:[e.jsx("polyline",{points:"6 9 6 2 18 2 18 9"}),e.jsx("path",{d:"M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"}),e.jsx("rect",{x:"6",y:"14",width:"12",height:"8"})]}),"Tisk"]})}),e.jsx("button",{onClick:K,className:"admin-btn admin-btn-secondary",children:"Vyplnit měsíc"}),e.jsxs("button",{onClick:V,className:"admin-btn admin-btn-primary",children:[e.jsxs("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("line",{x1:"12",y1:"5",x2:"12",y2:"19"}),e.jsx("line",{x1:"5",y1:"12",x2:"19",y2:"12"})]}),"Přidat záznam"]})]})]}),e.jsx(T.div,{className:"admin-card",style:{marginBottom:"1.5rem"},initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.1},children:e.jsx("div",{className:"admin-card-body",children:e.jsxs("div",{className:"admin-form-row",children:[e.jsx(ce,{label:"Měsíc",children:e.jsx(j,{mode:"month",value:n,onChange:g=>r(g)})}),e.jsx(ce,{label:"Zaměstnanec",children:e.jsxs("select",{value:m,onChange:g=>l(g.target.value),className:"admin-form-select",children:[e.jsx("option",{value:"",children:"Všichni"}),h.users.map(g=>e.jsx("option",{value:g.id,children:g.name},g.id))]})})]})})}),Object.keys(h.user_totals).length>0&&e.jsx(T.div,{className:"admin-grid admin-grid-3",style:{marginBottom:"1.5rem"},initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.15},children:Object.entries(h.user_totals).map(([g,u])=>e.jsx("div",{className:"admin-card",children:e.jsxs("div",{className:"admin-card-body",children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"0.5rem",marginBottom:"0.5rem"},children:[e.jsx("span",{style:{fontWeight:600},children:u.name}),e.jsx("span",{className:`attendance-working-badge ${u.working?"working":"finished"}`,children:u.working?"✓":"✗"})]}),e.jsx("div",{className:"admin-stat-value",children:Q(u.minutes)}),e.jsx("div",{className:"admin-stat-label",children:"odpracováno"}),e.jsxs("div",{style:{marginTop:"0.5rem",display:"flex",flexWrap:"wrap",gap:"0.25rem"},children:[u.vacation_hours>0&&e.jsxs("span",{className:"attendance-leave-badge badge-vacation",children:["Dov: ",u.vacation_hours,"h"]}),u.sick_hours>0&&e.jsxs("span",{className:"attendance-leave-badge badge-sick",children:["Nem: ",u.sick_hours,"h"]}),u.holiday_hours>0&&e.jsxs("span",{className:"attendance-leave-badge badge-holiday",children:["Sv: ",u.holiday_hours,"h"]}),u.unpaid_hours>0&&e.jsxs("span",{className:"attendance-leave-badge badge-unpaid",children:["Nep: ",u.unpaid_hours,"h"]})]}),u.fund!==null&&e.jsxs("div",{style:{marginTop:"0.5rem"},children:[e.jsxs("div",{className:"text-secondary",style:{display:"flex",justifyContent:"space-between",alignItems:"center",fontSize:"0.8rem"},children:[e.jsxs("span",{children:["Fond: ",u.worked_hours,"h / ",u.fund,"h"]}),u.overtime>0&&e.jsxs("span",{className:"text-warning fw-600",children:["+",u.overtime,"h"]}),u.overtime<=0&&u.missing>0&&e.jsxs("span",{className:"text-danger fw-600",children:["-",u.missing,"h"]})]}),e.jsx("div",{style:{marginTop:"0.375rem",height:"4px",background:"var(--bg-tertiary)",borderRadius:"2px",overflow:"hidden"},children:e.jsx("div",{style:{height:"100%",width:`${Math.min(100,u.covered/u.fund*100)}%`,background:Ke(u),borderRadius:"2px",transition:"width 0.3s ease"}})})]}),h.leave_balances[g]&&e.jsxs("div",{className:"text-secondary",style:{marginTop:"0.5rem",fontSize:"0.8rem"},children:["Zbývá dovolené: ",h.leave_balances[g].vacation_remaining.toFixed(1),"h / ",h.leave_balances[g].vacation_total,"h"]})]})},g))}),e.jsx(T.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.2},children:e.jsx("div",{className:"admin-card-body",children:e.jsx(We,{records:h.records,onEdit:se,onDelete:g=>F({show:!0,record:g})})})}),e.jsx(Be,{show:p,onClose:()=>w(!1),form:y,setForm:k,users:h.users,onSubmit:te,submitting:S,toggleUser:ae,toggleAllUsers:P}),e.jsx(he,{mode:"create",show:x,onClose:()=>E(!1),onSubmit:I,form:B,setForm:C,projectLogs:W,setProjectLogs:U,projectList:A,users:h.users,onShiftDateChange:H}),e.jsx(he,{mode:"edit",show:i&&!!M,onClose:()=>f(!1),onSubmit:ne,form:z,setForm:R,projectLogs:D,setProjectLogs:ee,projectList:A,editingRecord:M}),e.jsx(Ce,{isOpen:X.show,onClose:()=>F({show:!1,record:null}),onConfirm:Y,title:"Smazat záznam",message:"Opravdu chcete smazat tento záznam docházky?",confirmText:"Smazat",confirmVariant:"danger"})]}):e.jsx(ze,{})}export{De as default}; diff --git a/dist/assets/AttendanceBalances-BBDz3NFV.js b/dist/assets/AttendanceBalances-BBDz3NFV.js new file mode 100644 index 0000000..426dcbd --- /dev/null +++ b/dist/assets/AttendanceBalances-BBDz3NFV.js @@ -0,0 +1 @@ +import{j as e,m as p,A as J}from"./vendor-animation-0s3FMHwK.js";import{r}from"./vendor-react-BVs3cwbi.js";import{a as q,u as G,c as j,b as Q,F,C as X}from"./index-BBlIrj2z.js";import{F as K}from"./Forbidden-D25jV3Oq.js";import"./vendor-utils-Dyr8OjFr.js";const y="/api/admin",ee=t=>t<=0?"text-danger":t<20?"text-warning":"",se=t=>t.overtime>0?e.jsxs("span",{className:"text-warning fw-600",children:["+",t.overtime,"h"]}):t.missing>0?e.jsxs("span",{className:"text-danger",children:["-",t.missing,"h"]}):e.jsx("span",{className:"text-success",children:"0h"}),ae=(t,x,m)=>t.overtime>0?e.jsxs("span",{className:"text-warning fw-600",style:{fontSize:"11px"},children:["+",t.overtime,"h"]}):t.missing>0?e.jsxs("span",{className:"text-danger fw-600",style:{fontSize:"11px"},children:["-",t.missing,"h"]}):x&&!m?e.jsx("span",{className:"text-success",style:{fontSize:"11px"},children:"OK"}):null,te=(t,x,m)=>t.overtime>0?"linear-gradient(135deg, var(--warning), #d97706)":x?"linear-gradient(135deg, var(--success), #059669)":m?"var(--gradient)":"var(--danger)";function le(){const t=q(),{hasPermission:x}=G(),[m,M]=r.useState(!0),[o,P]=r.useState(new Date().getFullYear()),[k,W]=r.useState({users:[],balances:{}}),[z,O]=r.useState(!0),[c,I]=r.useState({months:{},holidays:[],users:[],balances:{}}),[T,$]=r.useState(!0),[w,A]=r.useState({months:{}}),[R,v]=r.useState(!1),[S,U]=r.useState(null),[h,f]=r.useState({vacation_total:160,vacation_used:0,sick_used:0}),[g,_]=r.useState({show:!1,userId:null,userName:""}),N=r.useCallback(async(a=!0)=>{a&&M(!0);try{const i=await(await j(`${y}/attendance.php?action=balances&year=${o}`,{})).json();i.success&&W(i.data)}catch{t.error("Nepodařilo se načíst data")}finally{a&&M(!1)}},[o,t]),B=r.useCallback(async()=>{O(!0);try{const s=await(await j(`${y}/attendance.php?action=workfund&year=${o}`)).json();s.success&&I(s.data)}catch{}finally{O(!1)}},[o]),L=r.useCallback(async()=>{$(!0);try{const s=await(await j(`${y}/attendance.php?action=project_report&year=${o}`)).json();s.success&&A(s.data)}catch{}finally{$(!1)}},[o]);if(r.useEffect(()=>{N(),B(),L()},[N,B,L]),Q(R),!x("attendance.balances"))return e.jsx(K,{});const Y=(a,s)=>{U({id:a,name:s.name}),f({vacation_total:s.vacation_total,vacation_used:s.vacation_used,sick_used:s.sick_used}),v(!0)},V=async()=>{try{const s=await(await j(`${y}/attendance.php?action=balances`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({user_id:S.id,year:o,action_type:"edit",...h})})).json();s.success?(v(!1),await N(!1),await new Promise(i=>setTimeout(i,300)),t.success(s.message)):t.error(s.error)}catch{t.error("Chyba připojení")}},H=async()=>{if(g.userId)try{const s=await(await j(`${y}/attendance.php?action=balances`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({user_id:g.userId,year:o,action_type:"reset"})})).json();s.success?(_({show:!1,userId:null,userName:""}),await N(!1),t.success(s.message)):t.error(s.error)}catch{t.error("Chyba připojení")}},D=[],b=new Date().getFullYear(),E=new Date().getMonth()+1;for(let a=b-5;a<=b+5;a++)D.push(a);const Z=a=>{if(!c.months||Object.keys(c.months).length===0)return null;let s=0,i=0,l=0;for(const u of Object.values(c.months)){s+=u.fund;const C=u.users?.[a];C&&(i+=C.worked,l+=C.covered)}const n=Math.max(0,Math.round((s-l)*10)/10),d=Math.max(0,Math.round((l-s)*10)/10);return{fund:s,worked:Math.round(i*10)/10,covered:Math.round(l*10)/10,missing:n,overtime:d}};return e.jsxs("div",{children:[e.jsxs(p.div,{className:"admin-page-header",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4},children:[e.jsx("div",{children:e.jsx("h1",{className:"admin-page-title",children:"Správa bilancí"})}),e.jsx("div",{className:"admin-page-actions",children:e.jsx("select",{value:o,onChange:a=>P(parseInt(a.target.value)),className:"admin-form-select",style:{minWidth:"100px"},children:D.map(a=>e.jsx("option",{value:a,children:a},a))})})]}),e.jsx(p.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.1},children:e.jsxs("div",{className:"admin-card-body",children:[m&&e.jsx("div",{className:"admin-skeleton",style:{gap:"1.25rem"},children:[0,1,2,3,4].map(a=>e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line w-1/4"}),e.jsx("div",{className:"admin-skeleton-line w-1/3"}),e.jsx("div",{className:"admin-skeleton-line w-1/4"})]},a))}),!m&&Object.keys(k.balances).length===0&&e.jsx("div",{className:"admin-empty-state",children:e.jsx("p",{children:"Žádní uživatelé k zobrazení."})}),!m&&Object.keys(k.balances).length>0&&e.jsx("div",{className:"admin-table-responsive",children:e.jsxs("table",{className:"admin-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Zaměstnanec"}),e.jsx("th",{children:"Nárok (h)"}),e.jsx("th",{children:"Čerpáno (h)"}),e.jsx("th",{children:"Zbývá (h)"}),e.jsx("th",{children:"Nemoc (h)"}),e.jsx("th",{children:"Fond roku"}),e.jsx("th",{children:"Odpracováno"}),e.jsx("th",{children:"+/−"}),e.jsx("th",{children:"Akce"})]})}),e.jsx("tbody",{children:Object.entries(k.balances).map(([a,s])=>{const i=Z(a);return e.jsxs("tr",{children:[e.jsx("td",{style:{fontWeight:500},children:s.name}),e.jsx("td",{className:"admin-mono",children:s.vacation_total}),e.jsx("td",{className:"admin-mono",children:s.vacation_used.toFixed(1)}),e.jsx("td",{className:"admin-mono",children:e.jsx("span",{className:ee(s.vacation_remaining),children:s.vacation_remaining.toFixed(1)})}),e.jsx("td",{className:"admin-mono",children:s.sick_used.toFixed(1)}),e.jsx("td",{className:"admin-mono",children:i?`${i.fund}h`:"—"}),e.jsx("td",{className:"admin-mono",children:i?`${i.worked}h`:"—"}),e.jsx("td",{className:"admin-mono",children:i?se(i):"—"}),e.jsx("td",{children:e.jsxs("div",{className:"admin-table-actions",children:[e.jsx("button",{onClick:()=>Y(a,s),className:"admin-btn-icon",title:"Upravit","aria-label":"Upravit",children:e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("path",{d:"M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"}),e.jsx("path",{d:"M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"})]})}),e.jsx("button",{onClick:()=>_({show:!0,userId:a,userName:s.name}),className:"admin-btn-icon danger",title:"Resetovat","aria-label":"Resetovat",children:e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("polyline",{points:"3 6 5 6 21 6"}),e.jsx("path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"})]})})]})})]},a)})})]})})]})}),!z&&c.months&&Object.keys(c.months).length>0&&e.jsxs(p.div,{initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.2},style:{marginTop:"1.5rem"},children:[e.jsxs("h2",{className:"admin-page-title",style:{fontSize:"1.25rem",marginBottom:"1rem"},children:["Měsíční přehled fondu ",o]}),e.jsx("div",{className:"admin-grid admin-grid-3",children:Object.entries(c.months).map(([a,s])=>{const i=o===b&&parseInt(a)===E;return e.jsx("div",{className:"admin-card",style:i?{borderColor:"var(--accent-color)",boxShadow:"0 0 0 1px var(--accent-color)"}:{},children:e.jsxs("div",{className:"admin-card-body",children:[e.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:"0.75rem"},children:[e.jsxs("h3",{style:{fontWeight:600,fontSize:"1rem",margin:0},children:[s.month_name,i&&e.jsx("span",{style:{marginLeft:"0.5rem",fontSize:"0.7rem",padding:"0.125rem 0.375rem",background:"var(--accent-light)",color:"var(--accent-color)",borderRadius:"var(--border-radius-sm)",fontWeight:500},children:"aktuální"})]}),e.jsxs("span",{className:"text-secondary",style:{fontSize:"12px"},children:[s.fund,"h (",s.business_days," dnů)"]})]}),e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"0.375rem"},children:c.users&&c.users.map(l=>{const n=s.users?.[l.id];if(!n)return null;const d=s.fund>0?Math.min(100,n.covered/s.fund*100):0,u=n.covered>=s.fund;return e.jsxs("div",{children:[e.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",fontSize:"12px"},children:[e.jsx("span",{style:{color:"var(--text-primary)"},children:n.name}),e.jsxs("span",{style:{display:"flex",gap:"0.5rem",alignItems:"center"},children:[e.jsxs("span",{className:"text-secondary",children:[n.worked,"h"]}),ae(n,u,i)]})]}),e.jsx("div",{style:{marginTop:"0.125rem",height:"3px",background:"var(--bg-tertiary)",borderRadius:"2px",overflow:"hidden"},children:e.jsx("div",{style:{height:"100%",width:`${d}%`,background:te(n,u,i),borderRadius:"2px",transition:"width 0.3s ease"}})})]},l.id)})})]})},a)})})]}),z&&e.jsx("div",{style:{marginTop:"1.5rem"},children:e.jsx("div",{className:"admin-skeleton",style:{gap:"1.25rem"},children:[0,1,2].map(a=>e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line w-1/4"}),e.jsx("div",{className:"admin-skeleton-line w-1/3"}),e.jsx("div",{className:"admin-skeleton-line w-1/4"})]},a))})}),!T&&w.months&&Object.keys(w.months).length>0&&e.jsxs(p.div,{initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.3},style:{marginTop:"1.5rem"},children:[e.jsxs("h2",{className:"admin-page-title",style:{fontSize:"1.25rem",marginBottom:"1rem"},children:["Měsíční přehled projektů ",o]}),e.jsx("div",{className:"admin-grid admin-grid-3",children:Object.entries(w.months).map(([a,s])=>{const i=o===b&&parseInt(a)===E,l=s.projects.reduce((n,d)=>n+d.hours,0);return s.projects.length===0?null:e.jsx("div",{className:"admin-card",style:i?{borderColor:"var(--accent-color)",boxShadow:"0 0 0 1px var(--accent-color)"}:{},children:e.jsxs("div",{className:"admin-card-body",children:[e.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:"0.75rem"},children:[e.jsxs("h3",{style:{fontWeight:600,fontSize:"1rem",margin:0},children:[s.month_name,i&&e.jsx("span",{style:{marginLeft:"0.5rem",fontSize:"0.7rem",padding:"0.125rem 0.375rem",background:"var(--accent-light)",color:"var(--accent-color)",borderRadius:"var(--border-radius-sm)",fontWeight:500},children:"aktuální"})]}),e.jsxs("span",{className:"text-secondary fw-600",style:{fontSize:"12px"},children:[l.toFixed(1),"h"]})]}),e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"0.75rem"},children:s.projects.map(n=>e.jsxs("div",{children:[e.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:"0.25rem"},children:[e.jsx("span",{style:{fontSize:"12px",fontWeight:600,color:"var(--text-primary)"},children:n.project_id?n.project_number:"Bez projektu"}),e.jsxs("span",{className:"text-secondary fw-600",style:{fontSize:"12px"},children:[n.hours.toFixed(1),"h"]})]}),n.project_id&&n.project_name&&e.jsx("div",{className:"text-muted",style:{fontSize:"0.7rem",marginBottom:"0.25rem"},children:n.project_name}),e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"0.125rem"},children:n.users.map(d=>{const u=n.hours>0?Math.min(100,d.hours/n.hours*100):0;return e.jsxs("div",{children:[e.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",fontSize:"11px"},children:[e.jsx("span",{className:"text-secondary",children:d.user_name}),e.jsxs("span",{className:"text-secondary",children:[d.hours.toFixed(1),"h"]})]}),e.jsx("div",{style:{marginTop:"1px",height:"3px",background:"var(--bg-tertiary)",borderRadius:"2px",overflow:"hidden"},children:e.jsx("div",{style:{height:"100%",width:`${u}%`,background:n.project_id?"var(--gradient)":"#94a3b8",borderRadius:"2px",transition:"width 0.3s ease"}})})]},d.user_id)})})]},n.project_id||"no-project"))})]})},a)})})]}),T&&e.jsx("div",{style:{marginTop:"1.5rem"},children:e.jsx("div",{className:"admin-skeleton",style:{gap:"1.25rem"},children:[0,1,2].map(a=>e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line w-1/4"}),e.jsx("div",{className:"admin-skeleton-line w-1/3"}),e.jsx("div",{className:"admin-skeleton-line w-1/4"})]},a))})}),e.jsx(J,{children:R&&S&&e.jsxs(p.div,{className:"admin-modal-overlay",initial:{opacity:0},animate:{opacity:1},exit:{opacity:0},transition:{duration:.2},children:[e.jsx("div",{className:"admin-modal-backdrop",onClick:()=>v(!1)}),e.jsxs(p.div,{className:"admin-modal",initial:{opacity:0,scale:.95,y:20},animate:{opacity:1,scale:1,y:0},exit:{opacity:0,scale:.95,y:20},transition:{duration:.2},children:[e.jsxs("div",{className:"admin-modal-header",children:[e.jsx("h2",{className:"admin-modal-title",children:"Upravit dovolenou"}),e.jsx("p",{className:"text-secondary",style:{marginTop:"0.25rem"},children:S.name})]}),e.jsx("div",{className:"admin-modal-body",children:e.jsxs("div",{className:"admin-form",children:[e.jsx(F,{label:"Nárok na dovolenou (hodiny)",children:e.jsx("input",{type:"number",value:h.vacation_total,onChange:a=>f({...h,vacation_total:parseFloat(a.target.value)}),min:"0",max:"500",step:"1",className:"admin-form-input"})}),e.jsx(F,{label:"Čerpáno dovolené (hodiny)",children:e.jsx("input",{type:"number",value:h.vacation_used,onChange:a=>f({...h,vacation_used:parseFloat(a.target.value)}),min:"0",max:"500",step:"0.5",className:"admin-form-input"})}),e.jsx(F,{label:"Čerpáno nemocenské (hodiny)",children:e.jsx("input",{type:"number",value:h.sick_used,onChange:a=>f({...h,sick_used:parseFloat(a.target.value)}),min:"0",max:"500",step:"0.5",className:"admin-form-input"})})]})}),e.jsxs("div",{className:"admin-modal-footer",children:[e.jsx("button",{type:"button",onClick:()=>v(!1),className:"admin-btn admin-btn-secondary",children:"Zrušit"}),e.jsx("button",{type:"button",onClick:V,className:"admin-btn admin-btn-primary",children:"Uložit"})]})]})]})}),e.jsx(X,{isOpen:g.show,onClose:()=>_({show:!1,userId:null,userName:""}),onConfirm:H,title:"Resetovat bilanci",message:`Opravdu chcete vynulovat čerpání dovolené a nemocenské pro ${g.userName} za rok ${o}?`,confirmText:"Resetovat",confirmVariant:"danger"})]})}export{le as default}; diff --git a/dist/assets/AttendanceCreate-j72Gsy_8.js b/dist/assets/AttendanceCreate-j72Gsy_8.js new file mode 100644 index 0000000..bd9cc71 --- /dev/null +++ b/dist/assets/AttendanceCreate-j72Gsy_8.js @@ -0,0 +1 @@ +import{j as e,m as u}from"./vendor-animation-0s3FMHwK.js";import{g as w,r as o,L as p}from"./vendor-react-BVs3cwbi.js";import{a as S,u as z,F as n,A as i,c as x}from"./index-BBlIrj2z.js";import{F as P}from"./Forbidden-D25jV3Oq.js";import"./vendor-utils-Dyr8OjFr.js";const j="/api/admin";function U(){const r=S(),{hasPermission:v}=z(),_=w(),[y,b]=o.useState(!0),[m,c]=o.useState(!1),[f,g]=o.useState([]),l=new Date().toISOString().split("T")[0],[s,t]=o.useState({user_id:"",shift_date:l,leave_type:"work",leave_hours:8,arrival_date:l,arrival_time:"",break_start_date:l,break_start_time:"",break_end_date:l,break_end_time:"",departure_date:l,departure_time:"",notes:""});o.useEffect(()=>{(async()=>{try{const d=await(await x(`${j}/attendance.php?action=users`,{})).json();d.success&&g(d.data.users)}catch{r.error("Nepodařilo se načíst uživatele")}finally{b(!1)}})()},[r]);const k=async a=>{if(a.preventDefault(),!s.user_id||!s.shift_date){r.error("Vyplňte zaměstnance a datum směny");return}c(!0);try{const d=await(await x(`${j}/attendance.php?action=create`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)})).json();d.success?(r.success(d.message),_(`/attendance/admin?month=${s.shift_date.substring(0,7)}`)):r.error(d.error)}catch{r.error("Chyba připojení")}finally{c(!1)}},N=a=>{t({...s,shift_date:a,arrival_date:a,break_start_date:a,break_end_date:a,departure_date:a})},h=s.leave_type==="work";return v("attendance.admin")?y?e.jsxs("div",{className:"admin-skeleton",style:{padding:0,gap:"1.5rem"},children:[e.jsx("div",{className:"admin-skeleton-row",style:{justifyContent:"space-between"},children:e.jsx("div",{className:"admin-skeleton-line h-8",style:{width:"200px"}})}),e.jsx("div",{className:"admin-card",style:{maxWidth:"600px"},children:e.jsxs("div",{className:"admin-skeleton",style:{gap:"1.25rem"},children:[[0,1,2,3,4].map(a=>e.jsxs("div",{children:[e.jsx("div",{className:"admin-skeleton-line w-1/4",style:{marginBottom:"0.5rem",height:"10px"}}),e.jsx("div",{className:"admin-skeleton-line w-full h-10"})]},a)),e.jsx("div",{className:"admin-skeleton-line h-10",style:{width:"120px",borderRadius:"8px"}})]})})]}):e.jsxs("div",{children:[e.jsxs(u.div,{className:"admin-page-header",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4},children:[e.jsx("div",{children:e.jsx("h1",{className:"admin-page-title",children:"Přidat záznam docházky"})}),e.jsx("div",{className:"admin-page-actions",children:e.jsx(p,{to:"/attendance/admin",className:"admin-btn admin-btn-secondary",children:"← Zpět na správu"})})]}),e.jsx(u.div,{className:"admin-card",style:{maxWidth:"600px"},initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.1},children:e.jsx("div",{className:"admin-card-body",children:e.jsxs("form",{onSubmit:k,className:"admin-form",children:[e.jsxs("div",{className:"admin-form-row",children:[e.jsx(n,{label:"Zaměstnanec",required:!0,children:e.jsxs("select",{value:s.user_id,onChange:a=>t({...s,user_id:a.target.value}),className:"admin-form-select",required:!0,children:[e.jsx("option",{value:"",children:"Vyberte zaměstnance"}),f.map(a=>e.jsx("option",{value:a.id,children:a.name},a.id))]})}),e.jsx(n,{label:"Datum směny",required:!0,children:e.jsx(i,{mode:"date",value:s.shift_date,onChange:a=>N(a),required:!0})})]}),e.jsx(n,{label:"Typ záznamu",required:!0,children:e.jsxs("select",{value:s.leave_type,onChange:a=>t({...s,leave_type:a.target.value}),className:"admin-form-select",children:[e.jsx("option",{value:"work",children:"Práce"}),e.jsx("option",{value:"vacation",children:"Dovolená"}),e.jsx("option",{value:"sick",children:"Nemoc"}),e.jsx("option",{value:"holiday",children:"Svátek"}),e.jsx("option",{value:"unpaid",children:"Neplacené volno"})]})}),!h&&e.jsxs(n,{label:"Počet hodin",children:[e.jsx("input",{type:"number",value:s.leave_hours,onChange:a=>t({...s,leave_hours:parseFloat(a.target.value)}),min:"0.5",max:"24",step:"0.5",className:"admin-form-input"}),e.jsx("small",{className:"admin-form-hint",children:"Výchozí 8 hodin pro celý den"})]}),h&&e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"admin-form-row",children:[e.jsx(n,{label:"Příchod - datum",children:e.jsx(i,{mode:"date",value:s.arrival_date,onChange:a=>t({...s,arrival_date:a})})}),e.jsx(n,{label:"Příchod - čas",children:e.jsx(i,{mode:"time",value:s.arrival_time,onChange:a=>t({...s,arrival_time:a})})})]}),e.jsxs("div",{className:"admin-form-row",children:[e.jsx(n,{label:"Začátek pauzy - datum",children:e.jsx(i,{mode:"date",value:s.break_start_date,onChange:a=>t({...s,break_start_date:a})})}),e.jsx(n,{label:"Začátek pauzy - čas",children:e.jsx(i,{mode:"time",value:s.break_start_time,onChange:a=>t({...s,break_start_time:a})})})]}),e.jsxs("div",{className:"admin-form-row",children:[e.jsx(n,{label:"Konec pauzy - datum",children:e.jsx(i,{mode:"date",value:s.break_end_date,onChange:a=>t({...s,break_end_date:a})})}),e.jsx(n,{label:"Konec pauzy - čas",children:e.jsx(i,{mode:"time",value:s.break_end_time,onChange:a=>t({...s,break_end_time:a})})})]}),e.jsxs("div",{className:"admin-form-row",children:[e.jsx(n,{label:"Odchod - datum",children:e.jsx(i,{mode:"date",value:s.departure_date,onChange:a=>t({...s,departure_date:a})})}),e.jsx(n,{label:"Odchod - čas",children:e.jsx(i,{mode:"time",value:s.departure_time,onChange:a=>t({...s,departure_time:a})})})]})]}),e.jsx(n,{label:"Poznámka",children:e.jsx("textarea",{value:s.notes,onChange:a=>t({...s,notes:a.target.value}),className:"admin-form-textarea",rows:3})}),e.jsxs("div",{className:"admin-form-actions",children:[e.jsx(p,{to:"/attendance/admin",className:"admin-btn admin-btn-secondary",children:"Zrušit"}),e.jsx("button",{type:"submit",disabled:m,className:"admin-btn admin-btn-primary",children:m?"Ukládám...":"Uložit"})]})]})})})]}):e.jsx(P,{})}export{U as default}; diff --git a/dist/assets/AttendanceHistory-DQLQHe_C.js b/dist/assets/AttendanceHistory-DQLQHe_C.js new file mode 100644 index 0000000..5d0870d --- /dev/null +++ b/dist/assets/AttendanceHistory-DQLQHe_C.js @@ -0,0 +1,88 @@ +import{j as e,m as f}from"./vendor-animation-0s3FMHwK.js";import{r as m}from"./vendor-react-BVs3cwbi.js";import{a9 as T}from"./vendor-utils-Dyr8OjFr.js";import{a as C,u as A,c as B,F as O,A as H}from"./index-BBlIrj2z.js";import{F as I}from"./Forbidden-D25jV3Oq.js";import{c as W,b as N,g as w,d as z,e as S,a as v,h as E,i as y,f as b}from"./attendanceHelpers-D6sLEw0q.js";const L="/api/admin",R=s=>s.break_start&&s.break_end?`${b(s.break_start)} - ${b(s.break_end)}`:s.break_start?`${b(s.break_start)} - ?`:"—",Z=s=>s.project_logs&&s.project_logs.length>0?e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"0.125rem"},children:s.project_logs.map((n,g)=>{let d,c,o=!1;if(n.hours!==null&&n.hours!==void 0)d=parseInt(n.hours)||0,c=parseInt(n.minutes)||0;else{o=!n.ended_at;const x=n.ended_at?new Date(n.ended_at):new Date,p=Math.floor((x-new Date(n.started_at))/6e4);d=Math.floor(p/60),c=p%60}return e.jsxs("span",{className:"admin-badge",style:{fontSize:"0.7rem",display:"inline-block",background:o?"var(--accent-light)":void 0},children:[n.project_name||`#${n.project_id}`," (",d,":",String(c).padStart(2,"0"),"h",o?" ▸":"",")"]},n.id||g)})}):s.project_name?e.jsx("span",{className:"admin-badge admin-badge-wrap",style:{fontSize:"0.75rem"},children:s.project_name}):"—",Y=s=>s.overtime>0?e.jsxs("span",{className:"leave-badge badge-overtime",children:["+",s.overtime,"h přesčas"]}):s.remaining>0?e.jsxs("span",{style:{color:"#dc2626"},children:["−",s.remaining,"h"]}):e.jsx("span",{style:{color:"#16a34a"},children:"splněno"});function Q(){const s=C(),{user:n,hasPermission:g}=A(),[d,c]=m.useState(!0),o=m.useRef(null),[x,p]=m.useState(()=>{const a=new Date;return`${a.getFullYear()}-${String(a.getMonth()+1).padStart(2,"0")}`}),[t,D]=m.useState({records:[],month_name:"",year:new Date().getFullYear(),total_minutes:0,vacation_hours:0,sick_hours:0,holiday_hours:0,unpaid_hours:0,leave_balance:null,monthly_fund:null}),_=m.useCallback(async()=>{c(!0);try{const a=await B(`${L}/attendance.php?action=history&month=${x}`);if(a.status===401)return;const i=await a.json();i.success&&D(i.data)}catch{s.error("Nepodařilo se načíst data")}finally{c(!1)}},[x,s]);if(m.useEffect(()=>{_()},[_]),!g("attendance.history"))return e.jsx(I,{});const $=()=>{if(!o.current)return;const a=window.open("","_blank");a.document.write(` + + + + + + Docházka - ${t.month_name} + + + + ${T.sanitize(o.current.innerHTML)} + + + `),a.document.close(),a.onload=()=>{a.print()}};return e.jsxs("div",{children:[e.jsxs(f.div,{className:"admin-page-header",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4},children:[e.jsxs("div",{children:[e.jsx("h1",{className:"admin-page-title",children:"Historie docházky"}),e.jsx("p",{className:"admin-page-subtitle",children:t.month_name})]}),e.jsx("div",{className:"admin-page-actions",children:t.records.length>0&&e.jsxs("button",{onClick:$,className:"admin-btn admin-btn-secondary",title:"Tisk docházky",children:[e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",style:{marginRight:"0.5rem"},children:[e.jsx("polyline",{points:"6 9 6 2 18 2 18 9"}),e.jsx("path",{d:"M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"}),e.jsx("rect",{x:"6",y:"14",width:"12",height:"8"})]}),"Tisk"]})})]}),e.jsx(f.div,{className:"admin-card",style:{marginBottom:"1.5rem"},initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.1},children:e.jsx("div",{className:"admin-card-body",children:e.jsx("div",{className:"admin-form-row",children:e.jsx(O,{label:"Měsíc",children:e.jsx(H,{mode:"month",value:x,onChange:a=>p(a)})})})})}),e.jsx(f.div,{className:"admin-card",style:{marginBottom:"1.5rem"},initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.15},children:e.jsxs("div",{className:"admin-card-body",children:[d&&e.jsx("div",{className:"admin-skeleton",style:{gap:"0.5rem"},children:e.jsxs("div",{className:"admin-skeleton-row",style:{gap:"1rem"},children:[e.jsx("div",{className:"admin-skeleton-line",style:{width:"48px",height:"48px",borderRadius:"12px",flexShrink:0}}),e.jsxs("div",{style:{flex:1},children:[e.jsx("div",{className:"admin-skeleton-line w-1/2",style:{marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line w-full",style:{height:"6px",borderRadius:"3px"}}),e.jsx("div",{className:"admin-skeleton-line w-1/3",style:{height:"10px",marginTop:"0.5rem"}})]})]})}),!d&&t.monthly_fund&&e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"1rem",flexWrap:"wrap"},children:[e.jsx("div",{className:"admin-stat-icon info",children:e.jsxs("svg",{width:"24",height:"24",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("rect",{x:"3",y:"4",width:"18",height:"18",rx:"2",ry:"2"}),e.jsx("line",{x1:"16",y1:"2",x2:"16",y2:"6"}),e.jsx("line",{x1:"8",y1:"2",x2:"8",y2:"6"}),e.jsx("line",{x1:"3",y1:"10",x2:"21",y2:"10"})]})}),e.jsxs("div",{style:{flex:1,minWidth:"200px"},children:[e.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"baseline",marginBottom:"0.375rem"},children:[e.jsxs("span",{style:{fontWeight:600,fontSize:"1rem",color:"var(--text-primary)"},children:["Fond: ",t.monthly_fund.worked,"h / ",t.monthly_fund.fund,"h"]}),e.jsxs("span",{className:"text-secondary",style:{fontSize:"0.8125rem"},children:[t.monthly_fund.business_days," prac. dnů"]})]}),e.jsx("div",{className:"attendance-balance-bar",children:e.jsx("div",{className:"attendance-balance-progress",style:{width:`${Math.min(100,t.monthly_fund.fund>0?t.monthly_fund.covered/t.monthly_fund.fund*100:0)}%`,background:t.monthly_fund.covered>=t.monthly_fund.fund?"linear-gradient(135deg, var(--success), #059669)":"var(--gradient)"}})}),e.jsxs("div",{className:"text-muted",style:{display:"flex",justifyContent:"space-between",fontSize:"0.75rem",marginTop:"0.375rem"},children:[e.jsxs("span",{children:["Pokryto: ",t.monthly_fund.covered,"h (práce ",t.monthly_fund.worked,"h",t.vacation_hours>0&&` + dovolená ${t.vacation_hours}h`,t.sick_hours>0&&` + nemoc ${t.sick_hours}h`,t.holiday_hours>0&&` + svátek ${t.holiday_hours}h`,t.unpaid_hours>0&&` + neplacené ${t.unpaid_hours}h`,")"]}),t.monthly_fund.overtime>0?e.jsxs("span",{className:"text-warning fw-600",children:["Přesčas: +",t.monthly_fund.overtime,"h"]}):e.jsxs("span",{children:["Zbývá: ",t.monthly_fund.remaining,"h"]})]})]})]}),!d&&!t.monthly_fund&&e.jsx("div",{className:"text-muted",style:{fontSize:"0.875rem",textAlign:"center",padding:"0.5rem 0"},children:"Fond měsíce není k dispozici"})]})}),e.jsx(f.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.2},children:e.jsxs("div",{className:"admin-card-body",children:[d&&e.jsx("div",{className:"admin-skeleton",style:{gap:"1.25rem"},children:[0,1,2,3,4].map(a=>e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line w-1/4"}),e.jsx("div",{className:"admin-skeleton-line w-1/3"}),e.jsx("div",{className:"admin-skeleton-line w-1/4"})]},a))}),!d&&t.records.length===0&&e.jsx("div",{className:"admin-empty-state",children:e.jsx("p",{children:"Za tento měsíc nejsou žádné záznamy."})}),!d&&t.records.length>0&&e.jsx("div",{className:"admin-table-responsive",children:e.jsxs("table",{className:"admin-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Datum"}),e.jsx("th",{children:"Typ"}),e.jsx("th",{children:"Příchod"}),e.jsx("th",{children:"Pauza"}),e.jsx("th",{children:"Odchod"}),e.jsx("th",{children:"Hodiny"}),e.jsx("th",{children:"Projekty"}),e.jsx("th",{children:"Poznámka"})]})}),e.jsx("tbody",{children:t.records.map(a=>{const i=a.leave_type||"work",l=i!=="work",h=l?(a.leave_hours||8)*60:W(a);return e.jsxs("tr",{children:[e.jsx("td",{className:"admin-mono",children:N(a.shift_date)}),e.jsx("td",{children:e.jsx("span",{className:`attendance-leave-badge ${z(i)}`,children:w(i)})}),e.jsx("td",{className:"admin-mono",children:l?"—":S(a.arrival_time)}),e.jsx("td",{className:"admin-mono",children:l?"—":R(a)}),e.jsx("td",{className:"admin-mono",children:l?"—":S(a.departure_time)}),e.jsx("td",{className:"admin-mono",children:h>0?v(h,!0):"—"}),e.jsx("td",{children:Z(a)}),e.jsx("td",{style:{maxWidth:"150px",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:a.notes||""})]},a.id)})})]})})]})}),t.records.length>0&&e.jsx("div",{ref:o,style:{display:"none"},children:e.jsxs("table",{className:"print-wrapper-table",children:[e.jsx("thead",{children:e.jsx("tr",{children:e.jsx("td",{children:e.jsxs("div",{className:"print-header",children:[e.jsxs("div",{className:"print-header-left",children:[e.jsx("img",{src:"/images/logo-light.png",alt:"BOHA",className:"print-logo"}),e.jsxs("div",{className:"print-header-text",children:[e.jsx("h1",{children:"EVIDENCE DOCHÁZKY"}),e.jsx("div",{className:"company",children:"BOHA Automation s.r.o."})]})]}),e.jsxs("div",{className:"print-header-right",children:[e.jsx("div",{className:"period",children:t.month_name}),e.jsxs("div",{className:"filters",children:["Zaměstnanec: ",n?.fullName||""]}),e.jsxs("div",{className:"generated",children:["Vygenerováno: ",new Date().toLocaleString("cs-CZ")]})]})]})})})}),e.jsx("tbody",{children:e.jsx("tr",{children:e.jsx("td",{children:e.jsxs("div",{className:"user-section",children:[e.jsxs("div",{className:"user-header",children:[e.jsx("h3",{children:n?.fullName||""}),e.jsxs("span",{className:"total",children:["Odpracováno: ",v(t.total_minutes,!0)]})]}),t.leave_balance&&e.jsxs("div",{className:"leave-summary",children:[e.jsxs("strong",{children:["Dovolená ",t.year,":"]})," Zbývá ",t.leave_balance.vacation_remaining.toFixed(1),"h z ",t.leave_balance.vacation_total,"h",t.vacation_hours>0&&e.jsxs(e.Fragment,{children:[" | ",e.jsxs("span",{className:"leave-badge badge-vacation",children:["Tento měsíc: ",t.vacation_hours,"h"]})]}),t.sick_hours>0&&e.jsxs(e.Fragment,{children:[" | ",e.jsxs("span",{className:"leave-badge badge-sick",children:["Nemoc: ",t.sick_hours,"h"]})]}),t.holiday_hours>0&&e.jsxs(e.Fragment,{children:[" | ",e.jsxs("span",{className:"leave-badge badge-holiday",children:["Svátek: ",t.holiday_hours,"h"]})]}),t.monthly_fund?.overtime>0&&e.jsxs(e.Fragment,{children:[" | ",e.jsxs("span",{className:"leave-badge badge-overtime",children:["Přesčas: +",t.monthly_fund.overtime,"h"]})]})]}),e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{style:{width:"70px"},children:"Datum"}),e.jsx("th",{style:{width:"70px"},children:"Typ"}),e.jsx("th",{className:"text-center",style:{width:"70px"},children:"Příchod"}),e.jsx("th",{className:"text-center",style:{width:"90px"},children:"Pauza"}),e.jsx("th",{className:"text-center",style:{width:"70px"},children:"Odchod"}),e.jsx("th",{className:"text-center",style:{width:"80px"},children:"Hodiny"}),e.jsx("th",{children:"Projekty"}),e.jsx("th",{children:"Poznámka"})]})}),e.jsx("tbody",{children:[...t.records].sort((a,i)=>a.shift_date.localeCompare(i.shift_date)).map(a=>{const i=a.leave_type||"work",l=i!=="work",h=E(a),P=Math.floor(h/60),F=h%60;return e.jsxs("tr",{children:[e.jsx("td",{children:N(a.shift_date)}),e.jsx("td",{children:e.jsx("span",{className:`leave-badge ${z(i)}`,children:w(i)})}),e.jsx("td",{className:"text-center",children:l?"—":y(a.arrival_time,a.shift_date)}),e.jsx("td",{className:"text-center",children:l||!a.break_start||!a.break_end?"—":`${y(a.break_start,a.shift_date)} - ${y(a.break_end,a.shift_date)}`}),e.jsx("td",{className:"text-center",children:l?"—":y(a.departure_time,a.shift_date)}),e.jsx("td",{className:"text-center",children:h>0?`${P}:${String(F).padStart(2,"0")}`:"—"}),e.jsx("td",{style:{fontSize:"8px"},children:a.project_logs&&a.project_logs.length>0?a.project_logs.map((r,M)=>{let u,j;if(r.hours!==null&&r.hours!==void 0)u=parseInt(r.hours)||0,j=parseInt(r.minutes)||0;else if(r.started_at&&r.ended_at){const k=Math.max(0,Math.floor((new Date(r.ended_at)-new Date(r.started_at))/6e4));u=Math.floor(k/60),j=k%60}else u=0,j=0;return e.jsxs("div",{children:[r.project_name||`#${r.project_id}`," (",u,":",String(j).padStart(2,"0"),"h)"]},r.id||M)}):a.project_name||"—"}),e.jsx("td",{children:a.notes||""})]},a.id)})}),e.jsxs("tfoot",{children:[e.jsxs("tr",{children:[e.jsx("td",{colSpan:6,className:"text-right",children:"Odpracováno:"}),e.jsx("td",{className:"text-center",children:v(t.total_minutes,!0)}),e.jsx("td",{colSpan:2})]}),t.monthly_fund&&e.jsxs("tr",{children:[e.jsx("td",{colSpan:6,className:"text-right",children:"Fond měsíce:"}),e.jsxs("td",{className:"text-center",children:[t.monthly_fund.covered,"h / ",t.monthly_fund.fund,"h"]}),e.jsx("td",{colSpan:2,children:Y(t.monthly_fund)})]})]})]})]})})})})]})})]})}export{Q as default}; diff --git a/dist/assets/AttendanceLocation-C1MPClO-.js b/dist/assets/AttendanceLocation-C1MPClO-.js new file mode 100644 index 0000000..4333edd --- /dev/null +++ b/dist/assets/AttendanceLocation-C1MPClO-.js @@ -0,0 +1 @@ +import{j as a,m as b}from"./vendor-animation-0s3FMHwK.js";import{g as z,h as F,r as c,L as M}from"./vendor-react-BVs3cwbi.js";import{a as D,u as E,c as R}from"./index-BBlIrj2z.js";import{F as O}from"./Forbidden-D25jV3Oq.js";import{b as T,f as x}from"./attendanceHelpers-D6sLEw0q.js";import"./vendor-utils-Dyr8OjFr.js";const C="/api/admin";function Y(){const m=D(),{hasPermission:w}=E(),p=z(),{id:j}=F(),[h,k]=c.useState(!0),[e,$]=c.useState(null),u=c.useRef(null),l=c.useRef(null);c.useEffect(()=>{(async()=>{try{const v=await(await R(`${C}/attendance.php?action=location&id=${j}`,{})).json();v.success?$(v.data.record):(m.error("Záznam nebyl nalezen"),p("/attendance/admin"))}catch{m.error("Nepodařilo se načíst data"),p("/attendance/admin")}finally{k(!1)}})()},[j,m,p]),c.useEffect(()=>{if(!e||h)return;const r=e.arrival_lat&&e.arrival_lng,i=e.departure_lat&&e.departure_lng;if(!(r||i)||!u.current)return;const P=async()=>{if(window.L){N();return}const s=document.createElement("link");s.rel="stylesheet",s.href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css",document.head.appendChild(s);const n=document.createElement("script");n.src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js",n.onload=N,document.body.appendChild(n)},N=()=>{l.current&&l.current.remove();const s=window.L,n=s.map(u.current);l.current=n,s.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",{attribution:"© OpenStreetMap contributors"}).addTo(n);const d=[],g=[];r&&g.push({lat:parseFloat(e.arrival_lat),lng:parseFloat(e.arrival_lng),type:"arrival",label:"Příchod",time:x(e.arrival_time),accuracy:e.arrival_accuracy||0}),i&&g.push({lat:parseFloat(e.departure_lat),lng:parseFloat(e.departure_lng),type:"departure",label:"Odchod",time:x(e.departure_time),accuracy:e.departure_accuracy||0}),g.forEach(t=>{const y=t.type==="arrival"?"#22c55e":"#ef4444";s.circleMarker([t.lat,t.lng],{radius:10,fillColor:y,color:"#fff",weight:2,opacity:1,fillOpacity:.8}).addTo(n).bindPopup(`${t.label}
          ${t.time}
          Přesnost: ${Math.round(t.accuracy)}m`),t.accuracy>0&&s.circle([t.lat,t.lng],{radius:t.accuracy,fillColor:y,color:y,weight:1,opacity:.3,fillOpacity:.1}).addTo(n),d.push([t.lat,t.lng])}),d.length===1?n.setView(d[0],16):d.length>1&&n.fitBounds(d,{padding:[50,50]})};return P(),()=>{l.current&&(l.current.remove(),l.current=null)}},[e,h]);const _=r=>{if(!r)return"—";const i=new Date(r);return`${i.getDate()}.${i.getMonth()+1}.${i.getFullYear()} ${x(r)}`};if(!w("attendance.admin"))return a.jsx(O,{});if(h)return a.jsxs("div",{className:"admin-skeleton",style:{padding:0,gap:"1.5rem"},children:[a.jsx("div",{className:"admin-skeleton-row",style:{justifyContent:"space-between"},children:a.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"0.75rem"},children:[a.jsx("div",{className:"admin-skeleton-line",style:{width:"32px",height:"32px",borderRadius:"8px"}}),a.jsx("div",{className:"admin-skeleton-line h-8",style:{width:"200px"}})]})}),a.jsx("div",{className:"admin-card",children:a.jsx("div",{className:"admin-skeleton-line",style:{width:"100%",height:"300px",borderRadius:"8px"}})}),a.jsx("div",{style:{display:"grid",gridTemplateColumns:"1fr 1fr",gap:"1.25rem"},children:[0,1].map(r=>a.jsx("div",{className:"admin-card",children:a.jsxs("div",{className:"admin-skeleton",style:{gap:"1rem"},children:[a.jsx("div",{className:"admin-skeleton-line h-8",style:{width:"50%"}}),a.jsx("div",{className:"admin-skeleton-line w-full"}),a.jsx("div",{className:"admin-skeleton-line w-3/4"})]})},r))})]});if(!e)return null;const f=e.arrival_lat&&e.arrival_lng,o=e.departure_lat&&e.departure_lng,L=f||o,A=e.shift_date.substring(0,7);return a.jsxs("div",{children:[a.jsxs(b.div,{className:"admin-page-header",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4},children:[a.jsx("div",{children:a.jsx("h1",{className:"admin-page-title",children:"Poloha záznamu"})}),a.jsx("div",{className:"admin-page-actions",children:a.jsx(M,{to:`/attendance/admin?month=${A}`,className:"admin-btn admin-btn-secondary",children:"← Zpět na správu"})})]}),a.jsxs(b.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.1},children:[a.jsx("div",{className:"admin-card-header",children:a.jsxs("h2",{className:"admin-card-title",children:[e.user_name," — ",T(e.shift_date)]})}),a.jsxs("div",{className:"admin-card-body",children:[L&&a.jsx("div",{ref:u,className:"attendance-location-map"}),a.jsxs("div",{className:"attendance-location-grid",children:[a.jsxs("div",{className:`attendance-location-card ${f?"":"empty"}`,children:[a.jsx("h3",{className:"attendance-location-title",children:"Příchod"}),a.jsx("div",{className:"attendance-location-time",children:e.arrival_time?_(e.arrival_time):"—"}),f?a.jsxs(a.Fragment,{children:[a.jsx("div",{className:"attendance-location-address",children:e.arrival_address||a.jsx("em",{children:"Adresa nezjištěna"})}),a.jsxs("div",{className:"attendance-location-coords",children:["GPS: ",e.arrival_lat,", ",e.arrival_lng,e.arrival_accuracy&&` (přesnost: ${Math.round(e.arrival_accuracy)}m)`]}),a.jsx("a",{href:`https://www.google.com/maps?q=${e.arrival_lat},${e.arrival_lng}`,target:"_blank",rel:"noopener noreferrer",className:"admin-btn admin-btn-secondary admin-btn-sm",style:{marginTop:"0.5rem"},children:"Otevřít v Google Maps"})]}):a.jsx("div",{className:"attendance-location-address",children:a.jsx("em",{children:"Poloha nebyla zaznamenána"})})]}),(o||e.departure_time)&&a.jsxs("div",{className:`attendance-location-card ${o?"":"empty"}`,children:[a.jsx("h3",{className:"attendance-location-title",children:"Odchod"}),a.jsx("div",{className:"attendance-location-time",children:e.departure_time?_(e.departure_time):"—"}),o?a.jsxs(a.Fragment,{children:[a.jsx("div",{className:"attendance-location-address",children:e.departure_address||a.jsx("em",{children:"Adresa nezjištěna"})}),a.jsxs("div",{className:"attendance-location-coords",children:["GPS: ",e.departure_lat,", ",e.departure_lng,e.departure_accuracy&&` (přesnost: ${Math.round(e.departure_accuracy)}m)`]}),a.jsx("a",{href:`https://www.google.com/maps?q=${e.departure_lat},${e.departure_lng}`,target:"_blank",rel:"noopener noreferrer",className:"admin-btn admin-btn-secondary admin-btn-sm",style:{marginTop:"0.5rem"},children:"Otevřít v Google Maps"})]}):a.jsx("div",{className:"attendance-location-address",children:a.jsx("em",{children:"Poloha nebyla zaznamenána"})})]})]})]})]})]})}export{Y as default}; diff --git a/dist/assets/AuditLog-DGV9ABTZ.js b/dist/assets/AuditLog-DGV9ABTZ.js new file mode 100644 index 0000000..f056d92 --- /dev/null +++ b/dist/assets/AuditLog-DGV9ABTZ.js @@ -0,0 +1 @@ +import{j as e,m as y}from"./vendor-animation-0s3FMHwK.js";import{r as i}from"./vendor-react-BVs3cwbi.js";import{u as B,a as E,c as N,d as I,F as h,A as b}from"./index-BBlIrj2z.js";import{F as D}from"./Forbidden-D25jV3Oq.js";import{P as F}from"./Pagination-B1sbY6V7.js";import"./vendor-utils-Dyr8OjFr.js";const _="/api/admin",k={create:"Vytvoření",update:"Úprava",delete:"Smazání",login:"Přihlášení",login_failed:"Neúspěšné přihlášení",logout:"Odhlášení",view:"Zobrazení",activate:"Aktivace",deactivate:"Deaktivace",password_change:"Změna hesla",permission_change:"Změna oprávnění",access_denied:"Přístup odepřen"},R={create:"admin-badge-success",update:"admin-badge-info",delete:"admin-badge-danger",login:"admin-badge-secondary",login_failed:"admin-badge-danger",logout:"admin-badge-secondary",view:"admin-badge-info",activate:"admin-badge-success",deactivate:"admin-badge-warning",password_change:"admin-badge-info",permission_change:"admin-badge-warning",access_denied:"admin-badge-danger"},w={user:"Uživatel",attendance:"Docházka",leave_request:"Žádost o nepřítomnost",offers_quotation:"Nabídka",offers_customer:"Zákazník",offers_item_template:"Šablona položky",offers_scope_template:"Šablona rozsahu",offers_settings:"Nastavení nabídek",orders_order:"Objednávka",invoices_invoice:"Faktura",projects_project:"Projekt",role:"Role",trips:"Jízda",vehicles:"Vozidlo",bank_account:"Bankovní účet"},Z=Object.entries(k).map(([o,t])=>({value:o,label:t})),W=Object.entries(w).map(([o,t])=>({value:o,label:t}));function G(){const{hasPermission:o}=B(),t=E(),[u,C]=i.useState([]),[p,g]=i.useState(!0),[r,S]=i.useState(null),[s,z]=i.useState({search:"",action:"",entity_type:"",date_from:"",date_to:""}),[P,x]=i.useState(!1),[v,A]=i.useState(90),[j,f]=i.useState(!1),c=i.useCallback(async(a=1,n=50)=>{g(!0);try{const d=new URLSearchParams({page:String(a),per_page:String(n)});s.search&&d.set("search",s.search),s.action&&d.set("action",s.action),s.entity_type&&d.set("entity_type",s.entity_type),s.date_from&&d.set("date_from",s.date_from),s.date_to&&d.set("date_to",s.date_to);const l=await(await N(`${_}/audit-log.php?${d.toString()}`)).json();l.success?(C(l.data.logs||[]),S({total:l.data.total,page:l.data.page,per_page:l.data.per_page,total_pages:l.data.pages})):t.error(l.error||"Nepodařilo se načíst audit log")}catch{t.error("Chyba připojení")}finally{g(!1)}},[s]);if(i.useEffect(()=>{c()},[c]),!o("settings.audit"))return e.jsx(D,{});const m=(a,n)=>{z(d=>({...d,[a]:n}))},T=a=>{c(a,r?.per_page||50)},O=a=>{c(1,a)},V=async()=>{f(!0);try{const n=await(await N(`${_}/audit-log.php`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:"cleanup",days:v})})).json();n.success?(t.success(n.message),x(!1),c()):t.error(n.error)}catch{t.error("Chyba připojení")}finally{f(!1)}},L=a=>a?new Date(a).toLocaleString("cs-CZ"):"-";return p&&u.length===0?e.jsxs("div",{className:"admin-skeleton",style:{padding:0,gap:"1.5rem"},children:[e.jsx("div",{className:"admin-skeleton-row",style:{justifyContent:"space-between"},children:e.jsxs("div",{children:[e.jsx("div",{className:"admin-skeleton-line h-8",style:{width:"160px",marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line",style:{width:"100px"}})]})}),e.jsx("div",{className:"admin-card",children:e.jsx("div",{className:"admin-skeleton",style:{gap:"0.75rem",padding:"1rem"},children:e.jsx("div",{className:"admin-skeleton-line h-10",style:{width:"100%",borderRadius:"8px"}})})}),e.jsx("div",{className:"admin-card",children:e.jsxs("div",{className:"admin-skeleton",style:{gap:"1rem"},children:[e.jsx("div",{className:"admin-skeleton-line h-10",style:{width:"100%",borderRadius:"4px"}}),Array.from({length:8},(a,n)=>e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line",style:{width:"120px"}}),e.jsx("div",{className:"admin-skeleton-line",style:{width:"80px"}}),e.jsx("div",{className:"admin-skeleton-line",style:{width:"70px",borderRadius:"10px"}}),e.jsx("div",{className:"admin-skeleton-line",style:{width:"80px"}}),e.jsx("div",{className:"admin-skeleton-line",style:{flex:1}}),e.jsx("div",{className:"admin-skeleton-line",style:{width:"90px"}})]},n))]})})]}):e.jsxs("div",{children:[e.jsxs(y.div,{className:"admin-page-header",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4},children:[e.jsxs("div",{children:[e.jsx("h1",{className:"admin-page-title",children:"Audit log"}),r&&e.jsxs("p",{className:"admin-page-subtitle",children:[r.total," ",I(r.total,"záznam","záznamy","záznamů")]})]}),e.jsxs("button",{className:"admin-btn admin-btn-secondary admin-btn-sm",onClick:()=>x(!0),children:[e.jsxs("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("polyline",{points:"3 6 5 6 21 6"}),e.jsx("path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"})]}),"Vyčistit"]})]}),P&&e.jsxs("div",{className:"admin-modal-overlay",style:{opacity:1},children:[e.jsx("div",{className:"admin-modal-backdrop",onClick:()=>!j&&x(!1)}),e.jsxs(y.div,{className:"admin-modal admin-confirm-modal",initial:{opacity:0,scale:.95,y:20},animate:{opacity:1,scale:1,y:0},transition:{duration:.2},children:[e.jsxs("div",{className:"admin-modal-body admin-confirm-content",children:[e.jsx("div",{className:"admin-confirm-icon admin-confirm-icon-danger",children:e.jsxs("svg",{width:"24",height:"24",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("polyline",{points:"3 6 5 6 21 6"}),e.jsx("path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"})]})}),e.jsx("h2",{className:"admin-confirm-title",children:"Vyčistit audit log"}),e.jsx("p",{className:"admin-confirm-message",children:"Smazat záznamy starší než:"}),e.jsx("div",{style:{margin:"0.75rem auto",maxWidth:"200px"},children:e.jsxs("select",{className:"admin-form-select",value:v,onChange:a=>A(parseInt(a.target.value)),children:[e.jsx("option",{value:30,children:"30 dní"}),e.jsx("option",{value:60,children:"60 dní"}),e.jsx("option",{value:90,children:"90 dní"}),e.jsx("option",{value:180,children:"180 dní"}),e.jsx("option",{value:365,children:"1 rok"}),e.jsx("option",{value:0,children:"Vše"})]})}),e.jsx("p",{className:"admin-confirm-message",style:{fontSize:"12px",opacity:.6},children:"Tato akce je nevratná."})]}),e.jsxs("div",{className:"admin-modal-footer",children:[e.jsx("button",{type:"button",onClick:()=>x(!1),className:"admin-btn admin-btn-secondary",disabled:j,children:"Zrušit"}),e.jsx("button",{type:"button",onClick:V,className:"admin-btn admin-btn-primary",disabled:j,children:j?"Mažu...":"Smazat"})]})]})]}),e.jsx(y.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.1},style:{marginBottom:"1rem"},children:e.jsx("div",{className:"admin-card-body",children:e.jsxs("div",{className:"admin-form-row",style:{gridTemplateColumns:"1.2fr 1fr 1fr 1fr 1fr"},children:[e.jsx(h,{label:"Hledat",children:e.jsx("input",{type:"text",className:"admin-form-input",placeholder:"Popis, uživatel...",value:s.search,onChange:a=>m("search",a.target.value)})}),e.jsx(h,{label:"Akce",children:e.jsxs("select",{className:"admin-form-select",value:s.action,onChange:a=>m("action",a.target.value),children:[e.jsx("option",{value:"",children:"Všechny"}),Z.map(a=>e.jsx("option",{value:a.value,children:a.label},a.value))]})}),e.jsx(h,{label:"Typ entity",children:e.jsxs("select",{className:"admin-form-select",value:s.entity_type,onChange:a=>m("entity_type",a.target.value),children:[e.jsx("option",{value:"",children:"Všechny"}),W.map(a=>e.jsx("option",{value:a.value,children:a.label},a.value))]})}),e.jsx(h,{label:"Od",children:e.jsx(b,{mode:"date",value:s.date_from,onChange:a=>m("date_from",a)})}),e.jsx(h,{label:"Do",children:e.jsx(b,{mode:"date",value:s.date_to,onChange:a=>m("date_to",a)})})]})})}),e.jsxs(y.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.15},children:[e.jsx("div",{className:"admin-table-wrapper",children:e.jsxs("table",{className:"admin-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Čas"}),e.jsx("th",{children:"Uživatel"}),e.jsx("th",{children:"Akce"}),e.jsx("th",{children:"Typ entity"}),e.jsx("th",{children:"Popis"}),e.jsx("th",{children:"IP"})]})}),e.jsxs("tbody",{children:[p&&Array.from({length:10},(a,n)=>e.jsxs("tr",{children:[e.jsx("td",{children:e.jsx("div",{className:"admin-skeleton-line",style:{width:"110px",height:"14px"}})}),e.jsx("td",{children:e.jsx("div",{className:"admin-skeleton-line",style:{width:"80px",height:"14px"}})}),e.jsx("td",{children:e.jsx("div",{className:"admin-skeleton-line",style:{width:"70px",height:"22px",borderRadius:"10px"}})}),e.jsx("td",{children:e.jsx("div",{className:"admin-skeleton-line",style:{width:"80px",height:"14px"}})}),e.jsx("td",{children:e.jsx("div",{className:"admin-skeleton-line",style:{width:"60%",height:"14px"}})}),e.jsx("td",{children:e.jsx("div",{className:"admin-skeleton-line",style:{width:"90px",height:"14px"}})})]},`skeleton-${n}`)),!p&&u.length===0&&e.jsx("tr",{children:e.jsx("td",{colSpan:"6",children:e.jsxs("div",{className:"admin-empty-state",children:[e.jsx("div",{className:"admin-empty-icon",children:e.jsxs("svg",{width:"28",height:"28",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"1.5",children:[e.jsx("path",{d:"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"}),e.jsx("polyline",{points:"14 2 14 8 20 8"}),e.jsx("line",{x1:"16",y1:"13",x2:"8",y2:"13"}),e.jsx("line",{x1:"16",y1:"17",x2:"8",y2:"17"})]})}),e.jsx("p",{children:"Žádné záznamy k zobrazení"})]})})}),!p&&u.map(a=>e.jsxs("tr",{children:[e.jsx("td",{className:"admin-mono",style:{whiteSpace:"nowrap"},children:L(a.created_at)}),e.jsx("td",{style:{fontWeight:500},children:a.username||"-"}),e.jsx("td",{children:e.jsx("span",{className:`admin-badge ${R[a.action]||"admin-badge-secondary"}`,children:k[a.action]||a.action})}),e.jsx("td",{children:w[a.entity_type]||a.entity_type||"-"}),e.jsx("td",{children:a.description||"-"}),e.jsx("td",{className:"admin-mono",children:a.user_ip||"-"})]},a.id))]})]})}),e.jsx(F,{pagination:r,onPageChange:T,onPerPageChange:O})]})]})}export{G as default}; diff --git a/dist/assets/CompanySettings-Cac8Rr8l.js b/dist/assets/CompanySettings-Cac8Rr8l.js new file mode 100644 index 0000000..5a67018 --- /dev/null +++ b/dist/assets/CompanySettings-Cac8Rr8l.js @@ -0,0 +1 @@ +import{j as e,m as y}from"./vendor-animation-0s3FMHwK.js";import{r}from"./vendor-react-BVs3cwbi.js";import{a as se,u as ne,c as j,F as i}from"./index-BBlIrj2z.js";import{F as ie}from"./Forbidden-D25jV3Oq.js";import"./vendor-utils-Dyr8OjFr.js";const p="/api/admin",k=["street","city_postal","country","company_id","vat_id"],T={street:"Ulice",city_postal:"Město + PSČ",country:"Země",company_id:"IČO",vat_id:"DIČ"},A=new Date().getFullYear().toString().slice(-2);function me(){const o=se(),{hasPermission:Z}=ne(),[K,M]=r.useState(!0),[C,w]=r.useState(!1),[F,B]=r.useState(!1),[S,G]=r.useState(null),[d,U]=r.useState({company_name:"",street:"",city:"",postal_code:"",country:"",company_id:"",vat_id:"",quotation_prefix:"N",default_currency:"EUR",default_vat_rate:21,order_type_code:"71",invoice_type_code:"81"}),[m,v]=r.useState([]),z=r.useRef(0),[D,f]=r.useState([...k]),[L,q]=r.useState([]),[V,H]=r.useState(!0),[P,E]=r.useState(!1),[x,I]=r.useState(null),[h,u]=r.useState({account_name:"",bank_name:"",account_number:"",iban:"",bic:"",currency:"CZK",is_default:!1}),g=r.useCallback(()=>{const a=[...k],s=[...D].filter(t=>t!=="company_name");for(const t of a)s.includes(t)||s.push(t);for(let t=0;tt.startsWith("custom_")?parseInt(t.split("_")[1]){const t=g(),n=a+s;if(n<0||n>=t.length)return;const l=[...t];[l[a],l[n]]=[l[n],l[a]],f(l)},J=a=>{if(T[a])return T[a];if(a.startsWith("custom_")){const s=parseInt(a.split("_")[1]),t=m[s];if(t)return t.name?`${t.name}: ${t.value||"..."}`:t.value||`Vlastní pole ${s+1}`}return a},N=r.useCallback(async()=>{try{const a=await j(`${p}/company-settings.php?action=logo`);if(a.ok){const s=await a.blob();G(t=>(t&&URL.revokeObjectURL(t),URL.createObjectURL(s)))}}catch{}},[]),W=r.useCallback(async()=>{try{const a=await j(`${p}/company-settings.php`);if(a.status===401)return;const s=await a.json();if(s.success){const t=s.data;U({company_name:t.company_name||"",street:t.street||"",city:t.city||"",postal_code:t.postal_code||"",country:t.country||"",company_id:t.company_id||"",vat_id:t.vat_id||"",quotation_prefix:t.quotation_prefix||"N",default_currency:t.default_currency||"EUR",default_vat_rate:t.default_vat_rate||21,order_type_code:t.order_type_code||"71",invoice_type_code:t.invoice_type_code||"81"});const n=Array.isArray(t.custom_fields)&&t.custom_fields.length>0?t.custom_fields.map(l=>({...l,_key:`cf-${++z.current}`})):[];v(n),Array.isArray(t.supplier_field_order)&&t.supplier_field_order.length>0?f(t.supplier_field_order):f([...k]),t.has_logo&&N()}else o.error(s.error||"Nepodařilo se načíst nastavení")}catch{o.error("Chyba připojení")}finally{M(!1)}},[o,N]),b=r.useCallback(async()=>{try{const a=await j(`${p}/bank-accounts.php`);if(a.status===401)return;const s=await a.json();s.success&&q(s.data)}catch{}finally{H(!1)}},[]),_=()=>{I(null),u({account_name:"",bank_name:"",account_number:"",iban:"",bic:"",currency:"CZK",is_default:!1})},Y=async()=>{if(!h.account_name.trim()){o.error("Název účtu je povinný");return}E(!0);try{const a=x!==null,s=a?`${p}/bank-accounts.php?id=${x}`:`${p}/bank-accounts.php`,n=await(await j(s,{method:a?"PUT":"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(h)})).json();n.success?(o.success(n.message),_(),b()):o.error(n.error||"Chyba při ukládání")}catch{o.error("Chyba připojení")}finally{E(!1)}},X=async a=>{if(confirm("Opravdu smazat tento bankovní účet?"))try{const t=await(await j(`${p}/bank-accounts.php?id=${a}`,{method:"DELETE"})).json();t.success?(o.success(t.message),x===a&&_(),b()):o.error(t.error||"Chyba při mazání")}catch{o.error("Chyba připojení")}},Q=a=>{I(a.id),u({account_name:a.account_name||"",bank_name:a.bank_name||"",account_number:a.account_number||"",iban:a.iban||"",bic:a.bic||"",currency:a.currency||"CZK",is_default:!!a.is_default})};r.useEffect(()=>{W(),b()},[W,b]);const ee=async()=>{w(!0);try{const a={...d,custom_fields:m.filter(n=>n.name.trim()||n.value.trim()),supplier_field_order:g()},t=await(await j(`${p}/company-settings.php`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)})).json();t.success?o.success(t.message||"Nastavení bylo uloženo"):o.error(t.error||"Nepodařilo se uložit nastavení")}catch{o.error("Chyba připojení")}finally{w(!1)}},ae=async a=>{const s=a.target.files[0];if(s){B(!0);try{const t=new FormData;t.append("logo",s);const l=await(await j(`${p}/company-settings.php?action=logo`,{method:"POST",body:t})).json();l.success?(o.success(l.message||"Logo bylo nahráno"),N()):o.error(l.error||"Nepodařilo se nahrát logo")}catch{o.error("Chyba připojení")}finally{B(!1),a.target.value=""}}},c=(a,s)=>{U(t=>({...t,[a]:s}))};if(!Z("offers.settings"))return e.jsx(ie,{});if(K)return e.jsxs("div",{className:"admin-skeleton",style:{padding:0,gap:"1.5rem"},children:[e.jsxs("div",{className:"admin-skeleton-row",style:{justifyContent:"space-between"},children:[e.jsxs("div",{children:[e.jsx("div",{className:"admin-skeleton-line h-8",style:{width:"200px",marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line",style:{width:"140px"}})]}),e.jsx("div",{className:"admin-skeleton-line h-10",style:{width:"120px",borderRadius:"8px"}})]}),e.jsx("div",{style:{display:"grid",gridTemplateColumns:"repeat(3, 1fr)",gap:"1.25rem"},children:[0,1,2,3,4,5].map(a=>e.jsx("div",{className:"admin-card",children:e.jsxs("div",{className:"admin-skeleton",style:{gap:"1.25rem"},children:[e.jsx("div",{className:"admin-skeleton-line h-8",style:{width:"60%"}}),[0,1,2].map(s=>e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line w-1/3"}),e.jsx("div",{className:"admin-skeleton-line w-1/2"})]},s))]})},a))})]});const O=g();function te(){return P?e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"admin-spinner",style:{width:14,height:14,borderWidth:2}}),"Ukládání..."]}):x!==null?"Uložit změny":e.jsxs(e.Fragment,{children:[e.jsxs("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("line",{x1:"12",y1:"5",x2:"12",y2:"19"}),e.jsx("line",{x1:"5",y1:"12",x2:"19",y2:"12"})]}),"Přidat účet"]})}return e.jsxs("div",{children:[e.jsxs(y.div,{className:"admin-page-header",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4},children:[e.jsxs("div",{children:[e.jsx("h1",{className:"admin-page-title",children:"Nastavení firmy"}),e.jsx("p",{className:"admin-page-subtitle",children:"Firemní údaje, číslování dokladů a výchozí hodnoty"})]}),e.jsx("button",{onClick:ee,className:"admin-btn admin-btn-primary",disabled:C,children:C?e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"admin-spinner",style:{width:16,height:16,borderWidth:2}}),"Ukládání..."]}):"Uložit nastavení"})]}),e.jsxs("div",{className:"offers-settings-grid",children:[e.jsxs(y.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.1},children:[e.jsx("div",{className:"admin-card-header",children:e.jsx("h3",{className:"admin-card-title",children:"Firemní údaje"})}),e.jsx("div",{className:"admin-card-body",children:e.jsxs("div",{className:"admin-form",children:[e.jsx(i,{label:"Název firmy",children:e.jsx("input",{type:"text",value:d.company_name,onChange:a=>c("company_name",a.target.value),className:"admin-form-input"})}),e.jsxs("div",{className:"admin-form-row",children:[e.jsx(i,{label:"Ulice",children:e.jsx("input",{type:"text",value:d.street,onChange:a=>c("street",a.target.value),className:"admin-form-input"})}),e.jsx(i,{label:"Město",children:e.jsx("input",{type:"text",value:d.city,onChange:a=>c("city",a.target.value),className:"admin-form-input"})})]}),e.jsxs("div",{className:"admin-form-row",children:[e.jsx(i,{label:"PSČ",children:e.jsx("input",{type:"text",value:d.postal_code,onChange:a=>c("postal_code",a.target.value),className:"admin-form-input"})}),e.jsx(i,{label:"Země",children:e.jsx("input",{type:"text",value:d.country,onChange:a=>c("country",a.target.value),className:"admin-form-input"})})]}),e.jsxs("div",{className:"admin-form-row",children:[e.jsx(i,{label:"IČO",children:e.jsx("input",{type:"text",value:d.company_id,onChange:a=>c("company_id",a.target.value),className:"admin-form-input"})}),e.jsx(i,{label:"DIČ",children:e.jsx("input",{type:"text",value:d.vat_id,onChange:a=>c("vat_id",a.target.value),className:"admin-form-input"})})]}),e.jsxs("div",{style:{marginTop:4},children:[e.jsx("label",{className:"admin-form-label",style:{display:"block",marginBottom:4},children:"Vlastní pole"}),m.map((a,s)=>e.jsxs("div",{style:{marginBottom:8},children:[e.jsxs("div",{className:"admin-form-row",style:{marginBottom:0,alignItems:"flex-end"},children:[e.jsx(i,{label:s===0?"Název":" ",style:{flex:1},children:e.jsx("input",{type:"text",value:a.name,onChange:t=>{const n=[...m];n[s]={...n[s],name:t.target.value},v(n)},className:"admin-form-input",placeholder:"Např. Tel."})}),e.jsx(i,{label:s===0?"Hodnota":" ",style:{flex:1},children:e.jsxs("div",{style:{display:"flex",gap:4,alignItems:"center"},children:[e.jsx("input",{type:"text",value:a.value,onChange:t=>{const n=[...m];n[s]={...n[s],value:t.target.value},v(n)},className:"admin-form-input",style:{flex:1}}),e.jsx("button",{type:"button",onClick:()=>{const t=`custom_${s}`;f(n=>n.filter(l=>l!==t).map(l=>{if(l.startsWith("custom_")){const $=parseInt(l.split("_")[1]);if($>s)return`custom_${$-1}`}return l})),v(m.filter((n,l)=>l!==s))},className:"admin-btn-icon danger",title:"Odebrat pole","aria-label":"Odebrat pole",children:e.jsxs("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),e.jsx("line",{x1:"6",y1:"6",x2:"18",y2:"18"})]})})]})})]}),e.jsxs("label",{className:"admin-form-checkbox",style:{marginTop:4},children:[e.jsx("input",{type:"checkbox",checked:a.showLabel!==!1,onChange:t=>{const n=[...m];n[s]={...n[s],showLabel:t.target.checked},v(n)}}),e.jsx("span",{style:{fontSize:"0.8rem"},children:"Zobrazit název v PDF"})]})]},a._key)),e.jsxs("button",{type:"button",onClick:()=>v([...m,{name:"",value:"",showLabel:!0,_key:`cf-${++z.current}`}]),className:"admin-btn admin-btn-secondary",style:{marginTop:4,fontSize:"0.85rem"},children:[e.jsxs("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("line",{x1:"12",y1:"5",x2:"12",y2:"19"}),e.jsx("line",{x1:"5",y1:"12",x2:"19",y2:"12"})]}),"Přidat pole"]})]})]})})]}),e.jsxs(y.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.15},children:[e.jsx("div",{className:"admin-card-header",children:e.jsx("h3",{className:"admin-card-title",children:"Bankovní účty"})}),e.jsx("div",{className:"admin-card-body",children:V?e.jsx("div",{className:"admin-skeleton",style:{gap:"1rem"},children:[0,1,2].map(a=>e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line w-1/3"}),e.jsx("div",{className:"admin-skeleton-line w-1/4"}),e.jsx("div",{className:"admin-skeleton-line w-1/4"})]},a))}):e.jsxs(e.Fragment,{children:[L.length>0&&e.jsx("div",{className:"admin-table-wrapper",style:{marginBottom:16},children:e.jsxs("table",{className:"admin-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Název"}),e.jsx("th",{children:"Banka"}),e.jsx("th",{children:"Číslo účtu"}),e.jsx("th",{children:"IBAN"}),e.jsx("th",{children:"BIC/SWIFT"}),e.jsx("th",{children:"Měna"}),e.jsx("th",{style:{width:70},children:"Výchozí"}),e.jsx("th",{style:{width:80}})]})}),e.jsx("tbody",{children:L.map(a=>e.jsxs("tr",{style:x===a.id?{background:"var(--bg-tertiary)"}:void 0,children:[e.jsx("td",{children:a.account_name}),e.jsx("td",{children:a.bank_name}),e.jsx("td",{className:"admin-mono",children:a.account_number}),e.jsx("td",{className:"admin-mono",children:a.iban}),e.jsx("td",{className:"admin-mono",children:a.bic}),e.jsx("td",{children:a.currency}),e.jsx("td",{style:{textAlign:"center"},children:a.is_default?e.jsx("span",{className:"text-accent fw-600",children:"✓"}):"–"}),e.jsx("td",{children:e.jsxs("div",{style:{display:"flex",gap:4},children:[e.jsx("button",{type:"button",onClick:()=>Q(a),className:"admin-btn-icon",title:"Upravit","aria-label":"Upravit",children:e.jsxs("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"}),e.jsx("path",{d:"M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"})]})}),e.jsx("button",{type:"button",onClick:()=>X(a.id),className:"admin-btn-icon danger",title:"Smazat","aria-label":"Smazat",children:e.jsxs("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),e.jsx("line",{x1:"6",y1:"6",x2:"18",y2:"18"})]})})]})})]},a.id))})]})}),e.jsxs("div",{style:{background:"var(--bg-tertiary)",borderRadius:"var(--border-radius)",padding:16},children:[e.jsx("h4",{className:"text-secondary",style:{margin:"0 0 12px",fontSize:"0.9rem"},children:x!==null?"Upravit účet":"Přidat nový účet"}),e.jsxs("div",{className:"admin-form",children:[e.jsxs("div",{className:"admin-form-row",children:[e.jsx(i,{label:"Název účtu",required:!0,children:e.jsx("input",{type:"text",value:h.account_name,onChange:a=>u(s=>({...s,account_name:a.target.value})),className:"admin-form-input",placeholder:"Např. Hlavní CZK účet"})}),e.jsx(i,{label:"Název banky",children:e.jsx("input",{type:"text",value:h.bank_name,onChange:a=>u(s=>({...s,bank_name:a.target.value})),className:"admin-form-input",placeholder:"Např. MONETA Money Bank, a.s."})})]}),e.jsxs("div",{className:"admin-form-row",children:[e.jsx(i,{label:"Číslo účtu",children:e.jsx("input",{type:"text",value:h.account_number,onChange:a=>u(s=>({...s,account_number:a.target.value})),className:"admin-form-input",placeholder:"123456789/0600"})}),e.jsx(i,{label:"Měna",children:e.jsxs("select",{value:h.currency,onChange:a=>u(s=>({...s,currency:a.target.value})),className:"admin-form-select",children:[e.jsx("option",{value:"CZK",children:"CZK"}),e.jsx("option",{value:"EUR",children:"EUR"}),e.jsx("option",{value:"USD",children:"USD"}),e.jsx("option",{value:"GBP",children:"GBP"})]})})]}),e.jsxs("div",{className:"admin-form-row",children:[e.jsx(i,{label:"IBAN",children:e.jsx("input",{type:"text",value:h.iban,onChange:a=>u(s=>({...s,iban:a.target.value})),className:"admin-form-input",placeholder:"CZ65 0800 0000 1920 0014 5399"})}),e.jsx(i,{label:"BIC / SWIFT",children:e.jsx("input",{type:"text",value:h.bic,onChange:a=>u(s=>({...s,bic:a.target.value})),className:"admin-form-input",placeholder:"GIBACZPX"})})]}),e.jsxs("label",{className:"admin-form-checkbox",children:[e.jsx("input",{type:"checkbox",checked:h.is_default,onChange:a=>u(s=>({...s,is_default:a.target.checked}))}),e.jsx("span",{children:"Výchozí účet (použije se automaticky při vytváření faktury)"})]}),e.jsxs("div",{style:{display:"flex",gap:8,marginTop:8},children:[e.jsx("button",{type:"button",onClick:Y,className:"admin-btn admin-btn-primary",disabled:P,style:{fontSize:"0.85rem"},children:te()}),x!==null&&e.jsx("button",{type:"button",onClick:_,className:"admin-btn admin-btn-secondary",style:{fontSize:"0.85rem"},children:"Zrušit"})]})]})]})]})})]}),e.jsxs(y.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.15},children:[e.jsx("div",{className:"admin-card-header",children:e.jsx("h3",{className:"admin-card-title",children:"Pořadí polí dodavatele v PDF"})}),e.jsxs("div",{className:"admin-card-body",children:[e.jsx("small",{className:"admin-form-hint",style:{display:"block",marginBottom:12},children:"Určuje pořadí řádků v adresním bloku dodavatele na PDF nabídce."}),e.jsx("div",{className:"admin-reorder-list",children:O.map((a,s)=>e.jsxs("div",{className:"admin-reorder-item",children:[e.jsxs("div",{className:"admin-reorder-arrows",children:[e.jsx("button",{type:"button",onClick:()=>R(s,-1),disabled:s===0,className:"admin-btn-icon",title:"Nahoru","aria-label":"Nahoru",children:e.jsx("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:e.jsx("path",{d:"M18 15l-6-6-6 6"})})}),e.jsx("button",{type:"button",onClick:()=>R(s,1),disabled:s===O.length-1,className:"admin-btn-icon",title:"Dolů","aria-label":"Dolů",children:e.jsx("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:e.jsx("path",{d:"M6 9l6 6 6-6"})})})]}),e.jsx("span",{className:`admin-reorder-label${a.startsWith("custom_")?" accent":""}`,children:J(a)})]},a))})]})]}),e.jsxs(y.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.2},children:[e.jsx("div",{className:"admin-card-header",children:e.jsx("h3",{className:"admin-card-title",children:"Logo"})}),e.jsx("div",{className:"admin-card-body",children:e.jsxs("div",{className:"offers-logo-section",children:[S&&e.jsx("div",{className:"offers-logo-preview",children:e.jsx("img",{src:S,alt:"Logo"})}),e.jsxs("label",{className:"admin-btn admin-btn-secondary",style:{cursor:"pointer"},children:[F?e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"admin-spinner",style:{width:16,height:16,borderWidth:2}}),"Nahrávání..."]}):e.jsxs(e.Fragment,{children:[e.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"}),e.jsx("polyline",{points:"17 8 12 3 7 8"}),e.jsx("line",{x1:"12",y1:"3",x2:"12",y2:"15"})]}),"Nahrát logo"]}),e.jsx("input",{type:"file",accept:"image/*",onChange:ae,style:{display:"none"},disabled:F})]}),e.jsx("small",{className:"admin-form-hint",children:"PNG, JPEG, GIF nebo WebP, max 5 MB"})]})})]}),e.jsxs(y.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.25},children:[e.jsx("div",{className:"admin-card-header",children:e.jsx("h3",{className:"admin-card-title",children:"Číslování dokladů"})}),e.jsx("div",{className:"admin-card-body",children:e.jsxs("div",{className:"admin-form",children:[e.jsxs(i,{label:"Nabídky — prefix",children:[e.jsx("input",{type:"text",value:d.quotation_prefix,onChange:a=>c("quotation_prefix",a.target.value),className:"admin-form-input",placeholder:"N",style:{maxWidth:120}}),e.jsxs("small",{className:"admin-form-hint",children:["Formát: ROK/PREFIX/ČÍSLO — ukázka: ",new Date().getFullYear(),"/",d.quotation_prefix||"N","/001"]})]}),e.jsx("hr",{style:{border:"none",borderTop:"1px solid var(--border-color)",margin:"0.75rem 0"}}),e.jsxs(i,{label:"Objednávky a projekty — typový kód",children:[e.jsx("input",{type:"text",value:d.order_type_code,onChange:a=>c("order_type_code",a.target.value),className:"admin-form-input",placeholder:"71",style:{maxWidth:120}}),e.jsxs("small",{className:"admin-form-hint",children:["Formát: RRKÓD#### — ukázka: ",A,d.order_type_code||"71","0001"]})]}),e.jsx("hr",{style:{border:"none",borderTop:"1px solid var(--border-color)",margin:"0.75rem 0"}}),e.jsxs(i,{label:"Faktury — typový kód",children:[e.jsx("input",{type:"text",value:d.invoice_type_code,onChange:a=>c("invoice_type_code",a.target.value),className:"admin-form-input",placeholder:"81",style:{maxWidth:120}}),e.jsxs("small",{className:"admin-form-hint",children:["Formát: RRKÓD#### — ukázka: ",A,d.invoice_type_code||"81","0001"]})]})]})})]}),e.jsxs(y.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.3},children:[e.jsx("div",{className:"admin-card-header",children:e.jsx("h3",{className:"admin-card-title",children:"Výchozí hodnoty"})}),e.jsx("div",{className:"admin-card-body",children:e.jsx("div",{className:"admin-form",children:e.jsxs("div",{className:"admin-form-row",children:[e.jsx(i,{label:"Výchozí měna",children:e.jsxs("select",{value:d.default_currency,onChange:a=>c("default_currency",a.target.value),className:"admin-form-select",children:[e.jsx("option",{value:"EUR",children:"EUR (€)"}),e.jsx("option",{value:"USD",children:"USD ($)"}),e.jsx("option",{value:"CZK",children:"CZK (Kč)"}),e.jsx("option",{value:"GBP",children:"GBP (£)"})]})}),e.jsx(i,{label:"Výchozí sazba DPH (%)",children:e.jsx("input",{type:"number",value:d.default_vat_rate,onChange:a=>c("default_vat_rate",parseFloat(a.target.value)||0),className:"admin-form-input",step:"0.1"})})]})})})]})]})]})}export{me as default}; diff --git a/dist/assets/Forbidden-D25jV3Oq.js b/dist/assets/Forbidden-D25jV3Oq.js new file mode 100644 index 0000000..bcb5e76 --- /dev/null +++ b/dist/assets/Forbidden-D25jV3Oq.js @@ -0,0 +1 @@ +import{j as e,m as i}from"./vendor-animation-0s3FMHwK.js";import{L as t}from"./vendor-react-BVs3cwbi.js";function o(){return e.jsxs(i.div,{className:"forbidden-page",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4},children:[e.jsx("div",{className:"forbidden-icon",children:e.jsxs("svg",{width:"80",height:"80",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("rect",{x:"3",y:"11",width:"18",height:"11",rx:"2",ry:"2"}),e.jsx("path",{d:"M7 11V7a5 5 0 0 1 10 0v4"}),e.jsx("circle",{cx:"12",cy:"16",r:"1"})]})}),e.jsx("h1",{className:"forbidden-title",children:"Přístup odepřen"}),e.jsx("p",{className:"forbidden-text",children:"Nemáte oprávnění pro zobrazení této stránky. Kontaktujte administrátora pro přidělení přístupu."}),e.jsx(t,{to:"/",className:"forbidden-link",children:"Zpět na přehled"})]})}export{o as F}; diff --git a/dist/assets/InvoiceCreate-D7azSaER.js b/dist/assets/InvoiceCreate-D7azSaER.js new file mode 100644 index 0000000..77b815a --- /dev/null +++ b/dist/assets/InvoiceCreate-D7azSaER.js @@ -0,0 +1,2 @@ +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/RichEditor-Bfur5pi6.js","assets/vendor-animation-0s3FMHwK.js","assets/vendor-react-BVs3cwbi.js","assets/RichEditor-7oN3-GhD.css"])))=>i.map(i=>d[i]); +import{a as _e,u as fe,F as y,A as G,g as P,c as w,_ as xe}from"./index-BBlIrj2z.js";import{j as e,m as O}from"./vendor-animation-0s3FMHwK.js";import{g as be,i as je,r,L as ve}from"./vendor-react-BVs3cwbi.js";import{F as ke}from"./Forbidden-D25jV3Oq.js";import{u as ge,a as L,b as Ne,D as we,r as Se,c as Ce,S as De,v as Ae,d as Ie,e as Pe,K as Oe,T as Ee,P as ze}from"./useSortableList-CgbuKaxB.js";import"./vendor-utils-Dyr8OjFr.js";const Te=r.lazy(()=>xe(()=>import("./RichEditor-Bfur5pi6.js"),__vite__mapDeps([0,1,2,3]))),S="/api/admin",Re=[{value:21,label:"21%"},{value:12,label:"12%"},{value:0,label:"0%"}];let Z=0;const Q=()=>({_key:`inv-${++Z}`,description:"",quantity:1,unit:"ks",unit_price:0,vat_rate:21});function Me(){const X=be(),[ee]=je(),C=_e(),{hasPermission:te,user:ae}=fe(),E=ee.get("fromOrder"),_=E&&/^\d+$/.test(E)?E:null,[i,d]=r.useState({customer_id:null,customer_name:"",order_id:_?Number(_):null,issue_date:new Date().toISOString().split("T")[0],due_date:new Date(Date.now()+14*864e5).toISOString().split("T")[0],tax_date:new Date().toISOString().split("T")[0],currency:"CZK",apply_vat:1,vat_rate:21,payment_method:"Příkazem",constant_symbol:"0308",issued_by:ae?.fullName||"",notes:"",bank_account_id:"",bank_name:"",bank_swift:"",bank_iban:"",bank_account:""}),[q,se]=r.useState([]),[j,B]=r.useState(14),[u,f]=r.useState([Q()]),[x,v]=r.useState({}),[H,M]=r.useState(!1),[ne,ie]=r.useState(!0),[D,K]=r.useState(""),[z,re]=r.useState([]),[k,V]=r.useState(""),[T,A]=r.useState(!1),g="boha_invoice_draft",I=!_,[R,F]=r.useState(null),W=r.useRef({form:i,items:u,dueDays:14}),oe=r.useRef(!1);r.useEffect(()=>{if(I)try{const t=localStorage.getItem(g);if(!t)return;const a=JSON.parse(t);if(!a||typeof a!="object"||!a.form||!Array.isArray(a.items)){localStorage.removeItem(g);return}const{form:n,items:o,savedAt:c}=a;d(s=>({...s,customer_id:n.customer_id??s.customer_id,customer_name:n.customer_name??s.customer_name,currency:n.currency??s.currency,apply_vat:n.apply_vat??s.apply_vat,payment_method:n.payment_method??s.payment_method,constant_symbol:n.constant_symbol??s.constant_symbol,issued_by:s.issued_by,notes:n.notes??s.notes,issue_date:n.issue_date??s.issue_date,due_date:n.due_date??s.due_date,tax_date:n.tax_date??s.tax_date,bank_account_id:n.bank_account_id??s.bank_account_id,bank_name:n.bank_name??s.bank_name,bank_swift:n.bank_swift??s.bank_swift,bank_iban:n.bank_iban??s.bank_iban,bank_account:n.bank_account??s.bank_account})),o.length>0&&f(o.map(s=>({...s,_key:s._key||`inv-${++Z}`}))),a.due_days&&B(Number(a.due_days)),oe.current=!0,c&&F(new Date(c))}catch{try{localStorage.removeItem(g)}catch{}}},[I]),r.useEffect(()=>{W.current={form:i,items:u,dueDays:j}},[i,u,j]),r.useEffect(()=>{if(!I)return;const t=setTimeout(()=>{try{const{form:a,items:n,dueDays:o}=W.current,{bank_name:c,bank_swift:s,bank_iban:l,bank_account:h,...p}=a,m=new Date().toISOString();localStorage.setItem(g,JSON.stringify({form:p,items:n,due_days:o,savedAt:m})),F(new Date(m))}catch{}},500);return()=>clearTimeout(t)},[i,u,I]);const ce=r.useCallback(()=>{try{localStorage.removeItem(g)}catch{}F(null)},[]),U=r.useMemo(()=>R?R.toLocaleTimeString("cs-CZ",{hour:"2-digit",minute:"2-digit"}):null,[R]);r.useEffect(()=>{(async()=>{try{const a=[w(`${S}/invoices.php?action=next_number`),w(`${S}/customers.php`),w(`${S}/bank-accounts.php`)];_&&a.push(w(`${S}/invoices.php?action=order_data&id=${_}`));const n=await Promise.all(a),o=n[0];if(o.ok){const l=await o.json();l.success&&K(l.data.number)}const c=n[1];if(c.ok){const l=await c.json();l.success&&re(l.data.customers)}const s=n[2];if(s.ok){const l=await s.json();if(l.success&&Array.isArray(l.data)){se(l.data);const h=l.data.find(p=>p.is_default);d(p=>{const m=p.bank_account_id,b=(m?l.data.find(ye=>String(ye.id)===String(m)):null)||h;return b?{...p,bank_account_id:b.id,bank_name:b.bank_name||"",bank_swift:b.bic||"",bank_iban:b.iban||"",bank_account:b.account_number||""}:p})}}if(_&&n[3]?.ok){const l=await n[3].json();if(l.success){const h=l.data,p=Number(h.vat_rate)||21;d(m=>({...m,customer_id:h.customer_id,customer_name:h.customer_name||"",order_id:h.id,currency:h.currency||"CZK",apply_vat:Number(h.apply_vat)||0,vat_rate:p})),h.items?.length>0&&f(h.items.map(m=>({_key:`inv-${++Z}`,description:m.description||"",quantity:Number(m.quantity)||1,unit:m.unit||"",unit_price:Number(m.unit_price)||0,vat_rate:p})))}}}catch{C.error("Chyba při načítání dat")}finally{ie(!1)}})()},[_]),r.useEffect(()=>{if(!i.issue_date)return;const t=new Date(i.issue_date);t.setDate(t.getDate()+j),d(a=>({...a,due_date:t.toISOString().split("T")[0]}))},[i.issue_date,j]);const J=r.useMemo(()=>{if(!k)return z;const t=k.toLowerCase();return z.filter(a=>(a.name||"").toLowerCase().includes(t)||(a.company_id||"").includes(k)||(a.city||"").toLowerCase().includes(t))},[z,k]);r.useEffect(()=>{const t=()=>A(!1);if(T)return document.addEventListener("click",t),()=>document.removeEventListener("click",t)},[T]);const le=t=>{const a=q.find(n=>n.id===Number(t));d(a?n=>({...n,bank_account_id:a.id,bank_name:a.bank_name||"",bank_swift:a.bic||"",bank_iban:a.iban||"",bank_account:a.account_number||""}):n=>({...n,bank_account_id:"",bank_name:"",bank_swift:"",bank_iban:"",bank_account:""}))},de=t=>{d(a=>({...a,customer_id:t.id,customer_name:t.name})),v(a=>({...a,customer_id:void 0})),V(""),A(!1)},N=(t,a,n)=>{f(o=>o.map((c,s)=>s===t?{...c,[a]:n}:c))},ue=()=>f(t=>[...t,Q()]),me=t=>{u.length<=1||f(a=>a.filter((n,o)=>o!==t))},he=ge(L(ze,{activationConstraint:{distance:5}}),L(Ee,{activationConstraint:{delay:200,tolerance:5}}),L(Oe)),{handleDragEnd:pe}=Ne(f,"_key"),$=r.useMemo(()=>{let t=0;const a={};u.forEach(o=>{const c=(Number(o.quantity)||0)*(Number(o.unit_price)||0);if(t+=c,i.apply_vat){const s=Number(o.vat_rate)||0;a[s]||(a[s]=0),a[s]+=c*s/100}});const n=Object.values(a).reduce((o,c)=>o+c,0);return{subtotal:t,vatByRate:a,totalVat:n,total:t+n}},[u,i.apply_vat]),Y=async t=>{t.preventDefault();const a={};if(i.customer_id||(a.customer_id="Vyberte zákazníka"),i.issue_date||(a.issue_date="Zadejte datum"),i.tax_date||(a.tax_date="Zadejte datum"),i.bank_account_id||(a.bank_account_id="Vyberte bankovní účet"),(u.length===0||u.every(n=>!n.description.trim()))&&(a.items="Přidejte alespoň jednu položku"),v(a),!(Object.keys(a).length>0)){M(!0);try{const o=await(await w(`${S}/invoices.php`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({...i,invoice_number:D,items:u.filter(c=>c.description.trim()).map((c,s)=>({...c,position:s}))})})).json();o.success?(ce(),C.success(o.message||"Faktura byla vytvořena"),X(`/invoices/${o.data.invoice_id}`)):C.error(o.error||"Nepodařilo se vytvořit fakturu")}catch{C.error("Chyba připojení")}finally{M(!1)}}};return te("invoices.create")?ne?e.jsxs("div",{className:"admin-skeleton",style:{padding:0,gap:"1.5rem"},children:[e.jsx("div",{className:"admin-skeleton-row",style:{justifyContent:"space-between"},children:e.jsx("div",{className:"admin-skeleton-line h-8",style:{width:"200px"}})}),e.jsx("div",{className:"admin-card",children:e.jsx("div",{className:"admin-skeleton",style:{gap:"1.25rem"},children:[0,1,2,3].map(t=>e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line w-1/4"}),e.jsx("div",{className:"admin-skeleton-line w-1/2"})]},t))})})]}):e.jsxs("div",{children:[e.jsxs(O.div,{className:"admin-page-header",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4},children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"1rem"},children:[e.jsx(ve,{to:"/invoices",className:"admin-btn-icon",title:"Zpět","aria-label":"Zpět",children:e.jsx("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:e.jsx("path",{d:"M19 12H5M12 19l-7-7 7-7"})})}),e.jsxs("div",{children:[e.jsxs("h1",{className:"admin-page-title",children:["Nová faktura ",D&&e.jsxs("span",{className:"text-tertiary",children:["(",D,")"]})]}),_?e.jsx("p",{className:"admin-page-subtitle",children:"Z objednávky"}):U&&e.jsxs("div",{className:"offers-draft-indicator",children:[e.jsx("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2.5",children:e.jsx("polyline",{points:"20 6 9 17 4 12"})}),"Koncept uložen ",U]})]})]}),e.jsx("div",{className:"admin-page-actions",children:e.jsx("button",{onClick:Y,className:"admin-btn admin-btn-primary",disabled:H,children:H?e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"admin-spinner",style:{width:16,height:16,borderWidth:2}}),"Ukládání..."]}):"Uložit"})})]}),e.jsxs("form",{onSubmit:Y,children:[e.jsxs(O.div,{className:"offers-editor-section",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.1},children:[e.jsx("h3",{className:"admin-card-title",children:"Základní údaje"}),e.jsxs("div",{className:"admin-form",children:[e.jsxs("div",{className:"offers-form-row-3",children:[e.jsx(y,{label:"Číslo faktury",children:e.jsx("input",{type:"text",value:D,onChange:t=>K(t.target.value),className:"admin-form-input"})}),e.jsx(y,{label:"Odběratel",error:x.customer_id,required:!0,children:i.customer_id?e.jsxs("div",{className:"offers-customer-selected",children:[e.jsx("span",{children:i.customer_name}),e.jsx("button",{type:"button",onClick:()=>d(t=>({...t,customer_id:null,customer_name:""})),className:"admin-btn-icon",title:"Odebrat zákazníka",children:e.jsxs("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),e.jsx("line",{x1:"6",y1:"6",x2:"18",y2:"18"})]})})]}):e.jsxs("div",{className:"offers-customer-select",onClick:t=>t.stopPropagation(),children:[e.jsx("input",{type:"text",value:k,onChange:t=>{V(t.target.value),A(!0)},onFocus:()=>A(!0),className:"admin-form-input",placeholder:"Hledat zákazníka (název, IČ, město)...",autoComplete:"off"}),T&&e.jsx("div",{className:"offers-customer-dropdown",children:J.length===0?e.jsx("div",{className:"offers-customer-dropdown-empty",children:"Žádní zákazníci"}):J.slice(0,10).map(t=>e.jsxs("div",{className:"offers-customer-dropdown-item",onMouseDown:()=>de(t),children:[e.jsx("div",{children:t.name}),(t.company_id||t.city)&&e.jsxs("div",{children:[t.company_id&&`IČ: ${t.company_id}`,t.city&&` · ${t.city}`]})]},t.id))})]})}),e.jsx(y,{label:"Vystavil",children:e.jsx("input",{type:"text",value:i.issued_by,className:"admin-form-input",readOnly:!0,style:{backgroundColor:"var(--bg-secondary)",cursor:"default"}})})]}),e.jsxs("div",{className:"admin-form-row",children:[e.jsx(y,{label:"Datum vystavení",error:x.issue_date,required:!0,children:e.jsx(G,{mode:"date",value:i.issue_date,onChange:t=>{d(a=>({...a,issue_date:t})),v(a=>({...a,issue_date:void 0}))}})}),e.jsxs(y,{label:"Splatnost (dny)",children:[e.jsx("select",{value:j,onChange:t=>B(Number(t.target.value)),className:"admin-form-select",children:Array.from({length:60},(t,a)=>a+1).map(t=>e.jsx("option",{value:t,children:t},t))}),i.due_date&&e.jsxs("span",{className:"text-tertiary",style:{fontSize:"0.75rem",marginTop:"0.25rem"},children:["Splatnost: ",new Date(i.due_date).toLocaleDateString("cs-CZ")]})]}),e.jsx(y,{label:"DÚZP",error:x.tax_date,required:!0,children:e.jsx(G,{mode:"date",value:i.tax_date,onChange:t=>{d(a=>({...a,tax_date:t})),v(a=>({...a,tax_date:void 0}))}})})]}),e.jsxs("div",{className:"offers-form-row-3",children:[e.jsx(y,{label:"Forma úhrady",children:e.jsxs("select",{value:i.payment_method,onChange:t=>d(a=>({...a,payment_method:t.target.value})),className:"admin-form-select",children:[e.jsx("option",{value:"Příkazem",children:"Příkazem"}),e.jsx("option",{value:"Hotově",children:"Hotově"}),e.jsx("option",{value:"Dobírka",children:"Dobírka"})]})}),e.jsx(y,{label:"Měna",children:e.jsxs("select",{value:i.currency,onChange:t=>d(a=>({...a,currency:t.target.value})),className:"admin-form-select",children:[e.jsx("option",{value:"CZK",children:"CZK (Kč)"}),e.jsx("option",{value:"EUR",children:"EUR (€)"}),e.jsx("option",{value:"USD",children:"USD ($)"})]})}),e.jsx(y,{label:"DPH",children:e.jsx("div",{style:{display:"flex",alignItems:"center",gap:"0.75rem"},children:e.jsxs("label",{className:"admin-form-checkbox",style:{whiteSpace:"nowrap"},children:[e.jsx("input",{type:"checkbox",checked:!!i.apply_vat,onChange:t=>d(a=>({...a,apply_vat:t.target.checked?1:0}))}),e.jsx("span",{children:"Uplatnit DPH"})]})})})]}),e.jsx(y,{label:"Bankovní účet",error:x.bank_account_id,required:!0,children:e.jsxs("select",{value:i.bank_account_id,onChange:t=>{le(t.target.value),v(a=>({...a,bank_account_id:void 0}))},className:"admin-form-select",children:[e.jsx("option",{value:"",children:"— Vyberte účet —"}),q.map(t=>e.jsxs("option",{value:t.id,children:[t.account_name,t.account_number?` (${t.account_number})`:"",t.is_default?" ★":""]},t.id))]})})]})]}),e.jsxs(O.div,{className:"offers-editor-section",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.2},children:[e.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:"1rem"},children:[e.jsxs("div",{children:[e.jsx("h3",{className:"admin-card-title",style:{margin:0},children:"Položky"}),x.items&&e.jsx("span",{className:"admin-form-error",children:x.items})]}),e.jsx("button",{type:"button",onClick:ue,className:"admin-btn admin-btn-primary admin-btn-sm",children:"+ Přidat položku"})]}),e.jsx("div",{className:"offers-items-table",children:e.jsx(we,{sensors:he,collisionDetection:Ce,modifiers:[Se],onDragEnd:pe,children:e.jsxs("table",{className:"admin-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{style:{width:"2rem"}}),e.jsx("th",{style:{width:"2rem",textAlign:"center"},children:"#"}),e.jsx("th",{children:"Popis"}),e.jsx("th",{style:{width:"5.5rem",textAlign:"center"},children:"Množství"}),e.jsx("th",{style:{width:"5.5rem",textAlign:"center"},children:"Jednotka"}),e.jsx("th",{style:{width:"5.5rem",textAlign:"center"},children:"Jedn. cena"}),i.apply_vat?e.jsx("th",{style:{width:"5rem",textAlign:"center"},children:"DPH"}):null,e.jsx("th",{style:{width:"8rem",textAlign:"right"},children:"Celkem"}),e.jsx("th",{style:{width:"2.5rem"}})]})}),e.jsx(De,{items:u.map(t=>String(t._key)),strategy:Ae,children:e.jsx("tbody",{children:u.map((t,a)=>{const n=(Number(t.quantity)||0)*(Number(t.unit_price)||0);return e.jsx(Ie,{id:String(t._key),children:({attributes:o,listeners:c})=>e.jsxs(e.Fragment,{children:[e.jsx("td",{style:{width:"2rem"},children:e.jsx(Pe,{listeners:c,attributes:o})}),e.jsx("td",{className:"text-tertiary",style:{textAlign:"center",fontWeight:500},children:a+1}),e.jsx("td",{children:e.jsx("input",{type:"text",value:t.description,onChange:s=>N(a,"description",s.target.value),className:"admin-form-input",placeholder:"Popis položky...",style:{fontWeight:500}})}),e.jsx("td",{children:e.jsx("input",{type:"number",value:t.quantity,onChange:s=>N(a,"quantity",s.target.value),className:"admin-form-input",min:"0",step:"any",style:{textAlign:"center",height:"2.25rem",padding:"0.375rem 0.5rem"}})}),e.jsx("td",{children:e.jsx("input",{type:"text",value:t.unit,onChange:s=>N(a,"unit",s.target.value),className:"admin-form-input",placeholder:"ks",style:{textAlign:"center",height:"2.25rem",padding:"0.375rem 0.5rem"}})}),e.jsx("td",{children:e.jsx("input",{type:"number",value:t.unit_price,onChange:s=>N(a,"unit_price",s.target.value),className:"admin-form-input",step:"any",style:{textAlign:"right",height:"2.25rem",padding:"0.375rem 0.5rem"}})}),i.apply_vat?e.jsx("td",{children:e.jsx("select",{value:t.vat_rate,onChange:s=>N(a,"vat_rate",Number(s.target.value)),className:"admin-form-input",style:{textAlign:"center",height:"2.25rem",padding:"0.375rem 0.5rem"},children:Re.map(s=>e.jsx("option",{value:s.value,children:s.label},s.value))})}):null,e.jsx("td",{style:{textAlign:"right",fontWeight:600,whiteSpace:"nowrap"},children:P(n,i.currency)}),e.jsx("td",{children:u.length>1&&e.jsx("button",{type:"button",onClick:()=>me(a),className:"admin-btn-icon danger",title:"Odebrat","aria-label":"Odebrat",children:e.jsxs("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),e.jsx("line",{x1:"6",y1:"6",x2:"18",y2:"18"})]})})})]})},t._key)})})})]})})}),e.jsxs("div",{className:"offers-totals-summary",children:[e.jsxs("div",{className:"offers-totals-row",children:[e.jsx("span",{children:"Mezisoučet:"}),e.jsx("span",{children:P($.subtotal,i.currency)})]}),i.apply_vat&&Object.entries($.vatByRate).map(([t,a])=>e.jsxs("div",{className:"offers-totals-row",children:[e.jsxs("span",{children:["DPH ",t,"%:"]}),e.jsx("span",{children:P(a,i.currency)})]},t)),e.jsxs("div",{className:"offers-totals-row offers-totals-total",children:[e.jsx("span",{children:"Celkem k úhradě:"}),e.jsx("span",{children:P($.total,i.currency)})]})]})]}),e.jsxs(O.div,{className:"offers-editor-section",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.25},children:[e.jsx("h3",{className:"admin-card-title",children:"Veřejné poznámky na faktuře"}),e.jsx(r.Suspense,{fallback:e.jsx("div",{className:"admin-form-input",style:{minHeight:120}}),children:e.jsx(Te,{value:i.notes,onChange:t=>d(a=>({...a,notes:t})),placeholder:"Poznámky zobrazené na faktuře...",minHeight:"120px"})})]})]})]}):e.jsx(ke,{})}export{Me as default}; diff --git a/dist/assets/InvoiceDetail-CxmXBolF.js b/dist/assets/InvoiceDetail-CxmXBolF.js new file mode 100644 index 0000000..d734bca --- /dev/null +++ b/dist/assets/InvoiceDetail-CxmXBolF.js @@ -0,0 +1,2 @@ +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/RichEditor-Bfur5pi6.js","assets/vendor-animation-0s3FMHwK.js","assets/vendor-react-BVs3cwbi.js","assets/RichEditor-7oN3-GhD.css"])))=>i.map(i=>d[i]); +import{a as re,u as le,F as d,e as w,g as f,C as R,c as x,_ as de}from"./index-BBlIrj2z.js";import{j as e,m as y,A as oe}from"./vendor-animation-0s3FMHwK.js";import{h as ce,g as me,r as i,L as V}from"./vendor-react-BVs3cwbi.js";import{F as he}from"./Forbidden-D25jV3Oq.js";import{a9 as ue}from"./vendor-utils-Dyr8OjFr.js";const pe=i.lazy(()=>de(()=>import("./RichEditor-Bfur5pi6.js"),__vite__mapDeps([0,1,2,3]))),j="/api/admin",U={issued:"Vystavena",paid:"Zaplacena",overdue:"Po splatnosti"},xe={issued:"admin-badge-invoice-issued",paid:"admin-badge-invoice-paid",overdue:"admin-badge-invoice-overdue"},Z={paid:"Zaplaceno"},ye={paid:"admin-btn admin-btn-primary"},je=[{value:21,label:"21%"},{value:12,label:"12%"},{value:0,label:"0%"}];function ke(){const{id:c}=ce(),r=re(),{hasPermission:h}=le(),_=me(),[q,H]=i.useState(!0),[a,J]=i.useState(null),[o,E]=i.useState(""),[g,b]=i.useState(!1),[$,D]=i.useState(null),[u,C]=i.useState({show:!1,status:null}),[F,O]=i.useState(!1),[K,S]=i.useState(!1),[G,A]=i.useState(!1),[Q,L]=i.useState(!1),[M,P]=i.useState(!1),[N,k]=i.useState([]),W=i.useRef(0),T=async()=>{try{const t=await x(`${j}/invoices.php?action=detail&id=${c}`);if(t.status===401)return;const s=await t.json();s.success?(J(s.data),E(s.data.notes||"")):(r.error(s.error||"Nepodařilo se načíst fakturu"),_("/invoices"))}catch{r.error("Chyba připojení"),_("/invoices")}finally{H(!1)}};i.useEffect(()=>{T()},[c]);const z=i.useMemo(()=>{if(!a?.items)return{subtotal:0,vatByRate:{},totalVat:0,total:0};let t=0;const s={};a.items.forEach(l=>{const m=(Number(l.quantity)||0)*(Number(l.unit_price)||0);if(t+=m,Number(a.apply_vat)){const p=Number(l.vat_rate)||0;s[p]||(s[p]=0),s[p]+=m*p/100}});const n=Object.values(s).reduce((l,m)=>l+m,0);return{subtotal:t,vatByRate:s,totalVat:n,total:t+n}},[a]);if(!h("invoices.view"))return e.jsx(he,{});const X=async()=>{if(u.status){D(u.status),C({show:!1,status:null});try{const s=await(await x(`${j}/invoices.php?id=${c}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({status:u.status})})).json();s.success?(r.success(s.message||"Stav byl změněn"),T()):r.error(s.error||"Nepodařilo se změnit stav")}catch{r.error("Chyba připojení")}finally{D(null)}}},Y=async()=>{b(!0);try{const s=await(await x(`${j}/invoices.php?id=${c}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({notes:o})})).json();s.success?r.success("Poznámky byly uloženy"):r.error(s.error||"Nepodařilo se uložit poznámky")}catch{r.error("Chyba připojení")}finally{b(!1)}},B=async(t="cs")=>{S(!1);const s=window.open("","_blank");O(!0);try{const n=await x(`${j}/invoices-pdf.php?id=${c}&lang=${encodeURIComponent(t)}`);if(!n.ok){s.close(),r.error("Nepodařilo se vygenerovat PDF");return}const l=await n.text();s.document.open(),s.document.write(l),s.document.close(),s.onload=()=>s.print()}catch{s.close(),r.error("Chyba připojení")}finally{O(!1)}},ee=()=>{k(a.items.map(t=>({_key:`ei-${++W.current}`,description:t.description||"",quantity:Number(t.quantity)||1,unit:t.unit||"",unit_price:Number(t.unit_price)||0,vat_rate:Number(t.vat_rate)||21}))),P(!0)},v=(t,s,n)=>{k(l=>l.map((m,p)=>p===t?{...m,[s]:n}:m))},te=()=>{k(t=>[...t,{_key:`ei-${++W.current}`,description:"",quantity:1,unit:"ks",unit_price:0,vat_rate:21}])},se=t=>{N.length<=1||k(s=>s.filter((n,l)=>l!==t))},ae=async()=>{b(!0);try{const s=await(await x(`${j}/invoices.php?id=${c}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({items:N.filter(n=>n.description.trim()).map((n,l)=>({...n,position:l}))})})).json();s.success?(r.success("Položky byly uloženy"),P(!1),T()):r.error(s.error||"Nepodařilo se uložit položky")}catch{r.error("Chyba připojení")}finally{b(!1)}},ne=async()=>{L(!0);try{const s=await(await x(`${j}/invoices.php?id=${c}`,{method:"DELETE"})).json();s.success?(r.success(s.message||"Faktura byla smazána"),_("/invoices")):r.error(s.error||"Nepodařilo se smazat fakturu")}catch{r.error("Chyba připojení")}finally{L(!1),A(!1)}};if(q)return e.jsxs("div",{className:"admin-skeleton",style:{padding:0,gap:"1.5rem"},children:[e.jsxs("div",{className:"admin-skeleton-row",style:{justifyContent:"space-between"},children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"0.75rem"},children:[e.jsx("div",{className:"admin-skeleton-line",style:{width:"32px",height:"32px",borderRadius:"8px"}}),e.jsx("div",{className:"admin-skeleton-line h-8",style:{width:"200px"}})]}),e.jsxs("div",{className:"admin-skeleton-row",style:{gap:"0.5rem"},children:[e.jsx("div",{className:"admin-skeleton-line h-10",style:{width:"100px",borderRadius:"8px"}}),e.jsx("div",{className:"admin-skeleton-line h-10",style:{width:"100px",borderRadius:"8px"}})]})]}),e.jsx("div",{className:"admin-card",children:e.jsx("div",{className:"admin-skeleton",style:{gap:"1.25rem"},children:[0,1,2,3].map(t=>e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line w-1/4"}),e.jsx("div",{className:"admin-skeleton-line w-1/2"})]},t))})}),e.jsx("div",{className:"admin-card",children:e.jsx("div",{className:"admin-skeleton",style:{gap:"1.25rem"},children:[0,1,2].map(t=>e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{style:{flex:1},children:e.jsx("div",{className:"admin-skeleton-line w-full"})}),e.jsx("div",{style:{flex:1},children:e.jsx("div",{className:"admin-skeleton-line w-3/4"})}),e.jsx("div",{style:{flex:1},children:e.jsx("div",{className:"admin-skeleton-line w-1/2"})})]},t))})})]});if(!a)return null;const ie=a.status==="issued",I=a.status==="paid";return e.jsxs("div",{children:[e.jsxs(y.div,{className:"admin-page-header",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4},children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"1rem"},children:[e.jsx(V,{to:"/invoices",className:"admin-btn-icon",title:"Zpět","aria-label":"Zpět",children:e.jsx("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:e.jsx("path",{d:"M19 12H5M12 19l-7-7 7-7"})})}),e.jsx("div",{children:e.jsxs("h1",{className:"admin-page-title",style:{display:"flex",alignItems:"center",gap:"0.75rem"},children:["Faktura ",a.invoice_number,e.jsx("span",{className:`admin-badge ${xe[a.status]||""}`,children:U[a.status]||a.status})]})})]}),e.jsxs("div",{className:"admin-page-actions",children:[h("invoices.export")&&e.jsx("button",{onClick:()=>S(!0),className:"admin-btn admin-btn-secondary",disabled:F,children:F?e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"admin-spinner",style:{width:16,height:16,borderWidth:2}}),"PDF..."]}):e.jsxs(e.Fragment,{children:[e.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"}),e.jsx("polyline",{points:"14 2 14 8 20 8"})]}),"PDF"]})}),h("invoices.edit")&&a.valid_transitions?.length>0&&a.valid_transitions.map(t=>e.jsx("button",{onClick:()=>C({show:!0,status:t}),className:ye[t]||"admin-btn admin-btn-secondary",disabled:$===t,children:$===t?e.jsx("div",{className:"admin-spinner",style:{width:14,height:14,borderWidth:2}}):Z[t]||t},t)),h("invoices.delete")&&e.jsx("button",{onClick:()=>A(!0),className:"admin-btn admin-btn-primary",children:"Smazat"})]})]}),e.jsxs(y.div,{className:"offers-editor-section",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.1},children:[e.jsx("h3",{className:"admin-card-title",children:"Informace"}),e.jsxs("div",{className:"admin-form",children:[e.jsxs("div",{className:"offers-form-row-3",style:{marginBottom:"0.5rem"},children:[e.jsxs(d,{label:"Zákazník",children:[e.jsx("div",{style:{fontWeight:500},children:a.customer_name||"—"}),a.customer&&e.jsxs("div",{className:"text-tertiary",style:{fontSize:"0.8rem",marginTop:"0.2rem"},children:[a.customer.company_id&&`IČ: ${a.customer.company_id}`,a.customer.vat_id&&` · DIČ: ${a.customer.vat_id}`]})]}),e.jsx(d,{label:"Objednávka",children:e.jsx("div",{children:a.order_id?e.jsx(V,{to:`/orders/${a.order_id}`,className:"link-accent",children:a.order_number}):"—"})}),e.jsx(d,{label:"Měna",children:e.jsx("div",{children:a.currency})})]}),e.jsxs("div",{className:"offers-form-row-3",style:{marginBottom:"0.5rem"},children:[e.jsx(d,{label:"Datum vystavení",children:e.jsx("div",{children:w(a.issue_date)})}),e.jsx(d,{label:"Datum splatnosti",children:e.jsx("div",{className:a.status==="overdue"?"text-danger fw-600":"",children:w(a.due_date)})}),e.jsx(d,{label:"DÚZP",children:e.jsx("div",{children:w(a.tax_date)})})]}),e.jsxs("div",{className:"offers-form-row-3",children:[e.jsx(d,{label:"Forma úhrady",children:e.jsx("div",{children:a.payment_method})}),e.jsx(d,{label:"Variabilní symbol",children:e.jsx("div",{children:a.invoice_number})}),e.jsx(d,{label:"Vystavil",children:e.jsx("div",{children:a.issued_by||"—"})})]}),a.paid_date&&e.jsx("div",{className:"admin-form-row",style:{marginTop:"0.5rem"},children:e.jsx(d,{label:"Datum úhrady",children:e.jsx("div",{style:{color:"var(--success)",fontWeight:500},children:w(a.paid_date)})})})]})]}),e.jsxs(y.div,{className:"offers-editor-section",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.2},children:[e.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:"1rem"},children:[e.jsx("h3",{className:"admin-card-title",style:{margin:0},children:"Položky"}),ie&&h("invoices.edit")&&(M?e.jsxs("div",{style:{display:"flex",gap:"0.5rem"},children:[e.jsx("button",{type:"button",onClick:te,className:"admin-btn admin-btn-secondary admin-btn-sm",children:"+ Přidat položku"}),e.jsx("button",{onClick:ae,className:"admin-btn admin-btn-primary admin-btn-sm",disabled:g,children:g?"Ukládání...":"Uložit položky"}),e.jsx("button",{onClick:()=>P(!1),className:"admin-btn admin-btn-secondary admin-btn-sm",children:"Zrušit"})]}):e.jsx("button",{onClick:ee,className:"admin-btn admin-btn-secondary admin-btn-sm",children:"Upravit položky"}))]}),M?e.jsx(e.Fragment,{children:e.jsx("div",{className:"offers-items-table",children:e.jsxs("table",{className:"admin-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{style:{width:"2.5rem",textAlign:"center"},children:"#"}),e.jsx("th",{children:"Popis"}),e.jsx("th",{style:{width:"5.5rem",textAlign:"center"},children:"Množství"}),e.jsx("th",{style:{width:"5.5rem",textAlign:"center"},children:"Jednotka"}),e.jsx("th",{style:{width:"5.5rem",textAlign:"center"},children:"Jedn. cena"}),e.jsx("th",{style:{width:"5rem",textAlign:"center"},children:"%DPH"}),e.jsx("th",{style:{width:"5.5rem",textAlign:"center"}})]})}),e.jsx("tbody",{children:N.map((t,s)=>e.jsxs("tr",{children:[e.jsx("td",{className:"text-tertiary",style:{textAlign:"center",fontWeight:500},children:s+1}),e.jsx("td",{children:e.jsx("input",{type:"text",value:t.description,onChange:n=>v(s,"description",n.target.value),className:"admin-form-input",placeholder:"Popis položky...",style:{fontWeight:500}})}),e.jsx("td",{children:e.jsx("input",{type:"number",value:t.quantity,onChange:n=>v(s,"quantity",n.target.value),className:"admin-form-input",min:"0",step:"any",style:{textAlign:"center",height:"2.25rem",padding:"0.375rem 0.5rem"}})}),e.jsx("td",{children:e.jsx("input",{type:"text",value:t.unit,onChange:n=>v(s,"unit",n.target.value),className:"admin-form-input",style:{textAlign:"center",height:"2.25rem",padding:"0.375rem 0.5rem"}})}),e.jsx("td",{children:e.jsx("input",{type:"number",value:t.unit_price,onChange:n=>v(s,"unit_price",n.target.value),className:"admin-form-input",step:"any",style:{textAlign:"right",height:"2.25rem",padding:"0.375rem 0.5rem"}})}),e.jsx("td",{children:Number(a.apply_vat)?e.jsx("select",{value:t.vat_rate,onChange:n=>v(s,"vat_rate",Number(n.target.value)),className:"admin-form-input",style:{textAlign:"center",height:"2.25rem",padding:"0.375rem 0.5rem"},children:je.map(n=>e.jsx("option",{value:n.value,children:n.label},n.value))}):e.jsx("span",{className:"text-tertiary",style:{display:"block",textAlign:"center"},children:"0%"})}),e.jsx("td",{children:e.jsx("div",{style:{display:"flex",gap:"0.125rem",justifyContent:"center"},children:N.length>1&&e.jsx("button",{type:"button",onClick:()=>se(s),className:"admin-btn-icon danger",title:"Odebrat","aria-label":"Odebrat",children:e.jsxs("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),e.jsx("line",{x1:"6",y1:"6",x2:"18",y2:"18"})]})})})})]},t._key))})]})})}):e.jsx(e.Fragment,{children:a.items?.length>0?e.jsx("div",{className:"offers-items-table",children:e.jsxs("table",{className:"admin-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{style:{width:"2.5rem",textAlign:"center"},children:"#"}),e.jsx("th",{children:"Popis"}),e.jsx("th",{style:{width:"5.5rem",textAlign:"center"},children:"Množství"}),e.jsx("th",{style:{width:"5rem",textAlign:"center"},children:"Jednotka"}),e.jsx("th",{style:{width:"8rem",textAlign:"right"},children:"Jedn. cena"}),e.jsx("th",{style:{width:"4rem",textAlign:"center"},children:"%DPH"}),e.jsx("th",{style:{width:"9rem",textAlign:"right"},children:"Celkem"})]})}),e.jsx("tbody",{children:a.items.map((t,s)=>{const n=(Number(t.quantity)||0)*(Number(t.unit_price)||0),l=Number(a.apply_vat)?n*(Number(t.vat_rate)||0)/100:0;return e.jsxs("tr",{children:[e.jsx("td",{className:"text-tertiary",style:{textAlign:"center",fontWeight:500},children:s+1}),e.jsx("td",{style:{fontWeight:500},children:t.description||"—"}),e.jsxs("td",{style:{textAlign:"center"},children:[t.quantity," ",t.unit&&e.jsx("span",{className:"text-tertiary",children:t.unit})]}),e.jsx("td",{style:{textAlign:"center"},children:t.unit||"—"}),e.jsx("td",{className:"admin-mono",style:{textAlign:"right"},children:f(t.unit_price,a.currency)}),e.jsxs("td",{style:{textAlign:"center"},children:[Number(a.apply_vat)?Number(t.vat_rate):0,"%"]}),e.jsx("td",{className:"admin-mono",style:{textAlign:"right",fontWeight:600},children:f(n+l,a.currency)})]},t.id||s)})})]})}):e.jsx("p",{className:"text-tertiary",children:"Žádné položky."})}),e.jsxs("div",{className:"offers-totals-summary",children:[e.jsxs("div",{className:"offers-totals-row",children:[e.jsx("span",{children:"Mezisoučet:"}),e.jsx("span",{children:f(z.subtotal,a.currency)})]}),Number(a.apply_vat)>0&&Object.entries(z.vatByRate).map(([t,s])=>e.jsxs("div",{className:"offers-totals-row",children:[e.jsxs("span",{children:["DPH ",t,"%:"]}),e.jsx("span",{children:f(s,a.currency)})]},t)),e.jsxs("div",{className:"offers-totals-row offers-totals-total",children:[e.jsx("span",{children:"Celkem k úhradě:"}),e.jsx("span",{children:f(z.total,a.currency)})]})]})]}),e.jsxs(y.div,{className:"offers-editor-section",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.3},children:[e.jsx("h3",{className:"admin-card-title",children:"Veřejné poznámky na faktuře"}),I&&o&&o.trim()&&o!=="


          "&&e.jsx("div",{className:"ql-editor",style:{padding:0,minHeight:"auto"},dangerouslySetInnerHTML:{__html:ue.sanitize(o)}}),I&&(!o||!o.trim()||o==="


          ")&&e.jsx("p",{className:"text-tertiary",children:"Žádné poznámky."}),!I&&e.jsxs(e.Fragment,{children:[e.jsx(i.Suspense,{fallback:e.jsx("div",{className:"admin-form-input",style:{minHeight:120}}),children:e.jsx(pe,{value:o,onChange:t=>E(t),placeholder:"Poznámky zobrazené na faktuře...",minHeight:"120px"})}),h("invoices.edit")&&e.jsx("div",{style:{marginTop:"0.5rem"},children:e.jsx("button",{onClick:Y,className:"admin-btn admin-btn-secondary admin-btn-sm",disabled:g,children:g?"Ukládání...":"Uložit poznámky"})})]})]}),e.jsx(R,{isOpen:u.show,onClose:()=>C({show:!1,status:null}),onConfirm:X,title:"Změnit stav faktury",message:`Opravdu chcete změnit stav faktury "${a.invoice_number}" na "${U[u.status]}"?`,confirmText:Z[u.status]||"Potvrdit",cancelText:"Zrušit",type:"default"}),e.jsx(R,{isOpen:G,onClose:()=>A(!1),onConfirm:ne,title:"Smazat fakturu",message:`Opravdu chcete smazat fakturu "${a.invoice_number}"? Tato akce je nevratná.`,confirmText:"Smazat",cancelText:"Zrušit",type:"danger",loading:Q}),e.jsx(oe,{children:K&&e.jsxs(y.div,{className:"admin-modal-overlay",initial:{opacity:0},animate:{opacity:1},exit:{opacity:0},transition:{duration:.2},children:[e.jsx("div",{className:"admin-modal-backdrop",onClick:()=>S(!1)}),e.jsxs(y.div,{className:"admin-modal admin-confirm-modal",role:"dialog","aria-modal":"true",initial:{opacity:0,scale:.95,y:20},animate:{opacity:1,scale:1,y:0},exit:{opacity:0,scale:.95,y:20},transition:{duration:.2},children:[e.jsxs("div",{className:"admin-modal-body admin-confirm-content",children:[e.jsx("div",{className:"admin-confirm-icon admin-confirm-icon-info",children:e.jsxs("svg",{width:"24",height:"24",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"}),e.jsx("path",{d:"M2 12h20"}),e.jsx("path",{d:"M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"})]})}),e.jsx("h2",{className:"admin-confirm-title",children:"Jazyk faktury"}),e.jsx("p",{className:"admin-confirm-message",children:"V jakém jazyce chcete vygenerovat fakturu?"})]}),e.jsxs("div",{className:"admin-modal-footer",children:[e.jsx("button",{type:"button",onClick:()=>B("cs"),className:"admin-btn admin-btn-primary",children:"Čeština"}),e.jsx("button",{type:"button",onClick:()=>B("en"),className:"admin-btn admin-btn-primary",children:"English"})]})]})]})})]})}export{ke as default}; diff --git a/dist/assets/Invoices-BxKVmNYN.js b/dist/assets/Invoices-BxKVmNYN.js new file mode 100644 index 0000000..16ddd7b --- /dev/null +++ b/dist/assets/Invoices-BxKVmNYN.js @@ -0,0 +1,2 @@ +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/ReceivedInvoices-Cbz7NucU.js","assets/vendor-animation-0s3FMHwK.js","assets/vendor-react-BVs3cwbi.js","assets/index-BBlIrj2z.js","assets/vendor-utils-Dyr8OjFr.js","assets/index-BazDZfA0.css","assets/useListData-BVkTFDdr.js"])))=>i.map(i=>d[i]); +import{a as Ne,u as be,c as C,d as Q,e as _,g as O,C as we,_ as Se}from"./index-BBlIrj2z.js";import{j as e,m,A as X}from"./vendor-animation-0s3FMHwK.js";import{r as a,L as z}from"./vendor-react-BVs3cwbi.js";import{F as Ce}from"./Forbidden-D25jV3Oq.js";import{u as _e,a as ze,S as B}from"./useListData-BVkTFDdr.js";import{P as Be}from"./Pagination-B1sbY6V7.js";import"./vendor-utils-Dyr8OjFr.js";const Pe=a.lazy(()=>Se(()=>import("./ReceivedInvoices-Cbz7NucU.js"),__vite__mapDeps([0,1,2,3,4,5,6]))),P="/api/admin",ee="boha_invoice_draft",V=["leden","únor","březen","duben","květen","červen","červenec","srpen","září","říjen","listopad","prosinec"];function se(t){return!t||t.length===0?"0 Kč":t.map(o=>O(o.amount,o.currency)).join(" · ")}function D(t,o){return!t||t.length===0?{value:"0 Kč",detail:null}:t.some(v=>v.currency!=="CZK")&&o!==null&&o!==void 0?{value:O(o,"CZK"),detail:se(t)}:{value:se(t),detail:null}}const ae={issued:"Vystavena",paid:"Zaplacena",overdue:"Po splatnosti"},te={issued:"admin-badge-invoice-issued",paid:"admin-badge-invoice-paid",overdue:"admin-badge-invoice-overdue"},De=[{value:"",label:"Vše"},{value:"issued",label:"Vystavené"},{value:"paid",label:"Zaplacené"},{value:"overdue",label:"Po splatnosti"}];function Ee(){const t=Ne(),{hasPermission:o}=be(),[h,v]=a.useState("issued"),[ie,I]=a.useState(!1),{sort:ne,order:u,handleSort:j,activeSort:y}=_e("invoice_number"),[M,oe]=a.useState(""),[le,$]=a.useState(1),[x,re]=a.useState(""),f=new Date,[d,g]=a.useState(f.getMonth()+1),[p,R]=a.useState(f.getFullYear()),[n,de]=a.useState(null),[ce,H]=a.useState(!0),K=a.useRef(!1),k=a.useRef(0),[me,he]=a.useState(0),U=d===f.getMonth()+1&&p===f.getFullYear(),ue=`${V[d-1]} ${p}`,N=a.useCallback(async()=>{H(!0);try{const i=await(await C(`${P}/invoices.php?action=stats&month=${d}&year=${p}`)).json();i.success&&(de(i.data),K.current=!0,he(l=>l+1))}catch{}finally{H(!1)}},[d,p]);a.useEffect(()=>{N()},[N]);const xe=()=>{k.current=-1,d===1?(g(12),R(s=>s-1)):g(s=>s-1)},pe=()=>{U||(k.current=1,d===12?(g(1),R(s=>s+1)):g(s=>s+1))},[b,A]=a.useState({show:!1,invoice:null}),[ve,Z]=a.useState(!1),[L,Y]=a.useState(null),[F,T]=a.useState(null),[c,J]=a.useState(null);a.useEffect(()=>{try{const s=localStorage.getItem(ee);if(!s)return;const i=JSON.parse(s);i&&i.form&&Array.isArray(i.items)&&J(i)}catch{}},[]);const je=()=>{try{localStorage.removeItem(ee)}catch{}J(null)},{items:w,loading:ye,pagination:W,refetch:q}=ze("invoices.php",{dataKey:"invoices",search:M,sort:ne,order:u,page:le,extraParams:x?{status:x}:{},errorMsg:"Nepodařilo se načíst faktury"});if(!o("invoices.view"))return e.jsx(Ce,{});const fe=async()=>{if(b.invoice){Z(!0);try{const i=await(await C(`${P}/invoices.php?id=${b.invoice.id}`,{method:"DELETE"})).json();i.success?(A({show:!1,invoice:null}),t.success(i.message||"Faktura byla smazána"),q(),N()):t.error(i.error||"Nepodařilo se smazat fakturu")}catch{t.error("Chyba připojení")}finally{Z(!1)}}},ge=async s=>{if(s.status!=="paid")try{const l=await(await C(`${P}/invoices.php?id=${s.id}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({status:"paid"})})).json();l.success?(t.success("Faktura označena jako zaplacená"),q(),N()):t.error(l.error||"Nepodařilo se změnit stav")}catch{t.error("Chyba připojení")}},G=async(s,i="cs")=>{if(!L){T(null),Y(s.id);try{const l=await C(`${P}/invoices-pdf.php?id=${s.id}&lang=${encodeURIComponent(i)}`);if(l.status===401)return;if(!l.ok){t.error("Nepodařilo se vygenerovat PDF");return}const S=await l.text(),r=window.open("","_blank");r?(r.document.open(),r.document.write(S),r.document.close(),r.onload=()=>r.print()):t.error("Prohlížeč zablokoval vyskakovací okno")}catch{t.error("Chyba při generování PDF")}finally{Y(null)}}};return ye?e.jsx("div",{children:e.jsxs("div",{className:"admin-skeleton",style:{padding:0,gap:"1.5rem"},children:[e.jsxs("div",{className:"admin-skeleton-row",style:{justifyContent:"space-between"},children:[e.jsxs("div",{children:[e.jsx("div",{className:"admin-skeleton-line h-8",style:{width:"200px",marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line",style:{width:"140px"}})]}),e.jsx("div",{className:"admin-skeleton-line h-10",style:{width:"140px",borderRadius:"8px"}})]}),e.jsx("div",{className:"dash-kpi-grid dash-kpi-4",children:[0,1,2,3].map(s=>e.jsxs("div",{className:"admin-stat-card",children:[e.jsx("div",{className:"admin-skeleton-line",style:{width:"60%",height:"11px",marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line",style:{width:"40%",height:"28px",marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line",style:{width:"50%",height:"12px"}})]},s))}),e.jsx("div",{className:"admin-card",children:e.jsx("div",{className:"admin-skeleton",style:{gap:"1rem"},children:[0,1,2,3,4].map(s=>e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line",style:{width:"80px"}}),e.jsx("div",{className:"admin-skeleton-line w-1/4"}),e.jsx("div",{className:"admin-skeleton-line",style:{width:"70px"}}),e.jsx("div",{className:"admin-skeleton-line",style:{width:"90px"}}),e.jsx("div",{className:"admin-skeleton-line",style:{width:"90px"}}),e.jsx("div",{className:"admin-skeleton-line",style:{width:"100px"}})]},s))})})]})}):e.jsxs("div",{children:[e.jsxs(m.div,{className:"admin-page-header",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4},children:[e.jsxs("div",{children:[e.jsx("h1",{className:"admin-page-title",children:"Faktury"}),e.jsxs("p",{className:"admin-page-subtitle",children:[W?.total??w.length," ",Q(W?.total??w.length,"faktura","faktury","faktur")]})]}),o("invoices.create")&&e.jsx("div",{className:"admin-page-actions",children:h==="received"?e.jsxs("button",{className:"admin-btn admin-btn-primary",onClick:()=>I(!0),children:[e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"}),e.jsx("polyline",{points:"17 8 12 3 7 8"}),e.jsx("line",{x1:"12",y1:"3",x2:"12",y2:"15"})]}),"Nahrát faktury"]}):e.jsxs(z,{to:"/invoices/new",className:"admin-btn admin-btn-primary",children:[e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("line",{x1:"12",y1:"5",x2:"12",y2:"19"}),e.jsx("line",{x1:"5",y1:"12",x2:"19",y2:"12"})]}),"Nová faktura"]})})]}),e.jsxs(m.div,{initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.1},children:[e.jsxs("div",{className:"invoice-month-nav",children:[e.jsx("button",{className:"invoice-month-btn",onClick:xe,"aria-label":"Předchozí měsíc",children:e.jsx("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2.5",children:e.jsx("polyline",{points:"15 18 9 12 15 6"})})}),e.jsx("span",{children:ue}),e.jsx("button",{className:"invoice-month-btn",onClick:pe,disabled:U,"aria-label":"Následující měsíc",children:e.jsx("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2.5",children:e.jsx("polyline",{points:"9 18 15 12 9 6"})})})]}),e.jsxs("div",{className:"offers-tabs",style:{marginBottom:"1rem",justifyContent:"center"},children:[e.jsx("button",{className:`offers-tab ${h==="issued"?"active":""}`,onClick:()=>v("issued"),children:"Vydané"}),e.jsx("button",{className:`offers-tab ${h==="received"?"active":""}`,onClick:()=>v("received"),children:"Přijaté"})]})]}),h==="received"?e.jsx(m.div,{initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.15},children:e.jsx(a.Suspense,{fallback:e.jsx("div",{className:"dash-kpi-grid dash-kpi-4",style:{marginBottom:"1.5rem"},children:[0,1,2,3].map(s=>e.jsxs("div",{className:"admin-stat-card",children:[e.jsx("div",{className:"admin-skeleton-line",style:{width:"60%",height:"11px",marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line",style:{width:"40%",height:"28px",marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line",style:{width:"50%",height:"12px"}})]},s))}),children:e.jsx(Pe,{statsMonth:d,statsYear:p,uploadOpen:ie,setUploadOpen:I})})}):e.jsxs(e.Fragment,{children:[e.jsx(m.div,{initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.15},children:!K.current&&ce?e.jsx("div",{className:"dash-kpi-grid dash-kpi-4",style:{marginBottom:"1.5rem"},children:[0,1,2,3].map(s=>e.jsxs("div",{className:"admin-stat-card",children:[e.jsx("div",{className:"admin-skeleton-line",style:{width:"60%",height:"11px",marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line",style:{width:"40%",height:"28px",marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line",style:{width:"50%",height:"12px"}})]},s))}):n&&e.jsx("div",{style:{overflow:"hidden",marginBottom:"1.5rem"},children:e.jsx(X,{mode:"popLayout",initial:!1,custom:k.current,children:e.jsx(m.div,{className:"dash-kpi-grid dash-kpi-4",custom:k.current,variants:{enter:s=>({x:`${(s||0)*105}%`,opacity:0}),center:{x:"0%",opacity:1},exit:s=>({x:`${(s||0)*-105}%`,opacity:0})},initial:"enter",animate:"center",exit:"exit",transition:{type:"spring",stiffness:300,damping:30},children:(()=>{const s=D(n.paid_month,n.paid_month_czk),i=D(n.awaiting,n.awaiting_czk),l=D(n.overdue,n.overdue_czk),S=D(n.vat_month,n.vat_month_czk),r=(E,ke)=>E>0?`${E} ${Q(E,"faktura","faktury","faktur")}`:ke;return e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"admin-stat-card success",children:[e.jsxs("div",{className:"admin-stat-label",children:["Uhrazeno (",V[d-1],")"]}),e.jsx("div",{className:"admin-stat-value admin-mono",children:s.value}),e.jsx("div",{className:"admin-stat-footer",children:[s.detail,r(n.paid_month_count,"žádné úhrady")].filter(Boolean).join(" · ")})]}),e.jsxs("div",{className:"admin-stat-card warning",children:[e.jsxs("div",{className:"admin-stat-label",children:["Čeká úhrada ",e.jsx("span",{style:{fontWeight:400,opacity:.7},children:"· celkově"})]}),e.jsx("div",{className:"admin-stat-value admin-mono",children:i.value}),e.jsx("div",{className:"admin-stat-footer",children:[i.detail,r(n.awaiting_count,"vše uhrazeno")].filter(Boolean).join(" · ")})]}),e.jsxs("div",{className:"admin-stat-card danger",children:[e.jsxs("div",{className:"admin-stat-label",children:["Po splatnosti ",e.jsx("span",{style:{fontWeight:400,opacity:.7},children:"· celkově"})]}),e.jsx("div",{className:"admin-stat-value admin-mono",children:l.value}),e.jsx("div",{className:"admin-stat-footer",children:[l.detail,n.overdue_count===0?"vše v pořádku":r(n.overdue_count,"")].filter(Boolean).join(" · ")})]}),e.jsxs("div",{className:"admin-stat-card info",children:[e.jsxs("div",{className:"admin-stat-label",children:["DPH (",V[d-1],")"]}),e.jsx("div",{className:"admin-stat-value admin-mono",children:S.value}),e.jsx("div",{className:"admin-stat-footer",children:S.detail||"z vydaných faktur"})]})]})})()},me)})})}),e.jsx(m.div,{initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.2},children:e.jsx("div",{className:"offers-tabs",style:{marginBottom:"1.5rem"},children:De.map(s=>e.jsx("button",{className:`offers-tab ${x===s.value?"active":""}`,onClick:()=>{re(s.value),$(1)},children:s.label},s.value))})}),e.jsx(m.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.25},children:e.jsxs("div",{className:"admin-card-body",children:[e.jsx("div",{className:"admin-search-bar",style:{marginBottom:"1rem"},children:e.jsx("input",{type:"text",value:M,onChange:s=>{oe(s.target.value),$(1)},className:"admin-form-input",placeholder:"Hledat podle čísla faktury, zákazníka nebo IČ..."})}),w.length===0&&!(c&&!x)?e.jsxs("div",{className:"admin-empty-state",children:[e.jsx("div",{className:"admin-empty-icon",children:e.jsxs("svg",{width:"28",height:"28",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("path",{d:"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"}),e.jsx("polyline",{points:"14 2 14 8 20 8"}),e.jsx("line",{x1:"16",y1:"13",x2:"8",y2:"13"}),e.jsx("line",{x1:"16",y1:"17",x2:"8",y2:"17"}),e.jsx("polyline",{points:"10 9 9 9 8 9"})]})}),e.jsx("p",{children:"Zatím nejsou žádné faktury."}),o("invoices.create")&&e.jsx("p",{className:"text-tertiary",style:{fontSize:"0.875rem"},children:"Vytvořte první fakturu tlačítkem výše."})]}):e.jsx("div",{className:"admin-table-responsive",children:e.jsxs("table",{className:"admin-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsxs("th",{style:{cursor:"pointer"},onClick:()=>j("invoice_number"),children:["Číslo ",e.jsx(B,{column:"invoice_number",sort:y,order:u})]}),e.jsx("th",{children:"Zákazník"}),e.jsxs("th",{style:{cursor:"pointer"},onClick:()=>j("status"),children:["Stav ",e.jsx(B,{column:"status",sort:y,order:u})]}),e.jsxs("th",{style:{cursor:"pointer"},onClick:()=>j("issue_date"),children:["Vystaveno ",e.jsx(B,{column:"issue_date",sort:y,order:u})]}),e.jsxs("th",{style:{cursor:"pointer"},onClick:()=>j("due_date"),children:["Splatnost ",e.jsx(B,{column:"due_date",sort:y,order:u})]}),e.jsx("th",{style:{textAlign:"right"},children:"Celkem"}),e.jsx("th",{children:"Akce"})]})}),e.jsxs("tbody",{children:[c&&!M&&!x&&e.jsxs("tr",{className:"offers-draft-row",children:[e.jsx("td",{children:e.jsxs("span",{className:"offers-draft-row-label",children:["Koncept",c.savedAt&&e.jsxs("span",{style:{fontWeight:400,opacity:.8},children:[" · ",new Date(c.savedAt).toLocaleTimeString("cs-CZ",{hour:"2-digit",minute:"2-digit"})]})]})}),e.jsx("td",{children:c.form.customer_name||"—"}),e.jsx("td",{children:"—"}),e.jsx("td",{className:"admin-mono",children:c.form.issue_date?_(c.form.issue_date):"—"}),e.jsx("td",{className:"admin-mono",children:c.form.due_date?_(c.form.due_date):"—"}),e.jsx("td",{}),e.jsx("td",{children:e.jsxs("div",{className:"admin-table-actions",children:[e.jsx(z,{to:"/invoices/new",className:"admin-btn-icon",title:"Pokračovat v konceptu","aria-label":"Pokračovat v konceptu",children:e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"}),e.jsx("path",{d:"M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"})]})}),e.jsx("button",{onClick:je,className:"admin-btn-icon danger",title:"Zahodit koncept",children:e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("polyline",{points:"3 6 5 6 21 6"}),e.jsx("path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"})]})})]})})]}),w.map(s=>{const i=s.status==="overdue"||s.status==="issued"&&s.due_date&&new Date(s.due_date)ge(s),className:`admin-badge ${te[s.status]||""}`,style:{cursor:"pointer"},children:ae[s.status]||s.status})}),e.jsx("td",{className:"admin-mono",children:_(s.issue_date)}),e.jsx("td",{className:"admin-mono",style:s.status==="overdue"?{color:"var(--danger)",fontWeight:600}:void 0,children:_(s.due_date)}),e.jsx("td",{className:"admin-mono",style:{textAlign:"right",fontWeight:500},children:O(s.total,s.currency)}),e.jsx("td",{children:e.jsxs("div",{className:"admin-table-actions",children:[e.jsx(z,{to:`/invoices/${s.id}`,className:"admin-btn-icon",title:"Detail","aria-label":"Detail",children:e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"}),e.jsx("circle",{cx:"12",cy:"12",r:"3"})]})}),o("invoices.export")&&e.jsx("button",{onClick:()=>T(s),className:"admin-btn-icon",title:"PDF",disabled:L===s.id,children:L===s.id?e.jsx("div",{className:"admin-spinner",style:{width:18,height:18,borderWidth:2}}):e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"}),e.jsx("polyline",{points:"14 2 14 8 20 8"}),e.jsx("line",{x1:"16",y1:"13",x2:"8",y2:"13"}),e.jsx("line",{x1:"16",y1:"17",x2:"8",y2:"17"})]})}),o("invoices.delete")&&e.jsx("button",{onClick:()=>A({show:!0,invoice:s}),className:"admin-btn-icon danger",title:"Smazat",children:e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("polyline",{points:"3 6 5 6 21 6"}),e.jsx("path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"})]})})]})})]},s.id)})]})]})}),e.jsx(Be,{pagination:W,onPageChange:$})]})}),e.jsx(we,{isOpen:b.show,onClose:()=>A({show:!1,invoice:null}),onConfirm:fe,title:"Smazat fakturu",message:`Opravdu chcete smazat fakturu "${b.invoice?.invoice_number}"? Tato akce je nevratná.`,confirmText:"Smazat",cancelText:"Zrušit",type:"danger",loading:ve}),e.jsx(X,{children:F&&e.jsxs(m.div,{className:"admin-modal-overlay",initial:{opacity:0},animate:{opacity:1},exit:{opacity:0},transition:{duration:.2},children:[e.jsx("div",{className:"admin-modal-backdrop",onClick:()=>T(null)}),e.jsxs(m.div,{className:"admin-modal admin-confirm-modal",role:"dialog","aria-modal":"true",initial:{opacity:0,scale:.95,y:20},animate:{opacity:1,scale:1,y:0},exit:{opacity:0,scale:.95,y:20},transition:{duration:.2},children:[e.jsxs("div",{className:"admin-modal-body admin-confirm-content",children:[e.jsx("div",{className:"admin-confirm-icon admin-confirm-icon-info",children:e.jsxs("svg",{width:"24",height:"24",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"}),e.jsx("path",{d:"M2 12h20"}),e.jsx("path",{d:"M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"})]})}),e.jsx("h2",{className:"admin-confirm-title",children:"Jazyk faktury"}),e.jsx("p",{className:"admin-confirm-message",children:"V jakém jazyce chcete vygenerovat fakturu?"})]}),e.jsxs("div",{className:"admin-modal-footer",children:[e.jsx("button",{type:"button",onClick:()=>G(F,"cs"),className:"admin-btn admin-btn-primary",children:"Čeština"}),e.jsx("button",{type:"button",onClick:()=>G(F,"en"),className:"admin-btn admin-btn-primary",children:"English"})]})]})]})})]})]})}export{Ee as default}; diff --git a/dist/assets/LeaveApproval-BQyC3i8M.js b/dist/assets/LeaveApproval-BQyC3i8M.js new file mode 100644 index 0000000..a829068 --- /dev/null +++ b/dist/assets/LeaveApproval-BQyC3i8M.js @@ -0,0 +1 @@ +import{j as e,m as o,A as E}from"./vendor-animation-0s3FMHwK.js";import{r as t}from"./vendor-react-BVs3cwbi.js";import{u as q,a as W,b as I,c as m,d as w,C as V,F as J}from"./index-BBlIrj2z.js";import{b as r,e as z}from"./attendanceHelpers-D6sLEw0q.js";import{F as K}from"./Forbidden-D25jV3Oq.js";import"./vendor-utils-Dyr8OjFr.js";const p="/api/admin",f={vacation:"Dovolená",sick:"Nemoc",unpaid:"Neplacené volno"},A={vacation:"badge-vacation",sick:"badge-sick",unpaid:"badge-unpaid"},U={pending:"Čeká na schválení",approved:"Schváleno",rejected:"Zamítnuto",cancelled:"Zrušeno"},G={pending:"badge-pending",approved:"badge-approved",rejected:"badge-rejected",cancelled:"badge-cancelled"};function ae(){const{hasPermission:T}=q(),n=W(),[D,_]=t.useState(!0),[d,k]=t.useState("pending"),[S,R]=t.useState([]),[c,Z]=t.useState(0),[h,b]=t.useState([]),[l,N]=t.useState({open:!1,request:null}),[i,u]=t.useState({open:!1,request:null}),[x,y]=t.useState(""),[j,v]=t.useState(!1);I(i.open);const g=t.useCallback(async()=>{try{const s=await m(`${p}/leave-requests.php?action=pending`);if(s.status===401)return;const a=await s.json();a.success&&(R(a.data.requests),Z(a.data.count))}catch{n.error("Nepodařilo se načíst žádosti")}},[n]),C=t.useCallback(async()=>{try{const s=await m(`${p}/leave-requests.php?action=all&status=approved`);if(s.status===401)return;const a=await s.json(),$=await m(`${p}/leave-requests.php?action=all&status=rejected`);if($.status===401)return;const P=await $.json(),L=[...a.success?a.data:[],...P.success?P.data:[]].sort((M,O)=>new Date(O.reviewed_at)-new Date(M.reviewed_at));b(L)}catch{n.error("Nepodařilo se načíst vyřízené žádosti")}},[n]);if(t.useEffect(()=>{(async()=>{_(!0),await g(),_(!1)})()},[g]),t.useEffect(()=>{d==="processed"&&h.length===0&&C()},[d,h.length,C]),!T("attendance.approve"))return e.jsx(K,{});const B=async()=>{v(!0);try{const s=await m(`${p}/leave-requests.php?action=approve`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({request_id:l.request.id})});if(s.status===401)return;const a=await s.json();a.success?(N({open:!1,request:null}),await g(),b([]),n.success(a.message)):n.error(a.error)}catch{n.error("Chyba připojení")}finally{v(!1)}},F=async()=>{if(!x.trim()){n.error("Důvod zamítnutí je povinný");return}v(!0);try{const s=await m(`${p}/leave-requests.php?action=reject`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({request_id:i.request.id,note:x})});if(s.status===401)return;const a=await s.json();a.success?(u({open:!1,request:null}),y(""),await g(),b([]),n.success(a.message)):n.error(a.error)}catch{n.error("Chyba připojení")}finally{v(!1)}};return D?e.jsxs("div",{className:"admin-skeleton",style:{padding:0,gap:"1.5rem"},children:[e.jsx("div",{className:"admin-skeleton-row",style:{justifyContent:"space-between"},children:e.jsxs("div",{children:[e.jsx("div",{className:"admin-skeleton-line h-8",style:{width:"200px",marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line",style:{width:"140px"}})]})}),e.jsx("div",{className:"admin-card",children:e.jsx("div",{className:"admin-skeleton",style:{gap:"1.25rem"},children:[0,1,2,3,4].map(s=>e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line circle"}),e.jsxs("div",{style:{flex:1},children:[e.jsx("div",{className:"admin-skeleton-line w-1/3",style:{marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line w-1/4",style:{height:"10px"}})]}),e.jsx("div",{className:"admin-skeleton-line w-1/4"})]},s))})})]}):e.jsxs("div",{children:[e.jsx(o.div,{className:"admin-page-header",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4},children:e.jsxs("div",{children:[e.jsx("h1",{className:"admin-page-title",children:"Schvalování nepřítomnosti"}),e.jsx("p",{className:"admin-page-subtitle",children:c>0?`${c} ${w(c,"žádost čeká","žádosti čekají","žádostí čeká")} na schválení`:"Žádné čekající žádosti"})]})}),e.jsx(o.div,{initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.1},children:e.jsxs("div",{className:"offers-tabs",style:{marginBottom:"1.5rem"},children:[e.jsxs("button",{className:`offers-tab ${d==="pending"?"active":""}`,onClick:()=>k("pending"),children:["Ke schválení",c>0&&e.jsx("span",{className:"admin-badge badge-pending",style:{marginLeft:"0.5rem",fontSize:"0.7rem",padding:"0.15rem 0.5rem"},children:c})]}),e.jsx("button",{className:`offers-tab ${d==="processed"?"active":""}`,onClick:()=>k("processed"),children:"Vyřízené"})]})}),d==="pending"&&e.jsx(o.div,{initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.15},children:S.length===0?e.jsx("div",{className:"admin-card",children:e.jsx("div",{className:"admin-card-body",children:e.jsxs("div",{className:"admin-empty-state",children:[e.jsxs("svg",{width:"48",height:"48",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"1.5",className:"text-muted",style:{marginBottom:"1rem"},children:[e.jsx("path",{d:"M22 11.08V12a10 10 0 1 1-5.93-9.14"}),e.jsx("polyline",{points:"22 4 12 14.01 9 11.01"})]}),e.jsx("p",{children:"Žádné čekající žádosti"})]})})}):e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"1rem"},children:S.map(s=>e.jsx("div",{className:"admin-card",children:e.jsx("div",{className:"admin-card-body",style:{padding:"1.25rem"},children:e.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"flex-start",flexWrap:"wrap",gap:"1rem"},children:[e.jsxs("div",{style:{flex:1},children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"0.75rem",marginBottom:"0.5rem"},children:[e.jsx("strong",{style:{fontSize:"1rem"},children:s.employee_name}),e.jsx("span",{className:`attendance-leave-badge ${A[s.leave_type]||""}`,children:f[s.leave_type]||s.leave_type})]}),e.jsxs("div",{className:"text-secondary",style:{display:"flex",gap:"1.5rem",flexWrap:"wrap",fontSize:"0.875rem"},children:[e.jsxs("span",{children:[e.jsx("strong",{children:r(s.date_from)})," — ",e.jsx("strong",{children:r(s.date_to)})]}),e.jsxs("span",{children:[s.total_days," ",w(s.total_days,"den","dny","dnů")," (",s.total_hours,"h)"]}),e.jsxs("span",{className:"text-muted",children:["Podáno: ",z(s.created_at)]})]}),s.notes&&e.jsx("div",{className:"text-secondary",style:{marginTop:"0.5rem",fontSize:"0.875rem",fontStyle:"italic"},children:s.notes})]}),e.jsxs("div",{style:{display:"flex",gap:"0.5rem",flexShrink:0},children:[e.jsx("button",{onClick:()=>N({open:!0,request:s}),className:"admin-btn admin-btn-sm",style:{background:"var(--success-light)",color:"var(--success)",border:"none"},children:"Schválit"}),e.jsx("button",{onClick:()=>u({open:!0,request:s}),className:"admin-btn admin-btn-sm",style:{background:"var(--danger-light)",color:"var(--danger)",border:"none"},children:"Zamítnout"})]})]})})},s.id))})}),d==="processed"&&e.jsx(o.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.15},children:e.jsx("div",{className:"admin-card-body",children:h.length===0?e.jsx("div",{className:"admin-empty-state",children:e.jsx("p",{children:"Zatím žádné vyřízené žádosti"})}):e.jsx("div",{className:"admin-table-responsive",children:e.jsxs("table",{className:"admin-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Zaměstnanec"}),e.jsx("th",{children:"Typ"}),e.jsx("th",{children:"Od"}),e.jsx("th",{children:"Do"}),e.jsx("th",{children:"Dny"}),e.jsx("th",{children:"Stav"}),e.jsx("th",{children:"Schválil"}),e.jsx("th",{children:"Poznámka"}),e.jsx("th",{children:"Vyřízeno"})]})}),e.jsx("tbody",{children:h.map(s=>e.jsxs("tr",{children:[e.jsx("td",{children:e.jsx("strong",{children:s.employee_name})}),e.jsx("td",{children:e.jsx("span",{className:`attendance-leave-badge ${A[s.leave_type]||""}`,children:f[s.leave_type]||s.leave_type})}),e.jsx("td",{className:"admin-mono",children:r(s.date_from)}),e.jsx("td",{className:"admin-mono",children:r(s.date_to)}),e.jsx("td",{className:"admin-mono",children:s.total_days}),e.jsx("td",{children:e.jsx("span",{className:`admin-badge ${G[s.status]||""}`,children:U[s.status]||s.status})}),e.jsx("td",{children:s.reviewer_name||"—"}),e.jsx("td",{style:{maxWidth:"200px"},children:s.reviewer_note?e.jsx("span",{title:s.reviewer_note,children:s.reviewer_note.length>40?`${s.reviewer_note.substring(0,40)}...`:s.reviewer_note}):"—"}),e.jsx("td",{className:"admin-mono",style:{whiteSpace:"nowrap"},children:z(s.reviewed_at)})]},s.id))})]})})})}),e.jsx(V,{isOpen:l.open,onClose:()=>N({open:!1,request:null}),onConfirm:B,title:"Schválit žádost",message:l.request?`Schválit ${l.request.total_days} ${w(l.request.total_days,"den","dny","dnů")} ${f[l.request.leave_type]?.toLowerCase()||""} pro ${l.request.employee_name}?`:"",confirmText:"Schválit",type:"info",loading:j}),e.jsx(E,{children:i.open&&e.jsxs(o.div,{className:"admin-modal-overlay",initial:{opacity:0},animate:{opacity:1},exit:{opacity:0},transition:{duration:.2},children:[e.jsx("div",{className:"admin-modal-backdrop",onClick:()=>{u({open:!1,request:null}),y("")}}),e.jsxs(o.div,{className:"admin-modal",initial:{opacity:0,scale:.95,y:20},animate:{opacity:1,scale:1,y:0},exit:{opacity:0,scale:.95,y:20},transition:{duration:.2},children:[e.jsx("div",{className:"admin-modal-header",children:e.jsx("h2",{className:"admin-modal-title",children:"Zamítnout žádost"})}),e.jsxs("div",{className:"admin-modal-body",children:[i.request&&e.jsxs("p",{className:"text-secondary",style:{marginBottom:"1rem"},children:[i.request.employee_name," — ",f[i.request.leave_type],","," ",r(i.request.date_from)," — ",r(i.request.date_to)," (",i.request.total_days," dnů)"]}),e.jsx(J,{label:"Důvod zamítnutí",required:!0,children:e.jsx("textarea",{value:x,onChange:s=>y(s.target.value),placeholder:"Uveďte důvod zamítnutí...",className:"admin-form-textarea",rows:3,autoFocus:!0})})]}),e.jsxs("div",{className:"admin-modal-footer",children:[e.jsx("button",{type:"button",onClick:()=>{u({open:!1,request:null}),y("")},className:"admin-btn admin-btn-secondary",disabled:j,children:"Zrušit"}),e.jsx("button",{type:"button",onClick:F,disabled:j||!x.trim(),className:"admin-btn admin-btn-primary",children:j?"Zpracování...":"Zamítnout"})]})]})]})})]})}export{ae as default}; diff --git a/dist/assets/LeaveRequests-CJA9No9B.js b/dist/assets/LeaveRequests-CJA9No9B.js new file mode 100644 index 0000000..5c9573f --- /dev/null +++ b/dist/assets/LeaveRequests-CJA9No9B.js @@ -0,0 +1 @@ +import{j as e,m}from"./vendor-animation-0s3FMHwK.js";import{r as n}from"./vendor-react-BVs3cwbi.js";import{a as k,u as w,c as x,C as b}from"./index-BBlIrj2z.js";import{F as C}from"./Forbidden-D25jV3Oq.js";import{b as h,e as S}from"./attendanceHelpers-D6sLEw0q.js";import"./vendor-utils-Dyr8OjFr.js";const j="/api/admin",_={vacation:"Dovolená",sick:"Nemoc",unpaid:"Neplacené volno"},B={pending:"Čeká na schválení",approved:"Schváleno",rejected:"Zamítnuto",cancelled:"Zrušeno"},z={pending:"badge-pending",approved:"badge-approved",rejected:"badge-rejected",cancelled:"badge-cancelled"},D={vacation:"badge-vacation",sick:"badge-sick",unpaid:"badge-unpaid"};function $(){const t=k(),{hasPermission:p}=w(),[v,u]=n.useState(!0),[o,y]=n.useState([]),[c,i]=n.useState({open:!1,id:null}),[N,r]=n.useState(!1),l=n.useCallback(async()=>{try{const s=await x(`${j}/leave-requests.php`);if(s.status===401)return;const a=await s.json();a.success&&y(a.data)}catch{t.error("Nepodařilo se načíst žádosti")}finally{u(!1)}},[t]);if(n.useEffect(()=>{l()},[l]),!p("attendance.record"))return e.jsx(C,{});const g=async()=>{r(!0);try{const s=await x(`${j}/leave-requests.php?action=cancel`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({request_id:c.id})});if(s.status===401)return;const a=await s.json();a.success?(i({open:!1,id:null}),await l(),t.success(a.message)):t.error(a.error)}catch{t.error("Chyba připojení")}finally{r(!1)}};if(v)return e.jsx("div",{children:e.jsxs("div",{className:"admin-skeleton",style:{padding:0,gap:"1.5rem"},children:[e.jsxs("div",{className:"admin-skeleton-row",style:{justifyContent:"space-between"},children:[e.jsxs("div",{children:[e.jsx("div",{className:"admin-skeleton-line h-8",style:{width:"200px",marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line",style:{width:"140px"}})]}),e.jsx("div",{className:"admin-skeleton-line h-10",style:{width:"140px",borderRadius:"8px"}})]}),e.jsx("div",{className:"admin-card",children:e.jsxs("div",{className:"admin-skeleton",style:{gap:"1.25rem"},children:[e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line circle"}),e.jsxs("div",{style:{flex:1},children:[e.jsx("div",{className:"admin-skeleton-line w-1/3",style:{marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line w-1/4",style:{height:"10px"}})]}),e.jsx("div",{className:"admin-skeleton-line w-1/4"})]}),e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line circle"}),e.jsxs("div",{style:{flex:1},children:[e.jsx("div",{className:"admin-skeleton-line w-1/2",style:{marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line w-1/3",style:{height:"10px"}})]}),e.jsx("div",{className:"admin-skeleton-line w-1/4"})]}),e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line circle"}),e.jsxs("div",{style:{flex:1},children:[e.jsx("div",{className:"admin-skeleton-line w-3/4",style:{marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line w-1/4",style:{height:"10px"}})]}),e.jsx("div",{className:"admin-skeleton-line w-1/4"})]}),e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line circle"}),e.jsxs("div",{style:{flex:1},children:[e.jsx("div",{className:"admin-skeleton-line w-1/2",style:{marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line w-1/3",style:{height:"10px"}})]}),e.jsx("div",{className:"admin-skeleton-line w-1/4"})]}),e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line circle"}),e.jsxs("div",{style:{flex:1},children:[e.jsx("div",{className:"admin-skeleton-line w-1/3",style:{marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line w-1/4",style:{height:"10px"}})]}),e.jsx("div",{className:"admin-skeleton-line w-1/4"})]})]})})]})});function f(s){const a=d=>d.length>40?`${d.substring(0,40)}...`:d;return s.status==="rejected"&&s.reviewer_note?e.jsx("span",{style:{color:"var(--danger)",fontSize:"0.875rem"},title:s.reviewer_note,children:a(s.reviewer_note)}):s.notes?e.jsx("span",{className:"text-secondary",style:{fontSize:"0.875rem"},title:s.notes,children:a(s.notes)}):e.jsx("span",{className:"text-muted",children:"—"})}return e.jsxs("div",{children:[e.jsx(m.div,{className:"admin-page-header",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4},children:e.jsxs("div",{children:[e.jsx("h1",{className:"admin-page-title",children:"Moje žádosti"}),e.jsx("p",{className:"admin-page-subtitle",children:"Přehled žádostí o nepřítomnost"})]})}),e.jsx(m.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.1},children:e.jsx("div",{className:"admin-card-body",children:o.length===0?e.jsxs("div",{className:"admin-empty-state",children:[e.jsx("div",{className:"admin-empty-icon",children:e.jsxs("svg",{width:"28",height:"28",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("rect",{x:"3",y:"4",width:"18",height:"18",rx:"2",ry:"2"}),e.jsx("line",{x1:"16",y1:"2",x2:"16",y2:"6"}),e.jsx("line",{x1:"8",y1:"2",x2:"8",y2:"6"}),e.jsx("line",{x1:"3",y1:"10",x2:"21",y2:"10"})]})}),e.jsx("p",{children:"Zatím nemáte žádné žádosti"}),e.jsx("p",{style:{fontSize:"0.875rem",color:"var(--text-muted)"},children:"Novou žádost můžete podat na stránce Docházka"})]}):e.jsx("div",{className:"admin-table-responsive",children:e.jsxs("table",{className:"admin-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Typ"}),e.jsx("th",{children:"Od"}),e.jsx("th",{children:"Do"}),e.jsx("th",{children:"Dny"}),e.jsx("th",{children:"Hodiny"}),e.jsx("th",{children:"Stav"}),e.jsx("th",{children:"Poznámka"}),e.jsx("th",{children:"Podáno"}),e.jsx("th",{})]})}),e.jsx("tbody",{children:o.map(s=>e.jsxs("tr",{children:[e.jsx("td",{children:e.jsx("span",{className:`attendance-leave-badge ${D[s.leave_type]||""}`,children:_[s.leave_type]||s.leave_type})}),e.jsx("td",{className:"admin-mono",children:h(s.date_from)}),e.jsx("td",{className:"admin-mono",children:h(s.date_to)}),e.jsx("td",{className:"admin-mono",children:s.total_days}),e.jsxs("td",{className:"admin-mono",children:[s.total_hours,"h"]}),e.jsx("td",{children:e.jsx("span",{className:`admin-badge ${z[s.status]||""}`,children:B[s.status]||s.status})}),e.jsx("td",{style:{maxWidth:"200px"},children:f(s)}),e.jsx("td",{className:"admin-mono",style:{whiteSpace:"nowrap"},children:S(s.created_at)}),e.jsx("td",{children:s.status==="pending"&&e.jsx("button",{onClick:()=>i({open:!0,id:s.id}),className:"admin-btn admin-btn-secondary admin-btn-sm",children:"Zrušit"})})]},s.id))})]})})})}),e.jsx(b,{isOpen:c.open,onClose:()=>i({open:!1,id:null}),onConfirm:g,title:"Zrušit žádost",message:"Opravdu chcete zrušit tuto žádost o nepřítomnost?",confirmText:"Zrušit žádost",type:"warning",loading:N})]})}export{$ as default}; diff --git a/dist/assets/NotFound-Cm3yLPlV.js b/dist/assets/NotFound-Cm3yLPlV.js new file mode 100644 index 0000000..10adc55 --- /dev/null +++ b/dist/assets/NotFound-Cm3yLPlV.js @@ -0,0 +1 @@ +import{j as t,m as i}from"./vendor-animation-0s3FMHwK.js";import{L as e}from"./vendor-react-BVs3cwbi.js";function o(){return t.jsxs(i.div,{className:"admin-empty-state",style:{minHeight:"60vh",justifyContent:"center"},initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4},children:[t.jsx("div",{className:"admin-empty-icon",style:{width:80,height:80,marginBottom:"1.5rem"},children:t.jsxs("svg",{width:"36",height:"36",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round",children:[t.jsx("circle",{cx:"12",cy:"12",r:"10"}),t.jsx("path",{d:"M16 16s-1.5-2-4-2-4 2-4 2"}),t.jsx("line",{x1:"9",y1:"9",x2:"9.01",y2:"9"}),t.jsx("line",{x1:"15",y1:"9",x2:"15.01",y2:"9"})]})}),t.jsx("h2",{style:{fontSize:"1.5rem",fontWeight:600,marginBottom:"0.5rem",color:"var(--text-primary)"},children:"404"}),t.jsx("p",{children:"Stránka nebyla nalezena."}),t.jsx(e,{to:"/",className:"admin-btn admin-btn-primary",style:{marginTop:"0.5rem"},children:"Zpět na Dashboard"})]})}export{o as default}; diff --git a/dist/assets/OfferDetail-TQHeNuC6.js b/dist/assets/OfferDetail-TQHeNuC6.js new file mode 100644 index 0000000..bfa7d5a --- /dev/null +++ b/dist/assets/OfferDetail-TQHeNuC6.js @@ -0,0 +1 @@ +import{j as e,m as L,A as Ce}from"./vendor-animation-0s3FMHwK.js";import{r as o,h as ze,g as De,L as je}from"./vendor-react-BVs3cwbi.js";import{g as te,c as A,a as Pe,u as Ae,b as Ie,F,A as ve,C as ge}from"./index-BBlIrj2z.js";import{F as Te}from"./Forbidden-D25jV3Oq.js";import{u as $e,a as fe,b as Oe,D as Fe,r as Me,c as Be,S as Ee,v as Re,d as Le,e as We,K as Ze,T as Ve,P as He}from"./useSortableList-CgbuKaxB.js";import{a9 as Ke}from"./vendor-utils-Dyr8OjFr.js";import qe from"./RichEditor-Bfur5pi6.js";function Ue({items:y,setItems:c,updateItem:u,addItem:b,removeItem:z,itemTemplates:C,showItemTemplateMenu:I,setShowItemTemplateMenu:_,addItemFromTemplate:T,totals:i,currency:v,applyVat:k,vatRate:x,itemsError:l,readOnly:j}){const W=$e(fe(He,{activationConstraint:{distance:5}}),fe(Ve,{activationConstraint:{delay:200,tolerance:5}}),fe(Ze)),{handleDragEnd:p}=Oe(c,"_key");return e.jsxs(L.div,{className:"offers-editor-section",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.2},children:[e.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:"1rem"},children:[e.jsxs("div",{children:[e.jsx("h3",{className:"admin-card-title",style:{margin:0},children:"Položky"}),l&&e.jsx("span",{className:"admin-form-error",children:l})]}),!j&&e.jsxs("div",{style:{display:"flex",gap:"0.5rem",position:"relative"},children:[C.length>0&&e.jsxs("div",{style:{position:"relative"},children:[e.jsx("button",{type:"button",onClick:()=>_(d=>!d),className:"admin-btn admin-btn-secondary admin-btn-sm",children:"Ze šablony"}),I&&e.jsx("div",{className:"offers-template-menu",children:C.map(d=>e.jsxs("div",{className:"offers-template-menu-item",onClick:()=>T(d),children:[e.jsx("div",{style:{fontWeight:500},children:d.name}),d.default_price>0&&e.jsx("div",{style:{fontSize:"0.75rem",color:"var(--text-tertiary)"},children:Number(d.default_price).toFixed(2)})]},d.id))})]}),e.jsx("button",{type:"button",onClick:b,className:"admin-btn admin-btn-primary admin-btn-sm",children:"+ Přidat položku"})]})]}),e.jsx("div",{className:"offers-items-table",children:e.jsxs("table",{className:"admin-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[!j&&e.jsx("th",{style:{width:"2rem"}}),e.jsx("th",{style:{width:"2.5rem",textAlign:"center"},children:"#"}),e.jsx("th",{children:"Popis položky"}),e.jsx("th",{style:{width:"5.5rem",textAlign:"center"},children:"Množství"}),e.jsx("th",{style:{width:"5.5rem",textAlign:"center"},children:"Jednotka"}),e.jsx("th",{style:{width:"5.5rem",textAlign:"center"},children:"Jedn. cena"}),e.jsx("th",{style:{width:"4.5rem",textAlign:"center"},children:"V ceně"}),e.jsx("th",{style:{width:"8rem",textAlign:"right"},children:"Celkem"}),!j&&e.jsx("th",{style:{width:"2.5rem",textAlign:"center"}})]})}),e.jsx(Fe,{sensors:W,collisionDetection:Be,onDragEnd:p,modifiers:[Me],children:e.jsx(Ee,{items:y.map(d=>String(d._key)),strategy:Re,children:e.jsx("tbody",{children:y.map((d,g)=>{const D=(Number(d.quantity)||0)*(Number(d.unit_price)||0);return e.jsx(Le,{id:String(d._key),disabled:j,children:({attributes:O,listeners:P})=>e.jsxs(e.Fragment,{children:[!j&&e.jsx("td",{style:{width:"2rem"},children:e.jsx(We,{listeners:P,attributes:O})}),e.jsx("td",{style:{color:"var(--text-tertiary)",textAlign:"center",fontWeight:500},children:g+1}),e.jsxs("td",{children:[e.jsx("input",{type:"text",value:d.description,onChange:w=>u(g,"description",w.target.value),className:"admin-form-input",placeholder:"Název položky",style:{marginBottom:"0.5rem",fontWeight:500},readOnly:j}),e.jsx("input",{type:"text",value:d.item_description,onChange:w=>u(g,"item_description",w.target.value),className:"admin-form-input",placeholder:"Podrobný popis (volitelný)",style:{fontSize:"0.8rem",opacity:.8},readOnly:j})]}),e.jsx("td",{children:e.jsx("input",{type:"number",value:d.quantity,onChange:w=>u(g,"quantity",parseFloat(w.target.value)||0),className:"admin-form-input",min:"0",step:"1",style:{textAlign:"center",height:"2.25rem",padding:"0.375rem 0.5rem"},readOnly:j})}),e.jsx("td",{children:e.jsx("input",{type:"text",value:d.unit,onChange:w=>u(g,"unit",w.target.value),className:"admin-form-input",placeholder:"hod",style:{textAlign:"center",height:"2.25rem",padding:"0.375rem 0.5rem"},readOnly:j})}),e.jsx("td",{children:e.jsx("input",{type:"number",value:d.unit_price,onChange:w=>u(g,"unit_price",parseFloat(w.target.value)||0),className:"admin-form-input",min:"0",step:"0.01",style:{textAlign:"right",height:"2.25rem",padding:"0.375rem 0.5rem"},readOnly:j})}),e.jsx("td",{style:{textAlign:"center"},children:e.jsxs("label",{className:"admin-form-checkbox",style:{justifyContent:"center"},children:[e.jsx("input",{type:"checkbox",checked:d.is_included_in_total,onChange:w=>u(g,"is_included_in_total",w.target.checked),disabled:j}),e.jsx("span",{})]})}),e.jsx("td",{style:{textAlign:"right",fontWeight:600,whiteSpace:"nowrap",fontSize:"0.875rem"},children:te(D,v)}),!j&&e.jsx("td",{children:y.length>1&&e.jsx("button",{type:"button",onClick:()=>z(g),className:"admin-btn-icon danger",title:"Odebrat","aria-label":"Odebrat",children:e.jsxs("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),e.jsx("line",{x1:"6",y1:"6",x2:"18",y2:"18"})]})})})]})},d._key)})})})})]})}),e.jsxs("div",{className:"offers-totals-summary",children:[e.jsxs("div",{className:"offers-totals-row",children:[e.jsx("span",{children:"Mezisoučet:"}),e.jsx("span",{children:te(i.subtotal,v)})]}),k&&e.jsxs("div",{className:"offers-totals-row",children:[e.jsxs("span",{children:["DPH (",x,"%):"]}),e.jsx("span",{children:te(i.vatAmount,v)})]}),e.jsxs("div",{className:"offers-totals-row offers-totals-total",children:[e.jsx("span",{children:"Celkem k úhradě:"}),e.jsx("span",{children:te(i.total,v)})]})]})]})}function Je({sections:y,addSection:c,removeSection:u,updateSection:b,moveSection:z,scopeTemplates:C,showScopeTemplateMenu:I,setShowScopeTemplateMenu:_,loadScopeTemplate:T,form:i,updateForm:v,readOnly:k}){return e.jsxs(L.div,{className:"offers-editor-section",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.3},children:[e.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:"1rem"},children:[e.jsx("h3",{className:"admin-card-title",style:{margin:0},children:"Rozsah projektu"}),!k&&e.jsxs("div",{style:{display:"flex",gap:"0.5rem",position:"relative"},children:[C.length>0&&e.jsxs("div",{style:{position:"relative"},children:[e.jsx("button",{type:"button",onClick:()=>_(x=>!x),className:"admin-btn admin-btn-secondary admin-btn-sm",children:"Ze šablony"}),I&&e.jsx("div",{className:"offers-template-menu",children:C.map(x=>e.jsx("div",{className:"offers-template-menu-item",onClick:()=>T(x),children:x.name},x.id))})]}),e.jsx("button",{type:"button",onClick:c,className:"admin-btn admin-btn-primary admin-btn-sm",children:"+ Přidat sekci"})]})]}),e.jsx("div",{className:"admin-form",children:e.jsxs("div",{className:"admin-form-row",children:[e.jsxs("div",{className:"admin-form-group",children:[e.jsx("label",{className:"admin-form-label",children:"Název rozsahu"}),e.jsx("input",{type:"text",value:i.scope_title,onChange:x=>v("scope_title",x.target.value),className:"admin-form-input",placeholder:"Rozsah projektu",readOnly:k})]}),e.jsxs("div",{className:"admin-form-group",children:[e.jsx("label",{className:"admin-form-label",children:"Popis rozsahu"}),e.jsx("input",{type:"text",value:i.scope_description,onChange:x=>v("scope_description",x.target.value),className:"admin-form-input",placeholder:"Volitelný popis",readOnly:k})]})]})}),y.length===0?e.jsx("div",{className:"admin-empty-state",style:{padding:"2rem"},children:e.jsx("p",{style:{color:"var(--text-tertiary)"},children:'Žádné sekce rozsahu. Klikněte na "Přidat sekci" pro přidání.'})}):e.jsx("div",{className:"offers-scope-list",children:y.map((x,l)=>e.jsxs("div",{className:"offers-scope-section",children:[e.jsxs("div",{className:"offers-scope-section-header",children:[e.jsxs("span",{className:"offers-scope-number",children:[l+1,"."]}),e.jsx("span",{className:"offers-scope-title",children:i.language==="CZ"&&x.title_cz||x.title||`Sekce ${l+1}`}),!k&&e.jsxs("div",{className:"offers-scope-actions",children:[e.jsx("button",{type:"button",onClick:()=>z(l,-1),disabled:l===0,className:"admin-btn-icon",title:"Nahoru","aria-label":"Nahoru",children:e.jsx("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:e.jsx("path",{d:"M18 15l-6-6-6 6"})})}),e.jsx("button",{type:"button",onClick:()=>z(l,1),disabled:l===y.length-1,className:"admin-btn-icon",title:"Dolů","aria-label":"Dolů",children:e.jsx("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:e.jsx("path",{d:"M6 9l6 6 6-6"})})}),e.jsx("button",{type:"button",onClick:()=>u(l),className:"admin-btn-icon danger",title:"Odebrat","aria-label":"Odebrat",children:e.jsxs("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),e.jsx("line",{x1:"6",y1:"6",x2:"18",y2:"18"})]})})]})]}),e.jsxs("div",{className:"admin-form",children:[e.jsxs("div",{className:"admin-form-row",children:[e.jsxs("div",{className:"admin-form-group",children:[e.jsxs("label",{className:"admin-form-label",children:[e.jsx("span",{className:"offers-lang-badge",children:"EN"}),"Název sekce"]}),e.jsx("input",{type:"text",value:x.title,onChange:j=>b(l,"title",j.target.value),className:"admin-form-input",placeholder:"Název sekce (anglicky)",readOnly:k})]}),e.jsxs("div",{className:"admin-form-group",children:[e.jsxs("label",{className:"admin-form-label",children:[e.jsx("span",{className:"offers-lang-badge offers-lang-badge-cz",children:"CZ"}),"Název sekce"]}),e.jsx("input",{type:"text",value:x.title_cz,onChange:j=>b(l,"title_cz",j.target.value),className:"admin-form-input",placeholder:"Název sekce (česky)",readOnly:k})]})]}),e.jsxs("div",{className:"admin-form-group",children:[e.jsx("label",{className:"admin-form-label",children:"Obsah"}),k?x.content&&e.jsx("div",{className:"offers-scope-content rich-text-view",style:{padding:"1rem"},dangerouslySetInnerHTML:{__html:Ke.sanitize(x.content)}}):e.jsx(qe,{value:x.content,onChange:j=>b(l,"content",j),placeholder:"Obsah sekce...",minHeight:"150px"})]})]})]},x._key||l))})]})}function Ge({customers:y,customerId:c,customerName:u,onSelect:b,onClear:z,error:C,readOnly:I}){const[_,T]=o.useState(""),[i,v]=o.useState(!1);o.useEffect(()=>{const l=()=>v(!1);if(i)return document.addEventListener("click",l),()=>document.removeEventListener("click",l)},[i]);const k=o.useMemo(()=>{if(!_)return y;const l=_.toLowerCase();return y.filter(j=>(j.name||"").toLowerCase().includes(l)||(j.company_id||"").includes(_)||(j.city||"").toLowerCase().includes(l))},[y,_]),x=l=>{b(l),T(""),v(!1)};return e.jsxs("div",{className:`admin-form-group${C?" has-error":""}`,children:[e.jsx("label",{className:"admin-form-label required",children:"Zákazník"}),c&&e.jsxs("div",{className:"offers-customer-selected",children:[e.jsx("span",{children:u}),!I&&e.jsx("button",{type:"button",onClick:z,className:"admin-btn-icon",title:"Odebrat zákazníka","aria-label":"Odebrat zákazníka",children:e.jsxs("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),e.jsx("line",{x1:"6",y1:"6",x2:"18",y2:"18"})]})})]}),!c&&!I&&e.jsxs("div",{className:"offers-customer-select",onClick:l=>l.stopPropagation(),children:[e.jsx("input",{type:"text",value:_,onChange:l=>{T(l.target.value),v(!0)},onFocus:()=>v(!0),className:"admin-form-input",placeholder:"Hledat zákazníka..."}),i&&e.jsx("div",{className:"offers-customer-dropdown",children:k.length===0?e.jsx("div",{className:"offers-customer-dropdown-empty",children:"Žádní zákazníci"}):k.slice(0,10).map(l=>e.jsxs("div",{className:"offers-customer-dropdown-item",onMouseDown:()=>x(l),children:[e.jsx("div",{children:l.name}),l.city&&e.jsx("div",{children:l.city})]},l.id))})]}),C&&e.jsx("span",{className:"admin-form-error",children:C})]})}const M="/api/admin";let B=0;const be=()=>({_key:`item-${++B}`,description:"",item_description:"",quantity:1,unit:"",unit_price:0,is_included_in_total:!0}),Ye=()=>({_key:`sec-${++B}`,title:"",title_cz:"",content:""});function Qe(y){return y.map(c=>({...c,_key:c._key||`item-${++B}`}))}function Xe(y){return y.map(c=>({...c,_key:c._key||`sec-${++B}`}))}const q="boha_offer_draft",et={quotation_number:"",project_code:"",customer_id:null,customer_name:"",created_at:new Date().toISOString().split("T")[0],valid_until:new Date(Date.now()+30*24*60*60*1e3).toISOString().split("T")[0],currency:"EUR",language:"EN",vat_rate:21,apply_vat:!1,exchange_rate:"",exchange_rate_date:"",scope_title:"",scope_description:""};function tt({id:y,isEdit:c,alert:u,navigate:b}){const[z,C]=o.useState(c),[I,_]=o.useState(!1),[T,i]=o.useState({}),[v,k]=o.useState([]),[x,l]=o.useState([]),[j,W]=o.useState([]),[p,d]=o.useState({...et}),[g,D]=o.useState([be()]),[O,P]=o.useState([]),[w,ae]=o.useState(null),[ne,U]=o.useState("active"),[Z,V]=o.useState(null),J=o.useRef({form:p,items:g,sections:O}),G=o.useRef(!1);o.useEffect(()=>{(async()=>{try{const[s,a,m]=await Promise.all([A(`${M}/customers.php`),A(`${M}/offers-templates.php?action=items`),A(`${M}/offers-templates.php?action=scopes`)]),n=await s.json(),h=await a.json(),f=await m.json();n.success&&k(n.data.customers),h.success&&l(h.data.templates),f.success&&W(f.data.templates)}catch{}})()},[]),o.useEffect(()=>{if(!c)try{const t=localStorage.getItem(q);if(!t)return;const s=JSON.parse(t);if(!s||typeof s!="object"||!s.form||!Array.isArray(s.items)){localStorage.removeItem(q);return}const{form:a,items:m,sections:n,savedAt:h}=s;d(f=>({...f,project_code:a.project_code??f.project_code,customer_id:a.customer_id??f.customer_id,customer_name:a.customer_name??f.customer_name,created_at:a.created_at??f.created_at,valid_until:a.valid_until??f.valid_until,currency:a.currency??f.currency,language:a.language??f.language,vat_rate:a.vat_rate??f.vat_rate,apply_vat:a.apply_vat??f.apply_vat,exchange_rate:a.exchange_rate??f.exchange_rate,exchange_rate_date:a.exchange_rate_date??f.exchange_rate_date,scope_title:a.scope_title??f.scope_title,scope_description:a.scope_description??f.scope_description})),m.length&&D(Qe(m)),Array.isArray(n)&&n.length&&P(Xe(n)),G.current=!0,h&&V(new Date(h))}catch{try{localStorage.removeItem(q)}catch{}}},[c]),o.useEffect(()=>{J.current={form:p,items:g,sections:O}},[p,g,O]),o.useEffect(()=>{if(c)return;const t=setTimeout(()=>{try{const{form:s,items:a,sections:m}=J.current,{quotation_number:n,...h}=s,f=new Date().toISOString();localStorage.setItem(q,JSON.stringify({form:h,items:a,sections:m,savedAt:f})),V(new Date(f))}catch{}},500);return()=>clearTimeout(t)},[p,g,O,c]),o.useEffect(()=>{if(!c){const s=async()=>{try{const n=await(await A(`${M}/offers.php?action=next_number`)).json();n.success&&d(h=>({...h,quotation_number:n.data.number}))}catch{}},a=async()=>{try{const n=await(await A(`${M}/company-settings.php`)).json();if(n.success&&!G.current){const h=n.data;d(f=>({...f,currency:h.default_currency||f.currency,vat_rate:h.default_vat_rate||f.vat_rate}))}}catch{}};s(),a();return}(async()=>{try{const s=await A(`${M}/offers.php?action=detail&id=${y}`);if(s.status===401)return;const a=await s.json();a.success?ie(a.data):(u.error(a.error||"Nepodařilo se načíst nabídku"),b("/offers"))}catch{u.error("Chyba připojení"),b("/offers")}finally{C(!1)}})()},[c,y,u,b]);const ie=t=>{d({quotation_number:t.quotation_number||"",project_code:t.project_code||"",customer_id:t.customer_id||null,customer_name:t.customer_name||"",created_at:(t.created_at||"").substring(0,10),valid_until:(t.valid_until||"").substring(0,10),currency:t.currency||"EUR",language:t.language||"EN",vat_rate:t.vat_rate||21,apply_vat:!!t.apply_vat,exchange_rate:t.exchange_rate||"",exchange_rate_date:t.exchange_rate_date||"",scope_title:t.scope_title||"",scope_description:t.scope_description||""}),t.items?.length&&D(t.items.map(s=>({_key:`item-${++B}`,description:s.description||"",item_description:s.item_description||"",quantity:Number(s.quantity)||1,unit:s.unit||"",unit_price:Number(s.unit_price)||0,is_included_in_total:!!s.is_included_in_total}))),t.sections?.length&&(D(s=>s),P(t.sections.map(s=>({_key:`sec-${++B}`,title:s.title||"",title_cz:s.title_cz||"",content:s.content||""})))),ae(t.order||null),U(t.status||"active")},re=o.useMemo(()=>{const t=g.reduce((a,m)=>m.is_included_in_total?a+(Number(m.quantity)||0)*(Number(m.unit_price)||0):a,0),s=p.apply_vat?t*((Number(p.vat_rate)||0)/100):0;return{subtotal:t,vatAmount:s,total:t+s}},[g,p.apply_vat,p.vat_rate]),Y=o.useCallback(()=>{try{localStorage.removeItem(q)}catch{}V(null)},[]),oe=o.useMemo(()=>Z?Z.toLocaleTimeString("cs-CZ",{hour:"2-digit",minute:"2-digit"}):null,[Z]),le=(t,s)=>d(a=>({...a,[t]:s})),ce=t=>{d(s=>({...s,customer_id:t.id,customer_name:t.name})),i(s=>({...s,customer_id:void 0}))},de=()=>{d(t=>({...t,customer_id:null,customer_name:""}))},me=(t,s,a)=>{D(m=>m.map((n,h)=>h===t?{...n,[s]:a}:n))},ue=()=>D(t=>[...t,be()]),H=t=>{D(s=>s.length>1?s.filter((a,m)=>m!==t):s)},pe=t=>{D(s=>[...s,{_key:`item-${++B}`,description:t.name||"",item_description:t.description||"",quantity:1,unit:"",unit_price:Number(t.default_price)||0,is_included_in_total:!0}])},Q=()=>P(t=>[...t,Ye()]),E=t=>{P(s=>s.filter((a,m)=>m!==t))},X=(t,s,a)=>{P(m=>m.map((n,h)=>h===t?{...n,[s]:a}:n))},ee=(t,s)=>{P(a=>{const m=[...a],n=t+s;return n<0||n>=m.length?a:([m[t],m[n]]=[m[n],m[t]],m)})},R=async t=>{try{const a=await(await A(`${M}/offers-templates.php?action=scope_detail&id=${t.id}`)).json();if(a.success){const m=a.data;if(d(n=>({...n,scope_description:m.description||n.scope_description})),m.sections){const n=m.sections.map(h=>({_key:`sec-${++B}`,title:h.title||"",title_cz:h.title_cz||"",content:h.content||""}));P(h=>[...h,...n])}u.success(`Načtena šablona "${t.name}"`)}}catch{u.error("Nepodařilo se načíst šablonu")}},he=async()=>{const t={};if(p.customer_id||(t.customer_id="Vyberte zákazníka"),p.created_at||(t.created_at="Zadejte datum"),p.valid_until||(t.valid_until="Zadejte datum"),(g.length===0||g.every(s=>!s.description.trim()))&&(t.items="Přidejte alespoň jednu položku"),i(t),!(Object.keys(t).length>0)){_(!0);try{const s=K(),a=c?`${M}/offers.php?id=${y}`:`${M}/offers.php`,n=await(await A(a,{method:c?"PUT":"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)})).json();if(n.success){if(u.success(n.message||(c?"Nabídka byla uložena":"Nabídka byla vytvořena")),!c&&n.data?.id){Y();const h=n.data.id;setTimeout(()=>b(`/offers/${h}`,{replace:!0}),300)}}else u.error(n.error||"Nepodařilo se uložit nabídku")}catch{u.error("Chyba připojení")}finally{_(!1)}}},K=()=>({quotation:{project_code:p.project_code,customer_id:p.customer_id,created_at:p.created_at,valid_until:p.valid_until,currency:p.currency,language:p.language,vat_rate:p.vat_rate,apply_vat:p.apply_vat,exchange_rate:p.exchange_rate||null,exchange_rate_date:p.exchange_rate_date||null,scope_title:p.scope_title,scope_description:p.scope_description},items:g.map((t,s)=>({...t,position:s+1})),sections:O.map((t,s)=>({...t,position:s+1}))});return{loading:z,saving:I,errors:T,setErrors:i,form:p,updateForm:le,items:g,setItems:D,sections:O,customers:v,itemTemplates:x,scopeTemplates:j,orderInfo:w,offerStatus:ne,setOfferStatus:U,totals:re,draftSavedAtLabel:oe,clearDraft:Y,selectCustomer:ce,clearCustomer:de,updateItem:me,addItem:ue,removeItem:H,addItemFromTemplate:pe,addSection:Q,removeSection:E,updateSection:X,moveSection:ee,loadScopeTemplate:R,handleSave:he}}const se="/api/admin";function dt(){const{id:y}=ze(),c=!!y,u=Pe(),{hasPermission:b}=Ae(),z=De(),{loading:C,saving:I,errors:_,setErrors:T,form:i,updateForm:v,items:k,setItems:x,sections:l,customers:j,itemTemplates:W,scopeTemplates:p,orderInfo:d,offerStatus:g,setOfferStatus:D,totals:O,draftSavedAtLabel:P,selectCustomer:w,clearCustomer:ae,updateItem:ne,addItem:U,removeItem:Z,addItemFromTemplate:V,addSection:J,removeSection:G,updateSection:ie,moveSection:re,loadScopeTemplate:Y,handleSave:oe}=tt({id:y,isEdit:c,alert:u,navigate:z}),[le,ce]=o.useState(!1),[de,me]=o.useState(!1),[ue,H]=o.useState(!1),[pe,Q]=o.useState(!1),[E,X]=o.useState(!1),[ee,R]=o.useState(!1),[he,K]=o.useState(!1),[t,s]=o.useState(!1),[a,m]=o.useState(""),[n,h]=o.useState(null),[f,xe]=o.useState(!1);Ie(ee);const N=g==="invalidated",_e=c&&!N&&!d&&i.valid_until&&new Date(i.valid_until){if(!a.trim()){u.error("Číslo objednávky zákazníka je povinné");return}X(!0);try{const r=new FormData;r.append("quotationId",y),r.append("customerOrderNumber",a.trim()),n&&r.append("attachment",n);const $=await(await A(`${se}/orders.php`,{method:"POST",body:r})).json();$.success?(R(!1),u.success($.message||"Objednávka byla vytvořena"),z(`/orders/${$.data.order_id}`)):u.error($.error||"Nepodařilo se vytvořit objednávku")}catch{u.error("Chyba připojení")}finally{X(!1)}},Ne=async()=>{s(!0);try{const S=await(await A(`${se}/offers.php?action=invalidate&id=${y}`,{method:"POST"})).json();S.success?(K(!1),D("invalidated"),u.success(S.message||"Nabídka byla zneplatněna")):u.error(S.error||"Nepodařilo se zneplatnit nabídku")}catch{u.error("Chyba připojení")}finally{s(!1)}},ke=async()=>{Q(!0);try{const S=await(await A(`${se}/offers.php?id=${y}`,{method:"DELETE"})).json();S.success?(u.success(S.message||"Nabídka byla smazána"),z("/offers")):u.error(S.error||"Nepodařilo se smazat nabídku")}catch{u.error("Chyba připojení")}finally{Q(!1),H(!1)}},we=async()=>{if(!(!c||f)){xe(!0);try{const r=await A(`${se}/offers-pdf.php?id=${y}`);if(r.status===401)return;if(!r.ok){u.error("Nepodařilo se vygenerovat PDF");return}const S=await r.text(),$=window.open("","_blank");$?($.document.open(),$.document.write(S),$.document.close(),$.onload=()=>$.print()):u.error("Prohlížeč zablokoval vyskakovací okno")}catch{u.error("Chyba při generování PDF")}finally{xe(!1)}}},Se=c?N?"offers.view":"offers.edit":"offers.create";return b(Se)?C?e.jsxs("div",{className:"admin-skeleton",style:{padding:0,gap:"1.5rem"},children:[e.jsxs("div",{className:"admin-skeleton-row",style:{justifyContent:"space-between"},children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"0.75rem"},children:[e.jsx("div",{className:"admin-skeleton-line",style:{width:"32px",height:"32px",borderRadius:"8px"}}),e.jsx("div",{className:"admin-skeleton-line h-8",style:{width:"200px"}})]}),e.jsxs("div",{className:"admin-skeleton-row",style:{gap:"0.5rem"},children:[e.jsx("div",{className:"admin-skeleton-line h-10",style:{width:"100px",borderRadius:"8px"}}),e.jsx("div",{className:"admin-skeleton-line h-10",style:{width:"100px",borderRadius:"8px"}})]})]}),e.jsx("div",{className:"admin-card",children:e.jsx("div",{className:"admin-skeleton",style:{gap:"1.25rem"},children:[0,1,2,3].map(r=>e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line w-1/4"}),e.jsx("div",{className:"admin-skeleton-line w-1/2"})]},r))})}),e.jsx("div",{className:"admin-card",children:e.jsx("div",{className:"admin-skeleton",style:{gap:"1.25rem"},children:[0,1,2].map(r=>e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{style:{flex:1},children:e.jsx("div",{className:"admin-skeleton-line w-full"})}),e.jsx("div",{style:{flex:1},children:e.jsx("div",{className:"admin-skeleton-line w-3/4"})}),e.jsx("div",{style:{flex:1},children:e.jsx("div",{className:"admin-skeleton-line w-1/2"})})]},r))})}),e.jsx("div",{className:"admin-card",children:e.jsx("div",{className:"admin-skeleton",style:{gap:"1.25rem"},children:[0,1,2].map(r=>e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line w-1/3"}),e.jsx("div",{className:"admin-skeleton-line w-full"})]},r))})})]}):e.jsxs("div",{children:[e.jsxs(L.div,{className:"admin-page-header",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4},children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"1rem"},children:[e.jsx(je,{to:"/offers",className:"admin-btn-icon",title:"Zpět","aria-label":"Zpět",children:e.jsx("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:e.jsx("path",{d:"M19 12H5M12 19l-7-7 7-7"})})}),e.jsxs("div",{children:[e.jsxs("h1",{className:"admin-page-title",children:[c?`Nabídka ${i.quotation_number}`:"Nová nabídka",N&&e.jsx("span",{className:"admin-badge admin-badge-danger",style:{marginLeft:"0.75rem",verticalAlign:"middle",fontSize:"0.75rem"},children:"Zneplatněna"})]}),!c&&P&&e.jsxs("div",{className:"offers-draft-indicator",children:[e.jsx("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2.5",children:e.jsx("polyline",{points:"20 6 9 17 4 12"})}),"Koncept uložen ",P]})]})]}),e.jsxs("div",{className:"admin-page-actions",children:[c&&b("offers.export")&&e.jsx("button",{onClick:we,className:"admin-btn admin-btn-secondary",disabled:f,children:f?e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"admin-spinner",style:{width:16,height:16,borderWidth:2}}),"PDF..."]}):e.jsxs(e.Fragment,{children:[e.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"}),e.jsx("polyline",{points:"14 2 14 8 20 8"})]}),"PDF"]})}),c&&!N&&b("orders.create")&&!d&&e.jsxs("button",{onClick:()=>{m(""),h(null),R(!0)},className:"admin-btn admin-btn-secondary",children:[e.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"}),e.jsx("polyline",{points:"14 2 14 8 20 8"}),e.jsx("line",{x1:"12",y1:"11",x2:"12",y2:"17"}),e.jsx("line",{x1:"9",y1:"14",x2:"15",y2:"14"})]}),"Vytvořit objednávku"]}),c&&d&&e.jsxs(je,{to:`/orders/${d.id}`,className:"admin-btn admin-btn-secondary",children:[e.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"}),e.jsx("polyline",{points:"14 2 14 8 20 8"})]}),"Objednávka ",d.order_number]}),_e&&b("offers.edit")&&e.jsxs("button",{onClick:()=>K(!0),className:"admin-btn admin-btn-secondary",children:[e.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("circle",{cx:"12",cy:"12",r:"10"}),e.jsx("line",{x1:"4.93",y1:"4.93",x2:"19.07",y2:"19.07"})]}),"Zneplatnit"]}),!N&&e.jsx("button",{onClick:oe,className:"admin-btn admin-btn-primary",disabled:I,children:I?e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"admin-spinner",style:{width:16,height:16,borderWidth:2}}),"Ukládání..."]}):"Uložit"}),c&&b("offers.delete")&&e.jsx("button",{onClick:()=>H(!0),className:"admin-btn admin-btn-primary",children:"Smazat"})]})]}),e.jsxs(L.div,{className:`offers-editor-section${N?" offers-readonly":""}`,initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.1},children:[e.jsx("h3",{className:"admin-card-title",children:"Základní údaje"}),e.jsxs("div",{className:"admin-form",children:[e.jsxs("div",{className:"offers-form-row-3",children:[e.jsx(F,{label:"Číslo nabídky",children:e.jsx("input",{type:"text",value:i.quotation_number,className:"admin-form-input",readOnly:!0,style:{backgroundColor:"var(--bg-secondary)",cursor:"default"}})}),e.jsx(F,{label:"Kód projektu",children:e.jsx("input",{type:"text",value:i.project_code,onChange:r=>v("project_code",r.target.value),className:"admin-form-input",placeholder:"Volitelný kód projektu",readOnly:N})}),e.jsx(Ge,{customers:j,customerId:i.customer_id,customerName:i.customer_name,onSelect:w,onClear:ae,error:_.customer_id,readOnly:N})]}),e.jsxs("div",{className:"admin-form-row",children:[e.jsx(F,{label:"Datum vytvoření",error:_.created_at,required:!0,children:N?e.jsx("input",{type:"text",value:i.created_at,className:"admin-form-input",readOnly:!0}):e.jsx(ve,{mode:"date",value:i.created_at,onChange:r=>{v("created_at",r),T(S=>({...S,created_at:void 0}))}})}),e.jsx(F,{label:"Platnost do",error:_.valid_until,required:!0,children:N?e.jsx("input",{type:"text",value:i.valid_until,className:"admin-form-input",readOnly:!0}):e.jsx(ve,{mode:"date",value:i.valid_until,onChange:r=>{v("valid_until",r),T(S=>({...S,valid_until:void 0}))}})})]}),e.jsxs("div",{className:"admin-form-row",children:[e.jsx(F,{label:"Měna",children:e.jsxs("select",{value:i.currency,onChange:r=>v("currency",r.target.value),className:"admin-form-select",disabled:N,children:[e.jsx("option",{value:"EUR",children:"EUR (€)"}),e.jsx("option",{value:"USD",children:"USD ($)"}),e.jsx("option",{value:"CZK",children:"CZK (Kč)"}),e.jsx("option",{value:"GBP",children:"GBP (£)"})]})}),e.jsx(F,{label:"Jazyk nabídky",children:e.jsxs("select",{value:i.language,onChange:r=>v("language",r.target.value),className:"admin-form-select",disabled:N,children:[e.jsx("option",{value:"EN",children:"English"}),e.jsx("option",{value:"CZ",children:"Čeština"})]})})]}),e.jsxs("div",{className:"offers-form-row-3",children:[e.jsx(F,{label:"Sazba DPH (%)",children:e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"0.75rem"},children:[e.jsx("input",{type:"number",value:i.vat_rate,onChange:r=>v("vat_rate",parseFloat(r.target.value)||0),className:"admin-form-input",step:"0.1",style:{flex:1},readOnly:N}),e.jsxs("label",{className:"admin-form-checkbox",style:{whiteSpace:"nowrap"},children:[e.jsx("input",{type:"checkbox",checked:i.apply_vat,onChange:r=>v("apply_vat",r.target.checked),disabled:N}),e.jsx("span",{children:"Účtovat DPH"})]})]})}),e.jsx(F,{label:"Směnný kurz",children:e.jsx("input",{type:"number",value:i.exchange_rate,onChange:r=>v("exchange_rate",r.target.value),className:"admin-form-input",placeholder:"Volitelný",step:"0.0001",readOnly:N})})]})]})]}),e.jsx(Ue,{items:k,setItems:x,updateItem:ne,addItem:U,removeItem:Z,itemTemplates:W,showItemTemplateMenu:le,setShowItemTemplateMenu:ce,addItemFromTemplate:V,totals:O,currency:i.currency,applyVat:i.apply_vat,vatRate:i.vat_rate,itemsError:_.items,readOnly:N}),e.jsx(Je,{sections:l,addSection:J,removeSection:G,updateSection:ie,moveSection:re,scopeTemplates:p,showScopeTemplateMenu:de,setShowScopeTemplateMenu:me,loadScopeTemplate:Y,form:i,updateForm:v,readOnly:N}),e.jsx(Ce,{children:ee&&e.jsxs(L.div,{className:"admin-modal-overlay",initial:{opacity:0},animate:{opacity:1},exit:{opacity:0},transition:{duration:.2},children:[e.jsx("div",{className:"admin-modal-backdrop",onClick:()=>!E&&R(!1)}),e.jsxs(L.div,{className:"admin-modal",initial:{opacity:0,scale:.95,y:20},animate:{opacity:1,scale:1,y:0},exit:{opacity:0,scale:.95,y:20},transition:{duration:.2},children:[e.jsx("div",{className:"admin-modal-header",children:e.jsx("h2",{className:"admin-modal-title",children:"Vytvořit objednávku"})}),e.jsx("div",{className:"admin-modal-body",children:e.jsxs("div",{className:"admin-form",children:[e.jsx(F,{label:"Číslo objednávky zákazníka",required:!0,children:e.jsx("input",{type:"text",value:a,onChange:r=>m(r.target.value),onKeyDown:r=>r.key==="Enter"&&!E&&ye(),className:"admin-form-input",placeholder:"Např. PO-2026-001",autoFocus:!0})}),e.jsxs(F,{label:"Příloha (PDF)",children:[n?e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"0.5rem"},children:[e.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"var(--accent-color)",strokeWidth:"2",children:[e.jsx("path",{d:"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"}),e.jsx("polyline",{points:"14 2 14 8 20 8"})]}),e.jsxs("span",{style:{fontSize:"0.875rem"},children:[n.name," ",e.jsxs("span",{className:"text-tertiary",children:["(",(n.size/1024).toFixed(0)," KB)"]})]}),e.jsx("button",{type:"button",onClick:()=>h(null),className:"admin-btn-icon",title:"Odebrat",style:{marginLeft:"auto"},children:e.jsx("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:e.jsx("path",{d:"M18 6L6 18M6 6l12 12"})})})]}):e.jsxs("label",{className:"admin-btn admin-btn-secondary admin-btn-sm",style:{cursor:"pointer",display:"inline-flex",alignItems:"center",gap:"0.4rem"},children:[e.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"}),e.jsx("polyline",{points:"17 8 12 3 7 8"}),e.jsx("line",{x1:"12",y1:"3",x2:"12",y2:"15"})]}),"Vybrat soubor",e.jsx("input",{type:"file",accept:"application/pdf",onChange:r=>h(r.target.files[0]||null),style:{display:"none"}})]}),e.jsx("small",{className:"admin-form-hint",style:{marginTop:"0.25rem"},children:"Max 10 MB"})]})]})}),e.jsxs("div",{className:"admin-modal-footer",children:[e.jsx("button",{onClick:()=>R(!1),className:"admin-btn admin-btn-secondary",disabled:E,children:"Zrušit"}),e.jsx("button",{onClick:ye,className:"admin-btn admin-btn-primary",disabled:E||!a.trim(),children:E?"Vytváření...":"Vytvořit"})]})]})]})}),e.jsx(ge,{isOpen:he,onClose:()=>K(!1),onConfirm:Ne,title:"Zneplatnit nabídku",message:`Opravdu chcete zneplatnit nabídku "${i.quotation_number}"? Nabídka bude pouze pro čtení a nepůjde upravovat.`,confirmText:"Zneplatnit",cancelText:"Zrušit",type:"danger",loading:t}),e.jsx(ge,{isOpen:ue,onClose:()=>H(!1),onConfirm:ke,title:"Smazat nabídku",message:`Opravdu chcete smazat nabídku "${i.quotation_number}"? Budou smazány i všechny položky a sekce. Tato akce je nevratná.`,confirmText:"Smazat",cancelText:"Zrušit",type:"danger",loading:pe})]}):e.jsx(Te,{})}export{dt as default}; diff --git a/dist/assets/Offers-DwUrbYu8.js b/dist/assets/Offers-DwUrbYu8.js new file mode 100644 index 0000000..beda985 --- /dev/null +++ b/dist/assets/Offers-DwUrbYu8.js @@ -0,0 +1 @@ +import{j as e,m as C,A as ie}from"./vendor-animation-0s3FMHwK.js";import{g as oe,r as n,L as d}from"./vendor-react-BVs3cwbi.js";import{a as re,u as le,b as de,d as ce,e as S,g as he,C as H,F as I,c as y}from"./index-BBlIrj2z.js";import{F as me}from"./Forbidden-D25jV3Oq.js";import{u as xe,a as ue,S as f}from"./useListData-BVkTFDdr.js";import{P as pe}from"./Pagination-B1sbY6V7.js";import"./vendor-utils-Dyr8OjFr.js";const v="/api/admin",E="boha_offer_draft";function we(){const a=re(),{hasPermission:r}=le(),q=oe(),{sort:R,order:c,handleSort:x,activeSort:u}=xe("quotation_number"),[b,K]=n.useState(""),[J,O]=n.useState(1),[g,z]=n.useState({show:!1,quotation:null}),[U,W]=n.useState(!1),[k,_]=n.useState({show:!1,quotation:null}),[Y,A]=n.useState(!1),[G,V]=n.useState(null),[D,T]=n.useState(null),[l,$]=n.useState(null),[h,w]=n.useState({show:!1,quotation:null});de(h.show);const[N,F]=n.useState(""),[p,M]=n.useState(null),[o,L]=n.useState(null),{items:j,loading:Q,pagination:B,refetch:P}=ue("offers.php",{dataKey:"quotations",search:b,sort:R,order:c,page:J,errorMsg:"Nepodařilo se načíst nabídky"});n.useEffect(()=>{try{const t=localStorage.getItem(E);if(!t)return;const s=JSON.parse(t);s&&s.form&&Array.isArray(s.items)&&L(s)}catch{}},[]);const X=()=>{try{localStorage.removeItem(E)}catch{}L(null)},ee=(t,s)=>t?"offers-invalidated-row":s?"offers-expired-row":"";if(!r("offers.view"))return e.jsx(me,{});const te=async t=>{V(t.id);try{const i=await(await y(`${v}/offers.php?action=duplicate&id=${t.id}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({})})).json();i.success?(a.success(i.message||"Nabídka byla duplikována"),P()):a.error(i.error||"Nepodařilo se duplikovat nabídku")}catch{a.error("Chyba připojení")}finally{V(null)}},Z=async()=>{if(!(!N.trim()||!h.quotation)){$(h.quotation.id);try{const t=new FormData;t.append("quotationId",h.quotation.id),t.append("customerOrderNumber",N.trim()),p&&t.append("attachment",p);const i=await(await y(`${v}/orders.php`,{method:"POST",body:t})).json();i.success?(w({show:!1,quotation:null}),a.success(i.message||"Objednávka byla vytvořena"),q(`/orders/${i.data.order_id}`)):a.error(i.error||"Nepodařilo se vytvořit objednávku")}catch{a.error("Chyba připojení")}finally{$(null)}}},se=async()=>{if(g.quotation){W(!0);try{const s=await(await y(`${v}/offers.php?id=${g.quotation.id}`,{method:"DELETE"})).json();s.success?(z({show:!1,quotation:null}),a.success(s.message||"Nabídka byla smazána"),P()):a.error(s.error||"Nepodařilo se smazat nabídku")}catch{a.error("Chyba připojení")}finally{W(!1)}}},ae=async()=>{if(k.quotation){A(!0);try{const s=await(await y(`${v}/offers.php?action=invalidate&id=${k.quotation.id}`,{method:"POST"})).json();s.success?(_({show:!1,quotation:null}),a.success(s.message||"Nabídka byla zneplatněna"),P()):a.error(s.error||"Nepodařilo se zneplatnit nabídku")}catch{a.error("Chyba připojení")}finally{A(!1)}}},ne=async t=>{if(!D){T(t.id);try{const s=await y(`${v}/offers-pdf.php?id=${t.id}`);if(s.status===401)return;if(!s.ok){a.error("Nepodařilo se vygenerovat PDF");return}const i=await s.text(),m=window.open("","_blank");m?(m.document.open(),m.document.write(i),m.document.close(),m.onload=()=>m.print()):a.error("Prohlížeč zablokoval vyskakovací okno")}catch{a.error("Chyba při generování PDF")}finally{T(null)}}};return Q?e.jsxs("div",{className:"admin-skeleton",style:{padding:0,gap:"1.5rem"},children:[e.jsxs("div",{className:"admin-skeleton-row",style:{justifyContent:"space-between"},children:[e.jsxs("div",{children:[e.jsx("div",{className:"admin-skeleton-line h-8",style:{width:"200px",marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line",style:{width:"140px"}})]}),e.jsxs("div",{style:{display:"flex",gap:"0.5rem"},children:[e.jsx("div",{className:"admin-skeleton-line h-10",style:{width:"120px",borderRadius:"8px"}}),e.jsx("div",{className:"admin-skeleton-line h-10",style:{width:"140px",borderRadius:"8px"}})]})]}),e.jsx("div",{className:"admin-card",children:e.jsxs("div",{className:"admin-skeleton",style:{gap:"1.25rem"},children:[e.jsx("div",{className:"admin-skeleton-line h-10",style:{width:"100%",borderRadius:"8px",marginBottom:"0.5rem"}}),[0,1,2,3,4].map(t=>e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line circle"}),e.jsxs("div",{style:{flex:1},children:[e.jsx("div",{className:"admin-skeleton-line w-1/3",style:{marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line w-1/4",style:{height:"10px"}})]}),e.jsx("div",{className:"admin-skeleton-line w-1/4"})]},t))]})})]}):e.jsxs("div",{children:[e.jsxs(C.div,{className:"admin-page-header",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4},children:[e.jsxs("div",{children:[e.jsx("h1",{className:"admin-page-title",children:"Nabídky"}),e.jsxs("p",{className:"admin-page-subtitle",children:[B?.total??j.length," ",ce(B?.total??j.length,"nabídka","nabídky","nabídek")]})]}),e.jsxs("div",{className:"admin-page-actions",children:[r("offers.settings")&&e.jsxs(d,{to:"/offers/templates",className:"admin-btn admin-btn-secondary",children:[e.jsxs("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}),e.jsx("path",{d:"M3 9h18M9 21V9"})]}),"Šablony"]}),r("offers.create")&&e.jsxs(d,{to:"/offers/new",className:"admin-btn admin-btn-primary",children:[e.jsxs("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("line",{x1:"12",y1:"5",x2:"12",y2:"19"}),e.jsx("line",{x1:"5",y1:"12",x2:"19",y2:"12"})]}),"Nová nabídka"]})]})]}),e.jsx(C.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.1},children:e.jsxs("div",{className:"admin-card-body",children:[e.jsx("div",{className:"admin-search-bar",style:{marginBottom:"1rem"},children:e.jsx("input",{type:"text",value:b,onChange:t=>{K(t.target.value),O(1)},className:"admin-form-input",placeholder:"Hledat podle čísla, projektu nebo zákazníka..."})}),j.length===0&&!o?e.jsxs("div",{className:"admin-empty-state",children:[e.jsx("div",{className:"admin-empty-icon",children:e.jsxs("svg",{width:"28",height:"28",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("path",{d:"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"}),e.jsx("polyline",{points:"14 2 14 8 20 8"}),e.jsx("line",{x1:"12",y1:"18",x2:"12",y2:"12"}),e.jsx("line",{x1:"9",y1:"15",x2:"15",y2:"15"})]})}),e.jsx("p",{children:"Zatím nejsou žádné nabídky."}),r("offers.create")&&e.jsx(d,{to:"/offers/new",className:"admin-btn admin-btn-primary",children:"Vytvořit první nabídku"})]}):e.jsx("div",{className:"admin-table-responsive",children:e.jsxs("table",{className:"admin-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsxs("th",{style:{cursor:"pointer"},onClick:()=>x("quotation_number"),children:["Číslo ",e.jsx(f,{column:"quotation_number",sort:u,order:c})]}),e.jsxs("th",{style:{cursor:"pointer"},onClick:()=>x("project_code"),children:["Projekt ",e.jsx(f,{column:"project_code",sort:u,order:c})]}),e.jsx("th",{children:"Zákazník"}),e.jsxs("th",{style:{cursor:"pointer"},onClick:()=>x("created_at"),children:["Datum ",e.jsx(f,{column:"created_at",sort:u,order:c})]}),e.jsxs("th",{style:{cursor:"pointer"},onClick:()=>x("valid_until"),children:["Platnost ",e.jsx(f,{column:"valid_until",sort:u,order:c})]}),e.jsxs("th",{style:{cursor:"pointer"},onClick:()=>x("currency"),children:["Měna ",e.jsx(f,{column:"currency",sort:u,order:c})]}),e.jsx("th",{style:{textAlign:"right"},children:"Celkem"}),e.jsx("th",{children:"Akce"})]})}),e.jsxs("tbody",{children:[o&&!b&&e.jsxs("tr",{className:"offers-draft-row",children:[e.jsx("td",{children:e.jsxs("span",{className:"offers-draft-row-label",children:["Koncept",o.savedAt&&e.jsxs("span",{style:{fontWeight:400,opacity:.8},children:[" · ",new Date(o.savedAt).toLocaleTimeString("cs-CZ",{hour:"2-digit",minute:"2-digit"})]})]})}),e.jsx("td",{children:o.form.project_code||"—"}),e.jsx("td",{children:o.form.customer_name||"—"}),e.jsx("td",{className:"admin-mono",children:o.form.created_at?S(o.form.created_at):"—"}),e.jsx("td",{className:"admin-mono",children:o.form.valid_until?S(o.form.valid_until):"—"}),e.jsx("td",{children:e.jsx("span",{className:"admin-badge admin-badge-secondary",children:o.form.currency||"—"})}),e.jsx("td",{}),e.jsx("td",{children:e.jsxs("div",{className:"admin-table-actions",children:[e.jsx(d,{to:"/offers/new",className:"admin-btn-icon",title:"Pokračovat v konceptu","aria-label":"Pokračovat v konceptu",children:e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"}),e.jsx("path",{d:"M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"})]})}),e.jsx("button",{onClick:X,className:"admin-btn-icon danger",title:"Zahodit koncept",children:e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("polyline",{points:"3 6 5 6 21 6"}),e.jsx("path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"})]})})]})})]}),j.map(t=>{const s=t.status==="invalidated",i=!s&&!t.order_id&&t.valid_until&&new Date(t.valid_until)te(t),className:"admin-btn-icon",title:"Duplikovat",disabled:G===t.id,children:e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("rect",{x:"9",y:"9",width:"13",height:"13",rx:"2",ry:"2"}),e.jsx("path",{d:"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"})]})}),!s&&t.order_id?e.jsx(d,{to:`/orders/${t.order_id}`,className:"admin-btn-icon accent",title:"Zobrazit objednávku","aria-label":"Zobrazit objednávku",children:e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"}),e.jsx("polyline",{points:"14 2 14 8 20 8"}),e.jsx("text",{x:"12",y:"16.5",textAnchor:"middle",fill:"currentColor",stroke:"none",fontSize:"9",fontWeight:"700",children:"O"})]})}):!s&&r("orders.create")&&e.jsx("button",{onClick:()=>{F(""),M(null),w({show:!0,quotation:t})},className:"admin-btn-icon",title:"Vytvořit objednávku",disabled:l===t.id,children:l===t.id?e.jsx("div",{className:"admin-spinner",style:{width:18,height:18,borderWidth:2}}):e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"}),e.jsx("polyline",{points:"14 2 14 8 20 8"}),e.jsx("line",{x1:"12",y1:"11",x2:"12",y2:"17"}),e.jsx("line",{x1:"9",y1:"14",x2:"15",y2:"14"})]})}),i&&!s&&r("offers.edit")&&e.jsx("button",{onClick:()=>_({show:!0,quotation:t}),className:"admin-btn-icon",title:"Zneplatnit",children:e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("circle",{cx:"12",cy:"12",r:"10"}),e.jsx("line",{x1:"4.93",y1:"4.93",x2:"19.07",y2:"19.07"})]})}),r("offers.export")&&e.jsx("button",{onClick:()=>ne(t),className:"admin-btn-icon",title:"PDF",disabled:D===t.id,children:D===t.id?e.jsx("div",{className:"admin-spinner",style:{width:18,height:18,borderWidth:2}}):e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"}),e.jsx("polyline",{points:"14 2 14 8 20 8"}),e.jsx("line",{x1:"16",y1:"13",x2:"8",y2:"13"}),e.jsx("line",{x1:"16",y1:"17",x2:"8",y2:"17"})]})}),r("offers.delete")&&e.jsx("button",{onClick:()=>z({show:!0,quotation:t}),className:"admin-btn-icon danger",title:"Smazat",children:e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("polyline",{points:"3 6 5 6 21 6"}),e.jsx("path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"})]})})]})})]},t.id)}),j.length===0&&o&&b&&e.jsx("tr",{children:e.jsx("td",{colSpan:8,className:"text-muted",style:{textAlign:"center",padding:"1.5rem"},children:"Žádné nabídky odpovídající hledání."})})]})]})}),e.jsx(pe,{pagination:B,onPageChange:O})]})}),e.jsx(H,{isOpen:g.show,onClose:()=>z({show:!1,quotation:null}),onConfirm:se,title:"Smazat nabídku",message:`Opravdu chcete smazat nabídku "${g.quotation?.quotation_number}"? Budou smazány i všechny položky a sekce. Tato akce je nevratná.`,confirmText:"Smazat",cancelText:"Zrušit",type:"danger",loading:U}),e.jsx(H,{isOpen:k.show,onClose:()=>_({show:!1,quotation:null}),onConfirm:ae,title:"Zneplatnit nabídku",message:`Opravdu chcete zneplatnit nabídku "${k.quotation?.quotation_number}"? Nabídka bude pouze pro čtení a nepůjde upravovat.`,confirmText:"Zneplatnit",cancelText:"Zrušit",type:"danger",loading:Y}),e.jsx(ie,{children:h.show&&e.jsxs(C.div,{className:"admin-modal-overlay",initial:{opacity:0},animate:{opacity:1},exit:{opacity:0},transition:{duration:.2},children:[e.jsx("div",{className:"admin-modal-backdrop",onClick:()=>!l&&w({show:!1,quotation:null})}),e.jsxs(C.div,{className:"admin-modal",initial:{opacity:0,scale:.95,y:20},animate:{opacity:1,scale:1,y:0},exit:{opacity:0,scale:.95,y:20},transition:{duration:.2},children:[e.jsxs("div",{className:"admin-modal-header",children:[e.jsx("h2",{className:"admin-modal-title",children:"Vytvořit objednávku"}),e.jsxs("p",{className:"text-secondary",style:{marginTop:"0.25rem",fontSize:"0.875rem"},children:["Nabídka: ",e.jsx("strong",{children:h.quotation?.quotation_number})]})]}),e.jsx("div",{className:"admin-modal-body",children:e.jsxs("div",{className:"admin-form",children:[e.jsx(I,{label:"Číslo objednávky zákazníka",required:!0,children:e.jsx("input",{type:"text",value:N,onChange:t=>F(t.target.value),onKeyDown:t=>t.key==="Enter"&&!l&&Z(),className:"admin-form-input",placeholder:"Např. PO-2026-001",autoFocus:!0})}),e.jsxs(I,{label:"Příloha (PDF)",children:[p?e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"0.5rem"},children:[e.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"var(--accent-color)",strokeWidth:"2",children:[e.jsx("path",{d:"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"}),e.jsx("polyline",{points:"14 2 14 8 20 8"})]}),e.jsxs("span",{style:{fontSize:"0.875rem"},children:[p.name," ",e.jsxs("span",{className:"text-tertiary",children:["(",(p.size/1024).toFixed(0)," KB)"]})]}),e.jsx("button",{type:"button",onClick:()=>M(null),className:"admin-btn-icon",title:"Odebrat",style:{marginLeft:"auto"},children:e.jsx("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:e.jsx("path",{d:"M18 6L6 18M6 6l12 12"})})})]}):e.jsxs("label",{className:"admin-btn admin-btn-secondary admin-btn-sm",style:{cursor:"pointer",display:"inline-flex",alignItems:"center",gap:"0.4rem"},children:[e.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"}),e.jsx("polyline",{points:"17 8 12 3 7 8"}),e.jsx("line",{x1:"12",y1:"3",x2:"12",y2:"15"})]}),"Vybrat soubor",e.jsx("input",{type:"file",accept:"application/pdf",onChange:t=>M(t.target.files[0]||null),style:{display:"none"}})]}),e.jsx("small",{className:"admin-form-hint",style:{marginTop:"0.25rem"},children:"Max 10 MB"})]})]})}),e.jsxs("div",{className:"admin-modal-footer",children:[e.jsx("button",{onClick:()=>w({show:!1,quotation:null}),className:"admin-btn admin-btn-secondary",disabled:!!l,children:"Zrušit"}),e.jsx("button",{onClick:Z,className:"admin-btn admin-btn-primary",disabled:!!l||!N.trim(),children:l?"Vytváření...":"Vytvořit"})]})]})]})})]})}export{we as default}; diff --git a/dist/assets/OffersCustomers-BjvYTLYl.js b/dist/assets/OffersCustomers-BjvYTLYl.js new file mode 100644 index 0000000..3c20e52 --- /dev/null +++ b/dist/assets/OffersCustomers-BjvYTLYl.js @@ -0,0 +1 @@ +import{j as e,m as b,A as G}from"./vendor-animation-0s3FMHwK.js";import{r as l}from"./vendor-react-BVs3cwbi.js";import{a as Q,u as X,b as Y,c as S,F as m,C as ee}from"./index-BBlIrj2z.js";import{F as ae}from"./Forbidden-D25jV3Oq.js";import"./vendor-utils-Dyr8OjFr.js";const k="/api/admin",g=["street","city_postal","country","company_id","vat_id"],W={street:"Ulice",city_postal:"Město + PSČ",country:"Země",company_id:"IČO",vat_id:"DIČ"};function re(){const o=Q(),{hasPermission:x}=X(),[A,U]=l.useState(!0),[F,Z]=l.useState([]),[h,R]=l.useState(""),[B,N]=l.useState(!1),[p,z]=l.useState(null),[j,D]=l.useState(!1),[d,c]=l.useState({name:"",street:"",city:"",postal_code:"",country:"",company_id:"",vat_id:""}),[r,u]=l.useState([]),M=l.useRef(0),[O,y]=l.useState([...g]),[v,_]=l.useState({show:!1,customer:null}),[q,E]=l.useState(!1);Y(B);const C=l.useCallback(()=>{const a=[...g],t=[...O].filter(s=>s!=="name");for(const s of a)t.includes(s)||t.push(s);for(let s=0;ss.startsWith("custom_")?parseInt(s.split("_")[1]){const s=C(),n=a+t;if(n<0||n>=s.length)return;const i=[...s];[i[a],i[n]]=[i[n],i[a]],y(i)},V=a=>{if(W[a])return W[a];if(a.startsWith("custom_")){const t=parseInt(a.split("_")[1]),s=r[t];if(s)return s.name?`${s.name}: ${s.value||"..."}`:s.value||`Vlastní pole ${t+1}`}return a},f=l.useCallback(async()=>{try{const a=await S(`${k}/customers.php`);if(a.status===401)return;const t=await a.json();t.success?Z(t.data.customers):o.error(t.error||"Nepodařilo se načíst zákazníky")}catch{o.error("Chyba připojení")}finally{U(!1)}},[o]);l.useEffect(()=>{f()},[f]);const L=()=>{z(null),c({name:"",street:"",city:"",postal_code:"",country:"",company_id:"",vat_id:""}),u([]),y([...g]),N(!0)},H=a=>{z(a),c({name:a.name||"",street:a.street||"",city:a.city||"",postal_code:a.postal_code||"",country:a.country||"",company_id:a.company_id||"",vat_id:a.vat_id||""});const t=Array.isArray(a.custom_fields)&&a.custom_fields.length>0?a.custom_fields.map(s=>({...s,_key:`cf-${++M.current}`})):[];u(t),Array.isArray(a.customer_field_order)&&a.customer_field_order.length>0?y(a.customer_field_order):y([...g]),N(!0)},w=()=>{N(!1),z(null)},K=async()=>{if(!d.name.trim()){o.error("Název zákazníka je povinný");return}D(!0);try{const a=p?`${k}/customers.php?id=${p.id}`:`${k}/customers.php`,t={...d,custom_fields:r.filter(i=>i.name.trim()||i.value.trim()),customer_field_order:C()},n=await(await S(a,{method:p?"PUT":"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json();n.success?(w(),await new Promise(i=>setTimeout(i,300)),o.success(n.message||(p?"Zákazník byl aktualizován":"Zákazník byl vytvořen")),f()):o.error(n.error||"Nepodařilo se uložit zákazníka")}catch{o.error("Chyba připojení")}finally{D(!1)}},J=async()=>{if(v.customer){E(!0);try{const t=await(await S(`${k}/customers.php?id=${v.customer.id}`,{method:"DELETE"})).json();t.success?(_({show:!1,customer:null}),o.success(t.message||"Zákazník byl smazán"),f()):o.error(t.error||"Nepodařilo se smazat zákazníka")}catch{o.error("Chyba připojení")}finally{E(!1)}}};if(!x("offers.view"))return e.jsx(ae,{});const $=h?F.filter(a=>(a.name||"").toLowerCase().includes(h.toLowerCase())||(a.company_id||"").includes(h)||(a.city||"").toLowerCase().includes(h.toLowerCase())):F;if(A)return e.jsxs("div",{className:"admin-skeleton",style:{padding:0,gap:"1.5rem"},children:[e.jsxs("div",{className:"admin-skeleton-row",style:{justifyContent:"space-between"},children:[e.jsxs("div",{children:[e.jsx("div",{className:"admin-skeleton-line h-8",style:{width:"200px",marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line",style:{width:"140px"}})]}),e.jsx("div",{className:"admin-skeleton-line h-10",style:{width:"160px",borderRadius:"8px"}})]}),e.jsx("div",{className:"admin-card",children:e.jsxs("div",{className:"admin-skeleton",style:{gap:"1.25rem"},children:[e.jsx("div",{className:"admin-skeleton-line h-10",style:{width:"100%",borderRadius:"8px",marginBottom:"0.5rem"}}),[0,1,2,3,4].map(a=>e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line circle"}),e.jsxs("div",{style:{flex:1},children:[e.jsx("div",{className:"admin-skeleton-line w-1/3",style:{marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line w-1/4",style:{height:"10px"}})]}),e.jsx("div",{className:"admin-skeleton-line w-1/4"})]},a))]})})]});const P=C();return e.jsxs("div",{children:[e.jsxs(b.div,{className:"admin-page-header",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4},children:[e.jsxs("div",{children:[e.jsx("h1",{className:"admin-page-title",children:"Zákazníci"}),e.jsx("p",{className:"admin-page-subtitle",children:"Správa zákazníků pro nabídky"})]}),x("offers.create")&&e.jsxs("button",{onClick:L,className:"admin-btn admin-btn-primary",children:[e.jsxs("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("line",{x1:"12",y1:"5",x2:"12",y2:"19"}),e.jsx("line",{x1:"5",y1:"12",x2:"19",y2:"12"})]}),"Přidat zákazníka"]})]}),e.jsx(b.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.1},children:e.jsxs("div",{className:"admin-card-body",children:[e.jsx("div",{className:"admin-search-bar",style:{marginBottom:"1rem"},children:e.jsx("input",{type:"text",value:h,onChange:a=>R(a.target.value),className:"admin-form-input",placeholder:"Hledat zákazníky..."})}),$.length===0?e.jsxs("div",{className:"admin-empty-state",children:[e.jsx("p",{children:h?"Žádní zákazníci odpovídající hledání.":"Zatím nejsou žádní zákazníci."}),!h&&x("offers.create")&&e.jsx("button",{onClick:L,className:"admin-btn admin-btn-primary",children:"Přidat prvního zákazníka"})]}):e.jsx("div",{className:"admin-table-responsive",children:e.jsxs("table",{className:"admin-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Název"}),e.jsx("th",{children:"Město"}),e.jsx("th",{children:"IČO"}),e.jsx("th",{children:"DIČ"}),e.jsx("th",{children:"Nabídky"}),e.jsx("th",{children:"Akce"})]})}),e.jsx("tbody",{children:$.map(a=>e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx("div",{style:{fontWeight:500,color:"var(--text-primary)"},children:a.name}),a.street&&e.jsx("div",{className:"text-tertiary",style:{fontSize:"11px"},children:a.street})]}),e.jsx("td",{children:a.city||"—"}),e.jsx("td",{children:a.company_id||"—"}),e.jsx("td",{children:a.vat_id||"—"}),e.jsx("td",{children:e.jsx("span",{className:"admin-badge admin-badge-info",children:a.quotation_count||0})}),e.jsx("td",{children:e.jsxs("div",{className:"admin-table-actions",children:[x("offers.edit")&&e.jsx("button",{onClick:()=>H(a),className:"admin-btn-icon",title:"Upravit","aria-label":"Upravit",children:e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"}),e.jsx("path",{d:"M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"})]})}),x("offers.delete")&&e.jsx("button",{onClick:()=>_({show:!0,customer:a}),className:"admin-btn-icon danger",title:a.quotation_count>0?"Nelze smazat zákazníka s nabídkami":"Smazat","aria-label":a.quotation_count>0?"Nelze smazat zákazníka s nabídkami":"Smazat",disabled:a.quotation_count>0,children:e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("polyline",{points:"3 6 5 6 21 6"}),e.jsx("path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"})]})})]})})]},a.id))})]})})]})}),e.jsx(G,{children:B&&e.jsxs(b.div,{className:"admin-modal-overlay",initial:{opacity:0},animate:{opacity:1},exit:{opacity:0},transition:{duration:.2},children:[e.jsx("div",{className:"admin-modal-backdrop",onClick:w}),e.jsxs(b.div,{className:"admin-modal",style:{maxWidth:720},initial:{opacity:0,scale:.95,y:20},animate:{opacity:1,scale:1,y:0},exit:{opacity:0,scale:.95,y:20},transition:{duration:.2},children:[e.jsx("div",{className:"admin-modal-header",children:e.jsx("h2",{className:"admin-modal-title",children:p?"Upravit zákazníka":"Nový zákazník"})}),e.jsx("div",{className:"admin-modal-body",children:e.jsxs("div",{className:"admin-form",children:[e.jsx(m,{label:"Název",required:!0,children:e.jsx("input",{type:"text",value:d.name,onChange:a=>c(t=>({...t,name:a.target.value})),className:"admin-form-input",placeholder:"Název firmy / jméno"})}),e.jsx(m,{label:"Ulice",children:e.jsx("input",{type:"text",value:d.street,onChange:a=>c(t=>({...t,street:a.target.value})),className:"admin-form-input"})}),e.jsxs("div",{className:"admin-form-row",children:[e.jsx(m,{label:"Město",children:e.jsx("input",{type:"text",value:d.city,onChange:a=>c(t=>({...t,city:a.target.value})),className:"admin-form-input"})}),e.jsx(m,{label:"PSČ",children:e.jsx("input",{type:"text",value:d.postal_code,onChange:a=>c(t=>({...t,postal_code:a.target.value})),className:"admin-form-input"})})]}),e.jsx(m,{label:"Země",children:e.jsx("input",{type:"text",value:d.country,onChange:a=>c(t=>({...t,country:a.target.value})),className:"admin-form-input"})}),e.jsxs("div",{className:"admin-form-row",children:[e.jsx(m,{label:"IČO",children:e.jsx("input",{type:"text",value:d.company_id,onChange:a=>c(t=>({...t,company_id:a.target.value})),className:"admin-form-input"})}),e.jsx(m,{label:"DIČ",children:e.jsx("input",{type:"text",value:d.vat_id,onChange:a=>c(t=>({...t,vat_id:a.target.value})),className:"admin-form-input"})})]}),e.jsxs("div",{style:{marginTop:4},children:[e.jsx("label",{className:"admin-form-label",style:{display:"block",marginBottom:4},children:"Vlastní pole"}),r.map((a,t)=>e.jsxs("div",{style:{marginBottom:8},children:[e.jsxs("div",{className:"admin-form-row",style:{marginBottom:0,alignItems:"flex-end"},children:[e.jsx(m,{label:t===0?"Název":" ",style:{flex:1},children:e.jsx("input",{type:"text",value:a.name,onChange:s=>{const n=[...r];n[t]={...n[t],name:s.target.value},u(n)},className:"admin-form-input",placeholder:"Např. Kontakt"})}),e.jsx(m,{label:t===0?"Hodnota":" ",style:{flex:1},children:e.jsxs("div",{style:{display:"flex",gap:4,alignItems:"center"},children:[e.jsx("input",{type:"text",value:a.value,onChange:s=>{const n=[...r];n[t]={...n[t],value:s.target.value},u(n)},className:"admin-form-input",style:{flex:1}}),e.jsx("button",{type:"button",onClick:()=>{const s=`custom_${t}`;y(n=>n.filter(i=>i!==s).map(i=>{if(i.startsWith("custom_")){const T=parseInt(i.split("_")[1]);if(T>t)return`custom_${T-1}`}return i})),u(r.filter((n,i)=>i!==t))},className:"admin-btn-icon danger",title:"Odebrat pole","aria-label":"Odebrat pole",children:e.jsxs("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),e.jsx("line",{x1:"6",y1:"6",x2:"18",y2:"18"})]})})]})})]}),e.jsxs("label",{className:"admin-form-checkbox",style:{marginTop:4},children:[e.jsx("input",{type:"checkbox",checked:a.showLabel!==!1,onChange:s=>{const n=[...r];n[t]={...n[t],showLabel:s.target.checked},u(n)}}),e.jsx("span",{style:{fontSize:"0.8rem"},children:"Zobrazit název v PDF"})]})]},a._key)),e.jsxs("button",{type:"button",onClick:()=>u([...r,{name:"",value:"",showLabel:!0,_key:`cf-${++M.current}`}]),className:"admin-btn admin-btn-secondary",style:{marginTop:4,fontSize:"0.85rem"},children:[e.jsxs("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("line",{x1:"12",y1:"5",x2:"12",y2:"19"}),e.jsx("line",{x1:"5",y1:"12",x2:"19",y2:"12"})]}),"Přidat pole"]})]}),e.jsxs("div",{style:{marginTop:16},children:[e.jsx("label",{className:"admin-form-label",children:"Pořadí polí v PDF"}),e.jsx("small",{className:"admin-form-hint",style:{display:"block",marginBottom:8},children:"Určuje pořadí řádků v adresním bloku zákazníka na PDF nabídce."}),e.jsx("div",{className:"admin-reorder-list",children:P.map((a,t)=>e.jsxs("div",{className:"admin-reorder-item",children:[e.jsxs("div",{className:"admin-reorder-arrows",children:[e.jsx("button",{type:"button",onClick:()=>I(t,-1),disabled:t===0,className:"admin-btn-icon",title:"Nahoru","aria-label":"Nahoru",children:e.jsx("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:e.jsx("path",{d:"M18 15l-6-6-6 6"})})}),e.jsx("button",{type:"button",onClick:()=>I(t,1),disabled:t===P.length-1,className:"admin-btn-icon",title:"Dolů","aria-label":"Dolů",children:e.jsx("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:e.jsx("path",{d:"M6 9l6 6 6-6"})})})]}),e.jsx("span",{className:`admin-reorder-label${a.startsWith("custom_")?" accent":""}`,children:V(a)})]},a))})]})]})}),e.jsxs("div",{className:"admin-modal-footer",children:[e.jsx("button",{type:"button",onClick:w,className:"admin-btn admin-btn-secondary",disabled:j,children:"Zrušit"}),e.jsxs("button",{type:"button",onClick:K,className:"admin-btn admin-btn-primary",disabled:j,children:[j&&e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"admin-spinner",style:{width:16,height:16,borderWidth:2}}),"Ukládání..."]}),!j&&(p?"Uložit změny":"Vytvořit zákazníka")]})]})]})]})}),e.jsx(ee,{isOpen:v.show,onClose:()=>_({show:!1,customer:null}),onConfirm:J,title:"Smazat zákazníka",message:`Opravdu chcete smazat zákazníka "${v.customer?.name}"? Tato akce je nevratná.`,confirmText:"Smazat",cancelText:"Zrušit",type:"danger",loading:q})]})}export{re as default}; diff --git a/dist/assets/OffersTemplates-bzE8pdbp.js b/dist/assets/OffersTemplates-bzE8pdbp.js new file mode 100644 index 0000000..5c09b12 --- /dev/null +++ b/dist/assets/OffersTemplates-bzE8pdbp.js @@ -0,0 +1 @@ +import{j as e,m as k,A as W}from"./vendor-animation-0s3FMHwK.js";import{r as l}from"./vendor-react-BVs3cwbi.js";import{u as Z,a as D,b as U,c as g,F as j,C as A}from"./index-BBlIrj2z.js";import{F as H}from"./Forbidden-D25jV3Oq.js";import I from"./RichEditor-Bfur5pi6.js";import"./vendor-utils-Dyr8OjFr.js";const N="/api/admin";function ee(){const{hasPermission:n}=Z(),[y,w]=l.useState("items");return n("offers.settings")?e.jsxs("div",{children:[e.jsx(k.div,{className:"admin-page-header",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4},children:e.jsxs("div",{children:[e.jsx("h1",{className:"admin-page-title",children:"Šablony"}),e.jsx("p",{className:"admin-page-subtitle",children:"Šablony položek a rozsahu projektu"})]})}),e.jsxs("div",{className:"offers-tabs",children:[e.jsx("button",{className:`offers-tab ${y==="items"?"active":""}`,onClick:()=>w("items"),children:"Šablony položek"}),e.jsx("button",{className:`offers-tab ${y==="scopes"?"active":""}`,onClick:()=>w("scopes"),children:"Šablony rozsahu"})]}),y==="items"?e.jsx(K,{}):e.jsx(R,{})]}):e.jsx(H,{})}function K(){const n=D(),[y,w]=l.useState(!0),[f,F]=l.useState([]),[z,m]=l.useState(!1),[h,T]=l.useState(null),[p,_]=l.useState(!1),[c,d]=l.useState({name:"",description:"",default_price:0,category:""}),[u,x]=l.useState({show:!1,template:null}),[$,E]=l.useState(!1);U(z);const b=l.useCallback(async()=>{try{const a=await g(`${N}/offers-templates.php?action=items`);if(a.status===401)return;const o=await a.json();o.success&&F(o.data.templates)}catch{n.error("Nepodařilo se načíst šablony")}finally{w(!1)}},[n]);l.useEffect(()=>{b()},[b]);const C=()=>{T(null),d({name:"",description:"",default_price:0,category:""}),m(!0)},P=a=>{T(a),d({name:a.name||"",description:a.description||"",default_price:a.default_price||0,category:a.category||""}),m(!0)},M=async()=>{if(!c.name.trim()){n.error("Název šablony je povinný");return}_(!0);try{const a=h?{...c,id:h.id}:c,S=await(await g(`${N}/offers-templates.php?action=item`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)})).json();S.success?(m(!1),await new Promise(O=>setTimeout(O,300)),n.success(S.message),b()):n.error(S.error)}catch{n.error("Chyba připojení")}finally{_(!1)}},B=async()=>{if(u.template){E(!0);try{const o=await(await g(`${N}/offers-templates.php?action=item&id=${u.template.id}`,{method:"DELETE"})).json();o.success?(x({show:!1,template:null}),n.success(o.message),b()):n.error(o.error)}catch{n.error("Chyba připojení")}finally{E(!1)}}};return y?e.jsx("div",{className:"admin-card",children:e.jsx("div",{className:"admin-skeleton",style:{gap:"1.25rem"},children:[0,1,2,3,4].map(a=>e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line circle"}),e.jsxs("div",{style:{flex:1},children:[e.jsx("div",{className:"admin-skeleton-line w-1/3",style:{marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line w-1/4",style:{height:"10px"}})]}),e.jsx("div",{className:"admin-skeleton-line w-1/4"})]},a))})}):e.jsxs(e.Fragment,{children:[e.jsxs(k.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.1},children:[e.jsxs("div",{className:"admin-card-header",style:{display:"flex",justifyContent:"space-between",alignItems:"center"},children:[e.jsxs("h3",{className:"admin-card-title",children:["Šablony položek (",f.length,")"]}),e.jsxs("button",{onClick:C,className:"admin-btn admin-btn-primary admin-btn-sm",children:[e.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("line",{x1:"12",y1:"5",x2:"12",y2:"19"}),e.jsx("line",{x1:"5",y1:"12",x2:"19",y2:"12"})]}),"Přidat"]})]}),e.jsx("div",{className:"admin-card-body",children:f.length===0?e.jsx("div",{className:"admin-empty-state",children:e.jsx("p",{children:"Zatím žádné šablony položek."})}):e.jsx("div",{className:"admin-table-responsive",children:e.jsxs("table",{className:"admin-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Název"}),e.jsx("th",{children:"Popis"}),e.jsx("th",{children:"Cena"}),e.jsx("th",{children:"Kategorie"}),e.jsx("th",{children:"Akce"})]})}),e.jsx("tbody",{children:f.map(a=>e.jsxs("tr",{children:[e.jsx("td",{style:{fontWeight:500},children:a.name}),e.jsx("td",{style:{color:"var(--text-secondary)"},children:a.description||"—"}),e.jsx("td",{children:Number(a.default_price).toFixed(2)}),e.jsx("td",{style:{color:"var(--text-secondary)"},children:a.category||"—"}),e.jsx("td",{children:e.jsxs("div",{className:"admin-table-actions",children:[e.jsx("button",{onClick:()=>P(a),className:"admin-btn-icon",title:"Upravit","aria-label":"Upravit",children:e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"}),e.jsx("path",{d:"M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"})]})}),e.jsx("button",{onClick:()=>x({show:!0,template:a}),className:"admin-btn-icon danger",title:"Smazat","aria-label":"Smazat",children:e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("polyline",{points:"3 6 5 6 21 6"}),e.jsx("path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"})]})})]})})]},a.id))})]})})})]}),e.jsx(W,{children:z&&e.jsxs(k.div,{className:"admin-modal-overlay",initial:{opacity:0},animate:{opacity:1},exit:{opacity:0},transition:{duration:.2},children:[e.jsx("div",{className:"admin-modal-backdrop",onClick:()=>m(!1)}),e.jsxs(k.div,{className:"admin-modal",initial:{opacity:0,scale:.95,y:20},animate:{opacity:1,scale:1,y:0},exit:{opacity:0,scale:.95,y:20},transition:{duration:.2},children:[e.jsx("div",{className:"admin-modal-header",children:e.jsx("h2",{className:"admin-modal-title",children:h?"Upravit šablonu":"Nová šablona položky"})}),e.jsx("div",{className:"admin-modal-body",children:e.jsxs("div",{className:"admin-form",children:[e.jsx(j,{label:"Název",required:!0,children:e.jsx("input",{type:"text",value:c.name,onChange:a=>d(o=>({...o,name:a.target.value})),className:"admin-form-input"})}),e.jsx(j,{label:"Popis",children:e.jsx("textarea",{value:c.description,onChange:a=>d(o=>({...o,description:a.target.value})),className:"admin-form-input",rows:2})}),e.jsxs("div",{className:"admin-form-row",children:[e.jsx(j,{label:"Výchozí cena",children:e.jsx("input",{type:"number",value:c.default_price,onChange:a=>d(o=>({...o,default_price:parseFloat(a.target.value)||0})),className:"admin-form-input",step:"0.01"})}),e.jsx(j,{label:"Kategorie",children:e.jsx("input",{type:"text",value:c.category,onChange:a=>d(o=>({...o,category:a.target.value})),className:"admin-form-input"})})]})]})}),e.jsxs("div",{className:"admin-modal-footer",children:[e.jsx("button",{type:"button",onClick:()=>m(!1),className:"admin-btn admin-btn-secondary",disabled:p,children:"Zrušit"}),e.jsxs("button",{type:"button",onClick:M,className:"admin-btn admin-btn-primary",disabled:p,children:[p&&e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"admin-spinner",style:{width:16,height:16,borderWidth:2}}),"Ukládání..."]}),!p&&(h?"Uložit":"Vytvořit")]})]})]})]})}),e.jsx(A,{isOpen:u.show,onClose:()=>x({show:!1,template:null}),onConfirm:B,title:"Smazat šablonu",message:`Opravdu chcete smazat šablonu "${u.template?.name}"?`,confirmText:"Smazat",cancelText:"Zrušit",type:"danger",loading:$})]})}function R(){const n=D(),[y,w]=l.useState(!0),[f,F]=l.useState([]),[z,m]=l.useState(!1),[h,T]=l.useState(null),[p,_]=l.useState(!1),[c,d]=l.useState({name:"",sections:[]}),u=l.useRef(0),[x,$]=l.useState({show:!1,template:null}),[E,b]=l.useState(!1);U(z);const C=l.useCallback(async()=>{try{const s=await g(`${N}/offers-templates.php?action=scopes`);if(s.status===401)return;const t=await s.json();t.success&&F(t.data.templates)}catch{n.error("Nepodařilo se načíst šablony")}finally{w(!1)}},[n]);l.useEffect(()=>{C()},[C]);const P=()=>{T(null),d({name:"",sections:[{_key:`sc-${++u.current}`,title:"",title_cz:"",content:""}]}),m(!0)},M=async s=>{try{const i=await(await g(`${N}/offers-templates.php?action=scope_detail&id=${s.id}`)).json();i.success&&(T(i.data),d({name:i.data.name||"",sections:i.data.sections?.length?i.data.sections.map(r=>({_key:`sc-${++u.current}`,title:r.title||"",title_cz:r.title_cz||"",content:r.content||""})):[{_key:`sc-${++u.current}`,title:"",title_cz:"",content:""}]}),m(!0))}catch{n.error("Nepodařilo se načíst detail šablony")}},B=()=>{d(s=>({...s,sections:[...s.sections,{_key:`sc-${++u.current}`,title:"",title_cz:"",content:""}]}))},a=s=>{d(t=>({...t,sections:t.sections.filter((i,r)=>r!==s)}))},o=(s,t,i)=>{d(r=>({...r,sections:r.sections.map((v,V)=>V===s?{...v,[t]:i}:v)}))},S=(s,t)=>{d(i=>{const r=[...i.sections],v=s+t;return v<0||v>=r.length?i:([r[s],r[v]]=[r[v],r[s]],{...i,sections:r})})},O=async()=>{if(!c.name.trim()){n.error("Název šablony je povinný");return}_(!0);try{const s=h?{...c,id:h.id}:c,i=await(await g(`${N}/offers-templates.php?action=scope`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)})).json();i.success?(m(!1),await new Promise(r=>setTimeout(r,300)),n.success(i.message),C()):n.error(i.error)}catch{n.error("Chyba připojení")}finally{_(!1)}},L=async()=>{if(x.template){b(!0);try{const t=await(await g(`${N}/offers-templates.php?action=scope&id=${x.template.id}`,{method:"DELETE"})).json();t.success?($({show:!1,template:null}),n.success(t.message),C()):n.error(t.error)}catch{n.error("Chyba připojení")}finally{b(!1)}}};return y?e.jsx("div",{className:"admin-card",children:e.jsx("div",{className:"admin-skeleton",style:{gap:"1.25rem"},children:[0,1,2,3,4].map(s=>e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line circle"}),e.jsxs("div",{style:{flex:1},children:[e.jsx("div",{className:"admin-skeleton-line w-1/3",style:{marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line w-1/4",style:{height:"10px"}})]}),e.jsx("div",{className:"admin-skeleton-line w-1/4"})]},s))})}):e.jsxs(e.Fragment,{children:[e.jsxs(k.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.1},children:[e.jsxs("div",{className:"admin-card-header",style:{display:"flex",justifyContent:"space-between",alignItems:"center"},children:[e.jsxs("h3",{className:"admin-card-title",children:["Šablony rozsahu (",f.length,")"]}),e.jsxs("button",{onClick:P,className:"admin-btn admin-btn-primary admin-btn-sm",children:[e.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("line",{x1:"12",y1:"5",x2:"12",y2:"19"}),e.jsx("line",{x1:"5",y1:"12",x2:"19",y2:"12"})]}),"Přidat"]})]}),e.jsx("div",{className:"admin-card-body",children:f.length===0?e.jsx("div",{className:"admin-empty-state",children:e.jsx("p",{children:"Zatím žádné šablony rozsahu."})}):e.jsx("div",{className:"admin-table-responsive",children:e.jsxs("table",{className:"admin-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Název"}),e.jsx("th",{children:"Akce"})]})}),e.jsx("tbody",{children:f.map(s=>e.jsxs("tr",{children:[e.jsx("td",{style:{fontWeight:500},children:s.name}),e.jsx("td",{children:e.jsxs("div",{className:"admin-table-actions",children:[e.jsx("button",{onClick:()=>M(s),className:"admin-btn-icon",title:"Upravit","aria-label":"Upravit",children:e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"}),e.jsx("path",{d:"M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"})]})}),e.jsx("button",{onClick:()=>$({show:!0,template:s}),className:"admin-btn-icon danger",title:"Smazat","aria-label":"Smazat",children:e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("polyline",{points:"3 6 5 6 21 6"}),e.jsx("path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"})]})})]})})]},s.id))})]})})})]}),e.jsx(W,{children:z&&e.jsxs(k.div,{className:"admin-modal-overlay",initial:{opacity:0},animate:{opacity:1},exit:{opacity:0},transition:{duration:.2},children:[e.jsx("div",{className:"admin-modal-backdrop",onClick:()=>m(!1)}),e.jsxs(k.div,{className:"admin-modal admin-modal-lg",initial:{opacity:0,scale:.95,y:20},animate:{opacity:1,scale:1,y:0},exit:{opacity:0,scale:.95,y:20},transition:{duration:.2},children:[e.jsx("div",{className:"admin-modal-header",children:e.jsx("h2",{className:"admin-modal-title",children:h?"Upravit šablonu rozsahu":"Nová šablona rozsahu"})}),e.jsx("div",{className:"admin-modal-body",children:e.jsxs("div",{className:"admin-form",children:[e.jsx(j,{label:"Název šablony",required:!0,children:e.jsx("input",{type:"text",value:c.name,onChange:s=>d(t=>({...t,name:s.target.value})),className:"admin-form-input"})}),e.jsxs("div",{className:"admin-form-group",children:[e.jsx("label",{className:"admin-form-label",style:{marginBottom:"0.5rem"},children:"Sekce"}),e.jsx("div",{className:"offers-scope-list",children:c.sections.map((s,t)=>e.jsxs("div",{className:"offers-scope-section",children:[e.jsxs("div",{className:"offers-scope-section-header",children:[e.jsxs("span",{className:"offers-scope-number",children:[t+1,"."]}),e.jsx("span",{className:"offers-scope-title",children:s.title||s.title_cz||`Sekce ${t+1}`}),e.jsxs("div",{className:"offers-scope-actions",children:[e.jsx("button",{type:"button",onClick:()=>S(t,-1),disabled:t===0,className:"admin-btn-icon",title:"Posunout nahoru","aria-label":"Posunout nahoru",children:e.jsx("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:e.jsx("path",{d:"M18 15l-6-6-6 6"})})}),e.jsx("button",{type:"button",onClick:()=>S(t,1),disabled:t===c.sections.length-1,className:"admin-btn-icon",title:"Posunout dolů","aria-label":"Posunout dolů",children:e.jsx("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:e.jsx("path",{d:"M6 9l6 6 6-6"})})}),c.sections.length>1&&e.jsx("button",{type:"button",onClick:()=>a(t),className:"admin-btn-icon danger",title:"Odebrat","aria-label":"Odebrat",children:e.jsxs("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),e.jsx("line",{x1:"6",y1:"6",x2:"18",y2:"18"})]})})]})]}),e.jsxs("div",{className:"admin-form",children:[e.jsxs("div",{className:"admin-form-row",children:[e.jsx(j,{label:e.jsxs(e.Fragment,{children:[e.jsx("span",{className:"offers-lang-badge",children:"EN"})," Název sekce"]}),children:e.jsx("input",{type:"text",value:s.title,onChange:i=>o(t,"title",i.target.value),className:"admin-form-input",placeholder:"Název sekce (anglicky)"})}),e.jsx(j,{label:e.jsxs(e.Fragment,{children:[e.jsx("span",{className:"offers-lang-badge offers-lang-badge-cz",children:"CZ"})," Název sekce"]}),children:e.jsx("input",{type:"text",value:s.title_cz,onChange:i=>o(t,"title_cz",i.target.value),className:"admin-form-input",placeholder:"Název sekce (česky)"})})]}),e.jsx(j,{label:"Obsah",children:e.jsx(I,{value:s.content,onChange:i=>o(t,"content",i),placeholder:"Obsah sekce...",minHeight:"150px"})})]})]},s._key))}),e.jsx("div",{style:{marginTop:"0.75rem"},children:e.jsx("button",{type:"button",onClick:B,className:"admin-btn admin-btn-secondary admin-btn-sm",children:"+ Přidat sekci"})})]})]})}),e.jsxs("div",{className:"admin-modal-footer",children:[e.jsx("button",{type:"button",onClick:()=>m(!1),className:"admin-btn admin-btn-secondary",disabled:p,children:"Zrušit"}),e.jsxs("button",{type:"button",onClick:O,className:"admin-btn admin-btn-primary",disabled:p,children:[p&&e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"admin-spinner",style:{width:16,height:16,borderWidth:2}}),"Ukládání..."]}),!p&&(h?"Uložit":"Vytvořit")]})]})]})]})}),e.jsx(A,{isOpen:x.show,onClose:()=>$({show:!1,template:null}),onConfirm:L,title:"Smazat šablonu",message:`Opravdu chcete smazat šablonu "${x.template?.name}"?`,confirmText:"Smazat",cancelText:"Zrušit",type:"danger",loading:E})]})}export{ee as default}; diff --git a/dist/assets/OrderDetail-3O2WshUa.js b/dist/assets/OrderDetail-3O2WshUa.js new file mode 100644 index 0000000..60f491c --- /dev/null +++ b/dist/assets/OrderDetail-3O2WshUa.js @@ -0,0 +1 @@ +import{j as e,m as j}from"./vendor-animation-0s3FMHwK.js";import{h as Q,g as X,r as i,L as x}from"./vendor-react-BVs3cwbi.js";import{a9 as Y}from"./vendor-utils-Dyr8OjFr.js";import{a as ee,u as se,F as l,e as te,g as y,C as M,c as h}from"./index-BBlIrj2z.js";import{F as ae}from"./Forbidden-D25jV3Oq.js";const p="/api/admin",W={prijata:"Přijatá",v_realizaci:"V realizaci",dokoncena:"Dokončená",stornovana:"Stornována"},ne={prijata:"admin-badge-order-prijata",v_realizaci:"admin-badge-order-realizace",dokoncena:"admin-badge-order-dokoncena",stornovana:"admin-badge-order-stornovana"},I={v_realizaci:"Zahájit realizaci",dokoncena:"Dokončit"},ie={v_realizaci:"admin-btn admin-btn-primary",dokoncena:"admin-btn admin-btn-primary"};function me(){const{id:o}=Q(),n=ee(),{hasPermission:d}=se(),v=X(),[U,D]=i.useState(!0),[t,Z]=i.useState(null),[w,_]=i.useState(""),[S,C]=i.useState(!1),[z,A]=i.useState(null),[c,b]=i.useState({show:!1,status:null}),[R,u]=i.useState(!1),[T,$]=i.useState(""),[f,L]=i.useState(!1),[O,P]=i.useState(!1),[F,g]=i.useState(!1),[H,B]=i.useState(!1),N=async()=>{try{const s=await h(`${p}/orders.php?action=detail&id=${o}`);if(s.status===401)return;const a=await s.json();a.success?(Z(a.data),_(a.data.notes||"")):(n.error(a.error||"Nepodařilo se načíst objednávku"),v("/orders"))}catch{n.error("Chyba připojení"),v("/orders")}finally{D(!1)}};i.useEffect(()=>{N()},[o]);const k=i.useMemo(()=>{if(!t?.items)return{subtotal:0,vatAmount:0,total:0};const s=t.items.reduce((r,m)=>Number(m.is_included_in_total)?r+(Number(m.quantity)||0)*(Number(m.unit_price)||0):r,0),a=Number(t.apply_vat)?s*((Number(t.vat_rate)||0)/100):0;return{subtotal:s,vatAmount:a,total:s+a}},[t]);if(!d("orders.view"))return e.jsx(ae,{});const V=async()=>{if(c.status){A(c.status),b({show:!1,status:null});try{const a=await(await h(`${p}/orders.php?id=${o}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({status:c.status})})).json();a.success?(n.success(a.message||"Stav byl změněn"),N()):n.error(a.error||"Nepodařilo se změnit stav")}catch{n.error("Chyba připojení")}finally{A(null)}}},q=()=>{$(t.order_number),u(!0)},E=async()=>{const s=T.trim();if(s){if(s===t.order_number){u(!1);return}L(!0);try{const r=await(await h(`${p}/orders.php?id=${o}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({order_number:s})})).json();r.success?(n.success("Číslo objednávky bylo změněno"),u(!1),N()):n.error(r.error||"Nepodařilo se změnit číslo")}catch{n.error("Chyba připojení")}finally{L(!1)}}},J=async()=>{C(!0);try{const a=await(await h(`${p}/orders.php?id=${o}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({notes:w})})).json();a.success?n.success("Poznámky byly uloženy"):n.error(a.error||"Nepodařilo se uložit poznámky")}catch{n.error("Chyba připojení")}finally{C(!1)}},K=async()=>{const s=window.open("","_blank");P(!0);try{const a=await h(`${p}/orders.php?action=attachment&id=${o}`);if(!a.ok){s.close(),n.error("Nepodařilo se stáhnout přílohu");return}const r=await a.blob(),m=URL.createObjectURL(r);s.location.href=m,setTimeout(()=>URL.revokeObjectURL(m),6e4)}catch{s.close(),n.error("Chyba připojení")}finally{P(!1)}},G=async()=>{B(!0);try{const a=await(await h(`${p}/orders.php?id=${o}`,{method:"DELETE"})).json();a.success?(n.success(a.message||"Objednávka byla smazána"),v("/orders")):n.error(a.error||"Nepodařilo se smazat objednávku")}catch{n.error("Chyba připojení")}finally{B(!1),g(!1)}};return U?e.jsxs("div",{className:"admin-skeleton",style:{padding:0,gap:"1.5rem"},children:[e.jsxs("div",{className:"admin-skeleton-row",style:{justifyContent:"space-between"},children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"0.75rem"},children:[e.jsx("div",{className:"admin-skeleton-line",style:{width:"32px",height:"32px",borderRadius:"8px"}}),e.jsx("div",{className:"admin-skeleton-line h-8",style:{width:"200px"}})]}),e.jsxs("div",{className:"admin-skeleton-row",style:{gap:"0.5rem"},children:[e.jsx("div",{className:"admin-skeleton-line h-10",style:{width:"100px",borderRadius:"8px"}}),e.jsx("div",{className:"admin-skeleton-line h-10",style:{width:"100px",borderRadius:"8px"}})]})]}),e.jsx("div",{className:"admin-card",children:e.jsx("div",{className:"admin-skeleton",style:{gap:"1.25rem"},children:[0,1,2,3].map(s=>e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line w-1/4"}),e.jsx("div",{className:"admin-skeleton-line w-1/2"})]},s))})}),e.jsx("div",{className:"admin-card",children:e.jsx("div",{className:"admin-skeleton",style:{gap:"1.25rem"},children:[0,1,2].map(s=>e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{style:{flex:1},children:e.jsx("div",{className:"admin-skeleton-line w-full"})}),e.jsx("div",{style:{flex:1},children:e.jsx("div",{className:"admin-skeleton-line w-3/4"})}),e.jsx("div",{style:{flex:1},children:e.jsx("div",{className:"admin-skeleton-line w-1/2"})})]},s))})})]}):t?e.jsxs("div",{children:[e.jsxs(j.div,{className:"admin-page-header",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4},children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"1rem"},children:[e.jsx(x,{to:"/orders",className:"admin-btn-icon",title:"Zpět","aria-label":"Zpět",children:e.jsx("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:e.jsx("path",{d:"M19 12H5M12 19l-7-7 7-7"})})}),e.jsx("div",{children:e.jsxs("h1",{className:"admin-page-title",style:{display:"flex",alignItems:"center",gap:"0.75rem"},children:[R?e.jsxs("span",{style:{display:"inline-flex",alignItems:"center",gap:"0.5rem"},children:["Objednávka",e.jsx("input",{type:"text",value:T,onChange:s=>$(s.target.value),onKeyDown:s=>{s.key==="Enter"&&E(),s.key==="Escape"&&u(!1)},className:"admin-form-input",style:{width:"10rem",fontSize:"1rem",padding:"0.25rem 0.5rem",height:"auto"},autoFocus:!0,disabled:f}),e.jsx("button",{onClick:E,className:"admin-btn-icon",title:"Uložit","aria-label":"Uložit",disabled:f,children:e.jsx("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"var(--accent-color)",strokeWidth:"2",children:e.jsx("polyline",{points:"20 6 9 17 4 12"})})}),e.jsx("button",{onClick:()=>u(!1),className:"admin-btn-icon",title:"Zrušit","aria-label":"Zrušit",disabled:f,children:e.jsx("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:e.jsx("path",{d:"M18 6L6 18M6 6l12 12"})})})]}):e.jsxs("span",{style:{display:"inline-flex",alignItems:"center",gap:"0.5rem"},children:["Objednávka ",t.order_number,d("orders.edit")&&e.jsx("button",{onClick:q,className:"admin-btn-icon",title:"Změnit číslo","aria-label":"Změnit číslo",style:{opacity:.5},children:e.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"}),e.jsx("path",{d:"M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"})]})})]}),e.jsx("span",{className:`admin-badge ${ne[t.status]||""}`,children:W[t.status]||t.status})]})})]}),e.jsxs("div",{className:"admin-page-actions",children:[t.invoice?e.jsxs(x,{to:`/invoices/${t.invoice.id}`,className:"admin-btn admin-btn-secondary",children:[e.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"}),e.jsx("polyline",{points:"14 2 14 8 20 8"})]}),"Faktura ",t.invoice.invoice_number]}):d("invoices.create")&&t.status==="dokoncena"&&e.jsxs(x,{to:`/invoices/new?fromOrder=${t.id}`,className:"admin-btn admin-btn-secondary",children:[e.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"}),e.jsx("polyline",{points:"14 2 14 8 20 8"})]}),"Vytvořit fakturu"]}),d("orders.edit")&&t.valid_transitions?.filter(s=>s!=="stornovana").length>0&&t.valid_transitions.filter(s=>s!=="stornovana").map(s=>e.jsx("button",{onClick:()=>b({show:!0,status:s}),className:ie[s]||"admin-btn admin-btn-secondary",disabled:z===s,children:z===s?e.jsx("div",{className:"admin-spinner",style:{width:14,height:14,borderWidth:2}}):I[s]||s},s)),d("orders.delete")&&e.jsx("button",{onClick:()=>g(!0),className:"admin-btn admin-btn-primary",children:"Smazat"})]})]}),e.jsx(j.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.1},children:e.jsxs("div",{className:"admin-card-body",children:[e.jsx("h3",{className:"admin-card-title",children:"Informace"}),e.jsxs("div",{className:"admin-form-row",style:{marginBottom:"0.5rem"},children:[e.jsx(l,{label:"Nabídka",children:e.jsxs("div",{children:[e.jsx(x,{to:`/offers/${t.quotation_id}`,className:"link-accent",children:t.quotation_number}),t.project_code&&e.jsxs("span",{className:"text-tertiary",style:{marginLeft:"0.5rem"},children:["(",t.project_code,")"]})]})}),e.jsx(l,{label:"Projekt",children:e.jsx("div",{children:t.project?e.jsxs(x,{to:`/projects/${t.project.id}`,className:"link-accent",children:[t.project.project_number," — ",t.project.name]}):"—"})})]}),e.jsxs("div",{className:"admin-form-row admin-form-row-3",style:{marginBottom:"0.5rem"},children:[e.jsx(l,{label:"Zákazník",children:e.jsx("div",{style:{fontWeight:500},children:t.customer_name||"—"})}),e.jsx(l,{label:"Číslo obj. zákazníka",children:e.jsx("div",{children:t.customer_order_number||"—"})}),e.jsx(l,{label:"Měna",children:e.jsx("div",{children:t.currency})})]}),e.jsxs("div",{className:"admin-form-row admin-form-row-3",style:{marginBottom:"0.5rem"},children:[e.jsx(l,{label:"Datum vytvoření",children:e.jsx("div",{children:te(t.created_at)})}),e.jsx(l,{label:"Příloha",children:e.jsx("div",{children:t.attachment_name?e.jsxs("button",{onClick:K,className:"admin-btn admin-btn-secondary admin-btn-sm",style:{display:"inline-flex",alignItems:"center",gap:"0.4rem"},disabled:O,children:[O?e.jsx("div",{className:"admin-spinner",style:{width:14,height:14,borderWidth:2}}):e.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"}),e.jsx("polyline",{points:"14 2 14 8 20 8"})]}),t.attachment_name]}):"—"})})]})]})}),e.jsx(j.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.2},children:e.jsxs("div",{className:"admin-card-body",children:[e.jsx("h3",{className:"admin-card-title",children:"Položky"}),t.items?.length>0?e.jsx("div",{className:"admin-table-responsive",children:e.jsxs("table",{className:"admin-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{style:{width:"2.5rem",textAlign:"center"},children:"#"}),e.jsx("th",{children:"Popis"}),e.jsx("th",{style:{width:"5.5rem",textAlign:"center"},children:"Množství"}),e.jsx("th",{style:{width:"5.5rem",textAlign:"center"},children:"Jednotka"}),e.jsx("th",{style:{width:"8rem",textAlign:"right",whiteSpace:"nowrap"},children:"Jedn. cena"}),e.jsx("th",{style:{width:"4rem",textAlign:"center"},children:"V ceně"}),e.jsx("th",{style:{width:"9rem",textAlign:"right",whiteSpace:"nowrap"},children:"Celkem"})]})}),e.jsx("tbody",{children:t.items.map((s,a)=>{const r=(Number(s.quantity)||0)*(Number(s.unit_price)||0);return e.jsxs("tr",{children:[e.jsx("td",{style:{color:"var(--text-tertiary)",textAlign:"center",fontWeight:500},children:a+1}),e.jsxs("td",{children:[e.jsx("div",{style:{fontWeight:500},children:s.description||"—"}),s.item_description&&e.jsx("div",{style:{fontSize:"0.8rem",color:"var(--text-tertiary)",marginTop:"0.25rem"},children:s.item_description})]}),e.jsx("td",{style:{textAlign:"center"},children:s.quantity}),e.jsx("td",{style:{textAlign:"center"},children:s.unit||"—"}),e.jsx("td",{className:"admin-mono",style:{textAlign:"right",whiteSpace:"nowrap"},children:y(s.unit_price,t.currency)}),e.jsx("td",{style:{textAlign:"center"},children:Number(s.is_included_in_total)?"Ano":"Ne"}),e.jsx("td",{className:"admin-mono",style:{textAlign:"right",fontWeight:600,whiteSpace:"nowrap"},children:y(r,t.currency)})]},s.id||a)})})]})}):e.jsx("p",{style:{color:"var(--text-tertiary)"},children:"Žádné položky."}),e.jsxs("div",{className:"offers-totals-summary",children:[e.jsxs("div",{className:"offers-totals-row",children:[e.jsx("span",{children:"Mezisoučet:"}),e.jsx("span",{children:y(k.subtotal,t.currency)})]}),Number(t.apply_vat)>0&&e.jsxs("div",{className:"offers-totals-row",children:[e.jsxs("span",{children:["DPH (",t.vat_rate,"%):"]}),e.jsx("span",{children:y(k.vatAmount,t.currency)})]}),e.jsxs("div",{className:"offers-totals-row offers-totals-total",children:[e.jsx("span",{children:"Celkem k úhradě:"}),e.jsx("span",{children:y(k.total,t.currency)})]})]})]})}),t.sections?.length>0&&e.jsx(j.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.3},children:e.jsxs("div",{className:"admin-card-body",children:[e.jsx("h3",{className:"admin-card-title",children:"Rozsah projektu"}),t.scope_title&&e.jsx("div",{style:{fontWeight:500,marginBottom:"0.5rem"},children:t.scope_title}),t.scope_description&&e.jsx("div",{style:{color:"var(--text-secondary)",marginBottom:"1rem"},children:t.scope_description}),e.jsx("div",{className:"offers-scope-list",children:t.sections.map((s,a)=>e.jsxs("div",{className:"offers-scope-section",style:{cursor:"default"},children:[e.jsxs("div",{className:"offers-scope-section-header",children:[e.jsxs("span",{className:"offers-scope-number",children:[a+1,"."]}),e.jsx("span",{className:"offers-scope-title",children:(t.language==="CZ"?s.title_cz||s.title:s.title||s.title_cz)||`Sekce ${a+1}`})]}),s.content&&e.jsx("div",{className:"offers-scope-content rich-text-view",style:{padding:"1rem"},dangerouslySetInnerHTML:{__html:Y.sanitize(s.content)}})]},s.id||a))})]})}),e.jsx(j.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.4},children:e.jsxs("div",{className:"admin-card-body",children:[e.jsx("h3",{className:"admin-card-title",children:"Poznámky"}),e.jsx(l,{label:"Poznámky",children:e.jsx("textarea",{value:w,onChange:s=>_(s.target.value),className:"admin-form-input",rows:4,placeholder:"Interní poznámky k objednávce...",disabled:!d("orders.edit")})}),d("orders.edit")&&e.jsx("div",{style:{marginTop:"0.5rem"},children:e.jsx("button",{onClick:J,className:"admin-btn admin-btn-secondary admin-btn-sm",disabled:S,children:S?"Ukládání...":"Uložit poznámky"})})]})}),e.jsx(M,{isOpen:c.show,onClose:()=>b({show:!1,status:null}),onConfirm:V,title:"Změnit stav objednávky",message:`Opravdu chcete změnit stav objednávky "${t.order_number}" na "${W[c.status]}"?${c.status==="dokoncena"?" Projekt bude automaticky dokončen.":""}`,confirmText:I[c.status]||"Potvrdit",cancelText:"Zrušit",type:"default"}),e.jsx(M,{isOpen:F,onClose:()=>g(!1),onConfirm:G,title:"Smazat objednávku",message:`Opravdu chcete smazat objednávku "${t.order_number}"? Bude smazán i přidružený projekt. Tato akce je nevratná.`,confirmText:"Smazat",cancelText:"Zrušit",type:"danger",loading:H})]}):null}export{me as default}; diff --git a/dist/assets/Orders-CSsExPPr.js b/dist/assets/Orders-CSsExPPr.js new file mode 100644 index 0000000..f3f0570 --- /dev/null +++ b/dist/assets/Orders-CSsExPPr.js @@ -0,0 +1 @@ +import{j as e,m as k}from"./vendor-animation-0s3FMHwK.js";import{r as l,L as a}from"./vendor-react-BVs3cwbi.js";import{a as z,u as C,d as _,e as B,g as A,C as D,c as L}from"./index-BBlIrj2z.js";import{F as T}from"./Forbidden-D25jV3Oq.js";import{u as $,a as E,S as j}from"./useListData-BVkTFDdr.js";import{P as M}from"./Pagination-B1sbY6V7.js";import"./vendor-utils-Dyr8OjFr.js";const P="/api/admin",V={prijata:"Přijatá",v_realizaci:"V realizaci",dokoncena:"Dokončená",stornovana:"Stornována"},O={prijata:"admin-badge-order-prijata",v_realizaci:"admin-badge-order-realizace",dokoncena:"admin-badge-order-dokoncena",stornovana:"admin-badge-order-stornovana"};function U(){const r=z(),{hasPermission:d}=C(),{sort:y,order:i,handleSort:o,activeSort:c}=$("order_number"),[v,g]=l.useState(""),[N,u]=l.useState(1),[n,m]=l.useState({show:!1,order:null}),[f,p]=l.useState(!1),{items:t,loading:b,pagination:h,refetch:w}=E("orders.php",{dataKey:"orders",search:v,sort:y,order:i,page:N,errorMsg:"Nepodařilo se načíst objednávky"});if(!d("orders.view"))return e.jsx(T,{});const S=async()=>{if(n.order){p(!0);try{const x=await(await L(`${P}/orders.php?id=${n.order.id}`,{method:"DELETE"})).json();x.success?(m({show:!1,order:null}),r.success(x.message||"Objednávka byla smazána"),w()):r.error(x.error||"Nepodařilo se smazat objednávku")}catch{r.error("Chyba připojení")}finally{p(!1)}}};return b?e.jsx("div",{children:e.jsxs("div",{className:"admin-skeleton",style:{padding:0,gap:"1.5rem"},children:[e.jsxs("div",{className:"admin-skeleton-row",style:{justifyContent:"space-between"},children:[e.jsxs("div",{children:[e.jsx("div",{className:"admin-skeleton-line h-8",style:{width:"200px",marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line",style:{width:"140px"}})]}),e.jsx("div",{className:"admin-skeleton-line h-10",style:{width:"140px",borderRadius:"8px"}})]}),e.jsx("div",{className:"admin-card",children:e.jsxs("div",{className:"admin-skeleton",style:{gap:"1.25rem"},children:[e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line circle"}),e.jsxs("div",{style:{flex:1},children:[e.jsx("div",{className:"admin-skeleton-line w-1/3",style:{marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line w-1/4",style:{height:"10px"}})]}),e.jsx("div",{className:"admin-skeleton-line w-1/4"})]}),e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line circle"}),e.jsxs("div",{style:{flex:1},children:[e.jsx("div",{className:"admin-skeleton-line w-1/2",style:{marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line w-1/3",style:{height:"10px"}})]}),e.jsx("div",{className:"admin-skeleton-line w-1/4"})]}),e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line circle"}),e.jsxs("div",{style:{flex:1},children:[e.jsx("div",{className:"admin-skeleton-line w-3/4",style:{marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line w-1/4",style:{height:"10px"}})]}),e.jsx("div",{className:"admin-skeleton-line w-1/4"})]}),e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line circle"}),e.jsxs("div",{style:{flex:1},children:[e.jsx("div",{className:"admin-skeleton-line w-1/2",style:{marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line w-1/3",style:{height:"10px"}})]}),e.jsx("div",{className:"admin-skeleton-line w-1/4"})]}),e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line circle"}),e.jsxs("div",{style:{flex:1},children:[e.jsx("div",{className:"admin-skeleton-line w-1/3",style:{marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line w-1/4",style:{height:"10px"}})]}),e.jsx("div",{className:"admin-skeleton-line w-1/4"})]})]})})]})}):e.jsxs("div",{children:[e.jsx(k.div,{className:"admin-page-header",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4},children:e.jsxs("div",{children:[e.jsx("h1",{className:"admin-page-title",children:"Objednávky"}),e.jsxs("p",{className:"admin-page-subtitle",children:[h?.total??t.length," ",_(h?.total??t.length,"objednávka","objednávky","objednávek")]})]})}),e.jsx(k.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.1},children:e.jsxs("div",{className:"admin-card-body",children:[e.jsx("div",{className:"admin-search-bar",style:{marginBottom:"1rem"},children:e.jsx("input",{type:"text",value:v,onChange:s=>{g(s.target.value),u(1)},className:"admin-form-input",placeholder:"Hledat podle čísla, nabídky, projektu nebo zákazníka..."})}),t.length===0?e.jsxs("div",{className:"admin-empty-state",children:[e.jsx("div",{className:"admin-empty-icon",children:e.jsxs("svg",{width:"28",height:"28",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("path",{d:"M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"}),e.jsx("line",{x1:"3",y1:"6",x2:"21",y2:"6"}),e.jsx("path",{d:"M16 10a4 4 0 0 1-8 0"})]})}),e.jsx("p",{children:"Zatím nejsou žádné objednávky."}),e.jsx("p",{className:"text-tertiary",style:{fontSize:"0.875rem"},children:"Objednávky se vytvářejí z nabídek."})]}):e.jsx("div",{className:"admin-table-responsive",children:e.jsxs("table",{className:"admin-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsxs("th",{style:{cursor:"pointer"},onClick:()=>o("order_number"),children:["Číslo ",e.jsx(j,{column:"order_number",sort:c,order:i})]}),e.jsx("th",{children:"Nabídka"}),e.jsx("th",{children:"Zákazník"}),e.jsxs("th",{style:{cursor:"pointer"},onClick:()=>o("status"),children:["Stav ",e.jsx(j,{column:"status",sort:c,order:i})]}),e.jsxs("th",{style:{cursor:"pointer"},onClick:()=>o("created_at"),children:["Datum ",e.jsx(j,{column:"created_at",sort:c,order:i})]}),e.jsx("th",{style:{textAlign:"right"},children:"Celkem"}),e.jsx("th",{children:"Akce"})]})}),e.jsx("tbody",{children:t.map(s=>e.jsxs("tr",{children:[e.jsx("td",{className:"admin-mono",children:e.jsx(a,{to:`/orders/${s.id}`,className:"link-accent",children:s.order_number})}),e.jsx("td",{children:e.jsx(a,{to:`/offers/${s.quotation_id}`,className:"text-secondary",style:{textDecoration:"none"},children:s.quotation_number})}),e.jsx("td",{children:s.customer_name||"—"}),e.jsx("td",{children:e.jsx("span",{className:`admin-badge ${O[s.status]||""}`,children:V[s.status]||s.status})}),e.jsx("td",{className:"admin-mono",children:B(s.created_at)}),e.jsx("td",{className:"admin-mono",style:{textAlign:"right",fontWeight:500},children:A(s.total,s.currency)}),e.jsx("td",{children:e.jsxs("div",{className:"admin-table-actions",children:[e.jsx(a,{to:`/orders/${s.id}`,className:"admin-btn-icon",title:"Detail","aria-label":"Detail",children:e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"}),e.jsx("circle",{cx:"12",cy:"12",r:"3"})]})}),s.invoice_id?e.jsx(a,{to:`/invoices/${s.invoice_id}`,className:"admin-btn-icon accent",title:"Zobrazit fakturu","aria-label":"Zobrazit fakturu",children:e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"}),e.jsx("polyline",{points:"14 2 14 8 20 8"}),e.jsx("text",{x:"12",y:"16.5",textAnchor:"middle",fill:"currentColor",stroke:"none",fontSize:"9",fontWeight:"700",children:"F"})]})}):d("invoices.create")&&e.jsx(a,{to:`/invoices/new?fromOrder=${s.id}`,className:"admin-btn-icon",title:"Vytvořit fakturu","aria-label":"Vytvořit fakturu",children:e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"}),e.jsx("polyline",{points:"14 2 14 8 20 8"}),e.jsx("line",{x1:"12",y1:"11",x2:"12",y2:"17"}),e.jsx("line",{x1:"9",y1:"14",x2:"15",y2:"14"})]})}),d("orders.delete")&&e.jsx("button",{onClick:()=>m({show:!0,order:s}),className:"admin-btn-icon danger",title:"Smazat",children:e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("polyline",{points:"3 6 5 6 21 6"}),e.jsx("path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"})]})})]})})]},s.id))})]})}),e.jsx(M,{pagination:h,onPageChange:u})]})}),e.jsx(D,{isOpen:n.show,onClose:()=>m({show:!1,order:null}),onConfirm:S,title:"Smazat objednávku",message:`Opravdu chcete smazat objednávku "${n.order?.order_number}"? Bude smazán i přidružený projekt. Tato akce je nevratná.`,confirmText:"Smazat",cancelText:"Zrušit",type:"danger",loading:f})]})}export{U as default}; diff --git a/dist/assets/Pagination-B1sbY6V7.js b/dist/assets/Pagination-B1sbY6V7.js new file mode 100644 index 0000000..f1131a1 --- /dev/null +++ b/dist/assets/Pagination-B1sbY6V7.js @@ -0,0 +1 @@ +import{j as i}from"./vendor-animation-0s3FMHwK.js";import{r as b}from"./vendor-react-BVs3cwbi.js";function k({pagination:t,onPageChange:l,onPerPageChange:m}){const a=t?.page??1,n=t?.total_pages??1,d=t?.total??0,r=t?.per_page??25,h=b.useMemo(()=>{const s=[];let e=Math.max(1,a-Math.floor(2.5));const o=Math.min(n,e+5-1);o-e<4&&(e=Math.max(1,o-5+1)),e>1&&(s.push(1),e>2&&s.push("..."));for(let c=e;c<=o;c++)s.push(c);return ol(a-1),"aria-label":"Předchozí stránka",children:i.jsx("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2.5",strokeLinecap:"round",strokeLinejoin:"round",children:i.jsx("polyline",{points:"15 18 9 12 15 6"})})}),h.map((s,p)=>s==="..."?i.jsx("span",{className:"admin-pagination-ellipsis",children:"…"},`ellipsis-${p}`):i.jsx("button",{className:`admin-pagination-page${s===a?" active":""}`,onClick:()=>l(s),children:s},s)),i.jsx("button",{className:"admin-btn-secondary admin-btn-sm",disabled:a>=n,onClick:()=>l(a+1),"aria-label":"Další stránka",children:i.jsx("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2.5",strokeLinecap:"round",strokeLinejoin:"round",children:i.jsx("polyline",{points:"9 18 15 12 9 6"})})})]}),m&&i.jsx("select",{className:"admin-pagination-select",value:r,onChange:s=>m(Number(s.target.value)),"aria-label":"Záznamů na stránku",children:[10,25,50,100].map(s=>i.jsxs("option",{value:s,children:[s," / strana"]},s))})]})}export{k as P}; diff --git a/dist/assets/ProjectCreate-B8awV2Y4.js b/dist/assets/ProjectCreate-B8awV2Y4.js new file mode 100644 index 0000000..c90ca37 --- /dev/null +++ b/dist/assets/ProjectCreate-B8awV2Y4.js @@ -0,0 +1 @@ +import{j as e,m as w}from"./vendor-animation-0s3FMHwK.js";import{g as O,r,L as A}from"./vendor-react-BVs3cwbi.js";import{a as M,u as R,F as m,A as Z,c as x}from"./index-BBlIrj2z.js";import{F as $}from"./Forbidden-D25jV3Oq.js";import"./vendor-utils-Dyr8OjFr.js";const v="/api/admin";function U(){const _=O(),c=M(),{hasPermission:C}=R(),[a,d]=r.useState({project_number:"",name:"",customer_id:null,customer_name:"",start_date:new Date().toISOString().split("T")[0]}),[f,y]=r.useState(!1),[k,u]=r.useState({}),[S,z]=r.useState(!0),[p,E]=r.useState([]),[o,N]=r.useState(""),[h,l]=r.useState(!1);r.useEffect(()=>{(async()=>{try{const[t,n]=await Promise.all([x(`${v}/projects.php?action=next_number`),x(`${v}/customers.php`)]),i=await t.json();i.success&&d(P=>({...P,project_number:i.data.number}));const g=await n.json();g.success&&E(g.data.customers)}catch{c.error("Chyba při načítání dat")}finally{z(!1)}})()},[c]);const b=r.useMemo(()=>{if(!o)return p;const s=o.toLowerCase();return p.filter(t=>(t.name||"").toLowerCase().includes(s)||(t.company_id||"").includes(o)||(t.city||"").toLowerCase().includes(s))},[p,o]);if(r.useEffect(()=>{const s=()=>l(!1);if(h)return document.addEventListener("click",s),()=>document.removeEventListener("click",s)},[h]),!C("projects.create"))return e.jsx($,{});const F=s=>{d(t=>({...t,customer_id:s.id,customer_name:s.name})),u(t=>({...t,customer_id:void 0})),N(""),l(!1)},D=()=>{d(s=>({...s,customer_id:null,customer_name:""}))},j=(s,t)=>{d(n=>({...n,[s]:t})),u(n=>({...n,[s]:void 0}))},L=async()=>{const s={};if(a.name.trim()||(s.name="Název projektu je povinný"),a.customer_id||(s.customer_id="Vyberte zákazníka"),u(s),!(Object.keys(s).length>0)){y(!0);try{const t={name:a.name.trim(),customer_id:a.customer_id,start_date:a.start_date,project_number:a.project_number.trim()},i=await(await x(`${v}/projects.php`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json();i.success?_(`/projects/${i.data.project_id}`,{state:{created:!0}}):c.error(i.error||"Nepodařilo se vytvořit projekt")}catch{c.error("Chyba připojení")}finally{y(!1)}}};return S?e.jsxs("div",{className:"admin-skeleton",style:{padding:0,gap:"1.5rem"},children:[e.jsx("div",{className:"admin-skeleton-row",style:{justifyContent:"space-between"},children:e.jsx("div",{className:"admin-skeleton-line h-8",style:{width:"200px"}})}),e.jsx("div",{className:"admin-card",children:e.jsx("div",{className:"admin-skeleton",style:{gap:"1.25rem"},children:[0,1,2,3].map(s=>e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line w-1/4"}),e.jsx("div",{className:"admin-skeleton-line w-1/2"})]},s))})})]}):e.jsxs("div",{children:[e.jsxs(w.div,{className:"admin-page-header",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4},children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"1rem"},children:[e.jsx(A,{to:"/projects",className:"admin-btn-icon",title:"Zpět","aria-label":"Zpět",children:e.jsx("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:e.jsx("path",{d:"M19 12H5M12 19l-7-7 7-7"})})}),e.jsxs("div",{children:[e.jsx("h1",{className:"admin-page-title",children:"Nový projekt"}),e.jsx("p",{className:"admin-page-subtitle",children:"Ruční vytvoření projektu"})]})]}),e.jsx("div",{className:"admin-page-actions",children:e.jsx("button",{onClick:L,disabled:f,className:"admin-btn admin-btn-primary",children:f?"Ukládám...":"Uložit"})})]}),e.jsx(w.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.1},style:{overflow:"visible"},children:e.jsxs("div",{className:"admin-card-body",children:[e.jsx("h3",{className:"admin-card-title",children:"Základní údaje"}),e.jsxs("div",{className:"admin-form",children:[e.jsxs("div",{className:"admin-form-row",children:[e.jsx(m,{label:"Číslo projektu",children:e.jsx("input",{type:"text",value:a.project_number,onChange:s=>j("project_number",s.target.value),className:"admin-form-input",placeholder:"Ponechte prázdné pro automatické"})}),e.jsx(m,{label:"Název",error:k.name,required:!0,children:e.jsx("input",{type:"text",value:a.name,onChange:s=>j("name",s.target.value),className:"admin-form-input",placeholder:"Název projektu"})})]}),e.jsxs("div",{className:"admin-form-row",children:[e.jsx(m,{label:"Zákazník",error:k.customer_id,required:!0,children:a.customer_id?e.jsxs("div",{className:"offers-customer-selected",children:[e.jsx("span",{children:a.customer_name}),e.jsx("button",{type:"button",onClick:D,className:"admin-btn-icon",title:"Odebrat zákazníka","aria-label":"Odebrat zákazníka",children:e.jsxs("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),e.jsx("line",{x1:"6",y1:"6",x2:"18",y2:"18"})]})})]}):e.jsxs("div",{className:"offers-customer-select",onClick:s=>s.stopPropagation(),children:[e.jsx("input",{type:"text",value:o,onChange:s=>{N(s.target.value),l(!0)},onFocus:()=>l(!0),className:"admin-form-input",placeholder:"Hledat zákazníka..."}),h&&e.jsx("div",{className:"offers-customer-dropdown",children:b.length===0?e.jsx("div",{className:"offers-customer-dropdown-empty",children:"Žádní zákazníci"}):b.slice(0,20).map(s=>e.jsxs("div",{className:"offers-customer-dropdown-item",onMouseDown:()=>F(s),children:[e.jsx("div",{children:s.name}),s.city&&e.jsx("div",{children:s.city})]},s.id))})]})}),e.jsx(m,{label:"Datum zahájení",children:e.jsx(Z,{mode:"date",value:a.start_date,onChange:s=>j("start_date",s)})})]})]})]})})]})}export{U as default}; diff --git a/dist/assets/ProjectDetail-BWBiBOHM.js b/dist/assets/ProjectDetail-BWBiBOHM.js new file mode 100644 index 0000000..43250c9 --- /dev/null +++ b/dist/assets/ProjectDetail-BWBiBOHM.js @@ -0,0 +1 @@ +import{j as e,m as f}from"./vendor-animation-0s3FMHwK.js";import{h as K,g as Y,u as G,r as i,L as z}from"./vendor-react-BVs3cwbi.js";import{a as Q,u as V,F as l,A as B,C as X,c as p}from"./index-BBlIrj2z.js";import{F as ee}from"./Forbidden-D25jV3Oq.js";import"./vendor-utils-Dyr8OjFr.js";const h="/api/admin",te={aktivni:"Aktivní",dokonceny:"Dokončený",zruseny:"Zrušený"};function se(o){if(!o)return"";const s=new Date(o),x=s.getDate(),g=s.getMonth()+1,c=s.getFullYear(),u=String(s.getHours()).padStart(2,"0"),N=String(s.getMinutes()).padStart(2,"0");return`${x}. ${g}. ${c} ${u}:${N}`}function de(){const{id:o}=K(),s=Q(),{hasPermission:x,isAdmin:g}=V(),c=Y(),u=G(),[N,M]=i.useState(!0),[_,C]=i.useState(!1),[a,O]=i.useState(null),[d,$]=i.useState({name:"",status:"aktivni",start_date:"",end_date:""}),[Z,D]=i.useState(!1),[R,P]=i.useState(!1),[k,b]=i.useState([]),[w,W]=i.useState(!0),[j,A]=i.useState(""),[E,T]=i.useState(!1),[S,L]=i.useState(null);i.useEffect(()=>{u.state?.created&&(s.success("Projekt byl vytvořen"),c(u.pathname,{replace:!0,state:{}}))},[u.state]);const I=async()=>{try{const t=await p(`${h}/projects.php?action=notes&id=${o}`);if(t.status===401)return;const n=await t.json();n.success&&b(n.data.notes||[])}catch{}finally{W(!1)}};if(i.useEffect(()=>{(async()=>{try{const n=await p(`${h}/projects.php?action=detail&id=${o}`);if(n.status===401)return;const r=await n.json();if(r.success){const m=r.data;O(m),$({name:m.name||"",status:m.status||"aktivni",start_date:(m.start_date||"").substring(0,10),end_date:(m.end_date||"").substring(0,10)})}else s.error(r.error||"Nepodařilo se načíst projekt"),c("/projects")}catch{s.error("Chyba připojení"),c("/projects")}finally{M(!1)}})(),I()},[o,s,c]),!x("projects.view"))return e.jsx(ee,{});const v=(t,n)=>$(r=>({...r,[t]:n})),H=async()=>{if(!d.name.trim()){s.error("Název projektu je povinný");return}C(!0);try{const n=await(await p(`${h}/projects.php?id=${o}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:d.name,status:d.status,start_date:d.start_date||null,end_date:d.end_date||null})})).json();n.success?s.success(n.message||"Projekt byl aktualizován"):s.error(n.error||"Nepodařilo se uložit projekt")}catch{s.error("Chyba připojení")}finally{C(!1)}},U=async()=>{P(!0);try{const n=await(await p(`${h}/projects.php?id=${o}`,{method:"DELETE"})).json();n.success?(c("/projects"),setTimeout(()=>s.success("Projekt byl smazán"),300)):s.error(n.error||"Nepodařilo se smazat projekt")}catch{s.error("Chyba připojení")}finally{P(!1)}},F=async()=>{if(j.trim()){T(!0);try{const n=await(await p(`${h}/projects.php?action=add_note&id=${o}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({content:j.trim()})})).json();n.success?(b(r=>[n.data.note,...r]),A(""),s.success("Poznámka byla přidána")):s.error(n.error||"Nepodařilo se přidat poznámku")}catch{s.error("Chyba připojení")}finally{T(!1)}}},q=async t=>{L(t);try{const r=await(await p(`${h}/projects.php?action=delete_note¬eId=${t}`,{method:"DELETE"})).json();r.success?(b(m=>m.filter(J=>J.id!==t)),s.success("Poznámka byla smazána")):s.error(r.error||"Nepodařilo se smazat poznámku")}catch{s.error("Chyba připojení")}finally{L(null)}};if(N)return e.jsxs("div",{className:"admin-skeleton",style:{padding:0,gap:"1.5rem"},children:[e.jsxs("div",{className:"admin-skeleton-row",style:{justifyContent:"space-between"},children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"0.75rem"},children:[e.jsx("div",{className:"admin-skeleton-line",style:{width:"32px",height:"32px",borderRadius:"8px"}}),e.jsx("div",{className:"admin-skeleton-line h-8",style:{width:"200px"}})]}),e.jsxs("div",{className:"admin-skeleton-row",style:{gap:"0.5rem"},children:[e.jsx("div",{className:"admin-skeleton-line h-10",style:{width:"100px",borderRadius:"8px"}}),e.jsx("div",{className:"admin-skeleton-line h-10",style:{width:"100px",borderRadius:"8px"}})]})]}),e.jsx("div",{className:"admin-card",children:e.jsx("div",{className:"admin-skeleton",style:{gap:"1.25rem"},children:[0,1,2,3].map(t=>e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line w-1/4"}),e.jsx("div",{className:"admin-skeleton-line w-1/2"})]},t))})}),e.jsx("div",{className:"admin-card",children:e.jsx("div",{className:"admin-skeleton",style:{gap:"1.25rem"},children:[0,1,2].map(t=>e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line w-1/3",style:{marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line w-full"})]},t))})})]});if(!a)return null;const y=x("projects.edit");return e.jsxs("div",{children:[e.jsxs(f.div,{className:"admin-page-header",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4},children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"1rem"},children:[e.jsx(z,{to:"/projects",className:"admin-btn-icon",title:"Zpět","aria-label":"Zpět",children:e.jsx("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:e.jsx("path",{d:"M19 12H5M12 19l-7-7 7-7"})})}),e.jsx("div",{children:e.jsxs("h1",{className:"admin-page-title",children:["Projekt ",a.project_number]})})]}),y&&e.jsxs("div",{className:"admin-page-actions",children:[e.jsx("button",{onClick:H,className:"admin-btn admin-btn-primary",disabled:_,children:_?e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"admin-spinner",style:{width:16,height:16,borderWidth:2}}),"Ukládání..."]}):"Uložit"}),!a.order_id&&e.jsx("button",{onClick:()=>D(!0),className:"admin-btn admin-btn-primary",children:"Smazat"})]})]}),e.jsx(f.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.1},children:e.jsxs("div",{className:"admin-card-body",children:[e.jsx("h3",{className:"admin-card-title",children:"Základní údaje"}),e.jsxs("div",{className:"admin-form",children:[e.jsxs("div",{className:"admin-form-row",children:[e.jsx(l,{label:"Číslo projektu",children:e.jsx("input",{type:"text",value:a.project_number,className:"admin-form-input",readOnly:!0,style:{backgroundColor:"var(--bg-secondary)",cursor:"default"}})}),e.jsx(l,{label:"Název",children:e.jsx("input",{type:"text",value:d.name,onChange:t=>v("name",t.target.value),className:"admin-form-input",placeholder:"Název projektu",disabled:!y})})]}),e.jsxs("div",{className:"admin-form-row",children:[e.jsx(l,{label:"Zákazník",children:e.jsx("input",{type:"text",value:a.customer_name||"—",className:"admin-form-input",readOnly:!0,style:{backgroundColor:"var(--bg-secondary)",cursor:"default"}})}),e.jsx(l,{label:"Stav",children:e.jsxs("select",{value:d.status,onChange:t=>v("status",t.target.value),className:"admin-form-select",disabled:!y,children:[e.jsx("option",{value:"aktivni",children:"Aktivní"}),e.jsx("option",{value:"dokonceny",children:"Dokončený"}),e.jsx("option",{value:"zruseny",children:"Zrušený"})]})})]}),e.jsxs("div",{className:"admin-form-row",children:[e.jsx(l,{label:"Datum zahájení",children:e.jsx(B,{mode:"date",value:d.start_date,onChange:t=>v("start_date",t),disabled:!y})}),e.jsx(l,{label:"Datum ukončení",children:e.jsx(B,{mode:"date",value:d.end_date,onChange:t=>v("end_date",t),disabled:!y})})]})]})]})}),e.jsx(f.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.15},children:e.jsxs("div",{className:"admin-card-body",children:[e.jsx("h3",{className:"admin-card-title",children:"Poznámky"}),e.jsxs("div",{style:{marginBottom:"1rem"},children:[e.jsx("textarea",{value:j,onChange:t=>A(t.target.value),className:"admin-form-input",rows:2,placeholder:"Napište poznámku...",style:{resize:"vertical",width:"100%"},onKeyDown:t=>{t.key==="Enter"&&t.ctrlKey&&j.trim()&&F()}}),e.jsx("div",{style:{marginTop:"0.5rem"},children:e.jsx("button",{onClick:F,className:"admin-btn admin-btn-secondary admin-btn-sm",disabled:E||!j.trim(),children:E?e.jsx("div",{className:"admin-spinner",style:{width:16,height:16,borderWidth:2}}):"Přidat poznámku"})})]}),a.notes&&e.jsxs("div",{style:{padding:"0.75rem",background:"var(--bg-secondary)",borderRadius:"0.5rem",marginBottom:"0.5rem",fontSize:"0.85rem",color:"var(--text-secondary)"},children:[e.jsx("div",{style:{fontSize:"0.75rem",color:"var(--text-tertiary)",marginBottom:"0.25rem"},children:"Starší poznámka (před zavedením systému)"}),e.jsx("div",{style:{whiteSpace:"pre-wrap"},children:a.notes})]}),w&&e.jsx("div",{className:"admin-skeleton",style:{gap:"0.75rem"},children:[0,1,2].map(t=>e.jsx("div",{className:"admin-skeleton-line",style:{height:"52px",borderRadius:"8px"}},t))}),!w&&k.length===0&&!a.notes&&e.jsx("div",{style:{color:"var(--text-tertiary)",fontSize:"0.875rem",textAlign:"center",padding:"1rem 0"},children:"Zatím žádné poznámky"}),!w&&(k.length>0||a.notes)&&e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"0.5rem"},children:k.map(t=>e.jsx("div",{style:{padding:"0.75rem",background:"var(--bg-secondary)",borderRadius:"0.5rem",position:"relative"},children:e.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"flex-start",gap:"0.5rem"},children:[e.jsxs("div",{style:{flex:1},children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"0.5rem",marginBottom:"0.25rem"},children:[e.jsx("span",{style:{fontWeight:600,fontSize:"0.85rem"},children:t.user_name}),e.jsx("span",{style:{color:"var(--text-tertiary)",fontSize:"0.75rem"},children:se(t.created_at)})]}),e.jsx("div",{style:{whiteSpace:"pre-wrap",fontSize:"0.875rem",lineHeight:1.5},children:t.content})]}),g&&e.jsx("button",{onClick:()=>q(t.id),className:"admin-btn-icon",title:"Smazat poznámku",disabled:S===t.id,style:{flexShrink:0,opacity:S===t.id?.5:1},children:S===t.id?e.jsx("div",{className:"admin-spinner",style:{width:14,height:14,borderWidth:2}}):e.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("polyline",{points:"3 6 5 6 21 6"}),e.jsx("path",{d:"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"}),e.jsx("path",{d:"M10 11v6M14 11v6"})]})})]})},t.id))})]})}),e.jsx(f.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.2},children:e.jsxs("div",{className:"admin-card-body",children:[e.jsx("h3",{className:"admin-card-title",children:"Propojení"}),e.jsxs("div",{className:"admin-form-row",children:[e.jsx(l,{label:"Objednávka",children:e.jsx("div",{children:a.order_id?e.jsxs(z,{to:`/orders/${a.order_id}`,className:"link-accent",children:[a.order_number,a.order_status&&e.jsxs("span",{className:"text-tertiary",style:{fontWeight:400,marginLeft:"0.5rem"},children:["(",te[a.order_status]||a.order_status,")"]})]}):"—"})}),e.jsx(l,{label:"Nabídka",children:e.jsx("div",{children:a.quotation_id?e.jsx(z,{to:`/offers/${a.quotation_id}`,className:"link-accent",children:a.quotation_number}):"—"})})]})]})}),e.jsx(X,{isOpen:Z,onClose:()=>D(!1),onConfirm:U,title:"Smazat projekt",message:`Opravdu chcete smazat projekt "${a.project_number} – ${a.name}"? Tato akce je nevratná.`,confirmText:"Smazat",cancelText:"Zrušit",type:"danger",loading:R})]})}export{de as default}; diff --git a/dist/assets/Projects-DRnqfGWv.js b/dist/assets/Projects-DRnqfGWv.js new file mode 100644 index 0000000..0616331 --- /dev/null +++ b/dist/assets/Projects-DRnqfGWv.js @@ -0,0 +1 @@ +import{j as e,m as u}from"./vendor-animation-0s3FMHwK.js";import{r as d,L as o}from"./vendor-react-BVs3cwbi.js";import{a as B,u as L,d as P,e as N,C as A,c as T}from"./index-BBlIrj2z.js";import{F as D}from"./Forbidden-D25jV3Oq.js";import{u as E,a as M,S as l}from"./useListData-BVkTFDdr.js";import{P as $}from"./Pagination-B1sbY6V7.js";import"./vendor-utils-Dyr8OjFr.js";const W="/api/admin",I={aktivni:"Aktivní",dokonceny:"Dokončený",zruseny:"Zrušený"},H={aktivni:"admin-badge-project-aktivni",dokonceny:"admin-badge-project-dokonceny",zruseny:"admin-badge-project-zruseny"};function q(){const c=B(),{hasPermission:m}=L(),{sort:g,order:t,handleSort:i,activeSort:n}=E("project_number"),[v,w]=d.useState(""),[b,k]=d.useState(1),[j,y]=d.useState(null),[a,h]=d.useState(null),{items:r,setItems:f,loading:S,pagination:x}=M("projects.php",{dataKey:"projects",search:v,sort:g,order:t,page:b,errorMsg:"Nepodařilo se načíst projekty"});if(!m("projects.view"))return e.jsx(D,{});const C=async()=>{if(a){y(a.id);try{const p=await(await T(`${W}/projects.php?id=${a.id}`,{method:"DELETE"})).json();p.success?(c.success(p.message||"Projekt byl smazán"),f(_=>_.filter(z=>z.id!==a.id))):c.error(p.error||"Nepodařilo se smazat projekt")}catch{c.error("Chyba připojení")}finally{y(null),h(null)}}};return S?e.jsx("div",{children:e.jsxs("div",{className:"admin-skeleton",style:{padding:0,gap:"1.5rem"},children:[e.jsxs("div",{className:"admin-skeleton-row",style:{justifyContent:"space-between"},children:[e.jsxs("div",{children:[e.jsx("div",{className:"admin-skeleton-line h-8",style:{width:"200px",marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line",style:{width:"140px"}})]}),e.jsx("div",{className:"admin-skeleton-line h-10",style:{width:"140px",borderRadius:"8px"}})]}),e.jsx("div",{className:"admin-card",children:e.jsxs("div",{className:"admin-skeleton",style:{gap:"1.25rem"},children:[e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line circle"}),e.jsxs("div",{style:{flex:1},children:[e.jsx("div",{className:"admin-skeleton-line w-1/3",style:{marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line w-1/4",style:{height:"10px"}})]}),e.jsx("div",{className:"admin-skeleton-line w-1/4"})]}),e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line circle"}),e.jsxs("div",{style:{flex:1},children:[e.jsx("div",{className:"admin-skeleton-line w-1/2",style:{marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line w-1/3",style:{height:"10px"}})]}),e.jsx("div",{className:"admin-skeleton-line w-1/4"})]}),e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line circle"}),e.jsxs("div",{style:{flex:1},children:[e.jsx("div",{className:"admin-skeleton-line w-3/4",style:{marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line w-1/4",style:{height:"10px"}})]}),e.jsx("div",{className:"admin-skeleton-line w-1/4"})]}),e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line circle"}),e.jsxs("div",{style:{flex:1},children:[e.jsx("div",{className:"admin-skeleton-line w-1/2",style:{marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line w-1/3",style:{height:"10px"}})]}),e.jsx("div",{className:"admin-skeleton-line w-1/4"})]}),e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line circle"}),e.jsxs("div",{style:{flex:1},children:[e.jsx("div",{className:"admin-skeleton-line w-1/3",style:{marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line w-1/4",style:{height:"10px"}})]}),e.jsx("div",{className:"admin-skeleton-line w-1/4"})]})]})})]})}):e.jsxs("div",{children:[e.jsxs(u.div,{className:"admin-page-header",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4},children:[e.jsxs("div",{children:[e.jsx("h1",{className:"admin-page-title",children:"Projekty"}),e.jsxs("p",{className:"admin-page-subtitle",children:[x?.total??r.length," ",P(x?.total??r.length,"projekt","projekty","projektů")]})]}),m("projects.create")&&e.jsxs(o,{to:"/projects/new",className:"admin-btn admin-btn-primary",children:[e.jsxs("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("line",{x1:"12",y1:"5",x2:"12",y2:"19"}),e.jsx("line",{x1:"5",y1:"12",x2:"19",y2:"12"})]}),"Nový projekt"]})]}),e.jsx(u.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.1},children:e.jsxs("div",{className:"admin-card-body",children:[e.jsx("div",{className:"admin-search-bar",style:{marginBottom:"1rem"},children:e.jsx("input",{type:"text",value:v,onChange:s=>{w(s.target.value),k(1)},className:"admin-form-input",placeholder:"Hledat podle čísla, názvu nebo zákazníka..."})}),r.length===0?e.jsxs("div",{className:"admin-empty-state",children:[e.jsx("div",{className:"admin-empty-icon",children:e.jsx("svg",{width:"28",height:"28",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round",children:e.jsx("path",{d:"M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"})})}),e.jsx("p",{children:"Zatím nejsou žádné projekty."}),e.jsx("p",{style:{color:"var(--text-tertiary)",fontSize:"0.875rem"},children:"Vytvořte první projekt tlačítkem výše nebo automaticky při vytvoření objednávky."})]}):e.jsx("div",{className:"admin-table-responsive",children:e.jsxs("table",{className:"admin-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsxs("th",{style:{cursor:"pointer"},onClick:()=>i("project_number"),children:["Číslo ",e.jsx(l,{column:"project_number",sort:n,order:t})]}),e.jsxs("th",{style:{cursor:"pointer"},onClick:()=>i("name"),children:["Název ",e.jsx(l,{column:"name",sort:n,order:t})]}),e.jsx("th",{children:"Zákazník"}),e.jsxs("th",{style:{cursor:"pointer"},onClick:()=>i("status"),children:["Stav ",e.jsx(l,{column:"status",sort:n,order:t})]}),e.jsxs("th",{style:{cursor:"pointer"},onClick:()=>i("start_date"),children:["Začátek ",e.jsx(l,{column:"start_date",sort:n,order:t})]}),e.jsxs("th",{style:{cursor:"pointer"},onClick:()=>i("end_date"),children:["Konec ",e.jsx(l,{column:"end_date",sort:n,order:t})]}),e.jsx("th",{children:"Objednávka"}),e.jsx("th",{children:"Akce"})]})}),e.jsx("tbody",{children:r.map(s=>e.jsxs("tr",{children:[e.jsx("td",{className:"admin-mono",children:e.jsx(o,{to:`/projects/${s.id}`,className:"link-accent",children:s.project_number})}),e.jsx("td",{style:{fontWeight:500},children:s.name||"—"}),e.jsx("td",{children:s.customer_name||"—"}),e.jsx("td",{children:e.jsx("span",{className:`admin-badge ${H[s.status]||""}`,children:I[s.status]||s.status})}),e.jsx("td",{className:"admin-mono",children:N(s.start_date)}),e.jsx("td",{className:"admin-mono",children:N(s.end_date)}),e.jsx("td",{children:s.order_id?e.jsx(o,{to:`/orders/${s.order_id}`,className:"text-secondary",style:{textDecoration:"none"},children:s.order_number}):"—"}),e.jsx("td",{children:e.jsxs("div",{className:"admin-table-actions",children:[e.jsx(o,{to:`/projects/${s.id}`,className:"admin-btn-icon",title:"Upravit","aria-label":"Upravit",children:e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"}),e.jsx("path",{d:"M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"})]})}),!s.order_id&&m("projects.create")&&e.jsx("button",{onClick:()=>h(s),className:"admin-btn-icon danger",title:"Smazat projekt",disabled:j===s.id,children:j===s.id?e.jsx("div",{className:"admin-spinner",style:{width:16,height:16,borderWidth:2}}):e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("polyline",{points:"3 6 5 6 21 6"}),e.jsx("path",{d:"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"}),e.jsx("path",{d:"M10 11v6M14 11v6"})]})})]})})]},s.id))})]})}),e.jsx($,{pagination:x,onPageChange:k})]})}),e.jsx(A,{isOpen:!!a,onClose:()=>h(null),onConfirm:C,title:"Smazat projekt",message:`Opravdu chcete smazat projekt ${a?.project_number}?`,confirmText:"Smazat",type:"danger",loading:!!j})]})}export{q as default}; diff --git a/dist/assets/ReceivedInvoices-Cbz7NucU.js b/dist/assets/ReceivedInvoices-Cbz7NucU.js new file mode 100644 index 0000000..4245fbe --- /dev/null +++ b/dist/assets/ReceivedInvoices-Cbz7NucU.js @@ -0,0 +1 @@ +import{j as e,m as b,A as V}from"./vendor-animation-0s3FMHwK.js";import{r}from"./vendor-react-BVs3cwbi.js";import{a as we,u as ze,b as $e,c as y,e as ne,g as $,F as d,A as T,C as Fe,d as ie}from"./index-BBlIrj2z.js";import{u as De,S}from"./useListData-BVkTFDdr.js";import"./vendor-utils-Dyr8OjFr.js";const f="/api/admin",re={unpaid:"Neuhrazena",paid:"Uhrazena"},le={unpaid:"admin-badge-invoice-overdue",paid:"admin-badge-invoice-paid"},oe=["CZK","EUR","USD","GBP"],ce=[0,10,12,15,21],Ee=["leden","únor","březen","duben","květen","červen","červenec","srpen","září","říjen","listopad","prosinec"];function de(l){return!l||l.length===0?"0 Kč":l.map(c=>$(c.amount,c.currency)).join(" · ")}function q(l,c){return!l||l.length===0?{value:"0 Kč",detail:null}:l.some(w=>w.currency!=="CZK")&&c!==null&&c!==void 0?{value:$(c,"CZK"),detail:de(l)}:{value:de(l),detail:null}}function Pe(){return{supplier_name:"",invoice_number:"",amount:"",currency:"CZK",vat_rate:"21",issue_date:"",due_date:"",notes:""}}Ue.displayName="ReceivedInvoices";function Ue({statsMonth:l,statsYear:c,uploadOpen:O,setUploadOpen:w}){const o=we(),{hasPermission:A}=ze(),{sort:R,order:j,handleSort:N,activeSort:k}=De("created_at"),[F,me]=r.useState(""),[K,ue]=r.useState([]),[M,J]=r.useState(!0),[m,G]=r.useState(null),[he,Q]=r.useState(!0),Z=r.useRef(!1),D=r.useRef(0),[pe,ve]=r.useState(0),X=r.useRef(l),Y=r.useRef(c),[ee,z]=r.useState(!1),[i,h]=r.useState(null),[E,H]=r.useState({show:!1,invoice:null}),[je,ae]=r.useState(!1),[p,P]=r.useState(!1),[_,W]=r.useState([]),[u,U]=r.useState([]),[g,B]=r.useState({}),te=r.useRef(null);$e(O||ee),r.useEffect(()=>{const t=Y.current*12+X.current,a=c*12+l;a>t&&(D.current=1),a{J(!0);try{const t=new URLSearchParams({month:String(l),year:String(c)});F&&t.set("search",F),R&&t.set("sort",R),j&&t.set("order",j);const s=await(await y(`${f}/received-invoices.php?${t}`)).json();s.success&&ue(s.data.invoices||[])}catch{}finally{J(!1)}},[l,c,F,R,j]);r.useEffect(()=>{C()},[C]);const L=r.useCallback(async()=>{try{const a=await(await y(`${f}/received-invoices.php?action=stats&month=${l}&year=${c}`)).json();a.success&&(G(a.data),Z.current=!0)}catch{}},[l,c]);r.useEffect(()=>{Q(!0),(async()=>{try{const s=await(await y(`${f}/received-invoices.php?action=stats&month=${l}&year=${c}`)).json();s.success&&(G(s.data),Z.current=!0,ve(n=>n+1))}catch{}finally{Q(!1)}})()},[l,c]);const xe=t=>{const a=Array.from(t.target.files||[]);if(a.length===0)return;if(_.length+a.length>20){o.error("Maximálně 20 souborů najednou");return}const s=a.filter(n=>n.size>10*1024*1024?(o.error(`Soubor "${n.name}" je větší než 10 MB`),!1):["application/pdf","image/jpeg","image/png"].includes(n.type)?!0:(o.error(`Soubor "${n.name}": nepodporovaný formát`),!1));W(n=>[...n,...s]),U(n=>[...n,...s.map(()=>Pe())]),t.target.value=""},ye=t=>{W(s=>s.filter((n,v)=>v!==t)),U(s=>s.filter((n,v)=>v!==t));const a={...g};delete a[t],B(a)},x=(t,a,s)=>{if(U(n=>n.map((v,Se)=>Se===t?{...v,[a]:s}:v)),g[t]){const n={...g};n[t]?.[a]&&(delete n[t][a],Object.keys(n[t]).length===0&&delete n[t]),B(n)}},fe=()=>{const t={};return u.forEach((a,s)=>{const n={};a.supplier_name.trim()||(n.supplier_name="Povinné pole"),(!a.amount||parseFloat(a.amount)<=0)&&(n.amount="Částka musí být větší než 0"),Object.keys(n).length>0&&(t[s]=n)}),B(t),Object.keys(t).length===0},ge=async()=>{if(_.length===0){o.error("Vyberte alespoň jeden soubor");return}if(fe()){P(!0);try{const t=new FormData;_.forEach(n=>t.append("files[]",n)),t.append("invoices",JSON.stringify(u));const s=await(await y(`${f}/received-invoices.php`,{method:"POST",body:t})).json();s.success?(o.success(s.message||"Faktury byly nahrány"),w(!1),W([]),U([]),B({}),C(),L()):o.error(s.error||"Chyba při nahrávání")}catch{o.error("Chyba připojení")}finally{P(!1)}}},be=t=>{h({...t,amount:String(t.amount),vat_rate:String(t.vat_rate),_originalStatus:t.status}),z(!0)},Ne=async()=>{if(i){if(!i.supplier_name?.trim()){o.error("Dodavatel je povinný");return}if(!i.amount||parseFloat(i.amount)<=0){o.error("Částka musí být větší než 0");return}P(!0);try{const t={supplier_name:i.supplier_name,invoice_number:i.invoice_number||"",amount:parseFloat(i.amount),currency:i.currency,vat_rate:parseFloat(i.vat_rate),issue_date:i.issue_date||"",due_date:i.due_date||"",notes:i.notes||"",status:i.status},s=await(await y(`${f}/received-invoices.php?id=${i.id}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json();s.success?(o.success(s.message||"Faktura byla aktualizována"),z(!1),h(null),C(),L()):o.error(s.error||"Chyba při ukládání")}catch{o.error("Chyba připojení")}finally{P(!1)}}},ke=async()=>{if(E.invoice){ae(!0);try{const a=await(await y(`${f}/received-invoices.php?id=${E.invoice.id}`,{method:"DELETE"})).json();a.success?(o.success(a.message||"Faktura byla smazána"),H({show:!1,invoice:null}),C(),L()):o.error(a.error||"Chyba při mazání")}catch{o.error("Chyba připojení")}finally{ae(!1)}}},se=async t=>{const a=window.open("","_blank");try{const s=await y(`${f}/received-invoices.php?action=file&id=${t.id}`);if(!s.ok){a.close(),o.error("Nepodařilo se načíst soubor");return}const n=await s.blob(),v=URL.createObjectURL(n);a.location.href=v,setTimeout(()=>URL.revokeObjectURL(v),6e4)}catch{a.close(),o.error("Chyba připojení")}},_e=async t=>{if(t.status!=="paid")try{const s=await(await y(`${f}/received-invoices.php?id=${t.id}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({status:"paid"})})).json();s.success?(o.success("Faktura označena jako uhrazená"),C(),L()):o.error(s.error||"Nepodařilo se změnit stav")}catch{o.error("Chyba připojení")}},I=`${Ee[l-1]}`,Ce=()=>{if(!Z.current&&he)return e.jsx("div",{className:"dash-kpi-grid dash-kpi-4",style:{marginBottom:"1.5rem"},children:[0,1,2,3].map(n=>e.jsxs("div",{className:"admin-stat-card",children:[e.jsx("div",{className:"admin-skeleton-line",style:{width:"60%",height:"11px",marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line",style:{width:"40%",height:"28px",marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line",style:{width:"50%",height:"12px"}})]},n))});if(!m)return null;const t=q(m.total_month,m.total_month_czk),a=q(m.vat_month,m.vat_month_czk),s=q(m.unpaid,m.unpaid_czk);return e.jsx("div",{style:{overflow:"hidden",marginBottom:"1.5rem"},children:e.jsx(V,{mode:"popLayout",initial:!1,custom:D.current,children:e.jsxs(b.div,{className:"dash-kpi-grid dash-kpi-4",custom:D.current,variants:{enter:n=>({x:`${(n||0)*105}%`,opacity:0}),center:{x:"0%",opacity:1},exit:n=>({x:`${(n||0)*-105}%`,opacity:0})},initial:"enter",animate:"center",exit:"exit",transition:{type:"spring",stiffness:300,damping:30},children:[e.jsxs("div",{className:"admin-stat-card success",children:[e.jsxs("div",{className:"admin-stat-label",children:["Celkem (",I,")"]}),e.jsx("div",{className:"admin-stat-value admin-mono",children:t.value}),e.jsx("div",{className:"admin-stat-footer",children:t.detail||`${m.month_count} ${ie(m.month_count,"faktura","faktury","faktur")}`})]}),e.jsxs("div",{className:"admin-stat-card info",children:[e.jsxs("div",{className:"admin-stat-label",children:["DPH k odpočtu (",I,")"]}),e.jsx("div",{className:"admin-stat-value admin-mono",children:a.value}),e.jsx("div",{className:"admin-stat-footer",children:a.detail||"z přijatých faktur"})]}),e.jsxs("div",{className:"admin-stat-card warning",children:[e.jsxs("div",{className:"admin-stat-label",children:["Neuhrazeno ",e.jsx("span",{style:{fontWeight:400,opacity:.7},children:"· celkově"})]}),e.jsx("div",{className:"admin-stat-value admin-mono",children:s.value}),e.jsx("div",{className:"admin-stat-footer",children:s.detail||(m.unpaid_count===0?"vše uhrazeno":`${m.unpaid_count} ${ie(m.unpaid_count,"faktura","faktury","faktur")}`)})]}),e.jsxs("div",{className:"admin-stat-card",children:[e.jsxs("div",{className:"admin-stat-label",children:["Počet (",I,")"]}),e.jsx("div",{className:"admin-stat-value admin-mono",children:m.month_count}),e.jsx("div",{className:"admin-stat-footer",children:m.month_count===0?"žádné faktury":"přijatých faktur"})]})]},pe)})})};return e.jsxs(e.Fragment,{children:[Ce(),e.jsx(b.div,{initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.1}}),e.jsx(b.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.15},children:e.jsxs("div",{className:"admin-card-body",children:[e.jsx("div",{className:"admin-search-bar",style:{marginBottom:"1rem"},children:e.jsx("input",{type:"text",value:F,onChange:t=>me(t.target.value),className:"admin-form-input",placeholder:"Hledat podle dodavatele nebo čísla faktury..."})}),M&&e.jsx("div",{className:"admin-skeleton",style:{gap:"1rem"},children:[0,1,2,3,4].map(t=>e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line w-1/4"}),e.jsx("div",{className:"admin-skeleton-line w-1/4"}),e.jsx("div",{className:"admin-skeleton-line w-1/4"})]},t))}),!M&&K.length===0&&e.jsxs("div",{className:"admin-empty-state",children:[e.jsx("div",{className:"admin-empty-icon",children:e.jsxs("svg",{width:"28",height:"28",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("path",{d:"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"}),e.jsx("polyline",{points:"14 2 14 8 20 8"}),e.jsx("line",{x1:"16",y1:"13",x2:"8",y2:"13"}),e.jsx("line",{x1:"16",y1:"17",x2:"8",y2:"17"})]})}),e.jsx("p",{children:"Žádné přijaté faktury v tomto měsíci."}),A("invoices.create")&&e.jsx("p",{style:{color:"var(--text-tertiary)",fontSize:"0.875rem"},children:"Nahrajte faktury tlačítkem výše."})]}),!M&&K.length>0&&e.jsx("div",{className:"admin-table-responsive",children:e.jsxs("table",{className:"admin-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsxs("th",{style:{cursor:"pointer"},onClick:()=>N("supplier_name"),children:["Dodavatel ",e.jsx(S,{column:"supplier_name",sort:k,order:j})]}),e.jsxs("th",{style:{cursor:"pointer"},onClick:()=>N("invoice_number"),children:["Č. faktury ",e.jsx(S,{column:"invoice_number",sort:k,order:j})]}),e.jsxs("th",{style:{cursor:"pointer"},onClick:()=>N("status"),children:["Stav ",e.jsx(S,{column:"status",sort:k,order:j})]}),e.jsxs("th",{style:{cursor:"pointer"},onClick:()=>N("issue_date"),children:["Vystaveno ",e.jsx(S,{column:"issue_date",sort:k,order:j})]}),e.jsxs("th",{style:{cursor:"pointer"},onClick:()=>N("due_date"),children:["Splatnost ",e.jsx(S,{column:"due_date",sort:k,order:j})]}),e.jsxs("th",{style:{textAlign:"right",cursor:"pointer"},onClick:()=>N("amount"),children:["Částka ",e.jsx(S,{column:"amount",sort:k,order:j})]}),e.jsx("th",{children:"Akce"})]})}),e.jsx("tbody",{children:K.map(t=>e.jsxs("tr",{children:[e.jsx("td",{children:t.supplier_name}),e.jsx("td",{className:"admin-mono",children:t.invoice_number?e.jsx("span",{className:"link-accent",style:{cursor:"pointer"},onClick:()=>se(t),children:t.invoice_number}):"—"}),e.jsx("td",{children:t.status==="paid"?e.jsx("span",{className:`admin-badge ${le[t.status]}`,children:re[t.status]}):e.jsx("button",{onClick:()=>_e(t),className:`admin-badge ${le[t.status]||""}`,style:{cursor:"pointer"},children:re[t.status]||t.status})}),e.jsx("td",{className:"admin-mono",children:ne(t.issue_date)}),e.jsx("td",{className:"admin-mono",children:ne(t.due_date)}),e.jsx("td",{className:"admin-mono",style:{textAlign:"right",fontWeight:500},children:$(t.amount,t.currency)}),e.jsx("td",{children:e.jsxs("div",{className:"admin-table-actions",children:[t.file_name&&e.jsx("button",{className:"admin-btn-icon",title:"Zobrazit soubor",onClick:()=>se(t),children:e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"}),e.jsx("circle",{cx:"12",cy:"12",r:"3"})]})}),A("invoices.edit")&&e.jsx("button",{className:"admin-btn-icon",title:"Upravit",onClick:()=>be(t),children:e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"}),e.jsx("path",{d:"M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"})]})}),A("invoices.delete")&&e.jsx("button",{className:"admin-btn-icon danger",title:"Smazat",onClick:()=>H({show:!0,invoice:t}),children:e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("polyline",{points:"3 6 5 6 21 6"}),e.jsx("path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"})]})})]})})]},t.id))})]})})]})}),e.jsx(V,{children:O&&e.jsxs(b.div,{className:"admin-modal-overlay",initial:{opacity:0},animate:{opacity:1},exit:{opacity:0},transition:{duration:.2},children:[e.jsx("div",{className:"admin-modal-backdrop",onClick:()=>!p&&w(!1)}),e.jsxs(b.div,{className:"admin-modal admin-modal-lg",initial:{opacity:0,scale:.95,y:20},animate:{opacity:1,scale:1,y:0},exit:{opacity:0,scale:.95,y:20},transition:{duration:.2},children:[e.jsx("div",{className:"admin-modal-header",children:e.jsx("h2",{className:"admin-modal-title",children:"Nahrát přijaté faktury"})}),e.jsxs("div",{className:"admin-modal-body",children:[e.jsxs("div",{style:{marginBottom:"1rem"},children:[e.jsx("input",{ref:te,type:"file",multiple:!0,accept:"application/pdf,image/jpeg,image/png",style:{display:"none"},onChange:xe}),e.jsxs("button",{className:"admin-btn admin-btn-secondary admin-btn-sm",onClick:()=>te.current?.click(),children:[e.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"}),e.jsx("polyline",{points:"17 8 12 3 7 8"}),e.jsx("line",{x1:"12",y1:"3",x2:"12",y2:"15"})]}),"Vybrat soubory"]}),e.jsx("span",{style:{marginLeft:"0.75rem",fontSize:"0.8125rem",color:"var(--text-tertiary)"},children:"PDF, JPEG, PNG · max 10 MB · max 20 souborů"})]}),_.length===0&&e.jsx("div",{className:"admin-empty-state",style:{padding:"2rem 0"},children:e.jsx("p",{style:{color:"var(--text-tertiary)"},children:"Zatím nebyly vybrány žádné soubory."})}),e.jsx("div",{className:"received-upload-list",children:_.map((t,a)=>e.jsxs("div",{className:"received-upload-card",children:[e.jsxs("div",{className:"received-upload-card-header",children:[e.jsxs("div",{className:"received-upload-file-info",children:[e.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"}),e.jsx("polyline",{points:"14 2 14 8 20 8"})]}),e.jsx("span",{className:"received-upload-file-name",children:t.name}),e.jsxs("span",{className:"received-upload-file-size",children:[Math.round(t.size/1024)," KB"]})]}),e.jsx("button",{className:"admin-btn-icon danger",style:{width:"24px",height:"24px"},onClick:()=>ye(a),children:e.jsxs("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),e.jsx("line",{x1:"6",y1:"6",x2:"18",y2:"18"})]})})]}),e.jsxs("div",{className:"received-upload-card-fields",children:[e.jsx(d,{label:"Dodavatel",error:g[a]?.supplier_name,required:!0,children:e.jsx("input",{type:"text",className:`admin-form-input${g[a]?.supplier_name?" has-error":""}`,value:u[a]?.supplier_name||"",onChange:s=>x(a,"supplier_name",s.target.value)})}),e.jsx(d,{label:"Č. faktury",children:e.jsx("input",{type:"text",className:"admin-form-input",value:u[a]?.invoice_number||"",onChange:s=>x(a,"invoice_number",s.target.value)})}),e.jsxs("div",{className:"received-upload-row",children:[e.jsx(d,{label:"Částka",error:g[a]?.amount,required:!0,style:{flex:1},children:e.jsx("input",{type:"number",step:"0.01",min:"0",className:`admin-form-input${g[a]?.amount?" has-error":""}`,value:u[a]?.amount||"",onChange:s=>x(a,"amount",s.target.value)})}),e.jsx(d,{label:"Měna",style:{width:"90px"},children:e.jsx("select",{className:"admin-form-select",value:u[a]?.currency||"CZK",onChange:s=>x(a,"currency",s.target.value),children:oe.map(s=>e.jsx("option",{value:s,children:s},s))})}),e.jsx(d,{label:"DPH %",style:{width:"90px"},children:e.jsx("select",{className:"admin-form-select",value:u[a]?.vat_rate||"21",onChange:s=>x(a,"vat_rate",s.target.value),children:ce.map(s=>e.jsxs("option",{value:String(s),children:[s,"%"]},s))})})]}),u[a]?.amount&&e.jsxs("div",{style:{fontSize:"0.75rem",color:"var(--text-tertiary)",marginTop:"-0.25rem",marginBottom:"0.5rem"},children:["DPH: ",$(parseFloat(u[a].amount||0)*parseFloat(u[a].vat_rate||21)/100,u[a].currency||"CZK")]}),e.jsxs("div",{className:"received-upload-row",children:[e.jsx(d,{label:"Datum vystavení",style:{flex:1},children:e.jsx(T,{mode:"date",value:u[a]?.issue_date||"",onChange:s=>x(a,"issue_date",s)})}),e.jsx(d,{label:"Datum splatnosti",style:{flex:1},children:e.jsx(T,{mode:"date",value:u[a]?.due_date||"",onChange:s=>x(a,"due_date",s)})})]}),e.jsx(d,{label:"Poznámka",children:e.jsx("input",{type:"text",className:"admin-form-input",value:u[a]?.notes||"",onChange:s=>x(a,"notes",s.target.value)})})]})]},`${t.name}-${a}`))})]}),e.jsxs("div",{className:"admin-modal-footer",children:[e.jsx("button",{className:"admin-btn admin-btn-secondary",onClick:()=>!p&&w(!1),disabled:p,children:"Zrušit"}),e.jsx("button",{className:"admin-btn admin-btn-primary",onClick:ge,disabled:p||_.length===0,children:p?"Nahrávání...":"Uložit vše"})]})]})]})}),e.jsx(V,{children:ee&&i&&e.jsxs(b.div,{className:"admin-modal-overlay",initial:{opacity:0},animate:{opacity:1},exit:{opacity:0},transition:{duration:.2},children:[e.jsx("div",{className:"admin-modal-backdrop",onClick:()=>!p&&z(!1)}),e.jsx(b.div,{className:"admin-modal",initial:{opacity:0,scale:.95,y:20},animate:{opacity:1,scale:1,y:0},exit:{opacity:0,scale:.95,y:20},transition:{duration:.2},children:(()=>{const t=i._originalStatus==="paid";return e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"admin-modal-header",children:e.jsx("h2",{className:"admin-modal-title",children:t?"Detail přijaté faktury":"Upravit přijatou fakturu"})}),e.jsx("div",{className:"admin-modal-body",children:e.jsxs("div",{className:"admin-form",children:[e.jsx(d,{label:"Dodavatel",required:!0,children:e.jsx("input",{type:"text",className:"admin-form-input",value:i.supplier_name,onChange:a=>h(s=>({...s,supplier_name:a.target.value})),readOnly:t})}),e.jsx(d,{label:"Č. faktury",children:e.jsx("input",{type:"text",className:"admin-form-input",value:i.invoice_number||"",onChange:a=>h(s=>({...s,invoice_number:a.target.value})),readOnly:t})}),e.jsxs("div",{className:"admin-form-row admin-form-row-3",children:[e.jsx(d,{label:"Částka",required:!0,children:e.jsx("input",{type:"number",step:"0.01",min:"0",className:"admin-form-input",value:i.amount,onChange:a=>h(s=>({...s,amount:a.target.value})),readOnly:t})}),e.jsx(d,{label:"Měna",children:e.jsx("select",{className:"admin-form-select",value:i.currency,onChange:a=>h(s=>({...s,currency:a.target.value})),disabled:t,children:oe.map(a=>e.jsx("option",{value:a,children:a},a))})}),e.jsx(d,{label:"DPH %",children:e.jsx("select",{className:"admin-form-select",value:i.vat_rate,onChange:a=>h(s=>({...s,vat_rate:a.target.value})),disabled:t,children:ce.map(a=>e.jsxs("option",{value:String(a),children:[a,"%"]},a))})})]}),i.amount&&e.jsxs("div",{style:{fontSize:"0.75rem",color:"var(--text-tertiary)",marginBottom:"0.75rem"},children:["DPH: ",$(parseFloat(i.amount||0)*parseFloat(i.vat_rate||21)/100,i.currency||"CZK")]}),e.jsxs("div",{className:"admin-form-row",children:[e.jsx(d,{label:"Datum vystavení",children:e.jsx(T,{mode:"date",value:i.issue_date||"",onChange:a=>h(s=>({...s,issue_date:a})),disabled:t})}),e.jsx(d,{label:"Datum splatnosti",children:e.jsx(T,{mode:"date",value:i.due_date||"",onChange:a=>h(s=>({...s,due_date:a})),disabled:t})})]}),e.jsx(d,{label:"Stav",children:e.jsxs("select",{className:"admin-form-select",value:i.status,onChange:a=>h(s=>({...s,status:a.target.value})),disabled:t,children:[e.jsx("option",{value:"unpaid",children:"Neuhrazena"}),e.jsx("option",{value:"paid",children:"Uhrazena"})]})}),e.jsx(d,{label:"Poznámka",children:e.jsx("textarea",{className:"admin-form-input",rows:3,value:i.notes||"",onChange:a=>h(s=>({...s,notes:a.target.value})),readOnly:t})})]})}),e.jsx("div",{className:"admin-modal-footer",children:t?e.jsx("button",{className:"admin-btn admin-btn-secondary",onClick:()=>z(!1),children:"Zavřít"}):e.jsxs(e.Fragment,{children:[e.jsx("button",{className:"admin-btn admin-btn-secondary",onClick:()=>!p&&z(!1),disabled:p,children:"Zrušit"}),e.jsx("button",{className:"admin-btn admin-btn-primary",onClick:Ne,disabled:p,children:p?"Ukládání...":"Uložit"})]})})]})})()})]})}),e.jsx(Fe,{isOpen:E.show,onClose:()=>H({show:!1,invoice:null}),onConfirm:ke,title:"Smazat přijatou fakturu",message:`Opravdu chcete smazat fakturu "${E.invoice?.supplier_name||""}"? Tato akce je nevratná.`,confirmText:"Smazat",cancelText:"Zrušit",type:"danger",loading:je})]})}export{Ue as default}; diff --git a/dist/assets/RichEditor-7oN3-GhD.css b/dist/assets/RichEditor-7oN3-GhD.css new file mode 100644 index 0000000..ee8e1f5 --- /dev/null +++ b/dist/assets/RichEditor-7oN3-GhD.css @@ -0,0 +1,7 @@ +/*! + * Quill Editor v2.0.3 + * https://quilljs.com + * Copyright (c) 2017-2024, Slab + * Copyright (c) 2014, Jason Chen + * Copyright (c) 2013, salesforce.com + */.ql-container{box-sizing:border-box;font-family:Helvetica,Arial,sans-serif;font-size:13px;height:100%;margin:0;position:relative}.ql-container.ql-disabled .ql-tooltip{visibility:hidden}.ql-container:not(.ql-disabled) li[data-list=checked]>.ql-ui,.ql-container:not(.ql-disabled) li[data-list=unchecked]>.ql-ui{cursor:pointer}.ql-clipboard{left:-100000px;height:1px;overflow-y:hidden;position:absolute;top:50%}.ql-clipboard p{margin:0;padding:0}.ql-editor{box-sizing:border-box;counter-reset:list-0 list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;line-height:1.42;height:100%;outline:none;overflow-y:auto;padding:12px 15px;tab-size:4;-moz-tab-size:4;text-align:left;white-space:pre-wrap;word-wrap:break-word}.ql-editor>*{cursor:text}.ql-editor p,.ql-editor ol,.ql-editor pre,.ql-editor blockquote,.ql-editor h1,.ql-editor h2,.ql-editor h3,.ql-editor h4,.ql-editor h5,.ql-editor h6{margin:0;padding:0}@supports (counter-set:none){.ql-editor p,.ql-editor h1,.ql-editor h2,.ql-editor h3,.ql-editor h4,.ql-editor h5,.ql-editor h6{counter-set:list-0 list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor p,.ql-editor h1,.ql-editor h2,.ql-editor h3,.ql-editor h4,.ql-editor h5,.ql-editor h6{counter-reset:list-0 list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}.ql-editor table{border-collapse:collapse}.ql-editor td{border:1px solid #000;padding:2px 5px}.ql-editor ol{padding-left:1.5em}.ql-editor li{list-style-type:none;padding-left:1.5em;position:relative}.ql-editor li>.ql-ui:before{display:inline-block;margin-left:-1.5em;margin-right:.3em;text-align:right;white-space:nowrap;width:1.2em}.ql-editor li[data-list=checked]>.ql-ui,.ql-editor li[data-list=unchecked]>.ql-ui{color:#777}.ql-editor li[data-list=bullet]>.ql-ui:before{content:"•"}.ql-editor li[data-list=checked]>.ql-ui:before{content:"☑"}.ql-editor li[data-list=unchecked]>.ql-ui:before{content:"☐"}@supports (counter-set:none){.ql-editor li[data-list]{counter-set:list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list]{counter-reset:list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered]{counter-increment:list-0}.ql-editor li[data-list=ordered]>.ql-ui:before{content:counter(list-0,decimal) ". "}.ql-editor li[data-list=ordered].ql-indent-1{counter-increment:list-1}.ql-editor li[data-list=ordered].ql-indent-1>.ql-ui:before{content:counter(list-1,lower-alpha) ". "}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-1{counter-set:list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-1{counter-reset:list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-2{counter-increment:list-2}.ql-editor li[data-list=ordered].ql-indent-2>.ql-ui:before{content:counter(list-2,lower-roman) ". "}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-2{counter-set:list-3 list-4 list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-2{counter-reset:list-3 list-4 list-5 list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-3{counter-increment:list-3}.ql-editor li[data-list=ordered].ql-indent-3>.ql-ui:before{content:counter(list-3,decimal) ". "}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-3{counter-set:list-4 list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-3{counter-reset:list-4 list-5 list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-4{counter-increment:list-4}.ql-editor li[data-list=ordered].ql-indent-4>.ql-ui:before{content:counter(list-4,lower-alpha) ". "}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-4{counter-set:list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-4{counter-reset:list-5 list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-5{counter-increment:list-5}.ql-editor li[data-list=ordered].ql-indent-5>.ql-ui:before{content:counter(list-5,lower-roman) ". "}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-5{counter-set:list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-5{counter-reset:list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-6{counter-increment:list-6}.ql-editor li[data-list=ordered].ql-indent-6>.ql-ui:before{content:counter(list-6,decimal) ". "}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-6{counter-set:list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-6{counter-reset:list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-7{counter-increment:list-7}.ql-editor li[data-list=ordered].ql-indent-7>.ql-ui:before{content:counter(list-7,lower-alpha) ". "}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-7{counter-set:list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-7{counter-reset:list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-8{counter-increment:list-8}.ql-editor li[data-list=ordered].ql-indent-8>.ql-ui:before{content:counter(list-8,lower-roman) ". "}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-8{counter-set:list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-8{counter-reset:list-9}}.ql-editor li[data-list=ordered].ql-indent-9{counter-increment:list-9}.ql-editor li[data-list=ordered].ql-indent-9>.ql-ui:before{content:counter(list-9,decimal) ". "}.ql-editor .ql-indent-1:not(.ql-direction-rtl){padding-left:3em}.ql-editor li.ql-indent-1:not(.ql-direction-rtl){padding-left:4.5em}.ql-editor .ql-indent-1.ql-direction-rtl.ql-align-right{padding-right:3em}.ql-editor li.ql-indent-1.ql-direction-rtl.ql-align-right{padding-right:4.5em}.ql-editor .ql-indent-2:not(.ql-direction-rtl){padding-left:6em}.ql-editor li.ql-indent-2:not(.ql-direction-rtl){padding-left:7.5em}.ql-editor .ql-indent-2.ql-direction-rtl.ql-align-right{padding-right:6em}.ql-editor li.ql-indent-2.ql-direction-rtl.ql-align-right{padding-right:7.5em}.ql-editor .ql-indent-3:not(.ql-direction-rtl){padding-left:9em}.ql-editor li.ql-indent-3:not(.ql-direction-rtl){padding-left:10.5em}.ql-editor .ql-indent-3.ql-direction-rtl.ql-align-right{padding-right:9em}.ql-editor li.ql-indent-3.ql-direction-rtl.ql-align-right{padding-right:10.5em}.ql-editor .ql-indent-4:not(.ql-direction-rtl){padding-left:12em}.ql-editor li.ql-indent-4:not(.ql-direction-rtl){padding-left:13.5em}.ql-editor .ql-indent-4.ql-direction-rtl.ql-align-right{padding-right:12em}.ql-editor li.ql-indent-4.ql-direction-rtl.ql-align-right{padding-right:13.5em}.ql-editor .ql-indent-5:not(.ql-direction-rtl){padding-left:15em}.ql-editor li.ql-indent-5:not(.ql-direction-rtl){padding-left:16.5em}.ql-editor .ql-indent-5.ql-direction-rtl.ql-align-right{padding-right:15em}.ql-editor li.ql-indent-5.ql-direction-rtl.ql-align-right{padding-right:16.5em}.ql-editor .ql-indent-6:not(.ql-direction-rtl){padding-left:18em}.ql-editor li.ql-indent-6:not(.ql-direction-rtl){padding-left:19.5em}.ql-editor .ql-indent-6.ql-direction-rtl.ql-align-right{padding-right:18em}.ql-editor li.ql-indent-6.ql-direction-rtl.ql-align-right{padding-right:19.5em}.ql-editor .ql-indent-7:not(.ql-direction-rtl){padding-left:21em}.ql-editor li.ql-indent-7:not(.ql-direction-rtl){padding-left:22.5em}.ql-editor .ql-indent-7.ql-direction-rtl.ql-align-right{padding-right:21em}.ql-editor li.ql-indent-7.ql-direction-rtl.ql-align-right{padding-right:22.5em}.ql-editor .ql-indent-8:not(.ql-direction-rtl){padding-left:24em}.ql-editor li.ql-indent-8:not(.ql-direction-rtl){padding-left:25.5em}.ql-editor .ql-indent-8.ql-direction-rtl.ql-align-right{padding-right:24em}.ql-editor li.ql-indent-8.ql-direction-rtl.ql-align-right{padding-right:25.5em}.ql-editor .ql-indent-9:not(.ql-direction-rtl){padding-left:27em}.ql-editor li.ql-indent-9:not(.ql-direction-rtl){padding-left:28.5em}.ql-editor .ql-indent-9.ql-direction-rtl.ql-align-right{padding-right:27em}.ql-editor li.ql-indent-9.ql-direction-rtl.ql-align-right{padding-right:28.5em}.ql-editor li.ql-direction-rtl{padding-right:1.5em}.ql-editor li.ql-direction-rtl>.ql-ui:before{margin-left:.3em;margin-right:-1.5em;text-align:left}.ql-editor table{table-layout:fixed;width:100%}.ql-editor table td{outline:none}.ql-editor .ql-code-block-container{font-family:monospace}.ql-editor .ql-video{display:block;max-width:100%}.ql-editor .ql-video.ql-align-center{margin:0 auto}.ql-editor .ql-video.ql-align-right{margin:0 0 0 auto}.ql-editor .ql-bg-black{background-color:#000}.ql-editor .ql-bg-red{background-color:#e60000}.ql-editor .ql-bg-orange{background-color:#f90}.ql-editor .ql-bg-yellow{background-color:#ff0}.ql-editor .ql-bg-green{background-color:#008a00}.ql-editor .ql-bg-blue{background-color:#06c}.ql-editor .ql-bg-purple{background-color:#93f}.ql-editor .ql-color-white{color:#fff}.ql-editor .ql-color-red{color:#e60000}.ql-editor .ql-color-orange{color:#f90}.ql-editor .ql-color-yellow{color:#ff0}.ql-editor .ql-color-green{color:#008a00}.ql-editor .ql-color-blue{color:#06c}.ql-editor .ql-color-purple{color:#93f}.ql-editor .ql-font-serif{font-family:Georgia,Times New Roman,serif}.ql-editor .ql-font-monospace{font-family:Monaco,Courier New,monospace}.ql-editor .ql-size-small{font-size:.75em}.ql-editor .ql-size-large{font-size:1.5em}.ql-editor .ql-size-huge{font-size:2.5em}.ql-editor .ql-direction-rtl{direction:rtl;text-align:inherit}.ql-editor .ql-align-center{text-align:center}.ql-editor .ql-align-justify{text-align:justify}.ql-editor .ql-align-right{text-align:right}.ql-editor .ql-ui{position:absolute}.ql-editor.ql-blank:before{color:#0009;content:attr(data-placeholder);font-style:italic;left:15px;pointer-events:none;position:absolute;right:15px}.ql-snow.ql-toolbar:after,.ql-snow .ql-toolbar:after{clear:both;content:"";display:table}.ql-snow.ql-toolbar button,.ql-snow .ql-toolbar button{background:none;border:none;cursor:pointer;display:inline-block;float:left;height:24px;padding:3px 5px;width:28px}.ql-snow.ql-toolbar button svg,.ql-snow .ql-toolbar button svg{float:left;height:100%}.ql-snow.ql-toolbar button:active:hover,.ql-snow .ql-toolbar button:active:hover{outline:none}.ql-snow.ql-toolbar input.ql-image[type=file],.ql-snow .ql-toolbar input.ql-image[type=file]{display:none}.ql-snow.ql-toolbar button:hover,.ql-snow .ql-toolbar button:hover,.ql-snow.ql-toolbar button:focus,.ql-snow .ql-toolbar button:focus,.ql-snow.ql-toolbar button.ql-active,.ql-snow .ql-toolbar button.ql-active,.ql-snow.ql-toolbar .ql-picker-label:hover,.ql-snow .ql-toolbar .ql-picker-label:hover,.ql-snow.ql-toolbar .ql-picker-label.ql-active,.ql-snow .ql-toolbar .ql-picker-label.ql-active,.ql-snow.ql-toolbar .ql-picker-item:hover,.ql-snow .ql-toolbar .ql-picker-item:hover,.ql-snow.ql-toolbar .ql-picker-item.ql-selected,.ql-snow .ql-toolbar .ql-picker-item.ql-selected{color:#06c}.ql-snow.ql-toolbar button:hover .ql-fill,.ql-snow .ql-toolbar button:hover .ql-fill,.ql-snow.ql-toolbar button:focus .ql-fill,.ql-snow .ql-toolbar button:focus .ql-fill,.ql-snow.ql-toolbar button.ql-active .ql-fill,.ql-snow .ql-toolbar button.ql-active .ql-fill,.ql-snow.ql-toolbar .ql-picker-label:hover .ql-fill,.ql-snow .ql-toolbar .ql-picker-label:hover .ql-fill,.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-fill,.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-fill,.ql-snow.ql-toolbar .ql-picker-item:hover .ql-fill,.ql-snow .ql-toolbar .ql-picker-item:hover .ql-fill,.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-fill,.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-fill,.ql-snow.ql-toolbar button:hover .ql-stroke.ql-fill,.ql-snow .ql-toolbar button:hover .ql-stroke.ql-fill,.ql-snow.ql-toolbar button:focus .ql-stroke.ql-fill,.ql-snow .ql-toolbar button:focus .ql-stroke.ql-fill,.ql-snow.ql-toolbar button.ql-active .ql-stroke.ql-fill,.ql-snow .ql-toolbar button.ql-active .ql-stroke.ql-fill,.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill,.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill,.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill,.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill,.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill,.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill,.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill,.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill{fill:#06c}.ql-snow.ql-toolbar button:hover .ql-stroke,.ql-snow .ql-toolbar button:hover .ql-stroke,.ql-snow.ql-toolbar button:focus .ql-stroke,.ql-snow .ql-toolbar button:focus .ql-stroke,.ql-snow.ql-toolbar button.ql-active .ql-stroke,.ql-snow .ql-toolbar button.ql-active .ql-stroke,.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke,.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke,.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke,.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke,.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke,.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke,.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke,.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke,.ql-snow.ql-toolbar button:hover .ql-stroke-miter,.ql-snow .ql-toolbar button:hover .ql-stroke-miter,.ql-snow.ql-toolbar button:focus .ql-stroke-miter,.ql-snow .ql-toolbar button:focus .ql-stroke-miter,.ql-snow.ql-toolbar button.ql-active .ql-stroke-miter,.ql-snow .ql-toolbar button.ql-active .ql-stroke-miter,.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke-miter,.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke-miter,.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter,.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter,.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke-miter,.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke-miter,.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter,.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter{stroke:#06c}@media (pointer:coarse){.ql-snow.ql-toolbar button:hover:not(.ql-active),.ql-snow .ql-toolbar button:hover:not(.ql-active){color:#444}.ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-fill,.ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-fill,.ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill,.ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill{fill:#444}.ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke,.ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke,.ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter,.ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter{stroke:#444}}.ql-snow,.ql-snow *{box-sizing:border-box}.ql-snow .ql-hidden{display:none}.ql-snow .ql-out-bottom,.ql-snow .ql-out-top{visibility:hidden}.ql-snow .ql-tooltip{position:absolute;transform:translateY(10px)}.ql-snow .ql-tooltip a{cursor:pointer;text-decoration:none}.ql-snow .ql-tooltip.ql-flip{transform:translateY(-10px)}.ql-snow .ql-formats{display:inline-block;vertical-align:middle}.ql-snow .ql-formats:after{clear:both;content:"";display:table}.ql-snow .ql-stroke{fill:none;stroke:#444;stroke-linecap:round;stroke-linejoin:round;stroke-width:2}.ql-snow .ql-stroke-miter{fill:none;stroke:#444;stroke-miterlimit:10;stroke-width:2}.ql-snow .ql-fill,.ql-snow .ql-stroke.ql-fill{fill:#444}.ql-snow .ql-empty{fill:none}.ql-snow .ql-even{fill-rule:evenodd}.ql-snow .ql-thin,.ql-snow .ql-stroke.ql-thin{stroke-width:1}.ql-snow .ql-transparent{opacity:.4}.ql-snow .ql-direction svg:last-child{display:none}.ql-snow .ql-direction.ql-active svg:last-child{display:inline}.ql-snow .ql-direction.ql-active svg:first-child{display:none}.ql-snow .ql-editor h1{font-size:2em}.ql-snow .ql-editor h2{font-size:1.5em}.ql-snow .ql-editor h3{font-size:1.17em}.ql-snow .ql-editor h4{font-size:1em}.ql-snow .ql-editor h5{font-size:.83em}.ql-snow .ql-editor h6{font-size:.67em}.ql-snow .ql-editor a{text-decoration:underline}.ql-snow .ql-editor blockquote{border-left:4px solid #ccc;margin-bottom:5px;margin-top:5px;padding-left:16px}.ql-snow .ql-editor code,.ql-snow .ql-editor .ql-code-block-container{background-color:#f0f0f0;border-radius:3px}.ql-snow .ql-editor .ql-code-block-container{margin-bottom:5px;margin-top:5px;padding:5px 10px}.ql-snow .ql-editor code{font-size:85%;padding:2px 4px}.ql-snow .ql-editor .ql-code-block-container{background-color:#23241f;color:#f8f8f2;overflow:visible}.ql-snow .ql-editor img{max-width:100%}.ql-snow .ql-picker{color:#444;display:inline-block;float:left;font-size:14px;font-weight:500;height:24px;position:relative;vertical-align:middle}.ql-snow .ql-picker-label{cursor:pointer;display:inline-block;height:100%;padding-left:8px;padding-right:2px;position:relative;width:100%}.ql-snow .ql-picker-label:before{display:inline-block;line-height:22px}.ql-snow .ql-picker-options{background-color:#fff;display:none;min-width:100%;padding:4px 8px;position:absolute;white-space:nowrap}.ql-snow .ql-picker-options .ql-picker-item{cursor:pointer;display:block;padding-bottom:5px;padding-top:5px}.ql-snow .ql-picker.ql-expanded .ql-picker-label{color:#ccc;z-index:2}.ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-fill{fill:#ccc}.ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-stroke{stroke:#ccc}.ql-snow .ql-picker.ql-expanded .ql-picker-options{display:block;margin-top:-1px;top:100%;z-index:1}.ql-snow .ql-color-picker,.ql-snow .ql-icon-picker{width:28px}.ql-snow .ql-color-picker .ql-picker-label,.ql-snow .ql-icon-picker .ql-picker-label{padding:2px 4px}.ql-snow .ql-color-picker .ql-picker-label svg,.ql-snow .ql-icon-picker .ql-picker-label svg{right:4px}.ql-snow .ql-icon-picker .ql-picker-options{padding:4px 0}.ql-snow .ql-icon-picker .ql-picker-item{height:24px;width:24px;padding:2px 4px}.ql-snow .ql-color-picker .ql-picker-options{padding:3px 5px;width:152px}.ql-snow .ql-color-picker .ql-picker-item{border:1px solid transparent;float:left;height:16px;margin:2px;padding:0;width:16px}.ql-snow .ql-picker:not(.ql-color-picker):not(.ql-icon-picker) svg{position:absolute;margin-top:-9px;right:0;top:50%;width:18px}.ql-snow .ql-picker.ql-header .ql-picker-label[data-label]:not([data-label=""]):before,.ql-snow .ql-picker.ql-font .ql-picker-label[data-label]:not([data-label=""]):before,.ql-snow .ql-picker.ql-size .ql-picker-label[data-label]:not([data-label=""]):before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-label]:not([data-label=""]):before,.ql-snow .ql-picker.ql-font .ql-picker-item[data-label]:not([data-label=""]):before,.ql-snow .ql-picker.ql-size .ql-picker-item[data-label]:not([data-label=""]):before{content:attr(data-label)}.ql-snow .ql-picker.ql-header{width:98px}.ql-snow .ql-picker.ql-header .ql-picker-label:before,.ql-snow .ql-picker.ql-header .ql-picker-item:before{content:"Normal"}.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]:before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]:before{content:"Heading 1"}.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]:before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]:before{content:"Heading 2"}.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]:before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]:before{content:"Heading 3"}.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]:before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]:before{content:"Heading 4"}.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]:before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]:before{content:"Heading 5"}.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]:before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]:before{content:"Heading 6"}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]:before{font-size:2em}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]:before{font-size:1.5em}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]:before{font-size:1.17em}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]:before{font-size:1em}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]:before{font-size:.83em}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]:before{font-size:.67em}.ql-snow .ql-picker.ql-font{width:108px}.ql-snow .ql-picker.ql-font .ql-picker-label:before,.ql-snow .ql-picker.ql-font .ql-picker-item:before{content:"Sans Serif"}.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=serif]:before,.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=serif]:before{content:"Serif"}.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=monospace]:before,.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=monospace]:before{content:"Monospace"}.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=serif]:before{font-family:Georgia,Times New Roman,serif}.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=monospace]:before{font-family:Monaco,Courier New,monospace}.ql-snow .ql-picker.ql-size{width:98px}.ql-snow .ql-picker.ql-size .ql-picker-label:before,.ql-snow .ql-picker.ql-size .ql-picker-item:before{content:"Normal"}.ql-snow .ql-picker.ql-size .ql-picker-label[data-value=small]:before,.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=small]:before{content:"Small"}.ql-snow .ql-picker.ql-size .ql-picker-label[data-value=large]:before,.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=large]:before{content:"Large"}.ql-snow .ql-picker.ql-size .ql-picker-label[data-value=huge]:before,.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=huge]:before{content:"Huge"}.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=small]:before{font-size:10px}.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=large]:before{font-size:18px}.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=huge]:before{font-size:32px}.ql-snow .ql-color-picker.ql-background .ql-picker-item{background-color:#fff}.ql-snow .ql-color-picker.ql-color .ql-picker-item{background-color:#000}.ql-code-block-container{position:relative}.ql-code-block-container .ql-ui{right:5px;top:5px}.ql-toolbar.ql-snow{border:1px solid #ccc;box-sizing:border-box;font-family:Helvetica Neue,Helvetica,Arial,sans-serif;padding:8px}.ql-toolbar.ql-snow .ql-formats{margin-right:15px}.ql-toolbar.ql-snow .ql-picker-label{border:1px solid transparent}.ql-toolbar.ql-snow .ql-picker-options{border:1px solid transparent;box-shadow:#0003 0 2px 8px}.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-label,.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-options{border-color:#ccc}.ql-toolbar.ql-snow .ql-color-picker .ql-picker-item.ql-selected,.ql-toolbar.ql-snow .ql-color-picker .ql-picker-item:hover{border-color:#000}.ql-toolbar.ql-snow+.ql-container.ql-snow{border-top:0}.ql-snow .ql-tooltip{background-color:#fff;border:1px solid #ccc;box-shadow:0 0 5px #ddd;color:#444;padding:5px 12px;white-space:nowrap}.ql-snow .ql-tooltip:before{content:"Visit URL:";line-height:26px;margin-right:8px}.ql-snow .ql-tooltip input[type=text]{display:none;border:1px solid #ccc;font-size:13px;height:26px;margin:0;padding:3px 5px;width:170px}.ql-snow .ql-tooltip a.ql-preview{display:inline-block;max-width:200px;overflow-x:hidden;text-overflow:ellipsis;vertical-align:top}.ql-snow .ql-tooltip a.ql-action:after{border-right:1px solid #ccc;content:"Edit";margin-left:16px;padding-right:8px}.ql-snow .ql-tooltip a.ql-remove:before{content:"Remove";margin-left:8px}.ql-snow .ql-tooltip a{line-height:26px}.ql-snow .ql-tooltip.ql-editing a.ql-preview,.ql-snow .ql-tooltip.ql-editing a.ql-remove{display:none}.ql-snow .ql-tooltip.ql-editing input[type=text]{display:inline-block}.ql-snow .ql-tooltip.ql-editing a.ql-action:after{border-right:0;content:"Save";padding-right:0}.ql-snow .ql-tooltip[data-mode=link]:before{content:"Enter link:"}.ql-snow .ql-tooltip[data-mode=formula]:before{content:"Enter formula:"}.ql-snow .ql-tooltip[data-mode=video]:before{content:"Enter video:"}.ql-snow a{color:#06c}.ql-container.ql-snow{border:1px solid #ccc} diff --git a/dist/assets/RichEditor-Bfur5pi6.js b/dist/assets/RichEditor-Bfur5pi6.js new file mode 100644 index 0000000..9beaaad --- /dev/null +++ b/dist/assets/RichEditor-Bfur5pi6.js @@ -0,0 +1,49 @@ +var ra=Object.defineProperty;var sa=(n,t,e)=>t in n?ra(n,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):n[t]=e;var x=(n,t,e)=>sa(n,typeof t!="symbol"?t+"":t,e);import{j as En}from"./vendor-animation-0s3FMHwK.js";import{j as Yt,k as zl,c as de,r as Ue}from"./vendor-react-BVs3cwbi.js";var Kl=typeof global=="object"&&global&&global.Object===Object&&global,ia=typeof self=="object"&&self&&self.Object===Object&&self,kt=Kl||ia||Function("return this")(),ee=kt.Symbol,Gl=Object.prototype,la=Gl.hasOwnProperty,oa=Gl.toString,bn=ee?ee.toStringTag:void 0;function aa(n){var t=la.call(n,bn),e=n[bn];try{n[bn]=void 0;var r=!0}catch{}var s=oa.call(n);return r&&(t?n[bn]=e:delete n[bn]),s}var ca=Object.prototype,ua=ca.toString;function ha(n){return ua.call(n)}var fa="[object Null]",da="[object Undefined]",Ui=ee?ee.toStringTag:void 0;function Xe(n){return n==null?n===void 0?da:fa:Ui&&Ui in Object(n)?aa(n):ha(n)}function Ut(n){return n!=null&&typeof n=="object"}var pe=Array.isArray;function ne(n){var t=typeof n;return n!=null&&(t=="object"||t=="function")}function Vl(n){return n}var ga="[object AsyncFunction]",pa="[object Function]",ma="[object GeneratorFunction]",ba="[object Proxy]";function yi(n){if(!ne(n))return!1;var t=Xe(n);return t==pa||t==ma||t==ga||t==ba}var Rs=kt["__core-js_shared__"],Fi=function(){var n=/[^.]+$/.exec(Rs&&Rs.keys&&Rs.keys.IE_PROTO||"");return n?"Symbol(src)_1."+n:""}();function ya(n){return!!Fi&&Fi in n}var va=Function.prototype,Ea=va.toString;function ye(n){if(n!=null){try{return Ea.call(n)}catch{}try{return n+""}catch{}}return""}var Aa=/[\\^$.*+?()[\]{}|]/g,Na=/^\[object .+?Constructor\]$/,wa=Function.prototype,Ta=Object.prototype,xa=wa.toString,La=Ta.hasOwnProperty,Sa=RegExp("^"+xa.call(La).replace(Aa,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");function _a(n){if(!ne(n)||ya(n))return!1;var t=yi(n)?Sa:Na;return t.test(ye(n))}function Oa(n,t){return n?.[t]}function ve(n,t){var e=Oa(n,t);return _a(e)?e:void 0}var Gs=ve(kt,"WeakMap"),Hi=Object.create,Ca=function(){function n(){}return function(t){if(!ne(t))return{};if(Hi)return Hi(t);n.prototype=t;var e=new n;return n.prototype=void 0,e}}();function qa(n,t,e){switch(e.length){case 0:return n.call(t);case 1:return n.call(t,e[0]);case 2:return n.call(t,e[0],e[1]);case 3:return n.call(t,e[0],e[1],e[2])}return n.apply(t,e)}function Ia(n,t){var e=-1,r=n.length;for(t||(t=Array(r));++e0){if(++t>=Ra)return arguments[0]}else t=0;return n.apply(void 0,arguments)}}function Da(n){return function(){return n}}var Ar=function(){try{var n=ve(Object,"defineProperty");return n({},"",{}),n}catch{}}(),ja=Ar?function(n,t){return Ar(n,"toString",{configurable:!0,enumerable:!1,value:Da(t),writable:!0})}:Vl,Pa=Ma(ja);function $a(n,t){for(var e=-1,r=n==null?0:n.length;++e-1&&n%1==0&&n-1&&n%1==0&&n<=Wa}function Sr(n){return n!=null&&Xl(n.length)&&!yi(n)}function Za(n,t,e){if(!ne(e))return!1;var r=typeof t;return(r=="number"?Sr(e)&&Wl(t,e.length):r=="string"&&t in e)?Rn(e[t],n):!1}function Xa(n){return Va(function(t,e){var r=-1,s=e.length,i=s>1?e[s-1]:void 0,l=s>2?e[2]:void 0;for(i=n.length>3&&typeof i=="function"?(s--,i):void 0,l&&Za(e[0],e[1],l)&&(i=s<3?void 0:i,s=1),t=Object(t);++r-1}function iu(n,t){var e=this.__data__,r=_r(e,n);return r<0?(++this.size,e.push([n,t])):e[r][1]=t,this}function zt(n){var t=-1,e=n==null?0:n.length;for(this.clear();++ta))return!1;var h=i.get(n),f=i.get(t);if(h&&f)return h==t&&f==n;var b=-1,g=!0,p=e&Zh?new wr:void 0;for(i.set(n,t),i.set(t,n);++b(n[n.TYPE=3]="TYPE",n[n.LEVEL=12]="LEVEL",n[n.ATTRIBUTE=13]="ATTRIBUTE",n[n.BLOT=14]="BLOT",n[n.INLINE=7]="INLINE",n[n.BLOCK=11]="BLOCK",n[n.BLOCK_BLOT=10]="BLOCK_BLOT",n[n.INLINE_BLOT=6]="INLINE_BLOT",n[n.BLOCK_ATTRIBUTE=9]="BLOCK_ATTRIBUTE",n[n.INLINE_ATTRIBUTE=5]="INLINE_ATTRIBUTE",n[n.ANY=15]="ANY",n))(C||{});class It{constructor(t,e,r={}){this.attrName=t,this.keyName=e;const s=C.TYPE&C.ATTRIBUTE;this.scope=r.scope!=null?r.scope&C.LEVEL|s:C.ATTRIBUTE,r.whitelist!=null&&(this.whitelist=r.whitelist)}static keys(t){return Array.from(t.attributes).map(e=>e.name)}add(t,e){return this.canAdd(t,e)?(t.setAttribute(this.keyName,e),!0):!1}canAdd(t,e){return this.whitelist==null?!0:typeof e=="string"?this.whitelist.indexOf(e.replace(/["']/g,""))>-1:this.whitelist.indexOf(e)>-1}remove(t){t.removeAttribute(this.keyName)}value(t){const e=t.getAttribute(this.keyName);return this.canAdd(t,e)&&e?e:""}}class He extends Error{constructor(t){t="[Parchment] "+t,super(t),this.message=t,this.name=this.constructor.name}}const mo=class ti{constructor(){this.attributes={},this.classes={},this.tags={},this.types={}}static find(t,e=!1){if(t==null)return null;if(this.blots.has(t))return this.blots.get(t)||null;if(e){let r=null;try{r=t.parentNode}catch{return null}return this.find(r,e)}return null}create(t,e,r){const s=this.query(e);if(s==null)throw new He(`Unable to create ${e} blot`);const i=s,l=e instanceof Node||e.nodeType===Node.TEXT_NODE?e:i.create(r),a=new i(t,l,r);return ti.blots.set(a.domNode,a),a}find(t,e=!1){return ti.find(t,e)}query(t,e=C.ANY){let r;return typeof t=="string"?r=this.types[t]||this.attributes[t]:t instanceof Text||t.nodeType===Node.TEXT_NODE?r=this.types.text:typeof t=="number"?t&C.LEVEL&C.BLOCK?r=this.types.block:t&C.LEVEL&C.INLINE&&(r=this.types.inline):t instanceof Element&&((t.getAttribute("class")||"").split(/\s+/).some(s=>(r=this.classes[s],!!r)),r=r||this.tags[t.tagName]),r==null?null:"scope"in r&&e&C.LEVEL&r.scope&&e&C.TYPE&r.scope?r:null}register(...t){return t.map(e=>{const r="blotName"in e,s="attrName"in e;if(!r&&!s)throw new He("Invalid definition");if(r&&e.blotName==="abstract")throw new He("Cannot register abstract class");const i=r?e.blotName:s?e.attrName:void 0;return this.types[i]=e,s?typeof e.keyName=="string"&&(this.attributes[e.keyName]=e):r&&(e.className&&(this.classes[e.className]=e),e.tagName&&(Array.isArray(e.tagName)?e.tagName=e.tagName.map(l=>l.toUpperCase()):e.tagName=e.tagName.toUpperCase(),(Array.isArray(e.tagName)?e.tagName:[e.tagName]).forEach(l=>{(this.tags[l]==null||e.className==null)&&(this.tags[l]=e)}))),e})}};mo.blots=new WeakMap;let Ge=mo;function fl(n,t){return(n.getAttribute("class")||"").split(/\s+/).filter(e=>e.indexOf(`${t}-`)===0)}class xf extends It{static keys(t){return(t.getAttribute("class")||"").split(/\s+/).map(e=>e.split("-").slice(0,-1).join("-"))}add(t,e){return this.canAdd(t,e)?(this.remove(t),t.classList.add(`${this.keyName}-${e}`),!0):!1}remove(t){fl(t,this.keyName).forEach(e=>{t.classList.remove(e)}),t.classList.length===0&&t.removeAttribute("class")}value(t){const e=(fl(t,this.keyName)[0]||"").slice(this.keyName.length+1);return this.canAdd(t,e)?e:""}}const Nt=xf;function Ms(n){const t=n.split("-"),e=t.slice(1).map(r=>r[0].toUpperCase()+r.slice(1)).join("");return t[0]+e}class Lf extends It{static keys(t){return(t.getAttribute("style")||"").split(";").map(e=>e.split(":")[0].trim())}add(t,e){return this.canAdd(t,e)?(t.style[Ms(this.keyName)]=e,!0):!1}remove(t){t.style[Ms(this.keyName)]="",t.getAttribute("style")||t.removeAttribute("style")}value(t){const e=t.style[Ms(this.keyName)];return this.canAdd(t,e)?e:""}}const re=Lf;class Sf{constructor(t){this.attributes={},this.domNode=t,this.build()}attribute(t,e){e?t.add(this.domNode,e)&&(t.value(this.domNode)!=null?this.attributes[t.attrName]=t:delete this.attributes[t.attrName]):(t.remove(this.domNode),delete this.attributes[t.attrName])}build(){this.attributes={};const t=Ge.find(this.domNode);if(t==null)return;const e=It.keys(this.domNode),r=Nt.keys(this.domNode),s=re.keys(this.domNode);e.concat(r).concat(s).forEach(i=>{const l=t.scroll.query(i,C.ATTRIBUTE);l instanceof It&&(this.attributes[l.attrName]=l)})}copy(t){Object.keys(this.attributes).forEach(e=>{const r=this.attributes[e].value(this.domNode);t.format(e,r)})}move(t){this.copy(t),Object.keys(this.attributes).forEach(e=>{this.attributes[e].remove(this.domNode)}),this.attributes={}}values(){return Object.keys(this.attributes).reduce((t,e)=>(t[e]=this.attributes[e].value(this.domNode),t),{})}}const Cr=Sf,bo=class{constructor(t,e){this.scroll=t,this.domNode=e,Ge.blots.set(e,this),this.prev=null,this.next=null}static create(t){if(this.tagName==null)throw new He("Blot definition missing tagName");let e,r;return Array.isArray(this.tagName)?(typeof t=="string"?(r=t.toUpperCase(),parseInt(r,10).toString()===r&&(r=parseInt(r,10))):typeof t=="number"&&(r=t),typeof r=="number"?e=document.createElement(this.tagName[r-1]):r&&this.tagName.indexOf(r)>-1?e=document.createElement(r):e=document.createElement(this.tagName[0])):e=document.createElement(this.tagName),this.className&&e.classList.add(this.className),e}get statics(){return this.constructor}attach(){}clone(){const t=this.domNode.cloneNode(!1);return this.scroll.create(t)}detach(){this.parent!=null&&this.parent.removeChild(this),Ge.blots.delete(this.domNode)}deleteAt(t,e){this.isolate(t,e).remove()}formatAt(t,e,r,s){const i=this.isolate(t,e);if(this.scroll.query(r,C.BLOT)!=null&&s)i.wrap(r,s);else if(this.scroll.query(r,C.ATTRIBUTE)!=null){const l=this.scroll.create(this.statics.scope);i.wrap(l),l.format(r,s)}}insertAt(t,e,r){const s=r==null?this.scroll.create("text",e):this.scroll.create(e,r),i=this.split(t);this.parent.insertBefore(s,i||void 0)}isolate(t,e){const r=this.split(t);if(r==null)throw new Error("Attempt to isolate at end");return r.split(e),r}length(){return 1}offset(t=this.parent){return this.parent==null||this===t?0:this.parent.children.offset(this)+this.parent.offset(t)}optimize(t){this.statics.requiredContainer&&!(this.parent instanceof this.statics.requiredContainer)&&this.wrap(this.statics.requiredContainer.blotName)}remove(){this.domNode.parentNode!=null&&this.domNode.parentNode.removeChild(this.domNode),this.detach()}replaceWith(t,e){const r=typeof t=="string"?this.scroll.create(t,e):t;return this.parent!=null&&(this.parent.insertBefore(r,this.next||void 0),this.remove()),r}split(t,e){return t===0?this:this.next}update(t,e){}wrap(t,e){const r=typeof t=="string"?this.scroll.create(t,e):t;if(this.parent!=null&&this.parent.insertBefore(r,this.next||void 0),typeof r.appendChild!="function")throw new He(`Cannot wrap ${t}`);return r.appendChild(this),r}};bo.blotName="abstract";let yo=bo;const vo=class extends yo{static value(t){return!0}index(t,e){return this.domNode===t||this.domNode.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_CONTAINED_BY?Math.min(e,1):-1}position(t,e){let r=Array.from(this.parent.domNode.childNodes).indexOf(this.domNode);return t>0&&(r+=1),[this.parent.domNode,r]}value(){return{[this.statics.blotName]:this.statics.value(this.domNode)||!0}}};vo.scope=C.INLINE_BLOT;let _f=vo;const Q=_f;class Of{constructor(){this.head=null,this.tail=null,this.length=0}append(...t){if(this.insertBefore(t[0],null),t.length>1){const e=t.slice(1);this.append(...e)}}at(t){const e=this.iterator();let r=e();for(;r&&t>0;)t-=1,r=e();return r}contains(t){const e=this.iterator();let r=e();for(;r;){if(r===t)return!0;r=e()}return!1}indexOf(t){const e=this.iterator();let r=e(),s=0;for(;r;){if(r===t)return s;s+=1,r=e()}return-1}insertBefore(t,e){t!=null&&(this.remove(t),t.next=e,e!=null?(t.prev=e.prev,e.prev!=null&&(e.prev.next=t),e.prev=t,e===this.head&&(this.head=t)):this.tail!=null?(this.tail.next=t,t.prev=this.tail,this.tail=t):(t.prev=null,this.head=this.tail=t),this.length+=1)}offset(t){let e=0,r=this.head;for(;r!=null;){if(r===t)return e;e+=r.length(),r=r.next}return-1}remove(t){this.contains(t)&&(t.prev!=null&&(t.prev.next=t.next),t.next!=null&&(t.next.prev=t.prev),t===this.head&&(this.head=t.next),t===this.tail&&(this.tail=t.prev),this.length-=1)}iterator(t=this.head){return()=>{const e=t;return t!=null&&(t=t.next),e}}find(t,e=!1){const r=this.iterator();let s=r();for(;s;){const i=s.length();if(tl?r(c,t-l,Math.min(e,l+h-t)):r(c,0,Math.min(h,t+e-l)),l+=h,c=a()}}map(t){return this.reduce((e,r)=>(e.push(t(r)),e),[])}reduce(t,e){const r=this.iterator();let s=r();for(;s;)e=t(e,s),s=r();return e}}function dl(n,t){const e=t.find(n);if(e)return e;try{return t.create(n)}catch{const r=t.create(C.INLINE);return Array.from(n.childNodes).forEach(s=>{r.domNode.appendChild(s)}),n.parentNode&&n.parentNode.replaceChild(r.domNode,n),r.attach(),r}}const Eo=class Zt extends yo{constructor(t,e){super(t,e),this.uiNode=null,this.build()}appendChild(t){this.insertBefore(t)}attach(){super.attach(),this.children.forEach(t=>{t.attach()})}attachUI(t){this.uiNode!=null&&this.uiNode.remove(),this.uiNode=t,Zt.uiClass&&this.uiNode.classList.add(Zt.uiClass),this.uiNode.setAttribute("contenteditable","false"),this.domNode.insertBefore(this.uiNode,this.domNode.firstChild)}build(){this.children=new Of,Array.from(this.domNode.childNodes).filter(t=>t!==this.uiNode).reverse().forEach(t=>{try{const e=dl(t,this.scroll);this.insertBefore(e,this.children.head||void 0)}catch(e){if(e instanceof He)return;throw e}})}deleteAt(t,e){if(t===0&&e===this.length())return this.remove();this.children.forEachAt(t,e,(r,s,i)=>{r.deleteAt(s,i)})}descendant(t,e=0){const[r,s]=this.children.find(e);return t.blotName==null&&t(r)||t.blotName!=null&&r instanceof t?[r,s]:r instanceof Zt?r.descendant(t,s):[null,-1]}descendants(t,e=0,r=Number.MAX_VALUE){let s=[],i=r;return this.children.forEachAt(e,r,(l,a,c)=>{(t.blotName==null&&t(l)||t.blotName!=null&&l instanceof t)&&s.push(l),l instanceof Zt&&(s=s.concat(l.descendants(t,a,i))),i-=c}),s}detach(){this.children.forEach(t=>{t.detach()}),super.detach()}enforceAllowedChildren(){let t=!1;this.children.forEach(e=>{t||this.statics.allowedChildren.some(r=>e instanceof r)||(e.statics.scope===C.BLOCK_BLOT?(e.next!=null&&this.splitAfter(e),e.prev!=null&&this.splitAfter(e.prev),e.parent.unwrap(),t=!0):e instanceof Zt?e.unwrap():e.remove())})}formatAt(t,e,r,s){this.children.forEachAt(t,e,(i,l,a)=>{i.formatAt(l,a,r,s)})}insertAt(t,e,r){const[s,i]=this.children.find(t);if(s)s.insertAt(i,e,r);else{const l=r==null?this.scroll.create("text",e):this.scroll.create(e,r);this.appendChild(l)}}insertBefore(t,e){t.parent!=null&&t.parent.children.remove(t);let r=null;this.children.insertBefore(t,e||null),t.parent=this,e!=null&&(r=e.domNode),(this.domNode.parentNode!==t.domNode||this.domNode.nextSibling!==r)&&this.domNode.insertBefore(t.domNode,r),t.attach()}length(){return this.children.reduce((t,e)=>t+e.length(),0)}moveChildren(t,e){this.children.forEach(r=>{t.insertBefore(r,e)})}optimize(t){if(super.optimize(t),this.enforceAllowedChildren(),this.uiNode!=null&&this.uiNode!==this.domNode.firstChild&&this.domNode.insertBefore(this.uiNode,this.domNode.firstChild),this.children.length===0)if(this.statics.defaultChild!=null){const e=this.scroll.create(this.statics.defaultChild.blotName);this.appendChild(e)}else this.remove()}path(t,e=!1){const[r,s]=this.children.find(t,e),i=[[this,t]];return r instanceof Zt?i.concat(r.path(s,e)):(r!=null&&i.push([r,s]),i)}removeChild(t){this.children.remove(t)}replaceWith(t,e){const r=typeof t=="string"?this.scroll.create(t,e):t;return r instanceof Zt&&this.moveChildren(r),super.replaceWith(r)}split(t,e=!1){if(!e){if(t===0)return this;if(t===this.length())return this.next}const r=this.clone();return this.parent&&this.parent.insertBefore(r,this.next||void 0),this.children.forEachAt(t,this.length(),(s,i,l)=>{const a=s.split(i,e);a!=null&&r.appendChild(a)}),r}splitAfter(t){const e=this.clone();for(;t.next!=null;)e.appendChild(t.next);return this.parent&&this.parent.insertBefore(e,this.next||void 0),e}unwrap(){this.parent&&this.moveChildren(this.parent,this.next||void 0),this.remove()}update(t,e){const r=[],s=[];t.forEach(i=>{i.target===this.domNode&&i.type==="childList"&&(r.push(...i.addedNodes),s.push(...i.removedNodes))}),s.forEach(i=>{if(i.parentNode!=null&&i.tagName!=="IFRAME"&&document.body.compareDocumentPosition(i)&Node.DOCUMENT_POSITION_CONTAINED_BY)return;const l=this.scroll.find(i);l!=null&&(l.domNode.parentNode==null||l.domNode.parentNode===this.domNode)&&l.detach()}),r.filter(i=>i.parentNode===this.domNode&&i!==this.uiNode).sort((i,l)=>i===l?0:i.compareDocumentPosition(l)&Node.DOCUMENT_POSITION_FOLLOWING?1:-1).forEach(i=>{let l=null;i.nextSibling!=null&&(l=this.scroll.find(i.nextSibling));const a=dl(i,this.scroll);(a.next!==l||a.next==null)&&(a.parent!=null&&a.parent.removeChild(this),this.insertBefore(a,l||void 0))}),this.enforceAllowedChildren()}};Eo.uiClass="";let Cf=Eo;const Et=Cf;function qf(n,t){if(Object.keys(n).length!==Object.keys(t).length)return!1;for(const e in n)if(n[e]!==t[e])return!1;return!0}const Be=class Me extends Et{static create(t){return super.create(t)}static formats(t,e){const r=e.query(Me.blotName);if(!(r!=null&&t.tagName===r.tagName)){if(typeof this.tagName=="string")return!0;if(Array.isArray(this.tagName))return t.tagName.toLowerCase()}}constructor(t,e){super(t,e),this.attributes=new Cr(this.domNode)}format(t,e){if(t===this.statics.blotName&&!e)this.children.forEach(r=>{r instanceof Me||(r=r.wrap(Me.blotName,!0)),this.attributes.copy(r)}),this.unwrap();else{const r=this.scroll.query(t,C.INLINE);if(r==null)return;r instanceof It?this.attributes.attribute(r,e):e&&(t!==this.statics.blotName||this.formats()[t]!==e)&&this.replaceWith(t,e)}}formats(){const t=this.attributes.values(),e=this.statics.formats(this.domNode,this.scroll);return e!=null&&(t[this.statics.blotName]=e),t}formatAt(t,e,r,s){this.formats()[r]!=null||this.scroll.query(r,C.ATTRIBUTE)?this.isolate(t,e).format(r,s):super.formatAt(t,e,r,s)}optimize(t){super.optimize(t);const e=this.formats();if(Object.keys(e).length===0)return this.unwrap();const r=this.next;r instanceof Me&&r.prev===this&&qf(e,r.formats())&&(r.moveChildren(this),r.remove())}replaceWith(t,e){const r=super.replaceWith(t,e);return this.attributes.copy(r),r}update(t,e){super.update(t,e),t.some(r=>r.target===this.domNode&&r.type==="attributes")&&this.attributes.build()}wrap(t,e){const r=super.wrap(t,e);return r instanceof Me&&this.attributes.move(r),r}};Be.allowedChildren=[Be,Q],Be.blotName="inline",Be.scope=C.INLINE_BLOT,Be.tagName="SPAN";let If=Be;const Ti=If,De=class ei extends Et{static create(t){return super.create(t)}static formats(t,e){const r=e.query(ei.blotName);if(!(r!=null&&t.tagName===r.tagName)){if(typeof this.tagName=="string")return!0;if(Array.isArray(this.tagName))return t.tagName.toLowerCase()}}constructor(t,e){super(t,e),this.attributes=new Cr(this.domNode)}format(t,e){const r=this.scroll.query(t,C.BLOCK);r!=null&&(r instanceof It?this.attributes.attribute(r,e):t===this.statics.blotName&&!e?this.replaceWith(ei.blotName):e&&(t!==this.statics.blotName||this.formats()[t]!==e)&&this.replaceWith(t,e))}formats(){const t=this.attributes.values(),e=this.statics.formats(this.domNode,this.scroll);return e!=null&&(t[this.statics.blotName]=e),t}formatAt(t,e,r,s){this.scroll.query(r,C.BLOCK)!=null?this.format(r,s):super.formatAt(t,e,r,s)}insertAt(t,e,r){if(r==null||this.scroll.query(e,C.INLINE)!=null)super.insertAt(t,e,r);else{const s=this.split(t);if(s!=null){const i=this.scroll.create(e,r);s.parent.insertBefore(i,s)}else throw new Error("Attempt to insertAt after block boundaries")}}replaceWith(t,e){const r=super.replaceWith(t,e);return this.attributes.copy(r),r}update(t,e){super.update(t,e),t.some(r=>r.target===this.domNode&&r.type==="attributes")&&this.attributes.build()}};De.blotName="block",De.scope=C.BLOCK_BLOT,De.tagName="P",De.allowedChildren=[Ti,De,Q];let Rf=De;const _n=Rf,ni=class extends Et{checkMerge(){return this.next!==null&&this.next.statics.blotName===this.statics.blotName}deleteAt(t,e){super.deleteAt(t,e),this.enforceAllowedChildren()}formatAt(t,e,r,s){super.formatAt(t,e,r,s),this.enforceAllowedChildren()}insertAt(t,e,r){super.insertAt(t,e,r),this.enforceAllowedChildren()}optimize(t){super.optimize(t),this.children.length>0&&this.next!=null&&this.checkMerge()&&(this.next.moveChildren(this),this.next.remove())}};ni.blotName="container",ni.scope=C.BLOCK_BLOT;let kf=ni;const qr=kf;class Bf extends Q{static formats(t,e){}format(t,e){super.formatAt(0,this.length(),t,e)}formatAt(t,e,r,s){t===0&&e===this.length()?this.format(r,s):super.formatAt(t,e,r,s)}formats(){return this.statics.formats(this.domNode,this.scroll)}}const lt=Bf,Mf={attributes:!0,characterData:!0,characterDataOldValue:!0,childList:!0,subtree:!0},Df=100,je=class extends Et{constructor(t,e){super(null,e),this.registry=t,this.scroll=this,this.build(),this.observer=new MutationObserver(r=>{this.update(r)}),this.observer.observe(this.domNode,Mf),this.attach()}create(t,e){return this.registry.create(this,t,e)}find(t,e=!1){const r=this.registry.find(t,e);return r?r.scroll===this?r:e?this.find(r.scroll.domNode.parentNode,!0):null:null}query(t,e=C.ANY){return this.registry.query(t,e)}register(...t){return this.registry.register(...t)}build(){this.scroll!=null&&super.build()}detach(){super.detach(),this.observer.disconnect()}deleteAt(t,e){this.update(),t===0&&e===this.length()?this.children.forEach(r=>{r.remove()}):super.deleteAt(t,e)}formatAt(t,e,r,s){this.update(),super.formatAt(t,e,r,s)}insertAt(t,e,r){this.update(),super.insertAt(t,e,r)}optimize(t=[],e={}){super.optimize(e);const r=e.mutationsMap||new WeakMap;let s=Array.from(this.observer.takeRecords());for(;s.length>0;)t.push(s.pop());const i=(c,h=!0)=>{c==null||c===this||c.domNode.parentNode!=null&&(r.has(c.domNode)||r.set(c.domNode,[]),h&&i(c.parent))},l=c=>{r.has(c.domNode)&&(c instanceof Et&&c.children.forEach(l),r.delete(c.domNode),c.optimize(e))};let a=t;for(let c=0;a.length>0;c+=1){if(c>=Df)throw new Error("[Parchment] Maximum optimize iterations reached");for(a.forEach(h=>{const f=this.find(h.target,!0);f!=null&&(f.domNode===h.target&&(h.type==="childList"?(i(this.find(h.previousSibling,!1)),Array.from(h.addedNodes).forEach(b=>{const g=this.find(b,!1);i(g,!1),g instanceof Et&&g.children.forEach(p=>{i(p,!1)})})):h.type==="attributes"&&i(f.prev)),i(f))}),this.children.forEach(l),a=Array.from(this.observer.takeRecords()),s=a.slice();s.length>0;)t.push(s.pop())}}update(t,e={}){t=t||this.observer.takeRecords();const r=new WeakMap;t.map(s=>{const i=this.find(s.target,!0);return i==null?null:r.has(i.domNode)?(r.get(i.domNode).push(s),null):(r.set(i.domNode,[s]),i)}).forEach(s=>{s!=null&&s!==this&&r.has(s.domNode)&&s.update(r.get(s.domNode)||[],e)}),e.mutationsMap=r,r.has(this.domNode)&&super.update(r.get(this.domNode),e),this.optimize(t,e)}};je.blotName="scroll",je.defaultChild=_n,je.allowedChildren=[_n,qr],je.scope=C.BLOCK_BLOT,je.tagName="DIV";let jf=je;const xi=jf,ri=class Ao extends Q{static create(t){return document.createTextNode(t)}static value(t){return t.data}constructor(t,e){super(t,e),this.text=this.statics.value(this.domNode)}deleteAt(t,e){this.domNode.data=this.text=this.text.slice(0,t)+this.text.slice(t+e)}index(t,e){return this.domNode===t?e:-1}insertAt(t,e,r){r==null?(this.text=this.text.slice(0,t)+e+this.text.slice(t),this.domNode.data=this.text):super.insertAt(t,e,r)}length(){return this.text.length}optimize(t){super.optimize(t),this.text=this.statics.value(this.domNode),this.text.length===0?this.remove():this.next instanceof Ao&&this.next.prev===this&&(this.insertAt(this.length(),this.next.value()),this.next.remove())}position(t,e=!1){return[this.domNode,t]}split(t,e=!1){if(!e){if(t===0)return this;if(t===this.length())return this.next}const r=this.scroll.create(this.domNode.splitText(t));return this.parent.insertBefore(r,this.next||void 0),this.text=this.statics.value(this.domNode),r}update(t,e){t.some(r=>r.type==="characterData"&&r.target===this.domNode)&&(this.text=this.statics.value(this.domNode))}value(){return this.text}};ri.blotName="text",ri.scope=C.INLINE_BLOT;let Pf=ri;const Tr=Pf,$f=Object.freeze(Object.defineProperty({__proto__:null,Attributor:It,AttributorStore:Cr,BlockBlot:_n,ClassAttributor:Nt,ContainerBlot:qr,EmbedBlot:lt,InlineBlot:Ti,LeafBlot:Q,ParentBlot:Et,Registry:Ge,Scope:C,ScrollBlot:xi,StyleAttributor:re,TextBlot:Tr},Symbol.toStringTag,{value:"Module"}));var si={exports:{}},it=-1,et=1,K=0;function On(n,t,e,r,s){if(n===t)return n?[[K,n]]:[];if(e!=null){var i=Zf(n,t,e);if(i)return i}var l=Li(n,t),a=n.substring(0,l);n=n.substring(l),t=t.substring(l),l=Ir(n,t);var c=n.substring(n.length-l);n=n.substring(0,n.length-l),t=t.substring(0,t.length-l);var h=Uf(n,t);return a&&h.unshift([K,a]),c&&h.push([K,c]),Si(h,s),r&&zf(h),h}function Uf(n,t){var e;if(!n)return[[et,t]];if(!t)return[[it,n]];var r=n.length>t.length?n:t,s=n.length>t.length?t:n,i=r.indexOf(s);if(i!==-1)return e=[[et,r.substring(0,i)],[K,s],[et,r.substring(i+s.length)]],n.length>t.length&&(e[0][0]=e[2][0]=it),e;if(s.length===1)return[[it,n],[et,t]];var l=Hf(n,t);if(l){var a=l[0],c=l[1],h=l[2],f=l[3],b=l[4],g=On(a,h),p=On(c,f);return g.concat([[K,b]],p)}return Ff(n,t)}function Ff(n,t){for(var e=n.length,r=t.length,s=Math.ceil((e+r)/2),i=s,l=2*s,a=new Array(l),c=new Array(l),h=0;he)p+=2;else if(_>r)g+=2;else if(b){var L=i+f-A;if(L>=0&&L=I)return gl(n,t,T,_)}}}for(var k=-E+m;k<=E-y;k+=2){var L=i+k,I;k===-E||k!==E&&c[L-1]e)y+=2;else if(U>r)m+=2;else if(!b){var w=i+f-k;if(w>=0&&w=I)return gl(n,t,T,_)}}}}return[[it,n],[et,t]]}function gl(n,t,e,r){var s=n.substring(0,e),i=t.substring(0,r),l=n.substring(e),a=t.substring(r),c=On(s,i),h=On(l,a);return c.concat(h)}function Li(n,t){if(!n||!t||n.charAt(0)!==t.charAt(0))return 0;for(var e=0,r=Math.min(n.length,t.length),s=r,i=0;er?n=n.substring(e-r):et.length?n:t,r=n.length>t.length?t:n;if(e.length<4||r.length*2=p.length?[T,_,L,I,w]:null}var i=s(e,r,Math.ceil(e.length/4)),l=s(e,r,Math.ceil(e.length/2)),a;if(!i&&!l)return null;l?i?a=i[4].length>l[4].length?i:l:a=l:a=i;var c,h,f,b;n.length>t.length?(c=a[0],h=a[1],f=a[2],b=a[3]):(f=a[0],b=a[1],c=a[2],h=a[3]);var g=a[4];return[c,h,f,b,g]}function zf(n){for(var t=!1,e=[],r=0,s=null,i=0,l=0,a=0,c=0,h=0;i0?e[r-1]:-1,l=0,a=0,c=0,h=0,s=null,t=!0)),i++;for(t&&Si(n),Vf(n),i=1;i=p?(g>=f.length/2||g>=b.length/2)&&(n.splice(i,0,[K,b.substring(0,g)]),n[i-1][1]=f.substring(0,f.length-g),n[i+1][1]=b.substring(g),i++):(p>=f.length/2||p>=b.length/2)&&(n.splice(i,0,[K,f.substring(0,p)]),n[i-1][0]=et,n[i-1][1]=b.substring(0,b.length-p),n[i+1][0]=it,n[i+1][1]=f.substring(p),i++),i++}i++}}var ml=/[^a-zA-Z0-9]/,bl=/\s/,yl=/[\r\n]/,Kf=/\n\r?\n$/,Gf=/^\r?\n\r?\n/;function Vf(n){function t(p,m){if(!p||!m)return 6;var y=p.charAt(p.length-1),E=m.charAt(0),A=y.match(ml),w=E.match(ml),T=A&&y.match(bl),_=w&&E.match(bl),L=T&&y.match(yl),I=_&&E.match(yl),k=L&&p.match(Kf),U=I&&m.match(Gf);return k||U?5:L||I?4:A&&!T&&_?3:T||_?2:A||w?1:0}for(var e=1;e=b&&(b=g,c=r,h=s,f=i)}n[e-1][1]!=c&&(c?n[e-1][1]=c:(n.splice(e-1,1),e--),n[e][1]=h,f?n[e+1][1]=f:(n.splice(e+1,1),e--))}e++}}function Si(n,t){n.push([K,""]);for(var e=0,r=0,s=0,i="",l="",a;e=0&&xo(n[c][1])){var h=n[c][1].slice(-1);if(n[c][1]=n[c][1].slice(0,-1),i=h+i,l=h+l,!n[c][1]){n.splice(c,1),e--;var f=c-1;n[f]&&n[f][0]===et&&(s++,l=n[f][1]+l,f--),n[f]&&n[f][0]===it&&(r++,i=n[f][1]+i,f--),c=f}}if(To(n[e][1])){var h=n[e][1].charAt(0);n[e][1]=n[e][1].slice(1),i+=h,l+=h}}if(e0||l.length>0){i.length>0&&l.length>0&&(a=Li(l,i),a!==0&&(c>=0?n[c][1]+=l.substring(0,a):(n.splice(0,0,[K,l.substring(0,a)]),e++),l=l.substring(a),i=i.substring(a)),a=Ir(l,i),a!==0&&(n[e][1]=l.substring(l.length-a)+n[e][1],l=l.substring(0,l.length-a),i=i.substring(0,i.length-a)));var b=s+r;i.length===0&&l.length===0?(n.splice(e-b,b),e=e-b):i.length===0?(n.splice(e-b,b,[et,l]),e=e-b+1):l.length===0?(n.splice(e-b,b,[it,i]),e=e-b+1):(n.splice(e-b,b,[it,i],[et,l]),e=e-b+2)}e!==0&&n[e-1][0]===K?(n[e-1][1]+=n[e][1],n.splice(e,1)):e++,s=0,r=0,i="",l="";break}}n[n.length-1][1]===""&&n.pop();var g=!1;for(e=1;e=55296&&n<=56319}function wo(n){return n>=56320&&n<=57343}function To(n){return wo(n.charCodeAt(0))}function xo(n){return No(n.charCodeAt(n.length-1))}function Wf(n){for(var t=[],e=0;e0&&t.push(n[e]);return t}function Ds(n,t,e,r){return xo(n)||To(r)?null:Wf([[K,n],[it,t],[et,e],[K,r]])}function Zf(n,t,e){var r=typeof e=="number"?{index:e,length:0}:e.oldRange,s=typeof e=="number"?null:e.newRange,i=n.length,l=t.length;if(r.length===0&&(s===null||s.length===0)){var a=r.index,c=n.slice(0,a),h=n.slice(a),f=s?s.index:null;t:{var b=a+l-i;if(f!==null&&f!==b||b<0||b>l)break t;var g=t.slice(0,b),p=t.slice(b);if(p!==h)break t;var m=Math.min(a,b),y=c.slice(0,m),E=g.slice(0,m);if(y!==E)break t;var A=c.slice(m),w=g.slice(m);return Ds(y,A,w,h)}t:{if(f!==null&&f!==a)break t;var T=a,g=t.slice(0,T),p=t.slice(T);if(g!==c)break t;var _=Math.min(i-T,l-T),L=h.slice(h.length-_),I=p.slice(p.length-_);if(L!==I)break t;var A=h.slice(0,h.length-_),w=p.slice(0,p.length-_);return Ds(c,A,w,L)}}if(r.length>0&&s&&s.length===0)t:{var y=n.slice(0,r.index),L=n.slice(r.index+r.length),m=y.length,_=L.length;if(l-1}function rs(o,u){var d=this.__data__,v=qe(d,o);return v<0?d.push([o,u]):d[v][1]=u,this}G.prototype.clear=Jr,G.prototype.delete=ts,G.prototype.get=es,G.prototype.has=ns,G.prototype.set=rs;function Z(o){var u=-1,d=o?o.length:0;for(this.clear();++u-1&&o%1==0&&o-1&&o%1==0&&o<=s}function _t(o){var u=typeof o;return!!o&&(u=="object"||u=="function")}function cr(o){return!!o&&typeof o=="object"}function mn(o){return ke(o)?Ce(o):ys(o)}function qs(){return[]}function Is(){return!1}n.exports=ir})(xr,xr.exports);var Lo=xr.exports,Lr={exports:{}};Lr.exports;(function(n,t){var e=200,r="__lodash_hash_undefined__",s=1,i=2,l=9007199254740991,a="[object Arguments]",c="[object Array]",h="[object AsyncFunction]",f="[object Boolean]",b="[object Date]",g="[object Error]",p="[object Function]",m="[object GeneratorFunction]",y="[object Map]",E="[object Number]",A="[object Null]",w="[object Object]",T="[object Promise]",_="[object Proxy]",L="[object RegExp]",I="[object Set]",k="[object String]",U="[object Symbol]",Bt="[object Undefined]",Gt="[object WeakMap]",se="[object ArrayBuffer]",ie="[object DataView]",jn="[object Float32Array]",Pn="[object Float64Array]",$n="[object Int8Array]",Pr="[object Int16Array]",$r="[object Int32Array]",Ur="[object Uint8Array]",Fr="[object Uint8ClampedArray]",j="[object Uint16Array]",Hr="[object Uint32Array]",zr=/[\\^$.*+?()[\]{}|]/g,dt=/^\[object .+?Constructor\]$/,Un=/^(?:0|[1-9]\d*)$/,P={};P[jn]=P[Pn]=P[$n]=P[Pr]=P[$r]=P[Ur]=P[Fr]=P[j]=P[Hr]=!0,P[a]=P[c]=P[se]=P[f]=P[ie]=P[b]=P[g]=P[p]=P[y]=P[E]=P[w]=P[L]=P[I]=P[k]=P[Gt]=!1;var Fn=typeof Yt=="object"&&Yt&&Yt.Object===Object&&Yt,Kr=typeof self=="object"&&self&&self.Object===Object&&self,gt=Fn||Kr||Function("return this")(),Hn=t&&!t.nodeType&&t,zn=Hn&&!0&&n&&!n.nodeType&&n,Ye=zn&&zn.exports===Hn,Qe=Ye&&Fn.process,Kn=function(){try{return Qe&&Qe.binding&&Qe.binding("util")}catch{}}(),Je=Kn&&Kn.isTypedArray;function Gn(o,u){for(var d=-1,v=o==null?0:o.length,R=0,q=[];++d-1}function as(o,u){var d=this.__data__,v=Ie(d,o);return v<0?(++this.size,d.push([o,u])):d[v][1]=u,this}Z.prototype.clear=ss,Z.prototype.delete=is,Z.prototype.get=ls,Z.prototype.has=os,Z.prototype.set=as;function nt(o){var u=-1,d=o==null?0:o.length;for(this.clear();++uB))return!1;var D=q.get(o);if(D&&q.get(u))return D==u;var X=-1,rt=!0,z=d&i?new Ce:void 0;for(q.set(o,u),q.set(u,o);++X-1&&o%1==0&&o-1&&o%1==0&&o<=l}function ar(o){var u=typeof o;return o!=null&&(u=="object"||u=="function")}function _t(o){return o!=null&&typeof o=="object"}var cr=Je?Vr(Je):As;function mn(o){return gn(o)?ys(o):Ns(o)}function qs(){return[]}function Is(){return!1}n.exports=Cs})(Lr,Lr.exports);var So=Lr.exports,_i={};Object.defineProperty(_i,"__esModule",{value:!0});const Yf=Lo,Qf=So;var ii;(function(n){function t(i={},l={},a=!1){typeof i!="object"&&(i={}),typeof l!="object"&&(l={});let c=Yf(l);a||(c=Object.keys(c).reduce((h,f)=>(c[f]!=null&&(h[f]=c[f]),h),{}));for(const h in i)i[h]!==void 0&&l[h]===void 0&&(c[h]=i[h]);return Object.keys(c).length>0?c:void 0}n.compose=t;function e(i={},l={}){typeof i!="object"&&(i={}),typeof l!="object"&&(l={});const a=Object.keys(i).concat(Object.keys(l)).reduce((c,h)=>(Qf(i[h],l[h])||(c[h]=l[h]===void 0?null:l[h]),c),{});return Object.keys(a).length>0?a:void 0}n.diff=e;function r(i={},l={}){i=i||{};const a=Object.keys(l).reduce((c,h)=>(l[h]!==i[h]&&i[h]!==void 0&&(c[h]=l[h]),c),{});return Object.keys(i).reduce((c,h)=>(i[h]!==l[h]&&l[h]===void 0&&(c[h]=null),c),a)}n.invert=r;function s(i,l,a=!1){if(typeof i!="object")return l;if(typeof l!="object")return;if(!a)return l;const c=Object.keys(l).reduce((h,f)=>(i[f]===void 0&&(h[f]=l[f]),h),{});return Object.keys(c).length>0?c:void 0}n.transform=s})(ii||(ii={}));_i.default=ii;var kr={};Object.defineProperty(kr,"__esModule",{value:!0});var li;(function(n){function t(e){return typeof e.delete=="number"?e.delete:typeof e.retain=="number"?e.retain:typeof e.retain=="object"&&e.retain!==null?1:typeof e.insert=="string"?e.insert.length:1}n.length=t})(li||(li={}));kr.default=li;var Oi={};Object.defineProperty(Oi,"__esModule",{value:!0});const vl=kr;class Jf{constructor(t){this.ops=t,this.index=0,this.offset=0}hasNext(){return this.peekLength()<1/0}next(t){t||(t=1/0);const e=this.ops[this.index];if(e){const r=this.offset,s=vl.default.length(e);if(t>=s-r?(t=s-r,this.index+=1,this.offset=0):this.offset+=t,typeof e.delete=="number")return{delete:t};{const i={};return e.attributes&&(i.attributes=e.attributes),typeof e.retain=="number"?i.retain=t:typeof e.retain=="object"&&e.retain!==null?i.retain=e.retain:typeof e.insert=="string"?i.insert=e.insert.substr(r,t):i.insert=e.insert,i}}else return{retain:1/0}}peek(){return this.ops[this.index]}peekLength(){return this.ops[this.index]?vl.default.length(this.ops[this.index])-this.offset:1/0}peekType(){const t=this.ops[this.index];return t?typeof t.delete=="number"?"delete":typeof t.retain=="number"||typeof t.retain=="object"&&t.retain!==null?"retain":"insert":"retain"}rest(){if(this.hasNext()){if(this.offset===0)return this.ops.slice(this.index);{const t=this.offset,e=this.index,r=this.next(),s=this.ops.slice(this.index);return this.offset=t,this.index=e,[r].concat(s)}}else return[]}}Oi.default=Jf;(function(n,t){Object.defineProperty(t,"__esModule",{value:!0}),t.AttributeMap=t.OpIterator=t.Op=void 0;const e=Xf,r=Lo,s=So,i=_i;t.AttributeMap=i.default;const l=kr;t.Op=l.default;const a=Oi;t.OpIterator=a.default;const c="\0",h=(b,g)=>{if(typeof b!="object"||b===null)throw new Error(`cannot retain a ${typeof b}`);if(typeof g!="object"||g===null)throw new Error(`cannot retain a ${typeof g}`);const p=Object.keys(b)[0];if(!p||p!==Object.keys(g)[0])throw new Error(`embed types not matched: ${p} != ${Object.keys(g)[0]}`);return[p,b[p],g[p]]};class f{constructor(g){Array.isArray(g)?this.ops=g:g!=null&&Array.isArray(g.ops)?this.ops=g.ops:this.ops=[]}static registerEmbed(g,p){this.handlers[g]=p}static unregisterEmbed(g){delete this.handlers[g]}static getHandler(g){const p=this.handlers[g];if(!p)throw new Error(`no handlers for embed type "${g}"`);return p}insert(g,p){const m={};return typeof g=="string"&&g.length===0?this:(m.insert=g,p!=null&&typeof p=="object"&&Object.keys(p).length>0&&(m.attributes=p),this.push(m))}delete(g){return g<=0?this:this.push({delete:g})}retain(g,p){if(typeof g=="number"&&g<=0)return this;const m={retain:g};return p!=null&&typeof p=="object"&&Object.keys(p).length>0&&(m.attributes=p),this.push(m)}push(g){let p=this.ops.length,m=this.ops[p-1];if(g=r(g),typeof m=="object"){if(typeof g.delete=="number"&&typeof m.delete=="number")return this.ops[p-1]={delete:m.delete+g.delete},this;if(typeof m.delete=="number"&&g.insert!=null&&(p-=1,m=this.ops[p-1],typeof m!="object"))return this.ops.unshift(g),this;if(s(g.attributes,m.attributes)){if(typeof g.insert=="string"&&typeof m.insert=="string")return this.ops[p-1]={insert:m.insert+g.insert},typeof g.attributes=="object"&&(this.ops[p-1].attributes=g.attributes),this;if(typeof g.retain=="number"&&typeof m.retain=="number")return this.ops[p-1]={retain:m.retain+g.retain},typeof g.attributes=="object"&&(this.ops[p-1].attributes=g.attributes),this}}return p===this.ops.length?this.ops.push(g):this.ops.splice(p,0,g),this}chop(){const g=this.ops[this.ops.length-1];return g&&typeof g.retain=="number"&&!g.attributes&&this.ops.pop(),this}filter(g){return this.ops.filter(g)}forEach(g){this.ops.forEach(g)}map(g){return this.ops.map(g)}partition(g){const p=[],m=[];return this.forEach(y=>{(g(y)?p:m).push(y)}),[p,m]}reduce(g,p){return this.ops.reduce(g,p)}changeLength(){return this.reduce((g,p)=>p.insert?g+l.default.length(p):p.delete?g-p.delete:g,0)}length(){return this.reduce((g,p)=>g+l.default.length(p),0)}slice(g=0,p=1/0){const m=[],y=new a.default(this.ops);let E=0;for(;E0&&m.next(E.retain-w)}const A=new f(y);for(;p.hasNext()||m.hasNext();)if(m.peekType()==="insert")A.push(m.next());else if(p.peekType()==="delete")A.push(p.next());else{const w=Math.min(p.peekLength(),m.peekLength()),T=p.next(w),_=m.next(w);if(_.retain){const L={};if(typeof T.retain=="number")L.retain=typeof _.retain=="number"?w:_.retain;else if(typeof _.retain=="number")T.retain==null?L.insert=T.insert:L.retain=T.retain;else{const k=T.retain==null?"insert":"retain",[U,Bt,Gt]=h(T[k],_.retain),se=f.getHandler(U);L[k]={[U]:se.compose(Bt,Gt,k==="retain")}}const I=i.default.compose(T.attributes,_.attributes,typeof T.retain=="number");if(I&&(L.attributes=I),A.push(L),!m.hasNext()&&s(A.ops[A.ops.length-1],L)){const k=new f(p.rest());return A.concat(k).chop()}}else typeof _.delete=="number"&&(typeof T.retain=="number"||typeof T.retain=="object"&&T.retain!==null)&&A.push(_)}return A.chop()}concat(g){const p=new f(this.ops.slice());return g.ops.length>0&&(p.push(g.ops[0]),p.ops=p.ops.concat(g.ops.slice(1))),p}diff(g,p){if(this.ops===g.ops)return new f;const m=[this,g].map(T=>T.map(_=>{if(_.insert!=null)return typeof _.insert=="string"?_.insert:c;const L=T===g?"on":"with";throw new Error("diff() called "+L+" non-document")}).join("")),y=new f,E=e(m[0],m[1],p,!0),A=new a.default(this.ops),w=new a.default(g.ops);return E.forEach(T=>{let _=T[1].length;for(;_>0;){let L=0;switch(T[0]){case e.INSERT:L=Math.min(w.peekLength(),_),y.push(w.next(L));break;case e.DELETE:L=Math.min(_,A.peekLength()),A.next(L),y.delete(L);break;case e.EQUAL:L=Math.min(A.peekLength(),w.peekLength(),_);const I=A.next(L),k=w.next(L);s(I.insert,k.insert)?y.retain(L,i.default.diff(I.attributes,k.attributes)):y.push(k).delete(L);break}_-=L}}),y.chop()}eachLine(g,p=` +`){const m=new a.default(this.ops);let y=new f,E=0;for(;m.hasNext();){if(m.peekType()!=="insert")return;const A=m.peek(),w=l.default.length(A)-m.peekLength(),T=typeof A.insert=="string"?A.insert.indexOf(p,w)-w:-1;if(T<0)y.push(m.next());else if(T>0)y.push(m.next(T));else{if(g(y,m.next(1).attributes||{},E)===!1)return;E+=1,y=new f}}y.length()>0&&g(y,{},E)}invert(g){const p=new f;return this.reduce((m,y)=>{if(y.insert)p.delete(l.default.length(y));else{if(typeof y.retain=="number"&&y.attributes==null)return p.retain(y.retain),m+y.retain;if(y.delete||typeof y.retain=="number"){const E=y.delete||y.retain;return g.slice(m,m+E).forEach(w=>{y.delete?p.push(w):y.retain&&y.attributes&&p.retain(l.default.length(w),i.default.invert(y.attributes,w.attributes))}),m+E}else if(typeof y.retain=="object"&&y.retain!==null){const E=g.slice(m,m+1),A=new a.default(E.ops).next(),[w,T,_]=h(y.retain,A.insert),L=f.getHandler(w);return p.retain({[w]:L.invert(T,_)},i.default.invert(y.attributes,A.attributes)),m+1}}return m},0),p.chop()}transform(g,p=!1){if(p=!!p,typeof g=="number")return this.transformPosition(g,p);const m=g,y=new a.default(this.ops),E=new a.default(m.ops),A=new f;for(;y.hasNext()||E.hasNext();)if(y.peekType()==="insert"&&(p||E.peekType()!=="insert"))A.retain(l.default.length(y.next()));else if(E.peekType()==="insert")A.push(E.next());else{const w=Math.min(y.peekLength(),E.peekLength()),T=y.next(w),_=E.next(w);if(T.delete)continue;if(_.delete)A.push(_);else{const L=T.retain,I=_.retain;let k=typeof I=="object"&&I!==null?I:w;if(typeof L=="object"&&L!==null&&typeof I=="object"&&I!==null){const U=Object.keys(L)[0];if(U===Object.keys(I)[0]){const Bt=f.getHandler(U);Bt&&(k={[U]:Bt.transform(L[U],I[U],p)})}}A.retain(k,i.default.transform(T.attributes,_.attributes,p))}}return A.chop()}transformPosition(g,p=!1){p=!!p;const m=new a.default(this.ops);let y=0;for(;m.hasNext()&&y<=g;){const E=m.peekLength(),A=m.peekType();if(m.next(),A==="delete"){g-=Math.min(E,g-y);continue}else A==="insert"&&(y":">",'"':""","'":"'"};function Br(n){return n.replace(/[&<>"']/g,t=>td[t])}const Ot=class Ot extends Ti{static compare(t,e){const r=Ot.order.indexOf(t),s=Ot.order.indexOf(e);return r>=0||s>=0?r-s:t===e?0:t0){const e=this.parent.isolate(this.offset(),this.length());this.moveChildren(e),e.wrap(this)}}};x(Ot,"allowedChildren",[Ot,wt,lt,At]),x(Ot,"order",["cursor","inline","link","underline","strike","italic","bold","script","code"]);let Rt=Ot;const El=1;class W extends _n{constructor(){super(...arguments);x(this,"cache",{})}delta(){return this.cache.delta==null&&(this.cache.delta=_o(this)),this.cache.delta}deleteAt(e,r){super.deleteAt(e,r),this.cache={}}formatAt(e,r,s,i){r<=0||(this.scroll.query(s,C.BLOCK)?e+r===this.length()&&this.format(s,i):super.formatAt(e,Math.min(r,this.length()-e-1),s,i),this.cache={})}insertAt(e,r,s){if(s!=null){super.insertAt(e,r,s),this.cache={};return}if(r.length===0)return;const i=r.split(` +`),l=i.shift();l.length>0&&(e(a=a.split(c,!0),a.insertAt(0,h),h.length),e+l.length)}insertBefore(e,r){const{head:s}=this.children;super.insertBefore(e,r),s instanceof wt&&s.remove(),this.cache={}}length(){return this.cache.length==null&&(this.cache.length=super.length()+El),this.cache.length}moveChildren(e,r){super.moveChildren(e,r),this.cache={}}optimize(e){super.optimize(e),this.cache={}}path(e){return super.path(e,!0)}removeChild(e){super.removeChild(e),this.cache={}}split(e){let r=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!1;if(r&&(e===0||e>=this.length()-El)){const i=this.clone();return e===0?(this.parent.insertBefore(i,this),this):(this.parent.insertBefore(i,this.next),i)}const s=super.split(e,r);return this.cache={},s}}W.blotName="block";W.tagName="P";W.defaultChild=wt;W.allowedChildren=[wt,Rt,lt,At];class ht extends lt{attach(){super.attach(),this.attributes=new Cr(this.domNode)}delta(){return new O().insert(this.value(),{...this.formats(),...this.attributes.values()})}format(t,e){const r=this.scroll.query(t,C.BLOCK_ATTRIBUTE);r!=null&&this.attributes.attribute(r,e)}formatAt(t,e,r,s){this.format(r,s)}insertAt(t,e,r){if(r!=null){super.insertAt(t,e,r);return}const s=e.split(` +`),i=s.pop(),l=s.map(c=>{const h=this.scroll.create(W.blotName);return h.insertAt(0,c),h}),a=this.split(t);l.forEach(c=>{this.parent.insertBefore(c,a)}),i&&this.parent.insertBefore(this.scroll.create("text",i),a)}}ht.scope=C.BLOCK_BLOT;function _o(n){let t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!0;return n.descendants(Q).reduce((e,r)=>r.length()===0?e:e.insert(r.value(),ct(r,{},t)),new O).insert(` +`,ct(n))}function ct(n){let t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},e=arguments.length>2&&arguments[2]!==void 0?arguments[2]:!0;return n==null||("formats"in n&&typeof n.formats=="function"&&(t={...t,...n.formats()},e&&delete t["code-token"]),n.parent==null||n.parent.statics.blotName==="scroll"||n.parent.statics.scope!==n.statics.scope)?t:ct(n.parent,t,e)}const at=class at extends lt{static value(){}constructor(t,e,r){super(t,e),this.selection=r,this.textNode=document.createTextNode(at.CONTENTS),this.domNode.appendChild(this.textNode),this.savedLength=0}detach(){this.parent!=null&&this.parent.removeChild(this)}format(t,e){if(this.savedLength!==0){super.format(t,e);return}let r=this,s=0;for(;r!=null&&r.statics.scope!==C.BLOCK_BLOT;)s+=r.offset(r.parent),r=r.parent;r!=null&&(this.savedLength=at.CONTENTS.length,r.optimize(),r.formatAt(s,at.CONTENTS.length,t,e),this.savedLength=0)}index(t,e){return t===this.textNode?0:super.index(t,e)}length(){return this.savedLength}position(){return[this.textNode,this.textNode.data.length]}remove(){super.remove(),this.parent=null}restore(){if(this.selection.composing||this.parent==null)return null;const t=this.selection.getNativeRange();for(;this.domNode.lastChild!=null&&this.domNode.lastChild!==this.textNode;)this.domNode.parentNode.insertBefore(this.domNode.lastChild,this.domNode);const e=this.prev instanceof At?this.prev:null,r=e?e.length():0,s=this.next instanceof At?this.next:null,i=s?s.text:"",{textNode:l}=this,a=l.data.split(at.CONTENTS).join("");l.data=at.CONTENTS;let c;if(e)c=e,(a||s)&&(e.insertAt(e.length(),a+i),s&&s.remove());else if(s)c=s,s.insertAt(0,a);else{const h=document.createTextNode(a);c=this.scroll.create(h),this.parent.insertBefore(c,this)}if(this.remove(),t){const h=(g,p)=>e&&g===e.domNode?p:g===l?r+p-1:s&&g===s.domNode?r+a.length+p:null,f=h(t.start.node,t.start.offset),b=h(t.end.node,t.end.offset);if(f!==null&&b!==null)return{startNode:c.domNode,startOffset:f,endNode:c.domNode,endOffset:b}}return null}update(t,e){if(t.some(r=>r.type==="characterData"&&r.target===this.textNode)){const r=this.restore();r&&(e.range=r)}}optimize(t){super.optimize(t);let{parent:e}=this;for(;e;){if(e.domNode.tagName==="A"){this.savedLength=at.CONTENTS.length,e.isolate(this.offset(e),this.length()).unwrap(),this.savedLength=0;break}e=e.parent}}value(){return""}};x(at,"blotName","cursor"),x(at,"className","ql-cursor"),x(at,"tagName","span"),x(at,"CONTENTS","\uFEFF");let Ve=at;var Oo={exports:{}};(function(n){var t=Object.prototype.hasOwnProperty,e="~";function r(){}Object.create&&(r.prototype=Object.create(null),new r().__proto__||(e=!1));function s(c,h,f){this.fn=c,this.context=h,this.once=f||!1}function i(c,h,f,b,g){if(typeof f!="function")throw new TypeError("The listener must be a function");var p=new s(f,b||c,g),m=e?e+h:h;return c._events[m]?c._events[m].fn?c._events[m]=[c._events[m],p]:c._events[m].push(p):(c._events[m]=p,c._eventsCount++),c}function l(c,h){--c._eventsCount===0?c._events=new r:delete c._events[h]}function a(){this._events=new r,this._eventsCount=0}a.prototype.eventNames=function(){var h=[],f,b;if(this._eventsCount===0)return h;for(b in f=this._events)t.call(f,b)&&h.push(e?b.slice(1):b);return Object.getOwnPropertySymbols?h.concat(Object.getOwnPropertySymbols(f)):h},a.prototype.listeners=function(h){var f=e?e+h:h,b=this._events[f];if(!b)return[];if(b.fn)return[b.fn];for(var g=0,p=b.length,m=new Array(p);g1?t-1:0),r=1;r(t[e]=Co.bind(console,e,n),t),{})}Kt.level=n=>{ci=n};Co.level=Kt.level;const js=Kt("quill:events"),rd=["selectionchange","mousedown","mouseup","click"];rd.forEach(n=>{document.addEventListener(n,function(){for(var t=arguments.length,e=new Array(t),r=0;r{const i=oi.get(s);i&&i.emitter&&i.emitter.handleDOM(...e)})})});class S extends nd{constructor(){super(),this.domListeners={},this.on("error",js.error)}emit(){for(var t=arguments.length,e=new Array(t),r=0;r1?e-1:0),s=1;s{let{node:l,handler:a}=i;(t.target===l||l.contains(t.target))&&a(t,...r)})}listenDOM(t,e,r){this.domListeners[t]||(this.domListeners[t]=[]),this.domListeners[t].push({node:e,handler:r})}}x(S,"events",{EDITOR_CHANGE:"editor-change",SCROLL_BEFORE_UPDATE:"scroll-before-update",SCROLL_BLOT_MOUNT:"scroll-blot-mount",SCROLL_BLOT_UNMOUNT:"scroll-blot-unmount",SCROLL_OPTIMIZE:"scroll-optimize",SCROLL_UPDATE:"scroll-update",SCROLL_EMBED_UPDATE:"scroll-embed-update",SELECTION_CHANGE:"selection-change",TEXT_CHANGE:"text-change",COMPOSITION_BEFORE_START:"composition-before-start",COMPOSITION_START:"composition-start",COMPOSITION_BEFORE_END:"composition-before-end",COMPOSITION_END:"composition-end"}),x(S,"sources",{API:"api",SILENT:"silent",USER:"user"});const Ps=Kt("quill:selection");class be{constructor(t){let e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:0;this.index=t,this.length=e}}class sd{constructor(t,e){this.emitter=e,this.scroll=t,this.composing=!1,this.mouseDown=!1,this.root=this.scroll.domNode,this.cursor=this.scroll.create("cursor",this),this.savedRange=new be(0,0),this.lastRange=this.savedRange,this.lastNative=null,this.handleComposition(),this.handleDragging(),this.emitter.listenDOM("selectionchange",document,()=>{!this.mouseDown&&!this.composing&&setTimeout(this.update.bind(this,S.sources.USER),1)}),this.emitter.on(S.events.SCROLL_BEFORE_UPDATE,()=>{if(!this.hasFocus())return;const r=this.getNativeRange();r!=null&&r.start.node!==this.cursor.textNode&&this.emitter.once(S.events.SCROLL_UPDATE,(s,i)=>{try{this.root.contains(r.start.node)&&this.root.contains(r.end.node)&&this.setNativeRange(r.start.node,r.start.offset,r.end.node,r.end.offset);const l=i.some(a=>a.type==="characterData"||a.type==="childList"||a.type==="attributes"&&a.target===this.root);this.update(l?S.sources.SILENT:s)}catch{}})}),this.emitter.on(S.events.SCROLL_OPTIMIZE,(r,s)=>{if(s.range){const{startNode:i,startOffset:l,endNode:a,endOffset:c}=s.range;this.setNativeRange(i,l,a,c),this.update(S.sources.SILENT)}}),this.update(S.sources.SILENT)}handleComposition(){this.emitter.on(S.events.COMPOSITION_BEFORE_START,()=>{this.composing=!0}),this.emitter.on(S.events.COMPOSITION_END,()=>{if(this.composing=!1,this.cursor.parent){const t=this.cursor.restore();if(!t)return;setTimeout(()=>{this.setNativeRange(t.startNode,t.startOffset,t.endNode,t.endOffset)},1)}})}handleDragging(){this.emitter.listenDOM("mousedown",document.body,()=>{this.mouseDown=!0}),this.emitter.listenDOM("mouseup",document.body,()=>{this.mouseDown=!1,this.update(S.sources.USER)})}focus(){this.hasFocus()||(this.root.focus({preventScroll:!0}),this.setRange(this.savedRange))}format(t,e){this.scroll.update();const r=this.getNativeRange();if(!(r==null||!r.native.collapsed||this.scroll.query(t,C.BLOCK))){if(r.start.node!==this.cursor.textNode){const s=this.scroll.find(r.start.node,!1);if(s==null)return;if(s instanceof Q){const i=s.split(r.start.offset);s.parent.insertBefore(this.cursor,i)}else s.insertBefore(this.cursor,r.start.node);this.cursor.attach()}this.cursor.format(t,e),this.scroll.optimize(),this.setNativeRange(this.cursor.textNode,this.cursor.textNode.data.length),this.update()}}getBounds(t){let e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:0;const r=this.scroll.length();t=Math.min(t,r-1),e=Math.min(t+e,r-1)-t;let s,[i,l]=this.scroll.leaf(t);if(i==null)return null;if(e>0&&l===i.length()){const[f]=this.scroll.leaf(t+1);if(f){const[b]=this.scroll.line(t),[g]=this.scroll.line(t+1);b===g&&(i=f,l=0)}}[s,l]=i.position(l,!0);const a=document.createRange();if(e>0)return a.setStart(s,l),[i,l]=this.scroll.leaf(t+e),i==null?null:([s,l]=i.position(l,!0),a.setEnd(s,l),a.getBoundingClientRect());let c="left",h;if(s instanceof Text){if(!s.data.length)return null;l0&&(c="right")}return{bottom:h.top+h.height,height:h.height,left:h[c],right:h[c],top:h.top,width:0}}getNativeRange(){const t=document.getSelection();if(t==null||t.rangeCount<=0)return null;const e=t.getRangeAt(0);if(e==null)return null;const r=this.normalizeNative(e);return Ps.info("getNativeRange",r),r}getRange(){const t=this.scroll.domNode;if("isConnected"in t&&!t.isConnected)return[null,null];const e=this.getNativeRange();return e==null?[null,null]:[this.normalizedToRange(e),e]}hasFocus(){return document.activeElement===this.root||document.activeElement!=null&&$s(this.root,document.activeElement)}normalizedToRange(t){const e=[[t.start.node,t.start.offset]];t.native.collapsed||e.push([t.end.node,t.end.offset]);const r=e.map(l=>{const[a,c]=l,h=this.scroll.find(a,!0),f=h.offset(this.scroll);return c===0?f:h instanceof Q?f+h.index(a,c):f+h.length()}),s=Math.min(Math.max(...r),this.scroll.length()-1),i=Math.min(s,...r);return new be(i,s-i)}normalizeNative(t){if(!$s(this.root,t.startContainer)||!t.collapsed&&!$s(this.root,t.endContainer))return null;const e={start:{node:t.startContainer,offset:t.startOffset},end:{node:t.endContainer,offset:t.endOffset},native:t};return[e.start,e.end].forEach(r=>{let{node:s,offset:i}=r;for(;!(s instanceof Text)&&s.childNodes.length>0;)if(s.childNodes.length>i)s=s.childNodes[i],i=0;else if(s.childNodes.length===i)s=s.lastChild,s instanceof Text?i=s.data.length:s.childNodes.length>0?i=s.childNodes.length:i=s.childNodes.length+1;else break;r.node=s,r.offset=i}),e}rangeToNative(t){const e=this.scroll.length(),r=(s,i)=>{s=Math.min(e-1,s);const[l,a]=this.scroll.leaf(s);return l?l.position(a,i):[null,-1]};return[...r(t.index,!1),...r(t.index+t.length,!0)]}setNativeRange(t,e){let r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:t,s=arguments.length>3&&arguments[3]!==void 0?arguments[3]:e,i=arguments.length>4&&arguments[4]!==void 0?arguments[4]:!1;if(Ps.info("setNativeRange",t,e,r,s),t!=null&&(this.root.parentNode==null||t.parentNode==null||r.parentNode==null))return;const l=document.getSelection();if(l!=null)if(t!=null){this.hasFocus()||this.root.focus({preventScroll:!0});const{native:a}=this.getNativeRange()||{};if(a==null||i||t!==a.startContainer||e!==a.startOffset||r!==a.endContainer||s!==a.endOffset){t instanceof Element&&t.tagName==="BR"&&(e=Array.from(t.parentNode.childNodes).indexOf(t),t=t.parentNode),r instanceof Element&&r.tagName==="BR"&&(s=Array.from(r.parentNode.childNodes).indexOf(r),r=r.parentNode);const c=document.createRange();c.setStart(t,e),c.setEnd(r,s),l.removeAllRanges(),l.addRange(c)}}else l.removeAllRanges(),this.root.blur()}setRange(t){let e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!1,r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:S.sources.API;if(typeof e=="string"&&(r=e,e=!1),Ps.info("setRange",t),t!=null){const s=this.rangeToNative(t);this.setNativeRange(...s,e)}else this.setNativeRange(null);this.update(r)}update(){let t=arguments.length>0&&arguments[0]!==void 0?arguments[0]:S.sources.USER;const e=this.lastRange,[r,s]=this.getRange();if(this.lastRange=r,this.lastNative=s,this.lastRange!=null&&(this.savedRange=this.lastRange),!Xt(e,this.lastRange)){if(!this.composing&&s!=null&&s.native.collapsed&&s.start.node!==this.cursor.textNode){const l=this.cursor.restore();l&&this.setNativeRange(l.startNode,l.startOffset,l.endNode,l.endOffset)}const i=[S.events.SELECTION_CHANGE,Fe(this.lastRange),Fe(e),t];this.emitter.emit(S.events.EDITOR_CHANGE,...i),t!==S.sources.SILENT&&this.emitter.emit(...i)}}}function $s(n,t){try{t.parentNode}catch{return!1}return n.contains(t)}const id=/^[ -~]*$/;class ld{constructor(t){this.scroll=t,this.delta=this.getDelta()}applyDelta(t){this.scroll.update();let e=this.scroll.length();this.scroll.batchStart();const r=Al(t),s=new O;return ad(r.ops.slice()).reduce((l,a)=>{const c=ft.Op.length(a);let h=a.attributes||{},f=!1,b=!1;if(a.insert!=null){if(s.retain(c),typeof a.insert=="string"){const m=a.insert;b=!m.endsWith(` +`)&&(e<=l||!!this.scroll.descendant(ht,l)[0]),this.scroll.insertAt(l,m);const[y,E]=this.scroll.line(l);let A=te({},ct(y));if(y instanceof W){const[w]=y.descendant(Q,E);w&&(A=te(A,ct(w)))}h=ft.AttributeMap.diff(A,h)||{}}else if(typeof a.insert=="object"){const m=Object.keys(a.insert)[0];if(m==null)return l;const y=this.scroll.query(m,C.INLINE)!=null;if(y)(e<=l||this.scroll.descendant(ht,l)[0])&&(b=!0);else if(l>0){const[E,A]=this.scroll.descendant(Q,l-1);E instanceof At?E.value()[A]!==` +`&&(f=!0):E instanceof lt&&E.statics.scope===C.INLINE_BLOT&&(f=!0)}if(this.scroll.insertAt(l,m,a.insert[m]),y){const[E]=this.scroll.descendant(Q,l);if(E){const A=te({},ct(E));h=ft.AttributeMap.diff(A,h)||{}}}}e+=c}else if(s.push(a),a.retain!==null&&typeof a.retain=="object"){const m=Object.keys(a.retain)[0];if(m==null)return l;this.scroll.updateEmbedAt(l,m,a.retain[m])}Object.keys(h).forEach(m=>{this.scroll.formatAt(l,c,m,h[m])});const g=f?1:0,p=b?1:0;return e+=g+p,s.retain(g),s.delete(p),l+c+g+p},0),s.reduce((l,a)=>typeof a.delete=="number"?(this.scroll.deleteAt(l,a.delete),l):l+ft.Op.length(a),0),this.scroll.batchEnd(),this.scroll.optimize(),this.update(r)}deleteText(t,e){return this.scroll.deleteAt(t,e),this.update(new O().retain(t).delete(e))}formatLine(t,e){let r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{};this.scroll.update(),Object.keys(r).forEach(i=>{this.scroll.lines(t,Math.max(e,1)).forEach(l=>{l.format(i,r[i])})}),this.scroll.optimize();const s=new O().retain(t).retain(e,Fe(r));return this.update(s)}formatText(t,e){let r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{};Object.keys(r).forEach(i=>{this.scroll.formatAt(t,e,i,r[i])});const s=new O().retain(t).retain(e,Fe(r));return this.update(s)}getContents(t,e){return this.delta.slice(t,t+e)}getDelta(){return this.scroll.lines().reduce((t,e)=>t.concat(e.delta()),new O)}getFormat(t){let e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:0,r=[],s=[];e===0?this.scroll.path(t).forEach(a=>{const[c]=a;c instanceof W?r.push(c):c instanceof Q&&s.push(c)}):(r=this.scroll.lines(t,e),s=this.scroll.descendants(Q,t,e));const[i,l]=[r,s].map(a=>{const c=a.shift();if(c==null)return{};let h=ct(c);for(;Object.keys(h).length>0;){const f=a.shift();if(f==null)return h;h=od(ct(f),h)}return h});return{...i,...l}}getHTML(t,e){const[r,s]=this.scroll.line(t);if(r){const i=r.length();return r.length()>=s+e&&!(s===0&&e===i)?Cn(r,s,e,!0):Cn(this.scroll,t,e,!0)}return""}getText(t,e){return this.getContents(t,e).filter(r=>typeof r.insert=="string").map(r=>r.insert).join("")}insertContents(t,e){const r=Al(e),s=new O().retain(t).concat(r);return this.scroll.insertContents(t,r),this.update(s)}insertEmbed(t,e,r){return this.scroll.insertAt(t,e,r),this.update(new O().retain(t).insert({[e]:r}))}insertText(t,e){let r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{};return e=e.replace(/\r\n/g,` +`).replace(/\r/g,` +`),this.scroll.insertAt(t,e),Object.keys(r).forEach(s=>{this.scroll.formatAt(t,e.length,s,r[s])}),this.update(new O().retain(t).insert(e,Fe(r)))}isBlank(){if(this.scroll.children.length===0)return!0;if(this.scroll.children.length>1)return!1;const t=this.scroll.children.head;if(t?.statics.blotName!==W.blotName)return!1;const e=t;return e.children.length>1?!1:e.children.head instanceof wt}removeFormat(t,e){const r=this.getText(t,e),[s,i]=this.scroll.line(t+e);let l=0,a=new O;s!=null&&(l=s.length()-i,a=s.delta().slice(i,i+l-1).insert(` +`));const h=this.getContents(t,e+l).diff(new O().insert(r).concat(a)),f=new O().retain(t).concat(h);return this.applyDelta(f)}update(t){let e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:[],r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:void 0;const s=this.delta;if(e.length===1&&e[0].type==="characterData"&&e[0].target.data.match(id)&&this.scroll.find(e[0].target)){const i=this.scroll.find(e[0].target),l=ct(i),a=i.offset(this.scroll),c=e[0].oldValue.replace(Ve.CONTENTS,""),h=new O().insert(c),f=new O().insert(i.value()),b=r&&{oldRange:Nl(r.oldRange,-a),newRange:Nl(r.newRange,-a)};t=new O().retain(a).concat(h.diff(f,b)).reduce((p,m)=>m.insert?p.insert(m.insert,l):p.push(m),new O),this.delta=s.compose(t)}else this.delta=this.getDelta(),(!t||!Xt(s.compose(t),this.delta))&&(t=s.diff(this.delta,r));return t}}function Pe(n,t,e){if(n.length===0){const[p]=Us(e.pop());return t<=0?`

        2. `:`${Pe([],t-1,e)}`}const[{child:r,offset:s,length:i,indent:l,type:a},...c]=n,[h,f]=Us(a);if(l>t)return e.push(a),l===t+1?`<${h}>${Cn(r,s,i)}${Pe(c,l,e)}`:`<${h}>
        3. ${Pe(n,t+1,e)}`;const b=e[e.length-1];if(l===t&&a===b)return`
        4. ${Cn(r,s,i)}${Pe(c,l,e)}`;const[g]=Us(e.pop());return`${Pe(n,t-1,e)}`}function Cn(n,t,e){let r=arguments.length>3&&arguments[3]!==void 0?arguments[3]:!1;if("html"in n&&typeof n.html=="function")return n.html(t,e);if(n instanceof At)return Br(n.value().slice(t,t+e)).replaceAll(" "," ");if(n instanceof Et){if(n.statics.blotName==="list-container"){const h=[];return n.children.forEachAt(t,e,(f,b,g)=>{const p="formats"in f&&typeof f.formats=="function"?f.formats():{};h.push({child:f,offset:b,length:g,indent:p.indent||0,type:p.list})}),Pe(h,-1,[])}const s=[];if(n.children.forEachAt(t,e,(h,f,b)=>{s.push(Cn(h,f,b))}),r||n.statics.blotName==="list")return s.join("");const{outerHTML:i,innerHTML:l}=n.domNode,[a,c]=i.split(`>${l}<`);return a==="${s.join("")}<${c}`:`${a}>${s.join("")}<${c}`}return n.domNode instanceof Element?n.domNode.outerHTML:""}function od(n,t){return Object.keys(t).reduce((e,r)=>{if(n[r]==null)return e;const s=t[r];return s===n[r]?e[r]=s:Array.isArray(s)?s.indexOf(n[r])<0?e[r]=s.concat([n[r]]):e[r]=s:e[r]=[s,n[r]],e},{})}function Us(n){const t=n==="ordered"?"ol":"ul";switch(n){case"checked":return[t,' data-list="checked"'];case"unchecked":return[t,' data-list="unchecked"'];default:return[t,""]}}function Al(n){return n.reduce((t,e)=>{if(typeof e.insert=="string"){const r=e.insert.replace(/\r\n/g,` +`).replace(/\r/g,` +`);return t.insert(r,e.attributes)}return t.push(e)},new O)}function Nl(n,t){let{index:e,length:r}=n;return new be(e+t,r)}function ad(n){const t=[];return n.forEach(e=>{typeof e.insert=="string"?e.insert.split(` +`).forEach((s,i)=>{i&&t.push({insert:` +`,attributes:e.attributes}),s&&t.push({insert:s,attributes:e.attributes})}):t.push(e)}),t}class Tt{constructor(t){let e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{};this.quill=t,this.options=e}}x(Tt,"DEFAULTS",{});const dr="\uFEFF";class Ci extends lt{constructor(t,e){super(t,e),this.contentNode=document.createElement("span"),this.contentNode.setAttribute("contenteditable","false"),Array.from(this.domNode.childNodes).forEach(r=>{this.contentNode.appendChild(r)}),this.leftGuard=document.createTextNode(dr),this.rightGuard=document.createTextNode(dr),this.domNode.appendChild(this.leftGuard),this.domNode.appendChild(this.contentNode),this.domNode.appendChild(this.rightGuard)}index(t,e){return t===this.leftGuard?0:t===this.rightGuard?1:super.index(t,e)}restore(t){let e=null,r;const s=t.data.split(dr).join("");if(t===this.leftGuard)if(this.prev instanceof At){const i=this.prev.length();this.prev.insertAt(i,s),e={startNode:this.prev.domNode,startOffset:i+s.length}}else r=document.createTextNode(s),this.parent.insertBefore(this.scroll.create(r),this),e={startNode:r,startOffset:s.length};else t===this.rightGuard&&(this.next instanceof At?(this.next.insertAt(0,s),e={startNode:this.next.domNode,startOffset:s.length}):(r=document.createTextNode(s),this.parent.insertBefore(this.scroll.create(r),this.next),e={startNode:r,startOffset:s.length}));return t.data=dr,e}update(t,e){t.forEach(r=>{if(r.type==="characterData"&&(r.target===this.leftGuard||r.target===this.rightGuard)){const s=this.restore(r.target);s&&(e.range=s)}})}}class cd{constructor(t,e){x(this,"isComposing",!1);this.scroll=t,this.emitter=e,this.setupListeners()}setupListeners(){this.scroll.domNode.addEventListener("compositionstart",t=>{this.isComposing||this.handleCompositionStart(t)}),this.scroll.domNode.addEventListener("compositionend",t=>{this.isComposing&&queueMicrotask(()=>{this.handleCompositionEnd(t)})})}handleCompositionStart(t){const e=t.target instanceof Node?this.scroll.find(t.target,!0):null;e&&!(e instanceof Ci)&&(this.emitter.emit(S.events.COMPOSITION_BEFORE_START,t),this.scroll.batchStart(),this.emitter.emit(S.events.COMPOSITION_START,t),this.isComposing=!0)}handleCompositionEnd(t){this.emitter.emit(S.events.COMPOSITION_BEFORE_END,t),this.scroll.batchEnd(),this.emitter.emit(S.events.COMPOSITION_END,t),this.isComposing=!1}}const wn=class wn{constructor(t,e){x(this,"modules",{});this.quill=t,this.options=e}init(){Object.keys(this.options.modules).forEach(t=>{this.modules[t]==null&&this.addModule(t)})}addModule(t){const e=this.quill.constructor.import(`modules/${t}`);return this.modules[t]=new e(this.quill,this.options.modules[t]||{}),this.modules[t]}};x(wn,"DEFAULTS",{modules:{}}),x(wn,"themes",{default:wn});let We=wn;const ud=n=>n.parentElement||n.getRootNode().host||null,hd=n=>{const t=n.getBoundingClientRect(),e="offsetWidth"in n&&Math.abs(t.width)/n.offsetWidth||1,r="offsetHeight"in n&&Math.abs(t.height)/n.offsetHeight||1;return{top:t.top,right:t.left+n.clientWidth*e,bottom:t.top+n.clientHeight*r,left:t.left}},gr=n=>{const t=parseInt(n,10);return Number.isNaN(t)?0:t},wl=(n,t,e,r,s,i)=>nr?0:nr?t-n>r-e?n+s-e:t-r+i:0,fd=(n,t)=>{const e=n.ownerDocument;let r=t,s=n;for(;s;){const i=s===e.body,l=i?{top:0,right:window.visualViewport?.width??e.documentElement.clientWidth,bottom:window.visualViewport?.height??e.documentElement.clientHeight,left:0}:hd(s),a=getComputedStyle(s),c=wl(r.left,r.right,l.left,l.right,gr(a.scrollPaddingLeft),gr(a.scrollPaddingRight)),h=wl(r.top,r.bottom,l.top,l.bottom,gr(a.scrollPaddingTop),gr(a.scrollPaddingBottom));if(c||h)if(i)e.defaultView?.scrollBy(c,h);else{const{scrollLeft:f,scrollTop:b}=s;h&&(s.scrollTop+=h),c&&(s.scrollLeft+=c);const g=s.scrollLeft-f,p=s.scrollTop-b;r={left:r.left-g,top:r.top-p,right:r.right-g,bottom:r.bottom-p}}s=i||a.position==="fixed"?null:ud(s)}},dd=100,gd=["block","break","cursor","inline","scroll","text"],pd=(n,t,e)=>{const r=new Ge;return gd.forEach(s=>{const i=t.query(s);i&&r.register(i)}),n.forEach(s=>{let i=t.query(s);i||e.error(`Cannot register "${s}" specified in "formats" config. Are you sure it was registered?`);let l=0;for(;i;)if(r.register(i),i="blotName"in i?i.requiredContainer??null:null,l+=1,l>dd){e.error(`Cycle detected in registering blot requiredContainer: "${s}"`);break}}),r},ze=Kt("quill"),pr=new Ge;Et.uiClass="ql-ui";var st;let N=(st=class{static debug(t){t===!0&&(t="log"),Kt.level(t)}static find(t){let e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!1;return oi.get(t)||pr.find(t,e)}static import(t){return this.imports[t]==null&&ze.error(`Cannot import ${t}. Are you sure it was registered?`),this.imports[t]}static register(){if(typeof(arguments.length<=0?void 0:arguments[0])!="string"){const t=arguments.length<=0?void 0:arguments[0],e=!!(!(arguments.length<=1)&&arguments[1]),r="attrName"in t?t.attrName:t.blotName;typeof r=="string"?this.register(`formats/${r}`,t,e):Object.keys(t).forEach(s=>{this.register(s,t[s],e)})}else{const t=arguments.length<=0?void 0:arguments[0],e=arguments.length<=1?void 0:arguments[1],r=!!(!(arguments.length<=2)&&arguments[2]);this.imports[t]!=null&&!r&&ze.warn(`Overwriting ${t} with`,e),this.imports[t]=e,(t.startsWith("blots/")||t.startsWith("formats/"))&&e&&typeof e!="boolean"&&e.blotName!=="abstract"&&pr.register(e),typeof e.register=="function"&&e.register(pr)}}constructor(t){let e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{};if(this.options=md(t,e),this.container=this.options.container,this.container==null){ze.error("Invalid Quill container",t);return}this.options.debug&&st.debug(this.options.debug);const r=this.container.innerHTML.trim();this.container.classList.add("ql-container"),this.container.innerHTML="",oi.set(this.container,this),this.root=this.addContainer("ql-editor"),this.root.classList.add("ql-blank"),this.emitter=new S;const s=xi.blotName,i=this.options.registry.query(s);if(!i||!("blotName"in i))throw new Error(`Cannot initialize Quill without "${s}" blot`);if(this.scroll=new i(this.options.registry,this.root,{emitter:this.emitter}),this.editor=new ld(this.scroll),this.selection=new sd(this.scroll,this.emitter),this.composition=new cd(this.scroll,this.emitter),this.theme=new this.options.theme(this,this.options),this.keyboard=this.theme.addModule("keyboard"),this.clipboard=this.theme.addModule("clipboard"),this.history=this.theme.addModule("history"),this.uploader=this.theme.addModule("uploader"),this.theme.addModule("input"),this.theme.addModule("uiNode"),this.theme.init(),this.emitter.on(S.events.EDITOR_CHANGE,l=>{l===S.events.TEXT_CHANGE&&this.root.classList.toggle("ql-blank",this.editor.isBlank())}),this.emitter.on(S.events.SCROLL_UPDATE,(l,a)=>{const c=this.selection.lastRange,[h]=this.selection.getRange(),f=c&&h?{oldRange:c,newRange:h}:void 0;bt.call(this,()=>this.editor.update(null,a,f),l)}),this.emitter.on(S.events.SCROLL_EMBED_UPDATE,(l,a)=>{const c=this.selection.lastRange,[h]=this.selection.getRange(),f=c&&h?{oldRange:c,newRange:h}:void 0;bt.call(this,()=>{const b=new O().retain(l.offset(this)).retain({[l.statics.blotName]:a});return this.editor.update(b,[],f)},st.sources.USER)}),r){const l=this.clipboard.convert({html:`${r}


          `,text:` +`});this.setContents(l)}this.history.clear(),this.options.placeholder&&this.root.setAttribute("data-placeholder",this.options.placeholder),this.options.readOnly&&this.disable(),this.allowReadOnlyEdits=!1}addContainer(t){let e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:null;if(typeof t=="string"){const r=t;t=document.createElement("div"),t.classList.add(r)}return this.container.insertBefore(t,e),t}blur(){this.selection.setRange(null)}deleteText(t,e,r){return[t,e,,r]=$t(t,e,r),bt.call(this,()=>this.editor.deleteText(t,e),r,t,-1*e)}disable(){this.enable(!1)}editReadOnly(t){this.allowReadOnlyEdits=!0;const e=t();return this.allowReadOnlyEdits=!1,e}enable(){let t=arguments.length>0&&arguments[0]!==void 0?arguments[0]:!0;this.scroll.enable(t),this.container.classList.toggle("ql-disabled",!t)}focus(){let t=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};this.selection.focus(),t.preventScroll||this.scrollSelectionIntoView()}format(t,e){let r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:S.sources.API;return bt.call(this,()=>{const s=this.getSelection(!0);let i=new O;if(s==null)return i;if(this.scroll.query(t,C.BLOCK))i=this.editor.formatLine(s.index,s.length,{[t]:e});else{if(s.length===0)return this.selection.format(t,e),i;i=this.editor.formatText(s.index,s.length,{[t]:e})}return this.setSelection(s,S.sources.SILENT),i},r)}formatLine(t,e,r,s,i){let l;return[t,e,l,i]=$t(t,e,r,s,i),bt.call(this,()=>this.editor.formatLine(t,e,l),i,t,0)}formatText(t,e,r,s,i){let l;return[t,e,l,i]=$t(t,e,r,s,i),bt.call(this,()=>this.editor.formatText(t,e,l),i,t,0)}getBounds(t){let e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:0,r=null;if(typeof t=="number"?r=this.selection.getBounds(t,e):r=this.selection.getBounds(t.index,t.length),!r)return null;const s=this.container.getBoundingClientRect();return{bottom:r.bottom-s.top,height:r.height,left:r.left-s.left,right:r.right-s.left,top:r.top-s.top,width:r.width}}getContents(){let t=arguments.length>0&&arguments[0]!==void 0?arguments[0]:0,e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:this.getLength()-t;return[t,e]=$t(t,e),this.editor.getContents(t,e)}getFormat(){let t=arguments.length>0&&arguments[0]!==void 0?arguments[0]:this.getSelection(!0),e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:0;return typeof t=="number"?this.editor.getFormat(t,e):this.editor.getFormat(t.index,t.length)}getIndex(t){return t.offset(this.scroll)}getLength(){return this.scroll.length()}getLeaf(t){return this.scroll.leaf(t)}getLine(t){return this.scroll.line(t)}getLines(){let t=arguments.length>0&&arguments[0]!==void 0?arguments[0]:0,e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:Number.MAX_VALUE;return typeof t!="number"?this.scroll.lines(t.index,t.length):this.scroll.lines(t,e)}getModule(t){return this.theme.modules[t]}getSelection(){return(arguments.length>0&&arguments[0]!==void 0?arguments[0]:!1)&&this.focus(),this.update(),this.selection.getRange()[0]}getSemanticHTML(){let t=arguments.length>0&&arguments[0]!==void 0?arguments[0]:0,e=arguments.length>1?arguments[1]:void 0;return typeof t=="number"&&(e=e??this.getLength()-t),[t,e]=$t(t,e),this.editor.getHTML(t,e)}getText(){let t=arguments.length>0&&arguments[0]!==void 0?arguments[0]:0,e=arguments.length>1?arguments[1]:void 0;return typeof t=="number"&&(e=e??this.getLength()-t),[t,e]=$t(t,e),this.editor.getText(t,e)}hasFocus(){return this.selection.hasFocus()}insertEmbed(t,e,r){let s=arguments.length>3&&arguments[3]!==void 0?arguments[3]:st.sources.API;return bt.call(this,()=>this.editor.insertEmbed(t,e,r),s,t)}insertText(t,e,r,s,i){let l;return[t,,l,i]=$t(t,0,r,s,i),bt.call(this,()=>this.editor.insertText(t,e,l),i,t,e.length)}isEnabled(){return this.scroll.isEnabled()}off(){return this.emitter.off(...arguments)}on(){return this.emitter.on(...arguments)}once(){return this.emitter.once(...arguments)}removeFormat(t,e,r){return[t,e,,r]=$t(t,e,r),bt.call(this,()=>this.editor.removeFormat(t,e),r,t)}scrollRectIntoView(t){fd(this.root,t)}scrollIntoView(){console.warn("Quill#scrollIntoView() has been deprecated and will be removed in the near future. Please use Quill#scrollSelectionIntoView() instead."),this.scrollSelectionIntoView()}scrollSelectionIntoView(){const t=this.selection.lastRange,e=t&&this.selection.getBounds(t.index,t.length);e&&this.scrollRectIntoView(e)}setContents(t){let e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:S.sources.API;return bt.call(this,()=>{t=new O(t);const r=this.getLength(),s=this.editor.deleteText(0,r),i=this.editor.insertContents(0,t),l=this.editor.deleteText(this.getLength()-1,1);return s.compose(i).compose(l)},e)}setSelection(t,e,r){t==null?this.selection.setRange(null,e||st.sources.API):([t,e,,r]=$t(t,e,r),this.selection.setRange(new be(Math.max(0,t),e),r),r!==S.sources.SILENT&&this.scrollSelectionIntoView())}setText(t){let e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:S.sources.API;const r=new O().insert(t);return this.setContents(r,e)}update(){let t=arguments.length>0&&arguments[0]!==void 0?arguments[0]:S.sources.USER;const e=this.scroll.update(t);return this.selection.update(t),e}updateContents(t){let e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:S.sources.API;return bt.call(this,()=>(t=new O(t),this.editor.applyDelta(t)),e,!0)}},x(st,"DEFAULTS",{bounds:null,modules:{clipboard:!0,keyboard:!0,history:!0,uploader:!0},placeholder:"",readOnly:!1,registry:pr,theme:"default"}),x(st,"events",S.events),x(st,"sources",S.sources),x(st,"version","2.0.3"),x(st,"imports",{delta:O,parchment:$f,"core/module":Tt,"core/theme":We}),st);function Tl(n){return typeof n=="string"?document.querySelector(n):n}function Fs(n){return Object.entries(n??{}).reduce((t,e)=>{let[r,s]=e;return{...t,[r]:s===!0?{}:s}},{})}function xl(n){return Object.fromEntries(Object.entries(n).filter(t=>t[1]!==void 0))}function md(n,t){const e=Tl(n);if(!e)throw new Error("Invalid Quill container");const s=!t.theme||t.theme===N.DEFAULTS.theme?We:N.import(`themes/${t.theme}`);if(!s)throw new Error(`Invalid theme ${t.theme}. Did you register it?`);const{modules:i,...l}=N.DEFAULTS,{modules:a,...c}=s.DEFAULTS;let h=Fs(t.modules);h!=null&&h.toolbar&&h.toolbar.constructor!==Object&&(h={...h,toolbar:{container:h.toolbar}});const f=te({},Fs(i),Fs(a),h),b={...l,...xl(c),...xl(t)};let g=t.registry;return g?t.formats&&ze.warn('Ignoring "formats" option because "registry" is specified'):g=t.formats?pd(t.formats,b.registry,ze):b.registry,{...b,registry:g,container:e,theme:s,modules:Object.entries(f).reduce((p,m)=>{let[y,E]=m;if(!E)return p;const A=N.import(`modules/${y}`);return A==null?(ze.error(`Cannot load ${y} module. Are you sure you registered it?`),p):{...p,[y]:te({},A.DEFAULTS||{},E)}},{}),bounds:Tl(b.bounds)}}function bt(n,t,e,r){if(!this.isEnabled()&&t===S.sources.USER&&!this.allowReadOnlyEdits)return new O;let s=e==null?null:this.getSelection();const i=this.editor.delta,l=n();if(s!=null&&(e===!0&&(e=s.index),r==null?s=Ll(s,l,t):r!==0&&(s=Ll(s,e,r,t)),this.setSelection(s,S.sources.SILENT)),l.length()>0){const a=[S.events.TEXT_CHANGE,l,i,t];this.emitter.emit(S.events.EDITOR_CHANGE,...a),t!==S.sources.SILENT&&this.emitter.emit(...a)}return l}function $t(n,t,e,r,s){let i={};return typeof n.index=="number"&&typeof n.length=="number"?typeof t!="number"?(s=r,r=e,e=t,t=n.length,n=n.index):(t=n.length,n=n.index):typeof t!="number"&&(s=r,r=e,e=t,t=0),typeof e=="object"?(i=e,s=r):typeof e=="string"&&(r!=null?i[e]=r:s=e),s=s||S.sources.API,[n,t,i,s]}function Ll(n,t,e,r){const s=typeof e=="number"?e:0;if(n==null)return null;let i,l;return t&&typeof t.transformPosition=="function"?[i,l]=[n.index,n.index+n.length].map(a=>t.transformPosition(a,r!==S.sources.USER)):[i,l]=[n.index,n.index+n.length].map(a=>a=0?a+s:Math.max(t,a+s)),new be(i,l-i)}class Ae extends qr{}function Sl(n){return n instanceof W||n instanceof ht}function _l(n){return typeof n.updateContent=="function"}class $e extends xi{constructor(t,e,r){let{emitter:s}=r;super(t,e),this.emitter=s,this.batch=!1,this.optimize(),this.enable(),this.domNode.addEventListener("dragstart",i=>this.handleDragStart(i))}batchStart(){Array.isArray(this.batch)||(this.batch=[])}batchEnd(){if(!this.batch)return;const t=this.batch;this.batch=!1,this.update(t)}emitMount(t){this.emitter.emit(S.events.SCROLL_BLOT_MOUNT,t)}emitUnmount(t){this.emitter.emit(S.events.SCROLL_BLOT_UNMOUNT,t)}emitEmbedUpdate(t,e){this.emitter.emit(S.events.SCROLL_EMBED_UPDATE,t,e)}deleteAt(t,e){const[r,s]=this.line(t),[i]=this.line(t+e);if(super.deleteAt(t,e),i!=null&&r!==i&&s>0){if(r instanceof ht||i instanceof ht){this.optimize();return}const l=i.children.head instanceof wt?null:i.children.head;r.moveChildren(i,l),r.remove()}this.optimize()}enable(){let t=arguments.length>0&&arguments[0]!==void 0?arguments[0]:!0;this.domNode.setAttribute("contenteditable",t?"true":"false")}formatAt(t,e,r,s){super.formatAt(t,e,r,s),this.optimize()}insertAt(t,e,r){if(t>=this.length())if(r==null||this.scroll.query(e,C.BLOCK)==null){const s=this.scroll.create(this.statics.defaultChild.blotName);this.appendChild(s),r==null&&e.endsWith(` +`)?s.insertAt(0,e.slice(0,-1),r):s.insertAt(0,e,r)}else{const s=this.scroll.create(e,r);this.appendChild(s)}else super.insertAt(t,e,r);this.optimize()}insertBefore(t,e){if(t.statics.scope===C.INLINE_BLOT){const r=this.scroll.create(this.statics.defaultChild.blotName);r.appendChild(t),super.insertBefore(r,e)}else super.insertBefore(t,e)}insertContents(t,e){const r=this.deltaToRenderBlocks(e.concat(new O().insert(` +`))),s=r.pop();if(s==null)return;this.batchStart();const i=r.shift();if(i){const c=i.type==="block"&&(i.delta.length()===0||!this.descendant(ht,t)[0]&&t{this.formatAt(b-1,1,m,p[m])}),t=b}let[l,a]=this.children.find(t);if(r.length&&(l&&(l=l.split(a),a=0),r.forEach(c=>{if(c.type==="block"){const h=this.createBlock(c.attributes,l||void 0);Hs(h,0,c.delta)}else{const h=this.create(c.key,c.value);this.insertBefore(h,l||void 0),Object.keys(c.attributes).forEach(f=>{h.format(f,c.attributes[f])})}})),s.type==="block"&&s.delta.length()){const c=l?l.offset(l.scroll)+a:this.length();Hs(this,c,s.delta)}this.batchEnd(),this.optimize()}isEnabled(){return this.domNode.getAttribute("contenteditable")==="true"}leaf(t){const e=this.path(t).pop();if(!e)return[null,-1];const[r,s]=e;return r instanceof Q?[r,s]:[null,-1]}line(t){return t===this.length()?this.line(t-1):this.descendant(Sl,t)}lines(){let t=arguments.length>0&&arguments[0]!==void 0?arguments[0]:0,e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:Number.MAX_VALUE;const r=(s,i,l)=>{let a=[],c=l;return s.children.forEachAt(i,l,(h,f,b)=>{Sl(h)?a.push(h):h instanceof qr&&(a=a.concat(r(h,f,c))),c-=b}),a};return r(this,t,e)}optimize(){let t=arguments.length>0&&arguments[0]!==void 0?arguments[0]:[],e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{};this.batch||(super.optimize(t,e),t.length>0&&this.emitter.emit(S.events.SCROLL_OPTIMIZE,t,e))}path(t){return super.path(t).slice(1)}remove(){}update(t){if(this.batch){Array.isArray(t)&&(this.batch=this.batch.concat(t));return}let e=S.sources.USER;typeof t=="string"&&(e=t),Array.isArray(t)||(t=this.observer.takeRecords()),t=t.filter(r=>{let{target:s}=r;const i=this.find(s,!0);return i&&!_l(i)}),t.length>0&&this.emitter.emit(S.events.SCROLL_BEFORE_UPDATE,e,t),super.update(t.concat([])),t.length>0&&this.emitter.emit(S.events.SCROLL_UPDATE,e,t)}updateEmbedAt(t,e,r){const[s]=this.descendant(i=>i instanceof ht,t);s&&s.statics.blotName===e&&_l(s)&&s.updateContent(r)}handleDragStart(t){t.preventDefault()}deltaToRenderBlocks(t){const e=[];let r=new O;return t.forEach(s=>{const i=s?.insert;if(i)if(typeof i=="string"){const l=i.split(` +`);l.slice(0,-1).forEach(c=>{r.insert(c,s.attributes),e.push({type:"block",delta:r,attributes:s.attributes??{}}),r=new O});const a=l[l.length-1];a&&r.insert(a,s.attributes)}else{const l=Object.keys(i)[0];if(!l)return;this.query(l,C.INLINE)?r.push(s):(r.length()&&e.push({type:"block",delta:r,attributes:{}}),r=new O,e.push({type:"blockEmbed",key:l,value:i[l],attributes:s.attributes??{}}))}}),r.length()&&e.push({type:"block",delta:r,attributes:{}}),e}createBlock(t,e){let r;const s={};Object.entries(t).forEach(a=>{let[c,h]=a;this.query(c,C.BLOCK&C.BLOT)!=null?r=c:s[c]=h});const i=this.create(r||this.statics.defaultChild.blotName,r?t[r]:void 0);this.insertBefore(i,e||void 0);const l=i.length();return Object.entries(s).forEach(a=>{let[c,h]=a;i.formatAt(0,l,c,h)}),i}}x($e,"blotName","scroll"),x($e,"className","ql-editor"),x($e,"tagName","DIV"),x($e,"defaultChild",W),x($e,"allowedChildren",[W,ht,Ae]);function Hs(n,t,e){e.reduce((r,s)=>{const i=ft.Op.length(s);let l=s.attributes||{};if(s.insert!=null){if(typeof s.insert=="string"){const a=s.insert;n.insertAt(r,a);const[c]=n.descendant(Q,r),h=ct(c);l=ft.AttributeMap.diff(h,l)||{}}else if(typeof s.insert=="object"){const a=Object.keys(s.insert)[0];if(a==null)return r;if(n.insertAt(r,a,s.insert[a]),n.scroll.query(a,C.INLINE)!=null){const[h]=n.descendant(Q,r),f=ct(h);l=ft.AttributeMap.diff(f,l)||{}}}}return Object.keys(l).forEach(a=>{n.formatAt(r,i,a,l[a])}),r+i},t)}const qi={scope:C.BLOCK,whitelist:["right","center","justify"]},bd=new It("align","align",qi),qo=new Nt("align","ql-align",qi),Io=new re("align","text-align",qi);class Ro extends re{value(t){let e=super.value(t);return e.startsWith("rgb(")?(e=e.replace(/^[^\d]+/,"").replace(/[^\d]+$/,""),`#${e.split(",").map(s=>`00${parseInt(s,10).toString(16)}`.slice(-2)).join("")}`):e}}const yd=new Nt("color","ql-color",{scope:C.INLINE}),Ii=new Ro("color","color",{scope:C.INLINE}),vd=new Nt("background","ql-bg",{scope:C.INLINE}),Ri=new Ro("background","background-color",{scope:C.INLINE});class Ne extends Ae{static create(t){const e=super.create(t);return e.setAttribute("spellcheck","false"),e}code(t,e){return this.children.map(r=>r.length()<=1?"":r.domNode.innerText).join(` +`).slice(t,t+e)}html(t,e){return`
          +${Br(this.code(t,e))}
          +
          `}}class J extends W{static register(){N.register(Ne)}}x(J,"TAB"," ");class ki extends Rt{}ki.blotName="code";ki.tagName="CODE";J.blotName="code-block";J.className="ql-code-block";J.tagName="DIV";Ne.blotName="code-block-container";Ne.className="ql-code-block-container";Ne.tagName="DIV";Ne.allowedChildren=[J];J.allowedChildren=[At,wt,Ve];J.requiredContainer=Ne;const Bi={scope:C.BLOCK,whitelist:["rtl"]},ko=new It("direction","dir",Bi),Bo=new Nt("direction","ql-direction",Bi),Mo=new re("direction","direction",Bi),Do={scope:C.INLINE,whitelist:["serif","monospace"]},jo=new Nt("font","ql-font",Do);class Ed extends re{value(t){return super.value(t).replace(/["']/g,"")}}const Po=new Ed("font","font-family",Do),$o=new Nt("size","ql-size",{scope:C.INLINE,whitelist:["small","large","huge"]}),Uo=new re("size","font-size",{scope:C.INLINE,whitelist:["10px","18px","32px"]}),Ad=Kt("quill:keyboard"),Nd=/Mac/i.test(navigator.platform)?"metaKey":"ctrlKey";class Mr extends Tt{static match(t,e){return["altKey","ctrlKey","metaKey","shiftKey"].some(r=>!!e[r]!==t[r]&&e[r]!==null)?!1:e.key===t.key||e.key===t.which}constructor(t,e){super(t,e),this.bindings={},Object.keys(this.options.bindings).forEach(r=>{this.options.bindings[r]&&this.addBinding(this.options.bindings[r])}),this.addBinding({key:"Enter",shiftKey:null},this.handleEnter),this.addBinding({key:"Enter",metaKey:null,ctrlKey:null,altKey:null},()=>{}),/Firefox/i.test(navigator.userAgent)?(this.addBinding({key:"Backspace"},{collapsed:!0},this.handleBackspace),this.addBinding({key:"Delete"},{collapsed:!0},this.handleDelete)):(this.addBinding({key:"Backspace"},{collapsed:!0,prefix:/^.?$/},this.handleBackspace),this.addBinding({key:"Delete"},{collapsed:!0,suffix:/^.?$/},this.handleDelete)),this.addBinding({key:"Backspace"},{collapsed:!1},this.handleDeleteRange),this.addBinding({key:"Delete"},{collapsed:!1},this.handleDeleteRange),this.addBinding({key:"Backspace",altKey:null,ctrlKey:null,metaKey:null,shiftKey:null},{collapsed:!0,offset:0},this.handleBackspace),this.listen()}addBinding(t){let e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{};const s=Td(t);if(s==null){Ad.warn("Attempted to add invalid keyboard binding",s);return}typeof e=="function"&&(e={handler:e}),typeof r=="function"&&(r={handler:r}),(Array.isArray(s.key)?s.key:[s.key]).forEach(l=>{const a={...s,key:l,...e,...r};this.bindings[a.key]=this.bindings[a.key]||[],this.bindings[a.key].push(a)})}listen(){this.quill.root.addEventListener("keydown",t=>{if(t.defaultPrevented||t.isComposing||t.keyCode===229&&(t.key==="Enter"||t.key==="Backspace"))return;const s=(this.bindings[t.key]||[]).concat(this.bindings[t.which]||[]).filter(A=>Mr.match(t,A));if(s.length===0)return;const i=N.find(t.target,!0);if(i&&i.scroll!==this.quill.scroll)return;const l=this.quill.getSelection();if(l==null||!this.quill.hasFocus())return;const[a,c]=this.quill.getLine(l.index),[h,f]=this.quill.getLeaf(l.index),[b,g]=l.length===0?[h,f]:this.quill.getLeaf(l.index+l.length),p=h instanceof Tr?h.value().slice(0,f):"",m=b instanceof Tr?b.value().slice(g):"",y={collapsed:l.length===0,empty:l.length===0&&a.length()<=1,format:this.quill.getFormat(l),line:a,offset:c,prefix:p,suffix:m,event:t};s.some(A=>{if(A.collapsed!=null&&A.collapsed!==y.collapsed||A.empty!=null&&A.empty!==y.empty||A.offset!=null&&A.offset!==y.offset)return!1;if(Array.isArray(A.format)){if(A.format.every(w=>y.format[w]==null))return!1}else if(typeof A.format=="object"&&!Object.keys(A.format).every(w=>A.format[w]===!0?y.format[w]!=null:A.format[w]===!1?y.format[w]==null:Xt(A.format[w],y.format[w])))return!1;return A.prefix!=null&&!A.prefix.test(y.prefix)||A.suffix!=null&&!A.suffix.test(y.suffix)?!1:A.handler.call(this,l,y,A)!==!0})&&t.preventDefault()})}handleBackspace(t,e){const r=/[\uD800-\uDBFF][\uDC00-\uDFFF]$/.test(e.prefix)?2:1;if(t.index===0||this.quill.getLength()<=1)return;let s={};const[i]=this.quill.getLine(t.index);let l=new O().retain(t.index-r).delete(r);if(e.offset===0){const[a]=this.quill.getLine(t.index-1);if(a&&!(a.statics.blotName==="block"&&a.length()<=1)){const h=i.formats(),f=this.quill.getFormat(t.index-1,1);if(s=ft.AttributeMap.diff(h,f)||{},Object.keys(s).length>0){const b=new O().retain(t.index+i.length()-2).retain(1,s);l=l.compose(b)}}}this.quill.updateContents(l,N.sources.USER),this.quill.focus()}handleDelete(t,e){const r=/^[\uD800-\uDBFF][\uDC00-\uDFFF]/.test(e.suffix)?2:1;if(t.index>=this.quill.getLength()-r)return;let s={};const[i]=this.quill.getLine(t.index);let l=new O().retain(t.index).delete(r);if(e.offset>=i.length()-1){const[a]=this.quill.getLine(t.index+1);if(a){const c=i.formats(),h=this.quill.getFormat(t.index,1);s=ft.AttributeMap.diff(c,h)||{},Object.keys(s).length>0&&(l=l.retain(a.length()-1).retain(1,s))}}this.quill.updateContents(l,N.sources.USER),this.quill.focus()}handleDeleteRange(t){Mi({range:t,quill:this.quill}),this.quill.focus()}handleEnter(t,e){const r=Object.keys(e.format).reduce((i,l)=>(this.quill.scroll.query(l,C.BLOCK)&&!Array.isArray(e.format[l])&&(i[l]=e.format[l]),i),{}),s=new O().retain(t.index).delete(t.length).insert(` +`,r);this.quill.updateContents(s,N.sources.USER),this.quill.setSelection(t.index+1,N.sources.SILENT),this.quill.focus()}}const wd={bindings:{bold:zs("bold"),italic:zs("italic"),underline:zs("underline"),indent:{key:"Tab",format:["blockquote","indent","list"],handler(n,t){return t.collapsed&&t.offset!==0?!0:(this.quill.format("indent","+1",N.sources.USER),!1)}},outdent:{key:"Tab",shiftKey:!0,format:["blockquote","indent","list"],handler(n,t){return t.collapsed&&t.offset!==0?!0:(this.quill.format("indent","-1",N.sources.USER),!1)}},"outdent backspace":{key:"Backspace",collapsed:!0,shiftKey:null,metaKey:null,ctrlKey:null,altKey:null,format:["indent","list"],offset:0,handler(n,t){t.format.indent!=null?this.quill.format("indent","-1",N.sources.USER):t.format.list!=null&&this.quill.format("list",!1,N.sources.USER)}},"indent code-block":Ol(!0),"outdent code-block":Ol(!1),"remove tab":{key:"Tab",shiftKey:!0,collapsed:!0,prefix:/\t$/,handler(n){this.quill.deleteText(n.index-1,1,N.sources.USER)}},tab:{key:"Tab",handler(n,t){if(t.format.table)return!0;this.quill.history.cutoff();const e=new O().retain(n.index).delete(n.length).insert(" ");return this.quill.updateContents(e,N.sources.USER),this.quill.history.cutoff(),this.quill.setSelection(n.index+1,N.sources.SILENT),!1}},"blockquote empty enter":{key:"Enter",collapsed:!0,format:["blockquote"],empty:!0,handler(){this.quill.format("blockquote",!1,N.sources.USER)}},"list empty enter":{key:"Enter",collapsed:!0,format:["list"],empty:!0,handler(n,t){const e={list:!1};t.format.indent&&(e.indent=!1),this.quill.formatLine(n.index,n.length,e,N.sources.USER)}},"checklist enter":{key:"Enter",collapsed:!0,format:{list:"checked"},handler(n){const[t,e]=this.quill.getLine(n.index),r={...t.formats(),list:"checked"},s=new O().retain(n.index).insert(` +`,r).retain(t.length()-e-1).retain(1,{list:"unchecked"});this.quill.updateContents(s,N.sources.USER),this.quill.setSelection(n.index+1,N.sources.SILENT),this.quill.scrollSelectionIntoView()}},"header enter":{key:"Enter",collapsed:!0,format:["header"],suffix:/^$/,handler(n,t){const[e,r]=this.quill.getLine(n.index),s=new O().retain(n.index).insert(` +`,t.format).retain(e.length()-r-1).retain(1,{header:null});this.quill.updateContents(s,N.sources.USER),this.quill.setSelection(n.index+1,N.sources.SILENT),this.quill.scrollSelectionIntoView()}},"table backspace":{key:"Backspace",format:["table"],collapsed:!0,offset:0,handler(){}},"table delete":{key:"Delete",format:["table"],collapsed:!0,suffix:/^$/,handler(){}},"table enter":{key:"Enter",shiftKey:null,format:["table"],handler(n){const t=this.quill.getModule("table");if(t){const[e,r,s,i]=t.getTable(n),l=xd(e,r,s,i);if(l==null)return;let a=e.offset();if(l<0){const c=new O().retain(a).insert(` +`);this.quill.updateContents(c,N.sources.USER),this.quill.setSelection(n.index+1,n.length,N.sources.SILENT)}else if(l>0){a+=e.length();const c=new O().retain(a).insert(` +`);this.quill.updateContents(c,N.sources.USER),this.quill.setSelection(a,N.sources.USER)}}}},"table tab":{key:"Tab",shiftKey:null,format:["table"],handler(n,t){const{event:e,line:r}=t,s=r.offset(this.quill.scroll);e.shiftKey?this.quill.setSelection(s-1,N.sources.USER):this.quill.setSelection(s+r.length(),N.sources.USER)}},"list autofill":{key:" ",shiftKey:null,collapsed:!0,format:{"code-block":!1,blockquote:!1,table:!1},prefix:/^\s*?(\d+\.|-|\*|\[ ?\]|\[x\])$/,handler(n,t){if(this.quill.scroll.query("list")==null)return!0;const{length:e}=t.prefix,[r,s]=this.quill.getLine(n.index);if(s>e)return!0;let i;switch(t.prefix.trim()){case"[]":case"[ ]":i="unchecked";break;case"[x]":i="checked";break;case"-":case"*":i="bullet";break;default:i="ordered"}this.quill.insertText(n.index," ",N.sources.USER),this.quill.history.cutoff();const l=new O().retain(n.index-s).delete(e+1).retain(r.length()-2-s).retain(1,{list:i});return this.quill.updateContents(l,N.sources.USER),this.quill.history.cutoff(),this.quill.setSelection(n.index-e,N.sources.SILENT),!1}},"code exit":{key:"Enter",collapsed:!0,format:["code-block"],prefix:/^$/,suffix:/^\s*$/,handler(n){const[t,e]=this.quill.getLine(n.index);let r=2,s=t;for(;s!=null&&s.length()<=1&&s.formats()["code-block"];)if(s=s.prev,r-=1,r<=0){const i=new O().retain(n.index+t.length()-e-2).retain(1,{"code-block":null}).delete(1);return this.quill.updateContents(i,N.sources.USER),this.quill.setSelection(n.index-1,N.sources.SILENT),!1}return!0}},"embed left":mr("ArrowLeft",!1),"embed left shift":mr("ArrowLeft",!0),"embed right":mr("ArrowRight",!1),"embed right shift":mr("ArrowRight",!0),"table down":Cl(!1),"table up":Cl(!0)}};Mr.DEFAULTS=wd;function Ol(n){return{key:"Tab",shiftKey:!n,format:{"code-block":!0},handler(t,e){let{event:r}=e;const s=this.quill.scroll.query("code-block"),{TAB:i}=s;if(t.length===0&&!r.shiftKey){this.quill.insertText(t.index,i,N.sources.USER),this.quill.setSelection(t.index+i.length,N.sources.SILENT);return}const l=t.length===0?this.quill.getLines(t.index,1):this.quill.getLines(t);let{index:a,length:c}=t;l.forEach((h,f)=>{n?(h.insertAt(0,i),f===0?a+=i.length:c+=i.length):h.domNode.textContent.startsWith(i)&&(h.deleteAt(0,i.length),f===0?a-=i.length:c-=i.length)}),this.quill.update(N.sources.USER),this.quill.setSelection(a,c,N.sources.SILENT)}}}function mr(n,t){return{key:n,shiftKey:t,altKey:null,[n==="ArrowLeft"?"prefix":"suffix"]:/^$/,handler(r){let{index:s}=r;n==="ArrowRight"&&(s+=r.length+1);const[i]=this.quill.getLeaf(s);return i instanceof lt?(n==="ArrowLeft"?t?this.quill.setSelection(r.index-1,r.length+1,N.sources.USER):this.quill.setSelection(r.index-1,N.sources.USER):t?this.quill.setSelection(r.index,r.length+1,N.sources.USER):this.quill.setSelection(r.index+r.length+1,N.sources.USER),!1):!0}}}function zs(n){return{key:n[0],shortKey:!0,handler(t,e){this.quill.format(n,!e.format[n],N.sources.USER)}}}function Cl(n){return{key:n?"ArrowUp":"ArrowDown",collapsed:!0,format:["table"],handler(t,e){const r=n?"prev":"next",s=e.line,i=s.parent[r];if(i!=null){if(i.statics.blotName==="table-row"){let l=i.children.head,a=s;for(;a.prev!=null;)a=a.prev,l=l.next;const c=l.offset(this.quill.scroll)+Math.min(e.offset,l.length()-1);this.quill.setSelection(c,0,N.sources.USER)}}else{const l=s.table()[r];l!=null&&(n?this.quill.setSelection(l.offset(this.quill.scroll)+l.length()-1,0,N.sources.USER):this.quill.setSelection(l.offset(this.quill.scroll),0,N.sources.USER))}return!1}}}function Td(n){if(typeof n=="string"||typeof n=="number")n={key:n};else if(typeof n=="object")n=Fe(n);else return null;return n.shortKey&&(n[Nd]=n.shortKey,delete n.shortKey),n}function Mi(n){let{quill:t,range:e}=n;const r=t.getLines(e);let s={};if(r.length>1){const i=r[0].formats(),l=r[r.length-1].formats();s=ft.AttributeMap.diff(l,i)||{}}t.deleteText(e,N.sources.USER),Object.keys(s).length>0&&t.formatLine(e.index,1,s,N.sources.USER),t.setSelection(e.index,N.sources.SILENT)}function xd(n,t,e,r){return t.prev==null&&t.next==null?e.prev==null&&e.next==null?r===0?-1:1:e.prev==null?-1:1:t.prev==null?-1:t.next==null?1:null}const Ld=/font-weight:\s*normal/,Sd=["P","OL","UL"],ql=n=>n&&Sd.includes(n.tagName),_d=n=>{Array.from(n.querySelectorAll("br")).filter(t=>ql(t.previousElementSibling)&&ql(t.nextElementSibling)).forEach(t=>{t.parentNode?.removeChild(t)})},Od=n=>{Array.from(n.querySelectorAll('b[style*="font-weight"]')).filter(t=>t.getAttribute("style")?.match(Ld)).forEach(t=>{const e=n.createDocumentFragment();e.append(...t.childNodes),t.parentNode?.replaceChild(e,t)})};function Cd(n){n.querySelector('[id^="docs-internal-guid-"]')&&(Od(n),_d(n))}const qd=/\bmso-list:[^;]*ignore/i,Id=/\bmso-list:[^;]*\bl(\d+)/i,Rd=/\bmso-list:[^;]*\blevel(\d+)/i,kd=(n,t)=>{const e=n.getAttribute("style"),r=e?.match(Id);if(!r)return null;const s=Number(r[1]),i=e?.match(Rd),l=i?Number(i[1]):1,a=new RegExp(`@list l${s}:level${l}\\s*\\{[^\\}]*mso-level-number-format:\\s*([\\w-]+)`,"i"),c=t.match(a),h=c&&c[1]==="bullet"?"bullet":"ordered";return{id:s,indent:l,type:h,element:n}},Bd=n=>{const t=Array.from(n.querySelectorAll("[style*=mso-list]")),e=[],r=[];t.forEach(l=>{(l.getAttribute("style")||"").match(qd)?e.push(l):r.push(l)}),e.forEach(l=>l.parentNode?.removeChild(l));const s=n.documentElement.innerHTML,i=r.map(l=>kd(l,s)).filter(l=>l);for(;i.length;){const l=[];let a=i.shift();for(;a;)l.push(a),a=i.length&&i[0]?.element===a.element.nextElementSibling&&i[0].id===a.id?i.shift():null;const c=document.createElement("ul");l.forEach(b=>{const g=document.createElement("li");g.setAttribute("data-list",b.type),b.indent>1&&g.setAttribute("class",`ql-indent-${b.indent-1}`),g.innerHTML=b.element.innerHTML,c.appendChild(g)});const h=l[0]?.element,{parentNode:f}=h??{};h&&f?.replaceChild(c,h),l.slice(1).forEach(b=>{let{element:g}=b;f?.removeChild(g)})}};function Md(n){n.documentElement.getAttribute("xmlns:w")==="urn:schemas-microsoft-com:office:word"&&Bd(n)}const Dd=[Md,Cd],jd=n=>{n.documentElement&&Dd.forEach(t=>{t(n)})},Pd=Kt("quill:clipboard"),$d=[[Node.TEXT_NODE,Qd],[Node.TEXT_NODE,Rl],["br",Kd],[Node.ELEMENT_NODE,Rl],[Node.ELEMENT_NODE,zd],[Node.ELEMENT_NODE,Hd],[Node.ELEMENT_NODE,Xd],["li",Wd],["ol, ul",Zd],["pre",Gd],["tr",Yd],["b",Ks("bold")],["i",Ks("italic")],["strike",Ks("strike")],["style",Vd]],Ud=[bd,ko].reduce((n,t)=>(n[t.keyName]=t,n),{}),Il=[Io,Ri,Ii,Mo,Po,Uo].reduce((n,t)=>(n[t.keyName]=t,n),{});class Fo extends Tt{constructor(t,e){super(t,e),this.quill.root.addEventListener("copy",r=>this.onCaptureCopy(r,!1)),this.quill.root.addEventListener("cut",r=>this.onCaptureCopy(r,!0)),this.quill.root.addEventListener("paste",this.onCapturePaste.bind(this)),this.matchers=[],$d.concat(this.options.matchers??[]).forEach(r=>{let[s,i]=r;this.addMatcher(s,i)})}addMatcher(t,e){this.matchers.push([t,e])}convert(t){let{html:e,text:r}=t,s=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{};if(s[J.blotName])return new O().insert(r||"",{[J.blotName]:s[J.blotName]});if(!e)return new O().insert(r||"",s);const i=this.convertHTML(e);return kn(i,` +`)&&(i.ops[i.ops.length-1].attributes==null||s.table)?i.compose(new O().retain(i.length()-1).delete(1)):i}normalizeHTML(t){jd(t)}convertHTML(t){const e=new DOMParser().parseFromString(t,"text/html");this.normalizeHTML(e);const r=e.body,s=new WeakMap,[i,l]=this.prepareMatching(r,s);return Di(this.quill.scroll,r,i,l,s)}dangerouslyPasteHTML(t,e){let r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:N.sources.API;if(typeof t=="string"){const s=this.convert({html:t,text:""});this.quill.setContents(s,e),this.quill.setSelection(0,N.sources.SILENT)}else{const s=this.convert({html:e,text:""});this.quill.updateContents(new O().retain(t).concat(s),r),this.quill.setSelection(t+s.length(),N.sources.SILENT)}}onCaptureCopy(t){let e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!1;if(t.defaultPrevented)return;t.preventDefault();const[r]=this.quill.selection.getRange();if(r==null)return;const{html:s,text:i}=this.onCopy(r,e);t.clipboardData?.setData("text/plain",i),t.clipboardData?.setData("text/html",s),e&&Mi({range:r,quill:this.quill})}normalizeURIList(t){return t.split(/\r?\n/).filter(e=>e[0]!=="#").join(` +`)}onCapturePaste(t){if(t.defaultPrevented||!this.quill.isEnabled())return;t.preventDefault();const e=this.quill.getSelection(!0);if(e==null)return;const r=t.clipboardData?.getData("text/html");let s=t.clipboardData?.getData("text/plain");if(!r&&!s){const l=t.clipboardData?.getData("text/uri-list");l&&(s=this.normalizeURIList(l))}const i=Array.from(t.clipboardData?.files||[]);if(!r&&i.length>0){this.quill.uploader.upload(e,i);return}if(r&&i.length>0){const l=new DOMParser().parseFromString(r,"text/html");if(l.body.childElementCount===1&&l.body.firstElementChild?.tagName==="IMG"){this.quill.uploader.upload(e,i);return}}this.onPaste(e,{html:r,text:s})}onCopy(t){const e=this.quill.getText(t);return{html:this.quill.getSemanticHTML(t),text:e}}onPaste(t,e){let{text:r,html:s}=e;const i=this.quill.getFormat(t.index),l=this.convert({text:r,html:s},i);Pd.log("onPaste",l,{text:r,html:s});const a=new O().retain(t.index).delete(t.length).concat(l);this.quill.updateContents(a,N.sources.USER),this.quill.setSelection(a.length()-t.length,N.sources.SILENT),this.quill.scrollSelectionIntoView()}prepareMatching(t,e){const r=[],s=[];return this.matchers.forEach(i=>{const[l,a]=i;switch(l){case Node.TEXT_NODE:s.push(a);break;case Node.ELEMENT_NODE:r.push(a);break;default:Array.from(t.querySelectorAll(l)).forEach(c=>{e.has(c)?e.get(c)?.push(a):e.set(c,[a])});break}}),[r,s]}}x(Fo,"DEFAULTS",{matchers:[]});function we(n,t,e,r){return r.query(t)?n.reduce((s,i)=>{if(!i.insert)return s;if(i.attributes&&i.attributes[t])return s.push(i);const l=e?{[t]:e}:{};return s.insert(i.insert,{...l,...i.attributes})},new O):n}function kn(n,t){let e="";for(let r=n.ops.length-1;r>=0&&e.lengthl(t,i,n),new O):t.nodeType===t.ELEMENT_NODE?Array.from(t.childNodes||[]).reduce((i,l)=>{let a=Di(n,l,e,r,s);return l.nodeType===t.ELEMENT_NODE&&(a=e.reduce((c,h)=>h(l,c,n),a),a=(s.get(l)||[]).reduce((c,h)=>h(l,c,n),a)),i.concat(a)},new O):new O}function Ks(n){return(t,e,r)=>we(e,n,!0,r)}function Hd(n,t,e){const r=It.keys(n),s=Nt.keys(n),i=re.keys(n),l={};return r.concat(s).concat(i).forEach(a=>{let c=e.query(a,C.ATTRIBUTE);c!=null&&(l[c.attrName]=c.value(n),l[c.attrName])||(c=Ud[a],c!=null&&(c.attrName===a||c.keyName===a)&&(l[c.attrName]=c.value(n)||void 0),c=Il[a],c!=null&&(c.attrName===a||c.keyName===a)&&(c=Il[a],l[c.attrName]=c.value(n)||void 0))}),Object.entries(l).reduce((a,c)=>{let[h,f]=c;return we(a,h,f,e)},t)}function zd(n,t,e){const r=e.query(n);if(r==null)return t;if(r.prototype instanceof lt){const s={},i=r.value(n);if(i!=null)return s[r.blotName]=i,new O().insert(s,r.formats(n,e))}else if(r.prototype instanceof _n&&!kn(t,` +`)&&t.insert(` +`),"blotName"in r&&"formats"in r&&typeof r.formats=="function")return we(t,r.blotName,r.formats(n,e),e);return t}function Kd(n,t){return kn(t,` +`)||t.insert(` +`),t}function Gd(n,t,e){const r=e.query("code-block"),s=r&&"formats"in r&&typeof r.formats=="function"?r.formats(n,e):!0;return we(t,"code-block",s,e)}function Vd(){return new O}function Wd(n,t,e){const r=e.query(n);if(r==null||r.blotName!=="list"||!kn(t,` +`))return t;let s=-1,i=n.parentNode;for(;i!=null;)["OL","UL"].includes(i.tagName)&&(s+=1),i=i.parentNode;return s<=0?t:t.reduce((l,a)=>a.insert?a.attributes&&typeof a.attributes.indent=="number"?l.push(a):l.insert(a.insert,{indent:s,...a.attributes||{}}):l,new O)}function Zd(n,t,e){const r=n;let s=r.tagName==="OL"?"ordered":"bullet";const i=r.getAttribute("data-checked");return i&&(s=i==="true"?"checked":"unchecked"),we(t,"list",s,e)}function Rl(n,t,e){if(!kn(t,` +`)){if(Qt(n,e)&&(n.childNodes.length>0||n instanceof HTMLParagraphElement))return t.insert(` +`);if(t.length()>0&&n.nextSibling){let r=n.nextSibling;for(;r!=null;){if(Qt(r,e))return t.insert(` +`);const s=e.query(r);if(s&&s.prototype instanceof ht)return t.insert(` +`);r=r.firstChild}}}return t}function Xd(n,t,e){const r={},s=n.style||{};return s.fontStyle==="italic"&&(r.italic=!0),s.textDecoration==="underline"&&(r.underline=!0),s.textDecoration==="line-through"&&(r.strike=!0),(s.fontWeight?.startsWith("bold")||parseInt(s.fontWeight,10)>=700)&&(r.bold=!0),t=Object.entries(r).reduce((i,l)=>{let[a,c]=l;return we(i,a,c,e)},t),parseFloat(s.textIndent||0)>0?new O().insert(" ").concat(t):t}function Yd(n,t,e){const r=n.parentElement?.tagName==="TABLE"?n.parentElement:n.parentElement?.parentElement;if(r!=null){const i=Array.from(r.querySelectorAll("tr")).indexOf(n)+1;return we(t,"table",i,e)}return t}function Qd(n,t,e){let r=n.data;if(n.parentElement?.tagName==="O:P")return t.insert(r.trim());if(!Ho(n)){if(r.trim().length===0&&r.includes(` +`)&&!Fd(n,e))return t;r=r.replace(/[^\S\u00a0]/g," "),r=r.replace(/ {2,}/g," "),(n.previousSibling==null&&n.parentElement!=null&&Qt(n.parentElement,e)||n.previousSibling instanceof Element&&Qt(n.previousSibling,e))&&(r=r.replace(/^ /,"")),(n.nextSibling==null&&n.parentElement!=null&&Qt(n.parentElement,e)||n.nextSibling instanceof Element&&Qt(n.nextSibling,e))&&(r=r.replace(/ $/,"")),r=r.replaceAll(" "," ")}return t.insert(r)}class zo extends Tt{constructor(e,r){super(e,r);x(this,"lastRecorded",0);x(this,"ignoreChange",!1);x(this,"stack",{undo:[],redo:[]});x(this,"currentRange",null);this.quill.on(N.events.EDITOR_CHANGE,(s,i,l,a)=>{s===N.events.SELECTION_CHANGE?i&&a!==N.sources.SILENT&&(this.currentRange=i):s===N.events.TEXT_CHANGE&&(this.ignoreChange||(!this.options.userOnly||a===N.sources.USER?this.record(i,l):this.transform(i)),this.currentRange=ui(this.currentRange,i))}),this.quill.keyboard.addBinding({key:"z",shortKey:!0},this.undo.bind(this)),this.quill.keyboard.addBinding({key:["z","Z"],shortKey:!0,shiftKey:!0},this.redo.bind(this)),/Win/i.test(navigator.platform)&&this.quill.keyboard.addBinding({key:"y",shortKey:!0},this.redo.bind(this)),this.quill.root.addEventListener("beforeinput",s=>{s.inputType==="historyUndo"?(this.undo(),s.preventDefault()):s.inputType==="historyRedo"&&(this.redo(),s.preventDefault())})}change(e,r){if(this.stack[e].length===0)return;const s=this.stack[e].pop();if(!s)return;const i=this.quill.getContents(),l=s.delta.invert(i);this.stack[r].push({delta:l,range:ui(s.range,l)}),this.lastRecorded=0,this.ignoreChange=!0,this.quill.updateContents(s.delta,N.sources.USER),this.ignoreChange=!1,this.restoreSelection(s)}clear(){this.stack={undo:[],redo:[]}}cutoff(){this.lastRecorded=0}record(e,r){if(e.ops.length===0)return;this.stack.redo=[];let s=e.invert(r),i=this.currentRange;const l=Date.now();if(this.lastRecorded+this.options.delay>l&&this.stack.undo.length>0){const a=this.stack.undo.pop();a&&(s=s.compose(a.delta),i=a.range)}else this.lastRecorded=l;s.length()!==0&&(this.stack.undo.push({delta:s,range:i}),this.stack.undo.length>this.options.maxStack&&this.stack.undo.shift())}redo(){this.change("redo","undo")}transform(e){kl(this.stack.undo,e),kl(this.stack.redo,e)}undo(){this.change("undo","redo")}restoreSelection(e){if(e.range)this.quill.setSelection(e.range,N.sources.USER);else{const r=tg(this.quill.scroll,e.delta);this.quill.setSelection(r,N.sources.USER)}}}x(zo,"DEFAULTS",{delay:1e3,maxStack:100,userOnly:!1});function kl(n,t){let e=t;for(let r=n.length-1;r>=0;r-=1){const s=n[r];n[r]={delta:e.transform(s.delta,!0),range:s.range&&ui(s.range,e)},e=s.delta.transform(e),n[r].delta.length()===0&&n.splice(r,1)}}function Jd(n,t){const e=t.ops[t.ops.length-1];return e==null?!1:e.insert!=null?typeof e.insert=="string"&&e.insert.endsWith(` +`):e.attributes!=null?Object.keys(e.attributes).some(r=>n.query(r,C.BLOCK)!=null):!1}function tg(n,t){const e=t.reduce((s,i)=>s+(i.delete||0),0);let r=t.length()-e;return Jd(n,t)&&(r-=1),r}function ui(n,t){if(!n)return n;const e=t.transformPosition(n.index),r=t.transformPosition(n.index+n.length);return{index:e,length:r-e}}class Ko extends Tt{constructor(t,e){super(t,e),t.root.addEventListener("drop",r=>{r.preventDefault();let s=null;if(document.caretRangeFromPoint)s=document.caretRangeFromPoint(r.clientX,r.clientY);else if(document.caretPositionFromPoint){const l=document.caretPositionFromPoint(r.clientX,r.clientY);s=document.createRange(),s.setStart(l.offsetNode,l.offset),s.setEnd(l.offsetNode,l.offset)}const i=s&&t.selection.normalizeNative(s);if(i){const l=t.selection.normalizedToRange(i);r.dataTransfer?.files&&this.upload(l,r.dataTransfer.files)}})}upload(t,e){const r=[];Array.from(e).forEach(s=>{s&&this.options.mimetypes?.includes(s.type)&&r.push(s)}),r.length>0&&this.options.handler.call(this,t,r)}}Ko.DEFAULTS={mimetypes:["image/png","image/jpeg"],handler(n,t){if(!this.quill.scroll.query("image"))return;const e=t.map(r=>new Promise(s=>{const i=new FileReader;i.onload=()=>{s(i.result)},i.readAsDataURL(r)}));Promise.all(e).then(r=>{const s=r.reduce((i,l)=>i.insert({image:l}),new O().retain(n.index).delete(n.length));this.quill.updateContents(s,S.sources.USER),this.quill.setSelection(n.index+r.length,S.sources.SILENT)})}};const eg=["insertText","insertReplacementText"];class ng extends Tt{constructor(t,e){super(t,e),t.root.addEventListener("beforeinput",r=>{this.handleBeforeInput(r)}),/Android/i.test(navigator.userAgent)||t.on(N.events.COMPOSITION_BEFORE_START,()=>{this.handleCompositionStart()})}deleteRange(t){Mi({range:t,quill:this.quill})}replaceText(t){let e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:"";if(t.length===0)return!1;if(e){const r=this.quill.getFormat(t.index,1);this.deleteRange(t),this.quill.updateContents(new O().retain(t.index).insert(e,r),N.sources.USER)}else this.deleteRange(t);return this.quill.setSelection(t.index+e.length,0,N.sources.SILENT),!0}handleBeforeInput(t){if(this.quill.composition.isComposing||t.defaultPrevented||!eg.includes(t.inputType))return;const e=t.getTargetRanges?t.getTargetRanges()[0]:null;if(!e||e.collapsed===!0)return;const r=rg(t);if(r==null)return;const s=this.quill.selection.normalizeNative(e),i=s?this.quill.selection.normalizedToRange(s):null;i&&this.replaceText(i,r)&&t.preventDefault()}handleCompositionStart(){const t=this.quill.getSelection();t&&this.replaceText(t)}}function rg(n){return typeof n.data=="string"?n.data:n.dataTransfer?.types.includes("text/plain")?n.dataTransfer.getData("text/plain"):null}const sg=/Mac/i.test(navigator.platform),ig=100,lg=n=>!!(n.key==="ArrowLeft"||n.key==="ArrowRight"||n.key==="ArrowUp"||n.key==="ArrowDown"||n.key==="Home"||sg&&n.key==="a"&&n.ctrlKey===!0);class og extends Tt{constructor(e,r){super(e,r);x(this,"isListening",!1);x(this,"selectionChangeDeadline",0);this.handleArrowKeys(),this.handleNavigationShortcuts()}handleArrowKeys(){this.quill.keyboard.addBinding({key:["ArrowLeft","ArrowRight"],offset:0,shiftKey:null,handler(e,r){let{line:s,event:i}=r;if(!(s instanceof Et)||!s.uiNode)return!0;const l=getComputedStyle(s.domNode).direction==="rtl";return l&&i.key!=="ArrowRight"||!l&&i.key!=="ArrowLeft"?!0:(this.quill.setSelection(e.index-1,e.length+(i.shiftKey?1:0),N.sources.USER),!1)}})}handleNavigationShortcuts(){this.quill.root.addEventListener("keydown",e=>{!e.defaultPrevented&&lg(e)&&this.ensureListeningToSelectionChange()})}ensureListeningToSelectionChange(){if(this.selectionChangeDeadline=Date.now()+ig,this.isListening)return;this.isListening=!0;const e=()=>{this.isListening=!1,Date.now()<=this.selectionChangeDeadline&&this.handleSelectionChange()};document.addEventListener("selectionchange",e,{once:!0})}handleSelectionChange(){const e=document.getSelection();if(!e)return;const r=e.getRangeAt(0);if(r.collapsed!==!0||r.startOffset!==0)return;const s=this.quill.scroll.find(r.startContainer);if(!(s instanceof Et)||!s.uiNode)return;const i=document.createRange();i.setStartAfter(s.uiNode),i.setEndAfter(s.uiNode),e.removeAllRanges(),e.addRange(i)}}N.register({"blots/block":W,"blots/block/embed":ht,"blots/break":wt,"blots/container":Ae,"blots/cursor":Ve,"blots/embed":Ci,"blots/inline":Rt,"blots/scroll":$e,"blots/text":At,"modules/clipboard":Fo,"modules/history":zo,"modules/keyboard":Mr,"modules/uploader":Ko,"modules/input":ng,"modules/uiNode":og});class ag extends Nt{add(t,e){let r=0;if(e==="+1"||e==="-1"){const s=this.value(t)||0;r=e==="+1"?s+1:s-1}else typeof e=="number"&&(r=e);return r===0?(this.remove(t),!0):super.add(t,r.toString())}canAdd(t,e){return super.canAdd(t,e)||super.canAdd(t,parseInt(e,10))}value(t){return parseInt(super.value(t),10)||void 0}}const cg=new ag("indent","ql-indent",{scope:C.BLOCK,whitelist:[1,2,3,4,5,6,7,8]});class hi extends W{}x(hi,"blotName","blockquote"),x(hi,"tagName","blockquote");class fi extends W{static formats(t){return this.tagName.indexOf(t.tagName)+1}}x(fi,"blotName","header"),x(fi,"tagName",["H1","H2","H3","H4","H5","H6"]);class Bn extends Ae{}Bn.blotName="list-container";Bn.tagName="OL";class Mn extends W{static create(t){const e=super.create();return e.setAttribute("data-list",t),e}static formats(t){return t.getAttribute("data-list")||void 0}static register(){N.register(Bn)}constructor(t,e){super(t,e);const r=e.ownerDocument.createElement("span"),s=i=>{if(!t.isEnabled())return;const l=this.statics.formats(e,t);l==="checked"?(this.format("list","unchecked"),i.preventDefault()):l==="unchecked"&&(this.format("list","checked"),i.preventDefault())};r.addEventListener("mousedown",s),r.addEventListener("touchstart",s),this.attachUI(r)}format(t,e){t===this.statics.blotName&&e?this.domNode.setAttribute("data-list",e):super.format(t,e)}}Mn.blotName="list";Mn.tagName="LI";Bn.allowedChildren=[Mn];Mn.requiredContainer=Bn;class qn extends Rt{static create(){return super.create()}static formats(){return!0}optimize(t){super.optimize(t),this.domNode.tagName!==this.statics.tagName[0]&&this.replaceWith(this.statics.blotName)}}x(qn,"blotName","bold"),x(qn,"tagName",["STRONG","B"]);class di extends qn{}x(di,"blotName","italic"),x(di,"tagName",["EM","I"]);class Jt extends Rt{static create(t){const e=super.create(t);return e.setAttribute("href",this.sanitize(t)),e.setAttribute("rel","noopener noreferrer"),e.setAttribute("target","_blank"),e}static formats(t){return t.getAttribute("href")}static sanitize(t){return Go(t,this.PROTOCOL_WHITELIST)?t:this.SANITIZED_URL}format(t,e){t!==this.statics.blotName||!e?super.format(t,e):this.domNode.setAttribute("href",this.constructor.sanitize(e))}}x(Jt,"blotName","link"),x(Jt,"tagName","A"),x(Jt,"SANITIZED_URL","about:blank"),x(Jt,"PROTOCOL_WHITELIST",["http","https","mailto","tel","sms"]);function Go(n,t){const e=document.createElement("a");e.href=n;const r=e.href.slice(0,e.href.indexOf(":"));return t.indexOf(r)>-1}class gi extends Rt{static create(t){return t==="super"?document.createElement("sup"):t==="sub"?document.createElement("sub"):super.create(t)}static formats(t){if(t.tagName==="SUB")return"sub";if(t.tagName==="SUP")return"super"}}x(gi,"blotName","script"),x(gi,"tagName",["SUB","SUP"]);class pi extends qn{}x(pi,"blotName","strike"),x(pi,"tagName",["S","STRIKE"]);class mi extends Rt{}x(mi,"blotName","underline"),x(mi,"tagName","U");class vr extends Ci{static create(t){if(window.katex==null)throw new Error("Formula module requires KaTeX.");const e=super.create(t);return typeof t=="string"&&(window.katex.render(t,e,{throwOnError:!1,errorColor:"#f00"}),e.setAttribute("data-value",t)),e}static value(t){return t.getAttribute("data-value")}html(){const{formula:t}=this.value();return`${t}`}}x(vr,"blotName","formula"),x(vr,"className","ql-formula"),x(vr,"tagName","SPAN");const Bl=["alt","height","width"];class bi extends lt{static create(t){const e=super.create(t);return typeof t=="string"&&e.setAttribute("src",this.sanitize(t)),e}static formats(t){return Bl.reduce((e,r)=>(t.hasAttribute(r)&&(e[r]=t.getAttribute(r)),e),{})}static match(t){return/\.(jpe?g|gif|png)$/.test(t)||/^data:image\/.+;base64/.test(t)}static sanitize(t){return Go(t,["http","https","data"])?t:"//:0"}static value(t){return t.getAttribute("src")}format(t,e){Bl.indexOf(t)>-1?e?this.domNode.setAttribute(t,e):this.domNode.removeAttribute(t):super.format(t,e)}}x(bi,"blotName","image"),x(bi,"tagName","IMG");const Ml=["height","width"];class Er extends ht{static create(t){const e=super.create(t);return e.setAttribute("frameborder","0"),e.setAttribute("allowfullscreen","true"),e.setAttribute("src",this.sanitize(t)),e}static formats(t){return Ml.reduce((e,r)=>(t.hasAttribute(r)&&(e[r]=t.getAttribute(r)),e),{})}static sanitize(t){return Jt.sanitize(t)}static value(t){return t.getAttribute("src")}format(t,e){Ml.indexOf(t)>-1?e?this.domNode.setAttribute(t,e):this.domNode.removeAttribute(t):super.format(t,e)}html(){const{video:t}=this.value();return`${t}`}}x(Er,"blotName","video"),x(Er,"className","ql-video"),x(Er,"tagName","IFRAME");const vn=new Nt("code-token","hljs",{scope:C.INLINE});class Ft extends Rt{static formats(t,e){for(;t!=null&&t!==e.domNode;){if(t.classList&&t.classList.contains(J.className))return super.formats(t,e);t=t.parentNode}}constructor(t,e,r){super(t,e,r),vn.add(this.domNode,r)}format(t,e){t!==Ft.blotName?super.format(t,e):e?vn.add(this.domNode,e):(vn.remove(this.domNode),this.domNode.classList.remove(this.statics.className))}optimize(){super.optimize(...arguments),vn.value(this.domNode)||this.unwrap()}}Ft.blotName="code-token";Ft.className="ql-token";class ut extends J{static create(t){const e=super.create(t);return typeof t=="string"&&e.setAttribute("data-language",t),e}static formats(t){return t.getAttribute("data-language")||"plain"}static register(){}format(t,e){t===this.statics.blotName&&e?this.domNode.setAttribute("data-language",e):super.format(t,e)}replaceWith(t,e){return this.formatAt(0,this.length(),Ft.blotName,!1),super.replaceWith(t,e)}}class Nn extends Ne{attach(){super.attach(),this.forceNext=!1,this.scroll.emitMount(this)}format(t,e){t===ut.blotName&&(this.forceNext=!0,this.children.forEach(r=>{r.format(t,e)}))}formatAt(t,e,r,s){r===ut.blotName&&(this.forceNext=!0),super.formatAt(t,e,r,s)}highlight(t){let e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!1;if(this.children.head==null)return;const s=`${Array.from(this.domNode.childNodes).filter(l=>l!==this.uiNode).map(l=>l.textContent).join(` +`)} +`,i=ut.formats(this.children.head.domNode);if(e||this.forceNext||this.cachedText!==s){if(s.trim().length>0||this.cachedText==null){const l=this.children.reduce((c,h)=>c.concat(_o(h,!1)),new O),a=t(s,i);l.diff(a).reduce((c,h)=>{let{retain:f,attributes:b}=h;return f?(b&&Object.keys(b).forEach(g=>{[ut.blotName,Ft.blotName].includes(g)&&this.formatAt(c,f,g,b[g])}),c+f):c},0)}this.cachedText=s,this.forceNext=!1}}html(t,e){const[r]=this.children.find(t);return`
          +${Br(this.code(t,e))}
          +
          `}optimize(t){if(super.optimize(t),this.parent!=null&&this.children.head!=null&&this.uiNode!=null){const e=ut.formats(this.children.head.domNode);e!==this.uiNode.value&&(this.uiNode.value=e)}}}Nn.allowedChildren=[ut];ut.requiredContainer=Nn;ut.allowedChildren=[Ft,Ve,At,wt];const ug=(n,t,e)=>{if(typeof n.versionString=="string"){const r=n.versionString.split(".")[0];if(parseInt(r,10)>=11)return n.highlight(e,{language:t}).value}return n.highlight(t,e).value};class Vo extends Tt{static register(){N.register(Ft,!0),N.register(ut,!0),N.register(Nn,!0)}constructor(t,e){if(super(t,e),this.options.hljs==null)throw new Error("Syntax module requires highlight.js. Please include the library on the page before Quill.");this.languages=this.options.languages.reduce((r,s)=>{let{key:i}=s;return r[i]=!0,r},{}),this.highlightBlot=this.highlightBlot.bind(this),this.initListener(),this.initTimer()}initListener(){this.quill.on(N.events.SCROLL_BLOT_MOUNT,t=>{if(!(t instanceof Nn))return;const e=this.quill.root.ownerDocument.createElement("select");this.options.languages.forEach(r=>{let{key:s,label:i}=r;const l=e.ownerDocument.createElement("option");l.textContent=i,l.setAttribute("value",s),e.appendChild(l)}),e.addEventListener("change",()=>{t.format(ut.blotName,e.value),this.quill.root.focus(),this.highlight(t,!0)}),t.uiNode==null&&(t.attachUI(e),t.children.head&&(e.value=ut.formats(t.children.head.domNode)))})}initTimer(){let t=null;this.quill.on(N.events.SCROLL_OPTIMIZE,()=>{t&&clearTimeout(t),t=setTimeout(()=>{this.highlight(),t=null},this.options.interval)})}highlight(){let t=arguments.length>0&&arguments[0]!==void 0?arguments[0]:null,e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!1;if(this.quill.selection.composing)return;this.quill.update(N.sources.USER);const r=this.quill.getSelection();(t==null?this.quill.scroll.descendants(Nn):[t]).forEach(i=>{i.highlight(this.highlightBlot,e)}),this.quill.update(N.sources.SILENT),r!=null&&this.quill.setSelection(r,N.sources.SILENT)}highlightBlot(t){let e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:"plain";if(e=this.languages[e]?e:"plain",e==="plain")return Br(t).split(` +`).reduce((s,i,l)=>(l!==0&&s.insert(` +`,{[J.blotName]:e}),s.insert(i)),new O);const r=this.quill.root.ownerDocument.createElement("div");return r.classList.add(J.className),r.innerHTML=ug(this.options.hljs,e,t),Di(this.quill.scroll,r,[(s,i)=>{const l=vn.value(s);return l?i.compose(new O().retain(i.length(),{[Ft.blotName]:l})):i}],[(s,i)=>s.data.split(` +`).reduce((l,a,c)=>(c!==0&&l.insert(` +`,{[J.blotName]:e}),l.insert(a)),i)],new WeakMap)}}Vo.DEFAULTS={hljs:window.hljs,interval:1e3,languages:[{key:"plain",label:"Plain"},{key:"bash",label:"Bash"},{key:"cpp",label:"C++"},{key:"cs",label:"C#"},{key:"css",label:"CSS"},{key:"diff",label:"Diff"},{key:"xml",label:"HTML/XML"},{key:"java",label:"Java"},{key:"javascript",label:"JavaScript"},{key:"markdown",label:"Markdown"},{key:"php",label:"PHP"},{key:"python",label:"Python"},{key:"ruby",label:"Ruby"},{key:"sql",label:"SQL"}]};const Tn=class Tn extends W{static create(t){const e=super.create();return t?e.setAttribute("data-row",t):e.setAttribute("data-row",ji()),e}static formats(t){if(t.hasAttribute("data-row"))return t.getAttribute("data-row")}cellOffset(){return this.parent?this.parent.children.indexOf(this):-1}format(t,e){t===Tn.blotName&&e?this.domNode.setAttribute("data-row",e):super.format(t,e)}row(){return this.parent}rowOffset(){return this.row()?this.row().rowOffset():-1}table(){return this.row()&&this.row().table()}};x(Tn,"blotName","table"),x(Tn,"tagName","TD");let vt=Tn;class Ht extends Ae{checkMerge(){if(super.checkMerge()&&this.next.children.head!=null){const t=this.children.head.formats(),e=this.children.tail.formats(),r=this.next.children.head.formats(),s=this.next.children.tail.formats();return t.table===e.table&&t.table===r.table&&t.table===s.table}return!1}optimize(t){super.optimize(t),this.children.forEach(e=>{if(e.next==null)return;const r=e.formats(),s=e.next.formats();if(r.table!==s.table){const i=this.splitAfter(e);i&&i.optimize(),this.prev&&this.prev.optimize()}})}rowOffset(){return this.parent?this.parent.children.indexOf(this):-1}table(){return this.parent&&this.parent.parent}}x(Ht,"blotName","table-row"),x(Ht,"tagName","TR");class qt extends Ae{}x(qt,"blotName","table-body"),x(qt,"tagName","TBODY");class Ze extends Ae{balanceCells(){const t=this.descendants(Ht),e=t.reduce((r,s)=>Math.max(s.children.length,r),0);t.forEach(r=>{new Array(e-r.children.length).fill(0).forEach(()=>{let s;r.children.head!=null&&(s=vt.formats(r.children.head.domNode));const i=this.scroll.create(vt.blotName,s);r.appendChild(i),i.optimize()})})}cells(t){return this.rows().map(e=>e.children.at(t))}deleteColumn(t){const[e]=this.descendant(qt);e==null||e.children.head==null||e.children.forEach(r=>{const s=r.children.at(t);s?.remove()})}insertColumn(t){const[e]=this.descendant(qt);e==null||e.children.head==null||e.children.forEach(r=>{const s=r.children.at(t),i=vt.formats(r.children.head.domNode),l=this.scroll.create(vt.blotName,i);r.insertBefore(l,s)})}insertRow(t){const[e]=this.descendant(qt);if(e==null||e.children.head==null)return;const r=ji(),s=this.scroll.create(Ht.blotName);e.children.head.children.forEach(()=>{const l=this.scroll.create(vt.blotName,r);s.appendChild(l)});const i=e.children.at(t);e.insertBefore(s,i)}rows(){const t=this.children.head;return t==null?[]:t.children.map(e=>e)}}x(Ze,"blotName","table-container"),x(Ze,"tagName","TABLE");Ze.allowedChildren=[qt];qt.requiredContainer=Ze;qt.allowedChildren=[Ht];Ht.requiredContainer=qt;Ht.allowedChildren=[vt];vt.requiredContainer=Ht;function ji(){return`row-${Math.random().toString(36).slice(2,6)}`}class hg extends Tt{static register(){N.register(vt),N.register(Ht),N.register(qt),N.register(Ze)}constructor(){super(...arguments),this.listenBalanceCells()}balanceTables(){this.quill.scroll.descendants(Ze).forEach(t=>{t.balanceCells()})}deleteColumn(){const[t,,e]=this.getTable();e!=null&&(t.deleteColumn(e.cellOffset()),this.quill.update(N.sources.USER))}deleteRow(){const[,t]=this.getTable();t!=null&&(t.remove(),this.quill.update(N.sources.USER))}deleteTable(){const[t]=this.getTable();if(t==null)return;const e=t.offset();t.remove(),this.quill.update(N.sources.USER),this.quill.setSelection(e,N.sources.SILENT)}getTable(){let t=arguments.length>0&&arguments[0]!==void 0?arguments[0]:this.quill.getSelection();if(t==null)return[null,null,null,-1];const[e,r]=this.quill.getLine(t.index);if(e==null||e.statics.blotName!==vt.blotName)return[null,null,null,-1];const s=e.parent;return[s.parent.parent,s,e,r]}insertColumn(t){const e=this.quill.getSelection();if(!e)return;const[r,s,i]=this.getTable(e);if(i==null)return;const l=i.cellOffset();r.insertColumn(l+t),this.quill.update(N.sources.USER);let a=s.rowOffset();t===0&&(a+=1),this.quill.setSelection(e.index+a,e.length,N.sources.SILENT)}insertColumnLeft(){this.insertColumn(0)}insertColumnRight(){this.insertColumn(1)}insertRow(t){const e=this.quill.getSelection();if(!e)return;const[r,s,i]=this.getTable(e);if(i==null)return;const l=s.rowOffset();r.insertRow(l+t),this.quill.update(N.sources.USER),t>0?this.quill.setSelection(e,N.sources.SILENT):this.quill.setSelection(e.index+s.children.length,e.length,N.sources.SILENT)}insertRowAbove(){this.insertRow(0)}insertRowBelow(){this.insertRow(1)}insertTable(t,e){const r=this.quill.getSelection();if(r==null)return;const s=new Array(t).fill(0).reduce(i=>{const l=new Array(e).fill(` +`).join("");return i.insert(l,{table:ji()})},new O().retain(r.index));this.quill.updateContents(s,N.sources.USER),this.quill.setSelection(r.index,N.sources.SILENT),this.balanceTables()}listenBalanceCells(){this.quill.on(N.events.SCROLL_OPTIMIZE,t=>{t.some(e=>["TD","TR","TBODY","TABLE"].includes(e.target.tagName)?(this.quill.once(N.events.TEXT_CHANGE,(r,s,i)=>{i===N.sources.USER&&this.balanceTables()}),!0):!1)})}}const Dl=Kt("quill:toolbar");class Pi extends Tt{constructor(t,e){if(super(t,e),Array.isArray(this.options.container)){const r=document.createElement("div");r.setAttribute("role","toolbar"),fg(r,this.options.container),t.container?.parentNode?.insertBefore(r,t.container),this.container=r}else typeof this.options.container=="string"?this.container=document.querySelector(this.options.container):this.container=this.options.container;if(!(this.container instanceof HTMLElement)){Dl.error("Container required for toolbar",this.options);return}this.container.classList.add("ql-toolbar"),this.controls=[],this.handlers={},this.options.handlers&&Object.keys(this.options.handlers).forEach(r=>{const s=this.options.handlers?.[r];s&&this.addHandler(r,s)}),Array.from(this.container.querySelectorAll("button, select")).forEach(r=>{this.attach(r)}),this.quill.on(N.events.EDITOR_CHANGE,()=>{const[r]=this.quill.selection.getRange();this.update(r)})}addHandler(t,e){this.handlers[t]=e}attach(t){let e=Array.from(t.classList).find(s=>s.indexOf("ql-")===0);if(!e)return;if(e=e.slice(3),t.tagName==="BUTTON"&&t.setAttribute("type","button"),this.handlers[e]==null&&this.quill.scroll.query(e)==null){Dl.warn("ignoring attaching to nonexistent format",e,t);return}const r=t.tagName==="SELECT"?"change":"click";t.addEventListener(r,s=>{let i;if(t.tagName==="SELECT"){if(t.selectedIndex<0)return;const a=t.options[t.selectedIndex];a.hasAttribute("selected")?i=!1:i=a.value||!1}else t.classList.contains("ql-active")?i=!1:i=t.value||!t.hasAttribute("value"),s.preventDefault();this.quill.focus();const[l]=this.quill.selection.getRange();if(this.handlers[e]!=null)this.handlers[e].call(this,i);else if(this.quill.scroll.query(e).prototype instanceof lt){if(i=prompt(`Enter ${e}`),!i)return;this.quill.updateContents(new O().retain(l.index).delete(l.length).insert({[e]:i}),N.sources.USER)}else this.quill.format(e,i,N.sources.USER);this.update(l)}),this.controls.push([e,t])}update(t){const e=t==null?{}:this.quill.getFormat(t);this.controls.forEach(r=>{const[s,i]=r;if(i.tagName==="SELECT"){let l=null;if(t==null)l=null;else if(e[s]==null)l=i.querySelector("option[selected]");else if(!Array.isArray(e[s])){let a=e[s];typeof a=="string"&&(a=a.replace(/"/g,'\\"')),l=i.querySelector(`option[value="${a}"]`)}l==null?(i.value="",i.selectedIndex=-1):l.selected=!0}else if(t==null)i.classList.remove("ql-active"),i.setAttribute("aria-pressed","false");else if(i.hasAttribute("value")){const l=e[s],a=l===i.getAttribute("value")||l!=null&&l.toString()===i.getAttribute("value")||l==null&&!i.getAttribute("value");i.classList.toggle("ql-active",a),i.setAttribute("aria-pressed",a.toString())}else{const l=e[s]!=null;i.classList.toggle("ql-active",l),i.setAttribute("aria-pressed",l.toString())}})}}Pi.DEFAULTS={};function jl(n,t,e){const r=document.createElement("button");r.setAttribute("type","button"),r.classList.add(`ql-${t}`),r.setAttribute("aria-pressed","false"),e!=null?(r.value=e,r.setAttribute("aria-label",`${t}: ${e}`)):r.setAttribute("aria-label",t),n.appendChild(r)}function fg(n,t){Array.isArray(t[0])||(t=[t]),t.forEach(e=>{const r=document.createElement("span");r.classList.add("ql-formats"),e.forEach(s=>{if(typeof s=="string")jl(r,s);else{const i=Object.keys(s)[0],l=s[i];Array.isArray(l)?dg(r,i,l):jl(r,i,l)}}),n.appendChild(r)})}function dg(n,t,e){const r=document.createElement("select");r.classList.add(`ql-${t}`),e.forEach(s=>{const i=document.createElement("option");s!==!1?i.setAttribute("value",String(s)):i.setAttribute("selected","selected"),r.appendChild(i)}),n.appendChild(r)}Pi.DEFAULTS={container:null,handlers:{clean(){const n=this.quill.getSelection();if(n!=null)if(n.length===0){const t=this.quill.getFormat();Object.keys(t).forEach(e=>{this.quill.scroll.query(e,C.INLINE)!=null&&this.quill.format(e,!1,N.sources.USER)})}else this.quill.removeFormat(n.index,n.length,N.sources.USER)},direction(n){const{align:t}=this.quill.getFormat();n==="rtl"&&t==null?this.quill.format("align","right",N.sources.USER):!n&&t==="right"&&this.quill.format("align",!1,N.sources.USER),this.quill.format("direction",n,N.sources.USER)},indent(n){const t=this.quill.getSelection(),e=this.quill.getFormat(t),r=parseInt(e.indent||0,10);if(n==="+1"||n==="-1"){let s=n==="+1"?1:-1;e.direction==="rtl"&&(s*=-1),this.quill.format("indent",r+s,N.sources.USER)}},link(n){n===!0&&(n=prompt("Enter link URL:")),this.quill.format("link",n,N.sources.USER)},list(n){const t=this.quill.getSelection(),e=this.quill.getFormat(t);n==="check"?e.list==="checked"||e.list==="unchecked"?this.quill.format("list",!1,N.sources.USER):this.quill.format("list","unchecked",N.sources.USER):this.quill.format("list",n,N.sources.USER)}}};const gg='',pg='',mg='',bg='',yg='',vg='',Eg='',Ag='',Pl='',Ng='',wg='',Tg='',xg='',Lg='',Sg='',_g='',Og='',Cg='',qg='',Ig='',Rg='',kg='',Bg='',Mg='',Dg='',jg='',Pg='',$g='',Ug='',Fg='',Hg='',zg='',Kg='',In={align:{"":gg,center:pg,right:mg,justify:bg},background:yg,blockquote:vg,bold:Eg,clean:Ag,code:Pl,"code-block":Pl,color:Ng,direction:{"":wg,rtl:Tg},formula:xg,header:{1:Lg,2:Sg,3:_g,4:Og,5:Cg,6:qg},italic:Ig,image:Rg,indent:{"+1":kg,"-1":Bg},link:Mg,list:{bullet:Dg,check:jg,ordered:Pg},script:{sub:$g,super:Ug},strike:Fg,table:Hg,underline:zg,video:Kg},Gg='';let $l=0;function Ul(n,t){n.setAttribute(t,`${n.getAttribute(t)!=="true"}`)}class Dr{constructor(t){this.select=t,this.container=document.createElement("span"),this.buildPicker(),this.select.style.display="none",this.select.parentNode.insertBefore(this.container,this.select),this.label.addEventListener("mousedown",()=>{this.togglePicker()}),this.label.addEventListener("keydown",e=>{switch(e.key){case"Enter":this.togglePicker();break;case"Escape":this.escape(),e.preventDefault();break}}),this.select.addEventListener("change",this.update.bind(this))}togglePicker(){this.container.classList.toggle("ql-expanded"),Ul(this.label,"aria-expanded"),Ul(this.options,"aria-hidden")}buildItem(t){const e=document.createElement("span");e.tabIndex="0",e.setAttribute("role","button"),e.classList.add("ql-picker-item");const r=t.getAttribute("value");return r&&e.setAttribute("data-value",r),t.textContent&&e.setAttribute("data-label",t.textContent),e.addEventListener("click",()=>{this.selectItem(e,!0)}),e.addEventListener("keydown",s=>{switch(s.key){case"Enter":this.selectItem(e,!0),s.preventDefault();break;case"Escape":this.escape(),s.preventDefault();break}}),e}buildLabel(){const t=document.createElement("span");return t.classList.add("ql-picker-label"),t.innerHTML=Gg,t.tabIndex="0",t.setAttribute("role","button"),t.setAttribute("aria-expanded","false"),this.container.appendChild(t),t}buildOptions(){const t=document.createElement("span");t.classList.add("ql-picker-options"),t.setAttribute("aria-hidden","true"),t.tabIndex="-1",t.id=`ql-picker-options-${$l}`,$l+=1,this.label.setAttribute("aria-controls",t.id),this.options=t,Array.from(this.select.options).forEach(e=>{const r=this.buildItem(e);t.appendChild(r),e.selected===!0&&this.selectItem(r)}),this.container.appendChild(t)}buildPicker(){Array.from(this.select.attributes).forEach(t=>{this.container.setAttribute(t.name,t.value)}),this.container.classList.add("ql-picker"),this.label=this.buildLabel(),this.buildOptions()}escape(){this.close(),setTimeout(()=>this.label.focus(),1)}close(){this.container.classList.remove("ql-expanded"),this.label.setAttribute("aria-expanded","false"),this.options.setAttribute("aria-hidden","true")}selectItem(t){let e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!1;const r=this.container.querySelector(".ql-selected");t!==r&&(r?.classList.remove("ql-selected"),t!=null&&(t.classList.add("ql-selected"),this.select.selectedIndex=Array.from(t.parentNode.children).indexOf(t),t.hasAttribute("data-value")?this.label.setAttribute("data-value",t.getAttribute("data-value")):this.label.removeAttribute("data-value"),t.hasAttribute("data-label")?this.label.setAttribute("data-label",t.getAttribute("data-label")):this.label.removeAttribute("data-label"),e&&(this.select.dispatchEvent(new Event("change")),this.close())))}update(){let t;if(this.select.selectedIndex>-1){const r=this.container.querySelector(".ql-picker-options").children[this.select.selectedIndex];t=this.select.options[this.select.selectedIndex],this.selectItem(r)}else this.selectItem(null);const e=t!=null&&t!==this.select.querySelector("option[selected]");this.label.classList.toggle("ql-active",e)}}class Wo extends Dr{constructor(t,e){super(t),this.label.innerHTML=e,this.container.classList.add("ql-color-picker"),Array.from(this.container.querySelectorAll(".ql-picker-item")).slice(0,7).forEach(r=>{r.classList.add("ql-primary")})}buildItem(t){const e=super.buildItem(t);return e.style.backgroundColor=t.getAttribute("value")||"",e}selectItem(t,e){super.selectItem(t,e);const r=this.label.querySelector(".ql-color-label"),s=t&&t.getAttribute("data-value")||"";r&&(r.tagName==="line"?r.style.stroke=s:r.style.fill=s)}}class Zo extends Dr{constructor(t,e){super(t),this.container.classList.add("ql-icon-picker"),Array.from(this.container.querySelectorAll(".ql-picker-item")).forEach(r=>{r.innerHTML=e[r.getAttribute("data-value")||""]}),this.defaultItem=this.container.querySelector(".ql-selected"),this.selectItem(this.defaultItem)}selectItem(t,e){super.selectItem(t,e);const r=t||this.defaultItem;if(r!=null){if(this.label.innerHTML===r.innerHTML)return;this.label.innerHTML=r.innerHTML}}}const Vg=n=>{const{overflowY:t}=getComputedStyle(n,null);return t!=="visible"&&t!=="clip"};class Xo{constructor(t,e){this.quill=t,this.boundsContainer=e||document.body,this.root=t.addContainer("ql-tooltip"),this.root.innerHTML=this.constructor.TEMPLATE,Vg(this.quill.root)&&this.quill.root.addEventListener("scroll",()=>{this.root.style.marginTop=`${-1*this.quill.root.scrollTop}px`}),this.hide()}hide(){this.root.classList.add("ql-hidden")}position(t){const e=t.left+t.width/2-this.root.offsetWidth/2,r=t.bottom+this.quill.root.scrollTop;this.root.style.left=`${e}px`,this.root.style.top=`${r}px`,this.root.classList.remove("ql-flip");const s=this.boundsContainer.getBoundingClientRect(),i=this.root.getBoundingClientRect();let l=0;if(i.right>s.right&&(l=s.right-i.right,this.root.style.left=`${e+l}px`),i.lefts.bottom){const a=i.bottom-i.top,c=t.bottom-t.top+a;this.root.style.top=`${r-c}px`,this.root.classList.add("ql-flip")}return l}show(){this.root.classList.remove("ql-editing"),this.root.classList.remove("ql-hidden")}}const Wg=[!1,"center","right","justify"],Zg=["#000000","#e60000","#ff9900","#ffff00","#008a00","#0066cc","#9933ff","#ffffff","#facccc","#ffebcc","#ffffcc","#cce8cc","#cce0f5","#ebd6ff","#bbbbbb","#f06666","#ffc266","#ffff66","#66b966","#66a3e0","#c285ff","#888888","#a10000","#b26b00","#b2b200","#006100","#0047b2","#6b24b2","#444444","#5c0000","#663d00","#666600","#003700","#002966","#3d1466"],Xg=[!1,"serif","monospace"],Yg=["1","2","3",!1],Qg=["small",!1,"large","huge"];class Dn extends We{constructor(t,e){super(t,e);const r=s=>{if(!document.body.contains(t.root)){document.body.removeEventListener("click",r);return}this.tooltip!=null&&!this.tooltip.root.contains(s.target)&&document.activeElement!==this.tooltip.textbox&&!this.quill.hasFocus()&&this.tooltip.hide(),this.pickers!=null&&this.pickers.forEach(i=>{i.container.contains(s.target)||i.close()})};t.emitter.listenDOM("click",document.body,r)}addModule(t){const e=super.addModule(t);return t==="toolbar"&&this.extendToolbar(e),e}buildButtons(t,e){Array.from(t).forEach(r=>{(r.getAttribute("class")||"").split(/\s+/).forEach(i=>{if(i.startsWith("ql-")&&(i=i.slice(3),e[i]!=null))if(i==="direction")r.innerHTML=e[i][""]+e[i].rtl;else if(typeof e[i]=="string")r.innerHTML=e[i];else{const l=r.value||"";l!=null&&e[i][l]&&(r.innerHTML=e[i][l])}})})}buildPickers(t,e){this.pickers=Array.from(t).map(s=>{if(s.classList.contains("ql-align")&&(s.querySelector("option")==null&&yn(s,Wg),typeof e.align=="object"))return new Zo(s,e.align);if(s.classList.contains("ql-background")||s.classList.contains("ql-color")){const i=s.classList.contains("ql-background")?"background":"color";return s.querySelector("option")==null&&yn(s,Zg,i==="background"?"#ffffff":"#000000"),new Wo(s,e[i])}return s.querySelector("option")==null&&(s.classList.contains("ql-font")?yn(s,Xg):s.classList.contains("ql-header")?yn(s,Yg):s.classList.contains("ql-size")&&yn(s,Qg)),new Dr(s)});const r=()=>{this.pickers.forEach(s=>{s.update()})};this.quill.on(S.events.EDITOR_CHANGE,r)}}Dn.DEFAULTS=te({},We.DEFAULTS,{modules:{toolbar:{handlers:{formula(){this.quill.theme.tooltip.edit("formula")},image(){let n=this.container.querySelector("input.ql-image[type=file]");n==null&&(n=document.createElement("input"),n.setAttribute("type","file"),n.setAttribute("accept",this.quill.uploader.options.mimetypes.join(", ")),n.classList.add("ql-image"),n.addEventListener("change",()=>{const t=this.quill.getSelection(!0);this.quill.uploader.upload(t,n.files),n.value=""}),this.container.appendChild(n)),n.click()},video(){this.quill.theme.tooltip.edit("video")}}}}});class Yo extends Xo{constructor(t,e){super(t,e),this.textbox=this.root.querySelector('input[type="text"]'),this.listen()}listen(){this.textbox.addEventListener("keydown",t=>{t.key==="Enter"?(this.save(),t.preventDefault()):t.key==="Escape"&&(this.cancel(),t.preventDefault())})}cancel(){this.hide(),this.restoreFocus()}edit(){let t=arguments.length>0&&arguments[0]!==void 0?arguments[0]:"link",e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:null;if(this.root.classList.remove("ql-hidden"),this.root.classList.add("ql-editing"),this.textbox==null)return;e!=null?this.textbox.value=e:t!==this.root.getAttribute("data-mode")&&(this.textbox.value="");const r=this.quill.getBounds(this.quill.selection.savedRange);r!=null&&this.position(r),this.textbox.select(),this.textbox.setAttribute("placeholder",this.textbox.getAttribute(`data-${t}`)||""),this.root.setAttribute("data-mode",t)}restoreFocus(){this.quill.focus({preventScroll:!0})}save(){let{value:t}=this.textbox;switch(this.root.getAttribute("data-mode")){case"link":{const{scrollTop:e}=this.quill.root;this.linkRange?(this.quill.formatText(this.linkRange,"link",t,S.sources.USER),delete this.linkRange):(this.restoreFocus(),this.quill.format("link",t,S.sources.USER)),this.quill.root.scrollTop=e;break}case"video":t=Jg(t);case"formula":{if(!t)break;const e=this.quill.getSelection(!0);if(e!=null){const r=e.index+e.length;this.quill.insertEmbed(r,this.root.getAttribute("data-mode"),t,S.sources.USER),this.root.getAttribute("data-mode")==="formula"&&this.quill.insertText(r+1," ",S.sources.USER),this.quill.setSelection(r+2,S.sources.USER)}break}}this.textbox.value="",this.hide()}}function Jg(n){let t=n.match(/^(?:(https?):\/\/)?(?:(?:www|m)\.)?youtube\.com\/watch.*v=([a-zA-Z0-9_-]+)/)||n.match(/^(?:(https?):\/\/)?(?:(?:www|m)\.)?youtu\.be\/([a-zA-Z0-9_-]+)/);return t?`${t[1]||"https"}://www.youtube.com/embed/${t[2]}?showinfo=0`:(t=n.match(/^(?:(https?):\/\/)?(?:www\.)?vimeo\.com\/(\d+)/))?`${t[1]||"https"}://player.vimeo.com/video/${t[2]}/`:n}function yn(n,t){let e=arguments.length>2&&arguments[2]!==void 0?arguments[2]:!1;t.forEach(r=>{const s=document.createElement("option");r===e?s.setAttribute("selected","selected"):s.setAttribute("value",String(r)),n.appendChild(s)})}const tp=[["bold","italic","link"],[{header:1},{header:2},"blockquote"]];class Qo extends Yo{constructor(t,e){super(t,e),this.quill.on(S.events.EDITOR_CHANGE,(r,s,i,l)=>{if(r===S.events.SELECTION_CHANGE)if(s!=null&&s.length>0&&l===S.sources.USER){this.show(),this.root.style.left="0px",this.root.style.width="",this.root.style.width=`${this.root.offsetWidth}px`;const a=this.quill.getLines(s.index,s.length);if(a.length===1){const c=this.quill.getBounds(s);c!=null&&this.position(c)}else{const c=a[a.length-1],h=this.quill.getIndex(c),f=Math.min(c.length()-1,s.index+s.length-h),b=this.quill.getBounds(new be(h,f));b!=null&&this.position(b)}}else document.activeElement!==this.textbox&&this.quill.hasFocus()&&this.hide()})}listen(){super.listen(),this.root.querySelector(".ql-close").addEventListener("click",()=>{this.root.classList.remove("ql-editing")}),this.quill.on(S.events.SCROLL_OPTIMIZE,()=>{setTimeout(()=>{if(this.root.classList.contains("ql-hidden"))return;const t=this.quill.getSelection();if(t!=null){const e=this.quill.getBounds(t);e!=null&&this.position(e)}},1)})}cancel(){this.show()}position(t){const e=super.position(t),r=this.root.querySelector(".ql-tooltip-arrow");return r.style.marginLeft="",e!==0&&(r.style.marginLeft=`${-1*e-r.offsetWidth/2}px`),e}}x(Qo,"TEMPLATE",['','
          ','','',"
          "].join(""));class Jo extends Dn{constructor(t,e){e.modules.toolbar!=null&&e.modules.toolbar.container==null&&(e.modules.toolbar.container=tp),super(t,e),this.quill.container.classList.add("ql-bubble")}extendToolbar(t){this.tooltip=new Qo(this.quill,this.options.bounds),t.container!=null&&(this.tooltip.root.appendChild(t.container),this.buildButtons(t.container.querySelectorAll("button"),In),this.buildPickers(t.container.querySelectorAll("select"),In))}}Jo.DEFAULTS=te({},Dn.DEFAULTS,{modules:{toolbar:{handlers:{link(n){n?this.quill.theme.tooltip.edit():this.quill.format("link",!1,N.sources.USER)}}}}});const ep=[[{header:["1","2","3",!1]}],["bold","italic","underline","link"],[{list:"ordered"},{list:"bullet"}],["clean"]];class ta extends Yo{constructor(){super(...arguments);x(this,"preview",this.root.querySelector("a.ql-preview"))}listen(){super.listen(),this.root.querySelector("a.ql-action").addEventListener("click",e=>{this.root.classList.contains("ql-editing")?this.save():this.edit("link",this.preview.textContent),e.preventDefault()}),this.root.querySelector("a.ql-remove").addEventListener("click",e=>{if(this.linkRange!=null){const r=this.linkRange;this.restoreFocus(),this.quill.formatText(r,"link",!1,S.sources.USER),delete this.linkRange}e.preventDefault(),this.hide()}),this.quill.on(S.events.SELECTION_CHANGE,(e,r,s)=>{if(e!=null){if(e.length===0&&s===S.sources.USER){const[i,l]=this.quill.scroll.descendant(Jt,e.index);if(i!=null){this.linkRange=new be(e.index-l,i.length());const a=Jt.formats(i.domNode);this.preview.textContent=a,this.preview.setAttribute("href",a),this.show();const c=this.quill.getBounds(this.linkRange);c!=null&&this.position(c);return}}else delete this.linkRange;this.hide()}})}show(){super.show(),this.root.removeAttribute("data-mode")}}x(ta,"TEMPLATE",['','','',''].join(""));class ea extends Dn{constructor(t,e){e.modules.toolbar!=null&&e.modules.toolbar.container==null&&(e.modules.toolbar.container=ep),super(t,e),this.quill.container.classList.add("ql-snow")}extendToolbar(t){t.container!=null&&(t.container.classList.add("ql-snow"),this.buildButtons(t.container.querySelectorAll("button"),In),this.buildPickers(t.container.querySelectorAll("select"),In),this.tooltip=new ta(this.quill,this.options.bounds),t.container.querySelector(".ql-link")&&this.quill.keyboard.addBinding({key:"k",shortKey:!0},(e,r)=>{t.handlers.link.call(t,!r.format.link)}))}}ea.DEFAULTS=te({},Dn.DEFAULTS,{modules:{toolbar:{handlers:{link(n){if(n){const t=this.quill.getSelection();if(t==null||t.length===0)return;let e=this.quill.getText(t);/^\S+@\S+\.\S+$/.test(e)&&e.indexOf("mailto:")!==0&&(e=`mailto:${e}`);const{tooltip:r}=this.quill.theme;r.edit("link",e)}else this.quill.format("link",!1,N.sources.USER)}}}}});N.register({"attributors/attribute/direction":ko,"attributors/class/align":qo,"attributors/class/background":vd,"attributors/class/color":yd,"attributors/class/direction":Bo,"attributors/class/font":jo,"attributors/class/size":$o,"attributors/style/align":Io,"attributors/style/background":Ri,"attributors/style/color":Ii,"attributors/style/direction":Mo,"attributors/style/font":Po,"attributors/style/size":Uo},!0);N.register({"formats/align":qo,"formats/direction":Bo,"formats/indent":cg,"formats/background":Ri,"formats/color":Ii,"formats/font":jo,"formats/size":$o,"formats/blockquote":hi,"formats/code-block":J,"formats/header":fi,"formats/list":Mn,"formats/bold":qn,"formats/code":ki,"formats/italic":di,"formats/link":Jt,"formats/script":gi,"formats/strike":pi,"formats/underline":mi,"formats/formula":vr,"formats/image":bi,"formats/video":Er,"modules/syntax":Vo,"modules/table":hg,"modules/toolbar":Pi,"themes/bubble":Jo,"themes/snow":ea,"ui/icons":In,"ui/picker":Dr,"ui/icon-picker":Zo,"ui/color-picker":Wo,"ui/tooltip":Xo},!0);var jr=class extends de.Component{constructor(n){super(n),this.editingAreaRef=Ue.createRef(),this.containerRef=Ue.createRef(),this.dirtyProps=["modules","formats","bounds","theme","children"],this.cleanProps=["id","className","style","placeholder","tabIndex","onChange","onChangeSelection","onFocus","onBlur","onKeyPress","onKeyDown","onKeyUp","useSemanticHTML"],this.state={generation:0},this.selection=null,this.onEditorChange=(e,r,s,i)=>{e==="text-change"?this.onEditorChangeText?.(this.props.useSemanticHTML!==!1?this.editor.getSemanticHTML():this.editor.root.innerHTML,r,i,this.unprivilegedEditor):e==="selection-change"&&this.onEditorChangeSelection?.(r,i,this.unprivilegedEditor)};const t=this.isControlled()?n.value:n.defaultValue;this.value=t??""}validateProps(n){if(de.Children.count(n.children)>1)throw new Error("The Quill editing area can only be composed of a single React element.");if(de.Children.count(n.children)&&de.Children.only(n.children)?.type==="textarea")throw new Error("Quill does not support editing on a