bookManagement/public/edit_books.js

406 lines
19 KiB
JavaScript

let allLocations = []; // To store fetched locations globally - ADDED HERE
document.addEventListener('DOMContentLoaded', async () => {
await fetchLocations(); // Fetch locations on page load
const bookListContainer = document.getElementById('book-list-container');
const modal = document.getElementById('edit-modal');
const closeModalButton = modal.querySelector('.close-button');
const modalBookTitle = document.getElementById('modal-book-title');
const modalBookIdInput = document.getElementById('modal-book-id');
const modalSearchQueryInput = document.getElementById('modal-search-query');
const modalSearchButton = document.getElementById('modal-search-button');
const modalSearchResultsContainer = document.getElementById('modal-search-results');
const modalEditLocationSelect = document.getElementById('modal-edit-location');
const modalEditFields = {
title: document.getElementById('modal-edit-title'),
authors: document.getElementById('modal-edit-authors'),
publishedDate: document.getElementById('modal-edit-publishedDate'),
isbn: document.getElementById('modal-edit-isbn'),
description: document.getElementById('modal-edit-description'),
pages: document.getElementById('modal-edit-pages'),
publishers: document.getElementById('modal-edit-publishers'),
subjects: document.getElementById('modal-edit-subjects'),
cover_small: document.getElementById('modal-edit-cover-small'),
cover_medium: document.getElementById('modal-edit-cover-medium')
};
const modalSaveButton = document.getElementById('modal-save-button');
let currentBooks = []; // To store the fetched books globally
// let allLocations = []; // To store fetched locations globally - REMOVED FROM HERE
let currentlyEditingBookId = null;
const addNewBookButton = document.getElementById('add-new-book-button');
addNewBookButton.addEventListener('click', () => {
currentlyEditingBookId = null; // Signal "add" mode
modalBookIdInput.value = ''; // Clear hidden book ID input
modalBookTitle.textContent = 'Add New Book'; // Update modal title for adding
// Clear all form fields
Object.values(modalEditFields).forEach(field => field.value = '');
modalEditLocationSelect.value = ''; // Reset location dropdown
modalSearchResultsContainer.innerHTML = ''; // Clear any previous search results
document.getElementById('modal-search-query').value = ''; // Clear search query too
openModal(); // Open the modal for new entry
});
// Authentication has been disabled for book updates
// --- Fetch and Display Books ---
async function fetchAndDisplayBooks() {
try {
const response = await fetch('/api/books');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
currentBooks = await response.json();
renderBookList(currentBooks);
} catch (error) {
console.error('Error fetching books:', error);
bookListContainer.innerHTML = '<p>Error loading books. See console for details.</p>';
}
}
function renderBookList(books) {
bookListContainer.innerHTML = ''; // Clear existing list
if (books.length === 0) {
bookListContainer.innerHTML = '<p>No books found in the library.</p>';
return;
}
const ul = document.createElement('ul');
ul.className = 'book-list'; // Add a class for styling if needed
books.forEach(book => {
const li = document.createElement('li');
const locationName = book.Location ? `${book.Location.name}${book.Location.shelf ? ' (Shelf: ' + book.Location.shelf + ')' : ''}` : 'No Location'; // Reverted to book.Location (capital L)
li.innerHTML = `
<div class="book-entry">
<img src="${book.cover_small || 'placeholder.png'}" alt="Cover for ${book.title || 'N/A'}" style="width:50px; height:auto; margin-right:10px; flex-shrink: 0;">
<div style="flex-grow: 1;">
<p><strong>Title:</strong> ${book.title || 'N/A'}</p>
<p><strong>Author(s):</strong> ${book.authors || 'N/A'}</p>
<p><strong>Published:</strong> ${book.publishedDate || 'N/A'}</p>
<p><strong>ISBN:</strong> ${book.isbn || 'N/A'}</p>
<p><strong>Publisher(s):</strong> ${book.publishers || 'N/A'}</p>
<p><strong>Location:</strong> ${locationName}</p>
</div>
<button class="edit-metadata-button" data-book-id="${book.id}" style="margin-left: 10px; align-self: center;">Edit Metadata</button>
</div>
`;
ul.appendChild(li);
});
bookListContainer.appendChild(ul);
// Add event listeners to new edit buttons
document.querySelectorAll('.edit-metadata-button').forEach(button => {
button.addEventListener('click', handleEditMetadataClick);
});
}
// --- Modal Logic ---
function openModal() {
modal.style.display = 'block';
}
function closeModal() {
modal.style.display = 'none';
modalSearchResultsContainer.innerHTML = ''; // Clear search results
// Clear form fields
Object.values(modalEditFields).forEach(input => input.value = '');
modalSearchQueryInput.value = '';
}
closeModalButton.onclick = closeModal;
window.onclick = (event) => {
if (event.target == modal) {
closeModal();
}
};
async function handleEditMetadataClick(event) {
event.preventDefault(); // Prevent default action
const bookId = event.target.dataset.bookId;
const bookToEdit = currentBooks.find(b => b.id.toString() === bookId);
if (!bookToEdit) {
alert('Book not found!');
return;
}
modalBookTitle.textContent = bookToEdit.title;
modalBookIdInput.value = bookToEdit.id;
currentlyEditingBookId = bookToEdit.id; // Ensure this is set for edit mode
// Pre-fill search query and existing data
modalSearchQueryInput.value = bookToEdit.title || '';
if (bookToEdit.isbn) {
modalSearchQueryInput.value += ` ISBN: ${bookToEdit.isbn}`;
}
modalEditFields.title.value = bookToEdit.title || '';
modalEditFields.authors.value = bookToEdit.authors || '';
modalEditFields.publishedDate.value = bookToEdit.publishedDate || '';
modalEditFields.isbn.value = bookToEdit.isbn || '';
modalEditFields.description.value = bookToEdit.description || '';
modalEditFields.pages.value = bookToEdit.number_of_pages || '';
modalEditFields.publishers.value = bookToEdit.publishers || '';
modalEditFields.subjects.value = bookToEdit.subjects || '';
modalEditFields.cover_small.value = bookToEdit.cover_small || '';
modalEditFields.cover_medium.value = bookToEdit.cover_medium || '';
// Populate and set location dropdown
modalEditLocationSelect.innerHTML = '<option value="">-- No Location --</option>'; // Clear existing options but keep default
allLocations.forEach(loc => {
const option = document.createElement('option');
option.value = loc.id;
option.textContent = `${loc.name}` + (loc.shelf ? ` (Shelf: ${loc.shelf})` : '');
modalEditLocationSelect.appendChild(option);
});
modalEditLocationSelect.value = bookToEdit.location_id || (bookToEdit.Location ? bookToEdit.Location.id : '');
openModal();
}
// --- Google Books API Search in Modal ---
modalSearchButton.addEventListener('click', handleModalSearch);
async function handleModalSearch() {
let originalQuery = modalSearchQueryInput.value.trim();
if (!originalQuery) {
alert('Please enter a search query.');
return;
}
let searchQuery = originalQuery;
const potentialIsbn = originalQuery.replace(/-/g, '');
let isIsbnQuery = false;
if (/^(\d{9}[\dX]|\d{13})$/.test(potentialIsbn)) {
searchQuery = potentialIsbn; // Use the cleaned ISBN for OpenLibrary
isIsbnQuery = true;
console.log(`Detected ISBN-like string "${originalQuery}", searching as ISBN "${searchQuery}"`);
} else if (originalQuery.toLowerCase().startsWith('isbn:')) {
searchQuery = originalQuery.substring(5).replace(/-/g, ''); // Remove 'isbn:' and hyphens
isIsbnQuery = true;
console.log(`Query "${originalQuery}" formatted for ISBN search, using "${searchQuery}"`);
} else {
console.log(`Performing general search for "${originalQuery}"`);
}
modalSearchResultsContainer.innerHTML = '<p>Searching...</p>';
const searchButton = document.getElementById('modal-search-button');
if (searchButton) searchButton.disabled = true;
let combinedResults = [];
try {
const googleQuery = isIsbnQuery ? `isbn:${searchQuery}` : searchQuery;
const googleUrl = `https://www.googleapis.com/books/v1/volumes?q=${encodeURIComponent(googleQuery)}&maxResults=5`;
const openLibFields = 'key,title,author_name,first_publish_year,isbn,cover_i,publisher,subject,number_of_pages_median,subtitle,first_sentence_value';
const openLibUrl = `https://openlibrary.org/search.json?q=${encodeURIComponent(searchQuery)}&limit=5&fields=${openLibFields}`;
console.log("Searching Google Books API with URL:", googleUrl);
console.log("Searching OpenLibrary API with URL:", openLibUrl);
const [googleResult, openLibResult] = await Promise.allSettled([
fetch(googleUrl).then(res => res.ok ? res.json() : Promise.reject(new Error(`Google Books API request failed: ${res.status} ${res.statusText}`))),
fetch(openLibUrl).then(res => res.ok ? res.json() : Promise.reject(new Error(`OpenLibrary API request failed: ${res.status} ${res.statusText}`)))
]);
if (googleResult.status === 'fulfilled' && googleResult.value.items) {
const googleBooks = googleResult.value.items.map(item => {
const volumeInfo = item.volumeInfo;
let isbn13 = '', isbn10 = '';
if (volumeInfo.industryIdentifiers) {
volumeInfo.industryIdentifiers.forEach(id => {
if (id.type === 'ISBN_13') isbn13 = id.identifier;
if (id.type === 'ISBN_10') isbn10 = id.identifier;
});
}
return {
title: volumeInfo.title,
authors: volumeInfo.authors ? volumeInfo.authors.join(', ') : '',
publishedDate: volumeInfo.publishedDate,
isbn: isbn13 || isbn10,
description: volumeInfo.description,
pageCount: volumeInfo.pageCount,
publishers: volumeInfo.publisher ? (Array.isArray(volumeInfo.publisher) ? volumeInfo.publisher.join(', ') : volumeInfo.publisher) : '',
subjects: volumeInfo.categories ? volumeInfo.categories.join(', ') : '',
cover_small: volumeInfo.imageLinks?.smallThumbnail,
cover_medium: volumeInfo.imageLinks?.thumbnail,
source: 'Google Books',
rawData: volumeInfo
};
});
combinedResults.push(...googleBooks);
} else if (googleResult.status === 'rejected') {
console.error('Error fetching from Google Books:', googleResult.reason);
}
if (openLibResult.status === 'fulfilled' && openLibResult.value.docs) {
const openLibBooks = openLibResult.value.docs.map(doc => {
return {
title: doc.title,
authors: doc.author_name ? doc.author_name.join(', ') : '',
publishedDate: doc.first_publish_year ? String(doc.first_publish_year) : '',
isbn: doc.isbn ? doc.isbn[0] : '', // Take first ISBN
description: doc.first_sentence_value || doc.subtitle || '',
pageCount: doc.number_of_pages_median,
publishers: doc.publisher ? doc.publisher.join(', ') : '',
subjects: doc.subject ? doc.subject.join(', ') : '',
cover_small: doc.cover_i ? `https://covers.openlibrary.org/b/id/${doc.cover_i}-S.jpg` : '',
cover_medium: doc.cover_i ? `https://covers.openlibrary.org/b/id/${doc.cover_i}-M.jpg` : '',
source: 'OpenLibrary',
rawData: doc
};
});
combinedResults.push(...openLibBooks);
}
else if (openLibResult.status === 'rejected') {
console.error('Error fetching from OpenLibrary:', openLibResult.reason);
}
displayModalSearchResults(combinedResults);
} catch (error) { // Catch any unexpected errors during the setup or Promise.allSettled itself
console.error('Error in handleModalSearch:', error);
modalSearchResultsContainer.innerHTML = `<p>An unexpected error occurred during search: ${error.message}. Please check console.</p>`;
} finally {
if (searchButton) searchButton.disabled = false;
}
}
function displayModalSearchResults(items) {
modalSearchResultsContainer.innerHTML = '';
if (!items || items.length === 0) {
modalSearchResultsContainer.innerHTML = '<p>No results found from any source.</p>';
return;
}
const ul = document.createElement('ul');
items.forEach(bookItem => {
const li = document.createElement('li');
li.innerHTML = `
<strong>${bookItem.title || 'N/A'}</strong> by ${bookItem.authors || 'N/A'} (<em>${bookItem.source}</em>)
<br>
Published: ${bookItem.publishedDate || 'N/A'}, ISBN: ${bookItem.isbn || 'N/A'}
<button class="select-book-result">Use this</button>
`;
li.querySelector('.select-book-result').addEventListener('click', () => {
populateFormWithSelectedBook(bookItem);
});
ul.appendChild(li);
});
modalSearchResultsContainer.appendChild(ul);
}
function populateFormWithSelectedBook(bookItem) {
modalEditFields.title.value = bookItem.title || '';
modalEditFields.authors.value = bookItem.authors || '';
modalEditFields.publishedDate.value = bookItem.publishedDate || '';
modalEditFields.isbn.value = bookItem.isbn || '';
modalEditFields.description.value = bookItem.description || '';
modalEditFields.pages.value = bookItem.pageCount || '';
modalEditFields.publishers.value = bookItem.publishers || '';
modalEditFields.subjects.value = bookItem.subjects || '';
modalEditFields.cover_small.value = bookItem.cover_small || '';
modalEditFields.cover_medium.value = bookItem.cover_medium || '';
}
// --- Save Changes ---
modalSaveButton.addEventListener('click', async () => {
const bookData = {
title: modalEditFields.title.value.trim(),
authors: modalEditFields.authors.value.trim(),
publishedDate: modalEditFields.publishedDate.value.trim(),
isbn: modalEditFields.isbn.value.trim(),
description: modalEditFields.description.value.trim(),
number_of_pages: modalEditFields.pages.value ? parseInt(modalEditFields.pages.value, 10) : null,
publishers: modalEditFields.publishers.value.trim(),
subjects: modalEditFields.subjects.value.trim(),
cover_small: modalEditFields.cover_small.value.trim(),
cover_medium: modalEditFields.cover_medium.value.trim(),
location_id: modalEditLocationSelect.value || null,
};
if (!bookData.title) {
alert('Title is required.');
return;
}
let url;
let method;
const headers = {
'Content-Type': 'application/json'
};
if (currentlyEditingBookId) { // Edit mode
url = `/api/books/${currentlyEditingBookId}`;
method = 'PUT';
// Basic Auth not currently implemented for PUT as per prior setup
} else { // Add mode
url = '/api/books';
method = 'POST';
// POST endpoint requires Basic Auth
const adminPassword = prompt("Enter admin password to add a new book:");
if (adminPassword === null) { // User cancelled prompt
return;
}
headers['Authorization'] = 'Basic ' + btoa('admin:' + adminPassword);
}
try {
const response = await fetch(url, {
method: method,
headers: headers,
body: JSON.stringify(bookData)
});
const responseBody = await response.text(); // Read body as text first to handle potential non-JSON errors
let result;
try {
result = JSON.parse(responseBody);
} catch (e) {
// If parsing fails, the response was not valid JSON
console.error('Failed to parse server response as JSON:', responseBody);
throw new Error(`Server returned non-JSON response (status: ${response.status}). Check console for details.`);
}
if (!response.ok) {
const errorMessage = result && result.error ? result.error : `HTTP error! Status: ${response.status}`;
throw new Error(errorMessage);
}
if (result.success) {
alert(`Book ${method === 'POST' ? 'added' : 'updated'} successfully!`);
closeModal();
fetchAndDisplayBooks(); // Refresh the list
} else {
alert(`Failed to ${method === 'POST' ? 'add' : 'update'} book: ${result.error || 'Unknown server error'}`);
}
} catch (error) {
console.error(`Error ${method === 'POST' ? 'adding' : 'saving'} book data:`, error);
alert(`Error ${method === 'POST' ? 'adding' : 'saving'}: ${error.message}`);
}
});
// Initial load
fetchAndDisplayBooks();
});
async function fetchLocations() {
try {
const response = await fetch('/api/locations');
if (!response.ok) {
throw new Error(`Failed to fetch locations: ${response.status}`);
}
allLocations = await response.json();
console.log('Locations fetched:', allLocations);
} catch (error) {
console.error('Error fetching locations:', error);
// Optionally, inform the user that locations could not be loaded
}
}