$(document).ready(() => { const STATUS_CONFIG = { active: { label: "Доступна", bgClass: "bg-green-100", textClass: "text-green-800", icon: ``, }, borrowed: { label: "Выдана", bgClass: "bg-yellow-100", textClass: "text-yellow-800", icon: ``, }, reserved: { label: "Забронирована", bgClass: "bg-blue-100", textClass: "text-blue-800", icon: ``, }, restoration: { label: "На реставрации", bgClass: "bg-orange-100", textClass: "text-orange-800", icon: ``, }, written_off: { label: "Списана", bgClass: "bg-red-100", textClass: "text-red-800", icon: ``, }, }; const pathParts = window.location.pathname.split("/"); const bookId = parseInt(pathParts[pathParts.length - 1]); let isDraggingOver = false; let currentBook = null; let cachedUsers = null; let selectedLoanUserId = null; let activeLoan = null; init(); function init() { if (!bookId || isNaN(bookId)) { Utils.showToast("Некорректный ID книги", "error"); return; } loadBookData(); setupEventHandlers(); setupCoverUpload(); } function getPreviewUrl(book) { if (!book.preview_urls) { return null; } const priorities = ["webp", "jpeg", "jpg", "png"]; for (const format of priorities) { if (book.preview_urls[format]) { return book.preview_urls[format]; } } const availableFormats = Object.keys(book.preview_urls); if (availableFormats.length > 0) { return book.preview_urls[availableFormats[0]]; } return null; } function setupEventHandlers() { $(document).on("click", (e) => { const $menu = $("#status-menu"); const $toggleBtn = $("#status-toggle-btn"); if ( $menu.length && !$menu.hasClass("hidden") && !$toggleBtn.is(e.target) && $toggleBtn.has(e.target).length === 0 && !$menu.has(e.target).length ) { $menu.addClass("hidden"); } }); $("#cancel-loan-btn").on("click", closeLoanModal); $("#user-search-input").on("input", handleUserSearch); $("#confirm-loan-btn").on("click", submitLoan); $("#refresh-loans-btn").on("click", loadLoans); const future = new Date(); future.setDate(future.getDate() + 14); $("#loan-due-date").val(future.toISOString().split("T")[0]); } function setupCoverUpload() { const $container = $("#book-cover-container"); const $fileInput = $("#cover-file-input"); $fileInput.on("change", function (e) { const file = e.target.files[0]; if (file) { uploadCover(file); } $(this).val(""); }); $container.on("dragenter", function (e) { e.preventDefault(); e.stopPropagation(); if (!window.canManage()) return; isDraggingOver = true; showDropOverlay(); }); $container.on("dragover", function (e) { e.preventDefault(); e.stopPropagation(); if (!window.canManage()) return; isDraggingOver = true; }); $container.on("dragleave", function (e) { e.preventDefault(); e.stopPropagation(); if (!window.canManage()) return; const rect = this.getBoundingClientRect(); const x = e.clientX; const y = e.clientY; if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) { isDraggingOver = false; hideDropOverlay(); } }); $container.on("drop", function (e) { e.preventDefault(); e.stopPropagation(); if (!window.canManage()) return; isDraggingOver = false; hideDropOverlay(); const files = e.dataTransfer?.files || []; if (files.length > 0) { const file = files[0]; if (!file.type.startsWith("image/")) { Utils.showToast("Пожалуйста, загрузите изображение", "error"); return; } uploadCover(file); } }); } function showDropOverlay() { const $container = $("#book-cover-container"); $container.find(".drop-overlay").remove(); const $overlay = $(`
Отпустите для загрузки
`); $container.append($overlay); } function hideDropOverlay() { $("#book-cover-container .drop-overlay").remove(); } async function uploadCover(file) { const $container = $("#book-cover-container"); const maxSize = 32 * 1024 * 1024; if (file.size > maxSize) { Utils.showToast("Файл слишком большой. Максимум 32 MB", "error"); return; } if (!file.type.startsWith("image/")) { Utils.showToast("Пожалуйста, загрузите изображение", "error"); return; } const $loader = $(`
Загрузка...
`); $container.find(".upload-loader").remove(); $container.append($loader); try { const formData = new FormData(); formData.append("file", file); const response = await Api.uploadFile( `/api/books/${bookId}/preview`, formData, ); if (!response) { return; } if (response.preview) { currentBook.preview_urls = response.preview; } else if (response.preview_urls) { currentBook.preview_urls = response.preview_urls; } else { currentBook = response; } Utils.showToast("Обложка успешно загружена", "success"); renderBookCover(currentBook); } catch (error) { console.error("Upload error:", error); Utils.showToast(error.message || "Ошибка загрузки обложки", "error"); } finally { $container.find(".upload-loader").remove(); } } async function deleteCover() { if (!confirm("Удалить обложку книги?")) { return; } const $container = $("#book-cover-container"); const $loader = $(`
`); $container.find(".upload-loader").remove(); $container.append($loader); try { await Api.delete(`/api/books/${bookId}/preview`); currentBook.preview_urls = null; Utils.showToast("Обложка удалена", "success"); renderBookCover(currentBook); } catch (error) { console.error("Delete error:", error); Utils.showToast(error.message || "Ошибка удаления обложки", "error"); } finally { $container.find(".upload-loader").remove(); } } function renderBookCover(book) { const $container = $("#book-cover-container"); const canManage = window.canManage(); const previewUrl = getPreviewUrl(book); if (previewUrl) { $container.html(` Обложка книги ${Utils.escapeHtml(book.title)} ${ canManage ? `
Заменить
` : "" } `); if (canManage) { $("#delete-cover-btn").on("click", function (e) { e.stopPropagation(); deleteCover(); }); $("#cover-replace-overlay").on("click", function () { $("#cover-file-input").trigger("click"); }); } } else { if (canManage) { $container.html(`
Добавить обложку или перетащите
`); $("#cover-upload-zone").on("click", function () { $("#cover-file-input").trigger("click"); }); } else { $container.html(`
`); } } } function loadBookData() { Api.get(`/api/books/${bookId}`) .then((book) => { currentBook = book; document.title = `LiB - ${book.title}`; renderBook(book); if (window.canManage()) { $("#edit-book-btn") .attr("href", `/book/${book.id}/edit`) .removeClass("hidden"); $("#loans-section").removeClass("hidden"); loadLoans(); } }) .catch((error) => { console.error(error); Utils.showToast("Книга не найдена", "error"); $("#book-loader").html( '

Ошибка загрузки

', ); }); } async function loadLoans() { if (!window.canManage()) return; try { const data = await Api.get( `/api/loans/?book_id=${bookId}&active_only=true&page=1&size=10`, ); activeLoan = data.loans.length > 0 ? data.loans[0] : null; renderLoans(data.loans); } catch (error) { console.error("Failed to load loans", error); $("#loans-container").html( '
Ошибка загрузки выдач
', ); } } function renderLoans(loans) { const $container = $("#loans-container"); $container.empty(); if (!loans || loans.length === 0) { $container.html( '
Нет активных выдач
', ); return; } loans.forEach((loan) => { const borrowedDate = new Date(loan.borrowed_at).toLocaleDateString( "ru-RU", ); const dueDate = new Date(loan.due_date).toLocaleDateString("ru-RU"); const isOverdue = !loan.returned_at && new Date(loan.due_date) < new Date(); const $loanCard = $(`
ID выдачи: ${loan.id} ${ isOverdue ? 'Просрочена' : "" }

Дата выдачи: ${borrowedDate}

Срок возврата: ${dueDate}

Пользователь ID: ${loan.user_id}

${ !loan.returned_at && currentBook.status === "reserved" ? `` : "" } ${ !loan.returned_at ? `` : "" }
`); $loanCard.find(".confirm-loan-btn").on("click", function () { const loanId = $(this).data("loan-id"); confirmLoan(loanId); }); $loanCard.find(".return-loan-btn").on("click", function () { const loanId = $(this).data("loan-id"); returnLoan(loanId); }); $container.append($loanCard); }); } async function confirmLoan(loanId) { try { await Api.post(`/api/loans/${loanId}/confirm`); Utils.showToast("Бронь подтверждена", "success"); loadBookData(); loadLoans(); } catch (error) { console.error(error); Utils.showToast(error.message || "Ошибка подтверждения брони", "error"); } } async function returnLoan(loanId) { if (!confirm("Вы уверены, что хотите вернуть эту книгу?")) { return; } try { await Api.post(`/api/loans/${loanId}/return`); Utils.showToast("Книга возвращена", "success"); loadBookData(); loadLoans(); } catch (error) { console.error(error); Utils.showToast(error.message || "Ошибка возврата книги", "error"); } } function getStatusConfig(status) { return ( STATUS_CONFIG[status] || { label: status || "Неизвестно", bgClass: "bg-gray-100", textClass: "text-gray-800", icon: "", } ); } function renderBook(book) { $("#book-title").text(book.title); $("#book-id").text(`ID: ${book.id}`); renderBookCover(book); if (book.page_count && book.page_count > 0) { $("#book-page-count-value").text(book.page_count); $("#book-page-count-text").removeClass("hidden"); } else { $("#book-page-count-text").addClass("hidden"); } $("#book-authors-text").text( book.authors.map((a) => a.name).join(", ") || "Автор неизвестен", ); $("#book-description").text(book.description || "Описание отсутствует"); renderStatusWidget(book); if (!window.canManage() && book.status === "active") { renderReserveButton(); } else { $("#book-actions-container").empty(); } if (book.genres && book.genres.length > 0) { $("#genres-section").removeClass("hidden"); const $genres = $("#genres-container"); $genres.empty(); book.genres.forEach((g) => { $genres.append(` ${Utils.escapeHtml(g.name)} `); }); } if (book.authors && book.authors.length > 0) { $("#authors-section").removeClass("hidden"); const $authors = $("#authors-container"); $authors.empty(); book.authors.forEach((a) => { $authors.append(`
${a.name.charAt(0).toUpperCase()}
${Utils.escapeHtml(a.name)}
`); }); } $("#book-loader").addClass("hidden"); $("#book-content").removeClass("hidden"); } function renderStatusWidget(book) { const $container = $("#book-status-container"); $container.empty(); const config = getStatusConfig(book.status); if (window.canManage()) { const $dropdownHTML = $(`
`); $container.append($dropdownHTML); $("#status-toggle-btn").on("click", (e) => { e.stopPropagation(); $("#status-menu").toggleClass("hidden"); }); $(".status-option").on("click", function () { const newStatus = $(this).data("status"); $("#status-menu").addClass("hidden"); if (newStatus === currentBook.status) return; if (newStatus === "borrowed") { openLoanModal(); } else { updateBookStatus(newStatus); } }); } else { $container.append(` ${config.icon} ${config.label} `); } } function renderReserveButton() { const $container = $("#book-actions-container"); $container.html(` `); $("#reserve-btn").on("click", function () { const user = window.getUser(); if (!user) { Utils.showToast("Необходима авторизация", "error"); return; } Api.post("/api/loans/", { book_id: currentBook.id, user_id: user.id, due_date: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString(), }) .then((loan) => { Utils.showToast("Книга забронирована", "success"); loadBookData(); }) .catch((err) => { Utils.showToast(err.message || "Ошибка бронирования", "error"); }); }); } async function updateBookStatus(newStatus) { const $toggleBtn = $("#status-toggle-btn"); const originalContent = $toggleBtn.html(); $toggleBtn.prop("disabled", true).addClass("opacity-75").html(` Обновление... `); try { const payload = { status: newStatus, }; const updatedBook = await Api.put( `/api/books/${currentBook.id}`, payload, ); currentBook = updatedBook; Utils.showToast("Статус успешно изменен", "success"); renderStatusWidget(updatedBook); loadLoans(); } catch (error) { console.error(error); Utils.showToast(error.message || "Ошибка при смене статуса", "error"); $toggleBtn .prop("disabled", false) .removeClass("opacity-75") .html(originalContent); } } function openLoanModal() { $("#loan-modal").removeClass("hidden"); $("#user-search-input").val("")[0].focus(); $("#users-list-container").html( '
Загрузка списка пользователей...
', ); $("#confirm-loan-btn").prop("disabled", true); selectedLoanUserId = null; fetchUsers(); } function closeLoanModal() { $("#loan-modal").addClass("hidden"); } async function fetchUsers() { if (cachedUsers) { renderUsersList(cachedUsers); return; } try { const data = await Api.get("/api/users?skip=0&limit=500"); cachedUsers = data.users; renderUsersList(cachedUsers); } catch (error) { console.error("Failed to load users", error); $("#users-list-container").html( '
Ошибка загрузки пользователей
', ); } } function renderUsersList(users) { const $container = $("#users-list-container"); $container.empty(); if (!users || users.length === 0) { $container.html( '
Пользователи не найдены
', ); return; } users.forEach((user) => { const roleBadges = user.roles .map((r) => { const color = r === "admin" ? "bg-purple-100 text-purple-800" : r === "librarian" ? "bg-blue-100 text-blue-800" : "bg-gray-100 text-gray-800"; return `${r}`; }) .join(""); const $item = $(`
${Utils.escapeHtml(user.full_name || user.username)}
@${Utils.escapeHtml(user.username)} • ${Utils.escapeHtml(user.email)}
${roleBadges}
`); $item.on("click", function () { $(".user-item").removeClass("bg-blue-100 border-l-4 border-blue-500"); $(this).addClass("bg-blue-100 border-l-4 border-blue-500"); selectedLoanUserId = user.id; $("#confirm-loan-btn") .prop("disabled", false) .text(`Выдать для ${user.username}`); }); $container.append($item); }); } function handleUserSearch() { const query = $(this).val().toLowerCase(); if (!cachedUsers) return; if (!query) { renderUsersList(cachedUsers); return; } const filtered = cachedUsers.filter( (u) => u.username.toLowerCase().includes(query) || (u.full_name && u.full_name.toLowerCase().includes(query)) || u.email.toLowerCase().includes(query), ); renderUsersList(filtered); } async function submitLoan() { if (!selectedLoanUserId) return; const dueDate = $("#loan-due-date").val(); if (!dueDate) { Utils.showToast("Выберите дату возврата", "error"); return; } const $btn = $("#confirm-loan-btn"); const originalText = $btn.text(); $btn.prop("disabled", true).text("Обработка..."); try { const payload = { book_id: currentBook.id, user_id: selectedLoanUserId, due_date: new Date(dueDate).toISOString(), }; if (window.isAdmin()) { await Api.post("/api/loans/issue", payload); } else { await Api.post("/api/loans/", payload); } Utils.showToast("Книга успешно выдана", "success"); closeLoanModal(); loadBookData(); loadLoans(); } catch (error) { console.error(error); Utils.showToast(error.message || "Ошибка выдачи", "error"); } finally { $btn.prop("disabled", false).text(originalText); } } });