$(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 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();
}
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 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}`);
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 = $(`
${Object.entries(STATUS_CONFIG)
.map(([key, conf]) => {
const isCurrent = book.status === key;
return `
`;
})
.join("")}
`);
$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/auth/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);
}
}
});