From 370ad7c1073248aeb95227170c5c4dcb772d1a3c Mon Sep 17 00:00:00 2001 From: knight Date: Wed, 11 Dec 2024 12:48:04 -0500 Subject: [PATCH] Enhance library management system with new features and improvements - Updated `docker-compose.yml` to include `ADMIN_PASSWORD` environment variable for enhanced security. - Modified `index.js` to allow checkout requests to capture user names and improved email notifications with detailed information. - Added a new endpoint to fetch books currently on loan, providing better tracking of borrowed items. - Implemented a `list_books_on_loan` function in `libraryManager.py` to display books on loan in a formatted table. - Updated `models.js` to include an `approved` column in the `Checkout` model for tracking approval status. - Enhanced the user interface in `public/index.html` with a dark mode toggle and improved checkout request handling. - Updated styles in `public/styles.css` to support dark mode and improve overall aesthetics. --- books.db | Bin 753664 -> 753664 bytes docker-compose.yml | 1 + index.js | 129 ++++++++++++++++++++++++++++++++++++++------- libraryManager.py | 25 ++++++++- models.js | 97 +++++++++++++++++----------------- public/index.html | 31 ++++++++++- public/styles.css | 77 +++++++++++++++++++++++++++ 7 files changed, 289 insertions(+), 71 deletions(-) diff --git a/books.db b/books.db index e9e4c69fc5a73c9a7894f4c6e25ca58744e653c0..31d02b25d04353ee0ae1233476207d5239968c93 100644 GIT binary patch delta 2428 zcmbW2Yitx%6vt;~@9gY*rq!i|0z0KbDSfcJr7eQk#aginWrgyn&xNwI(uOX5fGA1# zR(#@PaeY4vE7rG#g&p-%W1^pku1aEvUrCWa65>z$dq_ntZT z{7#d}nq;yjHLWlZmL%zYTsPv9CCUHm-m0*Pr!{^1z3xiMp;?mmZfAq%sOL)0d0)u& zi)MMwXA8_}_UyCvfb1ULmD!>d1p@MY%hHYR8+_){HEkeYunsVg?VlBbxYSQZ!?1x_ts}tWfwbhveJ-A zAM-Z&)_IT0K13%;!*Zr0z0iH$YtL-4izi&qw5OMNQc7lZ`ZLcwCDWPy-Sed)#nXB3 z5+$=dt^2M}GF|C9-=6G2XGA_^Z1xRB`1Sa48D9oA@Mds2zXrUXUk&>CVz8bs1TW_e z;9Oo07I8||m3%%pl~;jFc_nxmuK*YEIbb0#18cbn8hi#gi$f5Bk5wkj+mNrydLrNG@)g;iS57f7`J|+uL@gZG%S&Wc@>lrR#``wx?Ct4| zbawUj57Z3Adj|XCeO_!J>mNw^2blbxzN}exNV2n~MRK~s!SI6WP`El2GDCHXYHJsT z7u19ohRw=ga8WRbw{_LQ(0{xgPUey_i;S{}VvMr9447i%z$i(haoMogxH739R+u@; zqVz1N;91g!+3E=wkixwhVHfmKc7ooYpx;DYpgLx|pj&K0p}7f~HYTPSHd=--&M-HI z_m-Mqpgm#xLqEV6UYRz$8nI34xd|KHkJJ1S5gs8XZe0kU~2Nzm=x*TfoDd zN-WE%ybf`yuY-IA_@)rz9U;VKA;fz^h=#1p6GDs(2_s(NCbk(9M!Y19xK$YOqA=nPA;ir>h=dU01@6N9XSoJG&l&g(M{|;% zz6W?}JMiQHknRJX*b40J0aD$-<6XdGn}J6=fT2h30QcMmJWMHgh%)dXN%jC`;eJZO zeI(g#lI#|etd}I~hyXE?>@JdQ<6>Y_18^Hjwt*yjizK_9B-=%jwUcChB-tQIwu2j8NtZi?awt1y z9ASSIuWDq~E`$0Jdu|H9)FTT}^<>GmsDN=c9xqr8FP#JbNmU=I)s(hz;Z5LOyice`jIgPT+~?NsvdKVhLiEcKYTu$s z#+czyoSNjm*{LBNzyH64UOf6-N~U8?h^9>a8Vy_=vEiVxVM8=_1IKNtrclNtM@CR- zqiRw{rHJTa6it$%57H<`AA^={F;o0wh|urZh$c!$H0l(_S#^S((=gXmF?Y<~JzLIM Ri39eOD;H5Itf+f3@Fz;zNHhQd delta 2165 zcmc&!OKcNY6rInT8IQjcz_B4f#y~J>J{adP8GVSMBIpaYt@43(u*!p8_`xQs!FDtencx*R4Rz5>3NT&AQly3)5SZUci+4B zo_FrOPNy5w>Bh{8C95U{LHGh|Ki2btuq=PRezOT{<7=P#+K#L2qR-oC+P|I5k#fuXabBggxC`v=Yr z-`kH@UJUbF6UYDK)_pIv^GH`mcbD1SvFkvWX+0d&GB;SM6PCHpe%Bwg%W7~BU3PIL zPe5Pb2?mGydq-14rx$5FyniUc;;}vGUvD4s)oq!0<)TXqg+k){O^7$~V)n?rU5H2T z8yXrI;UGOgbfPu;Y+;*zMU42a3Z0?-HKLF@eIohh$eF>^Xj0AgsSsKa#-rI@`xAA! zPtBfOu!z}H_7=}=+o!vfS{lf{Wbg21q->Aw!z?THt1C0u8zf zxQbQ*bs7d9pg~~m61o&Wn`i~Fn)-no?7vnY8{6K4&o$2gHXZ?Z5`eV_0q%W(`lkT? zPC!>2;AK?nb^t0E-JOi5kI{U(1rTlqv@pJvjP7p6b~Pj07zM1W0kp3H)I4A3Lo#w?Rs<9!!y#pHE^s9#5i>_h%xE!VOcM=9Nl5bnha_atxTMI&Wrc4u z5<8le=L0zsCoGB^4sq^~=Qql@kyA1Xu9b9LRx|S7h*NSgbBw?|Oqh3BhWD@x?_qdc zR=Ba~g0aLX7TIwT*NH(uF)y=ZX=Y4w-UtUCJfo?y&-IV-2h&q!{tKyj9QmduSUNTnH$$5MU_h)J#@#1J86hLJ;_9GPakGR!G0vW(LT zps0g~DKe?pb7zz^_J(+ut7=>#is6!kTY{Gex9ke@r>ZHx5*DJ{Y9m>l1|*uSY8vmP z19V_WsCJAF^*6WP@p^A+kp0``$N`n0MDoJ>9a2;Wg5roX{GIxdJ4f 'Unauthorized' }); -// Apply auth middleware to all non-GET requests +// Apply auth middleware to all non-GET requests except for checkout app.use((req, res, next) => { - if (req.method !== 'GET') { + if (req.method !== 'GET' && !req.path.startsWith('/api/checkout')) { return authMiddleware(req, res, next); } next(); @@ -455,7 +455,7 @@ const checkoutLimiter = rateLimit({ // New endpoint to handle checkout requests app.post('/api/checkout/:isbn', checkoutLimiter, async (req, res) => { const { isbn } = req.params; - const { email } = req.body; + const { email, name } = req.body; // Ensure name is also captured const ip = req.ip; try { @@ -464,6 +464,19 @@ app.post('/api/checkout/:isbn', checkoutLimiter, async (req, res) => { // Update book status to Pending await book.update({ status: 'Pending' }); + // Find or create the user in the User table + const [user, created] = await User.findOrCreate({ + where: { email }, + defaults: { name } + }); + + // Create a new checkout record + await Checkout.create({ + book_id: book.id, + user_id: user.id, + checkout_date: new Date() + }); + // Log the requestor's email and IP console.log(`Checkout requested by ${email} from IP: ${ip}`); @@ -473,7 +486,21 @@ app.post('/api/checkout/:isbn', checkoutLimiter, async (req, res) => { to: process.env.ADMIN_EMAIL, subject: `Checkout Request for ISBN: ${isbn}`, html: ` -

A checkout request has been made for the book titled "${book.title}" with ISBN: ${isbn} by ${email}. Please review and approve.

+

A checkout request has been made. Please review and approve.

+ + + + + + + + + + + + + +
Book TitleISBNRequestor EmailRequestor Name
${book.title}${isbn}${email}${name}
Approve Deny ` @@ -503,24 +530,52 @@ app.get('/api/approve-checkout/:isbn/:email', async (req, res) => { try { const book = await Book.findOne({ where: { isbn } }); if (book && book.status === 'Pending') { - await book.update({ status: 'Checked Out' }); - - // Send email to the requesting user - const mailOptions = { - from: process.env.ADMIN_EMAIL, - to: email, - subject: `Checkout Approved for ISBN: ${isbn}`, - text: `Your checkout request for the book with ISBN: ${isbn} has been approved.` - }; - - transporter.sendMail(mailOptions, (error, info) => { - if (error) { - console.error('Error sending email:', error); - return res.status(500).json({ error: 'Failed to send approval email' }); + // Find the checkout record + const checkout = await Checkout.findOne({ + where: { + book_id: book.id, + user_id: (await User.findOne({ where: { email } })).id, + approved: false } - console.log('Approval email sent:', info.response); - res.json({ success: true, message: 'Checkout approved and user notified.' }); }); + + if (checkout) { + // Set the checkout date to now and return date to one month from now + const checkoutDate = new Date(); + const returnDate = new Date(); + returnDate.setMonth(returnDate.getMonth() + 1); + + await checkout.update({ + approved: true, + checkout_date: checkoutDate, + return_date: returnDate + }); + + await book.update({ status: 'Checked Out' }); + + // Send email to the requesting user + const mailOptions = { + from: process.env.ADMIN_EMAIL, + to: email, + subject: `Checkout Approved for ISBN: ${isbn}`, + html: ` +

Your checkout request for the book with ISBN: ${isbn} has been approved.

+

Checkout Date: ${checkoutDate.toDateString()}

+

Return Date: ${returnDate.toDateString()}

+ ` + }; + + transporter.sendMail(mailOptions, (error, info) => { + if (error) { + console.error('Error sending email:', error); + return res.status(500).json({ error: 'Failed to send approval email' }); + } + console.log('Approval email sent:', info.response); + res.json({ success: true, message: 'Checkout approved and user notified.' }); + }); + } else { + res.status(404).json({ error: 'Checkout record not found or already approved' }); + } } else { res.status(404).json({ error: 'Book not found or not Pending' }); } @@ -607,3 +662,37 @@ const httpsServer = https.createServer(credentials, app); httpsServer.listen(PORT, () => { console.log(`HTTPS Server running on https://localhost:${PORT}`); }); + +app.get('/api/books-on-loan', async (req, res) => { + try { + const loans = await Checkout.findAll({ + where: { + returned_date: null + }, + include: [ + { + model: Book, + attributes: ['isbn', 'title'] + }, + { + model: User, + attributes: ['name'] + } + ], + attributes: ['checkout_date', 'return_date'] + }); + + const result = loans.map(loan => ({ + isbn: loan.Book.isbn, + title: loan.Book.title, + name: loan.User.name, + checkout_date: loan.checkout_date, + return_date: loan.return_date + })); + + res.json(result); + } catch (error) { + console.error('Error fetching books on loan:', error); + res.status(500).send('Internal Server Error'); + } +}); diff --git a/libraryManager.py b/libraryManager.py index 205dc0d..e4d52c2 100644 --- a/libraryManager.py +++ b/libraryManager.py @@ -99,6 +99,24 @@ def search_books(): else: console.print("Failed to fetch books.", style="bold red") +def list_books_on_loan(): + response = requests.get(f"{API_BASE_URL}/api/books-on-loan", verify=False) + if response.status_code == 200: + loans = response.json() + table = Table(title="Books on Loan") + table.add_column("ISBN", justify="right", style="cyan", no_wrap=True) + table.add_column("Title", style="magenta") + table.add_column("User Name", style="blue") + table.add_column("Checkout Date", style="green") + table.add_column("Return Date", style="yellow") + + for loan in loans: + table.add_row(loan['isbn'], loan['title'], loan['name'], loan['checkout_date'], loan['return_date']) + + console.print(table) + else: + console.print("Failed to fetch books on loan.", style="bold red") + def main(): while True: console.print("\n[bold]Admin Console[/bold]") @@ -107,8 +125,9 @@ def main(): console.print("3. Remove Book") console.print("4. Change Book Status") console.print("5. Search Books") - console.print("6. Exit") - choice = Prompt.ask("Choose an option", choices=["1", "2", "3", "4", "5", "6"], default="6") + console.print("6. List Books on Loan") + console.print("7. Exit") + choice = Prompt.ask("Choose an option", choices=["1", "2", "3", "4", "5", "6", "7"], default="7") if choice == "1": list_books() @@ -121,6 +140,8 @@ def main(): elif choice == "5": search_books() elif choice == "6": + list_books_on_loan() + elif choice == "7": console.print("Exiting...", style="bold yellow") break diff --git a/models.js b/models.js index bf33e24..4962dec 100644 --- a/models.js +++ b/models.js @@ -105,59 +105,62 @@ const Book = sequelize.define('Book', { // Define a table for checkouts const Checkout = sequelize.define('Checkout', { -id: { - type: DataTypes.INTEGER, - autoIncrement: true, - primaryKey: true, -}, -book_id: { - type: DataTypes.INTEGER, - references: { - model: Book, - key: 'id' + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + book_id: { + type: DataTypes.INTEGER, + references: { + model: Book, + key: 'id' + } + }, + user_id: { + type: DataTypes.INTEGER, + allowNull: false, + }, + checkout_date: { + type: DataTypes.DATE, + }, + return_date: { + type: DataTypes.DATE, + }, + returned_date: { + type: DataTypes.DATE, + }, + approved: { // New column to track approval status + type: DataTypes.BOOLEAN, + defaultValue: false } -}, -user_id: { - type: DataTypes.INTEGER, - allowNull: false, -}, -checkout_date: { - type: DataTypes.DATE, - defaultValue: Sequelize.NOW -}, -return_date: { - type: DataTypes.DATE, -}, -returned_date: { - type: DataTypes.DATE, -} }, { -tableName: 'checkouts', -timestamps: false, // If your table doesn't have `createdAt` and `updatedAt` + tableName: 'checkouts', + timestamps: false, // If your table doesn't have `createdAt` and `updatedAt` }); // Define User table const User = sequelize.define('User', { -id: { - type: DataTypes.INTEGER, - autoIncrement: true, - primaryKey: true, -}, -name: { - type: DataTypes.TEXT, - allowNull: false, -}, -email: { - type: DataTypes.TEXT, - allowNull: false, -}, -role: { - type: DataTypes.TEXT, - defaultValue: 'user' -} + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + name: { + type: DataTypes.TEXT, + allowNull: false, + }, + email: { + type: DataTypes.TEXT, + allowNull: false, + }, + role: { + type: DataTypes.TEXT, + defaultValue: 'user' + } }, { -tableName: 'users', -timestamps: false, // If your table doesn't have `createdAt` and `updatedAt` + tableName: 'users', + timestamps: false, // If your table doesn't have `createdAt` and `updatedAt` }); // Define the relationships @@ -168,7 +171,7 @@ Book.hasOne(Checkout, { foreignKey: 'book_id' }); Checkout.belongsTo(User, { foreignKey: 'user_id' }); User.hasMany(Checkout, { foreignKey: 'user_id' }); -sequelize.sync({ alter: true }).then(() => { +sequelize.sync().then(() => { console.log('Database & tables synced!'); }); diff --git a/public/index.html b/public/index.html index 0e7776b..4b4e20b 100644 --- a/public/index.html +++ b/public/index.html @@ -7,6 +7,7 @@ +

Ramsey Library

@@ -90,8 +91,12 @@ function setCookie(name, value, days) { // Client-side function to request checkout function requestCheckout(isbn) { - let email = getCookie('lastEmail') || prompt('Please enter your email:'); - let name = getCookie('lastName') || prompt('Please enter your name:'); + let email = getCookie('lastEmail') || ''; + let name = getCookie('lastName') || ''; + + // Prompt the user for email and name, pre-filling with existing values if available + email = prompt('Please enter your email:', email); + name = prompt('Please enter your name:', name); if (!email || !name) { alert('Email and name are required to proceed with checkout.'); @@ -142,6 +147,28 @@ function requestCheckout(isbn) { rows.forEach(row => table.tBodies[0].appendChild(row)); table.setAttribute('data-sort-order', isAscending ? 'desc' : 'asc'); } + + // Dark mode toggle functionality + const darkModeToggle = document.getElementById('dark-mode-toggle'); + const body = document.body; + + // Check for saved user preference, if any, on load + if (localStorage.getItem('darkMode') === 'enabled') { + body.classList.add('dark-mode'); + darkModeToggle.textContent = '☀️'; // Set to sun icon if dark mode is enabled + } + + darkModeToggle.addEventListener('click', () => { + body.classList.toggle('dark-mode'); + // Toggle icon + if (body.classList.contains('dark-mode')) { + darkModeToggle.textContent = '☀️'; // Sun icon + localStorage.setItem('darkMode', 'enabled'); + } else { + darkModeToggle.textContent = '🌙'; // Moon icon + localStorage.setItem('darkMode', 'disabled'); + } + }); diff --git a/public/styles.css b/public/styles.css index e2811a5..673de36 100644 --- a/public/styles.css +++ b/public/styles.css @@ -67,6 +67,43 @@ h1 { background-color: #f0f0f0; } +body.dark-mode #search-bar { + background-color: #333; + color: #fff; + border-color: #444; +} + +#dark-mode-toggle { + position: fixed; + top: 10px; + right: 10px; + width: 40px; + height: 40px; + background-color: rgba(255, 255, 255, 0.5); + border: none; + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + transition: background-color 0.3s; + opacity: 0.7; +} + +#dark-mode-toggle:hover { + background-color: rgba(255, 255, 255, 0.7); + opacity: 1; +} + +body.dark-mode #dark-mode-toggle { + background-color: rgba(0, 0, 0, 0.5); +} + +body.dark-mode #dark-mode-toggle:hover { + background-color: rgba(0, 0, 0, 0.7); +} + table { width: 90%; margin: 20px auto; @@ -121,3 +158,43 @@ a:hover { outline-style: solid; outline-width: 2px; } + +body.dark-mode { + background-color: #121212; + color: #ffffff; +} + +body.dark-mode table { + background-color: #2b2b2b; +} + +body.dark-mode th, body.dark-mode td { + border-color: #444; +} + +body.dark-mode button { + background-color: #333; + color: #fff; +} + +body.dark-mode th { + background-color: #3a3a3a; +} + +body.dark-mode td { + background-color: #2b2b2b; +} + +body.dark-mode #book-table-body tr:nth-child(even) { + background-color: #333; +} + +body.dark-mode #book-table-body tr:nth-child(odd) { + background-color: #2b2b2b; +} + +body.dark-mode #book-table-body tr:hover { + background-color: #444; +} + +/* Add more styles as needed for other elements */