From a24338d922f63fb07569a0974646c82a432f3b21 Mon Sep 17 00:00:00 2001 From: jpie02 <128189069+jpie02@users.noreply.github.com> Date: Tue, 23 Sep 2025 14:08:47 +0200 Subject: [PATCH 1/3] Extend the input size limit on login and register email/username and password fields to 50. --- web_app/src/Librarian/LibrarianLogin.tsx | 4 ++-- web_app/src/Librarian/LibrarianSettings.tsx | 2 +- web_app/src/LibraryAdmin/LibraryAdminLogin.tsx | 4 ++-- web_app/src/LibraryAdmin/LibraryAdminRegisterForm.tsx | 10 +++++----- web_app/src/SystemAdmin/SystemAdminLogin.tsx | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/web_app/src/Librarian/LibrarianLogin.tsx b/web_app/src/Librarian/LibrarianLogin.tsx index 245adf7c..30dc4e24 100644 --- a/web_app/src/Librarian/LibrarianLogin.tsx +++ b/web_app/src/Librarian/LibrarianLogin.tsx @@ -159,7 +159,7 @@ const LibrarianLogin: React.FC = () => { value={username} onChange={handleInputChange} required - maxLength={25} + maxLength={50} className="p-2 border rounded-md w-full" /> @@ -191,7 +191,7 @@ const LibrarianLogin: React.FC = () => { value={password} onChange={handleInputChange} required - maxLength={25} + maxLength={50} className="p-2 border rounded-md w-full" /> diff --git a/web_app/src/Librarian/LibrarianSettings.tsx b/web_app/src/Librarian/LibrarianSettings.tsx index db863552..dfa4a689 100644 --- a/web_app/src/Librarian/LibrarianSettings.tsx +++ b/web_app/src/Librarian/LibrarianSettings.tsx @@ -141,7 +141,7 @@ const LibrarianSettings: React.FC = () => { /> -
{/* ✅ NEW FIELD */} +
{ value={email} onChange={handleInputChange} required - maxLength={25} + maxLength={50} className={`peer p-2 border rounded-md w-full ${!emailValid ? 'border-red-600' : 'border-gray-300'}`} />
@@ -203,7 +203,7 @@ const LibraryAdminLogin: React.FC = () => { value={password} onChange={handleInputChange} required - maxLength={25} + maxLength={50} className="peer p-2 border rounded-md w-full" />
diff --git a/web_app/src/LibraryAdmin/LibraryAdminRegisterForm.tsx b/web_app/src/LibraryAdmin/LibraryAdminRegisterForm.tsx index f9484309..c36ea486 100644 --- a/web_app/src/LibraryAdmin/LibraryAdminRegisterForm.tsx +++ b/web_app/src/LibraryAdmin/LibraryAdminRegisterForm.tsx @@ -150,7 +150,7 @@ const RegistrationForm: React.FC = () => { name="firstName" value={formData.firstName} onChange={handleInputChange} - maxLength={25} + maxLength={50} required className="bg-gray-100 w-full p-3 rounded-md text-gray-800 border border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#3B576C]" /> @@ -163,7 +163,7 @@ const RegistrationForm: React.FC = () => { name="lastName" value={formData.lastName} onChange={handleInputChange} - maxLength={25} + maxLength={50} required className="bg-gray-100 w-full p-3 rounded-md text-gray-800 border border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#3B576C]" /> @@ -176,7 +176,7 @@ const RegistrationForm: React.FC = () => { name="email" value={formData.email} onChange={handleInputChange} - maxLength={25} + maxLength={50} required className="bg-gray-100 w-full p-3 rounded-md text-gray-800 border border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#3B576C]" /> @@ -189,7 +189,7 @@ const RegistrationForm: React.FC = () => { name="password" value={formData.password} onChange={handleInputChange} - maxLength={25} + maxLength={50} required className="bg-gray-100 w-full p-3 rounded-md text-gray-800 border border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#3B576C]" /> @@ -202,7 +202,7 @@ const RegistrationForm: React.FC = () => { name="confirm_password" value={formData.confirm_password} onChange={handleInputChange} - maxLength={25} + maxLength={50} required className="bg-gray-100 w-full p-3 rounded-md text-gray-800 border border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#3B576C]" /> diff --git a/web_app/src/SystemAdmin/SystemAdminLogin.tsx b/web_app/src/SystemAdmin/SystemAdminLogin.tsx index 6dd50cae..176a7a35 100644 --- a/web_app/src/SystemAdmin/SystemAdminLogin.tsx +++ b/web_app/src/SystemAdmin/SystemAdminLogin.tsx @@ -169,7 +169,7 @@ const SysAdminLogin: React.FC = () => { value={email} onChange={handleInputChange} required - maxLength={25} + maxLength={50} className={`peer p-2 border rounded-md w-full ${ !emailValid ? 'border-red-500' : 'border-gray-300' }`} @@ -187,7 +187,7 @@ const SysAdminLogin: React.FC = () => { value={password} onChange={handleInputChange} required - maxLength={25} + maxLength={50} className="peer p-2 border border-gray-300 rounded-md w-full" /> From dc62406dc973475f7f1312ca2e1ae530a690a48b Mon Sep 17 00:00:00 2001 From: jpie02 <128189069+jpie02@users.noreply.github.com> Date: Tue, 23 Sep 2025 14:28:26 +0200 Subject: [PATCH 2/3] Background color change on LegalInfoPage.tsx, ProcessingInfoPage.tsx, UserManualPage.tsx and ContactInfoPage.tsx. --- web_app/src/Utils/ContactInfoPage.tsx | 2 +- web_app/src/Utils/LegalInfoPage.tsx | 4 ++-- web_app/src/Utils/ProcessingInfoPage.tsx | 2 +- web_app/src/Utils/UserManualPage.tsx | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/web_app/src/Utils/ContactInfoPage.tsx b/web_app/src/Utils/ContactInfoPage.tsx index b40bd974..14c4cf0d 100644 --- a/web_app/src/Utils/ContactInfoPage.tsx +++ b/web_app/src/Utils/ContactInfoPage.tsx @@ -3,7 +3,7 @@ import {Link} from "react-router-dom"; export const ContactInfoPage: React.FC = () => { return ( -
+
{/* Big Book Logo Image */} { return ( -
+
{/* Big Book Logo Image */} { alt="Book Rider Logo" src="/book-high-res.png" /> -

Informacje o kwestiach prawnych
związanych z systemem Book Rider

+

Informacje o kwestiach prawnych
związanych z systemem BookRider

+ } + /> + + + + ); +}; + +export default App; diff --git a/wa/src/LandingPage.tsx b/wa/src/LandingPage.tsx new file mode 100644 index 00000000..7855b06e --- /dev/null +++ b/wa/src/LandingPage.tsx @@ -0,0 +1,125 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +export const LandingPage: React.FC = () => { + return ( +
+ + {/* Top Bar */} +
+
+
+ + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + {/* Big Slogan Text */} +

+ Biblioteka + + prosto do Twoich + + drzwi +

+ + {/* Frame */} +
+
+
+ +
+ +
+
+
+ + {/* Main Buttons */} +
+ + + +
+ +
+ + + +
+ + {/* Big Book Logo Image */} + Book Rider Logo +
+
+
+ ); +}; + +export default LandingPage; diff --git a/wa/src/Librarian/LibrarianAddBook.tsx b/wa/src/Librarian/LibrarianAddBook.tsx new file mode 100644 index 00000000..4da762ee --- /dev/null +++ b/wa/src/Librarian/LibrarianAddBook.tsx @@ -0,0 +1,409 @@ +import React, { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; + +interface BookData { + title: string; + categoryName: string; + authors: string[]; + releaseYear: number; + publisher: string; + isbn: string; + language: string; + image?: string; +} + +interface Category { + name: string; +} + +interface Language { + name: string; +} + +interface Author { + name: string; +} + +interface Publisher { + name: string; +} + +const LibrarianAddBook: React.FC = () => { + const [title, setTitle] = useState(''); + const [categoryName, setCategoryName] = useState(''); + const [releaseYear, setReleaseYear] = useState(''); + const [publisher, setPublisher] = useState(''); + const [isbn, setIsbn] = useState(''); + const [language, setLanguage] = useState(''); + const [image, setImage] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [authors, setAuthors] = useState([]); + const navigate = useNavigate(); + + // Dropdown variables + const [categoryOptions, setCategoryOptions] = useState([]); + const [languageOptions, setLanguageOptions] = useState([]); + const [authorOptions, setAuthorOptions] = useState([]); + const [publisherOptions, setPublisherOptions] = useState([]); + const [authorInput, setAuthorInput] = useState(""); + const [showDropdown, setShowDropdown] = useState(false); + //const [publisherInput, setPublisherInput] = useState(''); + const [showPublisherDropdown, setShowPublisherDropdown] = useState(false); + + useEffect(() => { + const fetchDropdownData = async () => { + const token = localStorage.getItem('access_token'); + if (!token) return; + + try { + const [categoriesRes, languagesRes] = await Promise.all([ + fetch(`${API_BASE_URL}/api/categories`, { headers: { 'Authorization': `Bearer ${token}` } }), + fetch(`${API_BASE_URL}/api/languages`, { headers: { 'Authorization': `Bearer ${token}` } }), + ]); + + if (categoriesRes.ok) { + const categories: Category[] = await categoriesRes.json(); + setCategoryOptions(categories.map(c => c.name)); + } + if (languagesRes.ok) { + const languages: Language[] = await languagesRes.json(); + setLanguageOptions(languages.map(l => l.name)); + } + + await fetchAuthors(''); + await fetchPublishers(''); + } catch (error) { + console.error("Error fetching dropdown data: ", error); + } + }; + + fetchDropdownData(); + }, []); + + const handleAuthorSelect = async (author: string) => { + if (!authors.includes(author)) { + setAuthors([...authors, author]); + if (!authorOptions.includes(author)) { + try { + const token = localStorage.getItem('access_token'); + if (!token) { + return; + } + + const response = await fetch(`${API_BASE_URL}/api/authors`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ name: author }), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Błąd ${response.status}: ${errorText}`); + } + + await fetchAuthors(''); + await fetchPublishers(''); + + } catch (err) { + console.error("Error adding author:", err); + setError('Wystąpił błąd podczas dodawania autora.'); + } + } + } + setAuthorInput(""); + setShowDropdown(false); + }; + + const handleAuthorInput = (e: React.ChangeEvent) => { + const input = e.target.value; + setAuthorInput(e.target.value); + setShowDropdown(true); + fetchAuthors(input); + }; + + const handleRemoveAuthor = (authorToRemove: string) => { + setAuthors(authors.filter((author) => author !== authorToRemove)); + }; + + const fetchAuthors = async (query: string) => { + const token = localStorage.getItem('access_token'); + const response = await fetch(`${API_BASE_URL}/api/authors/search?name=${query}`, { + headers: { 'Authorization': `Bearer ${token}` }, + }); + const data: Author[] = await response.json(); + setAuthorOptions(data.map(a => a.name)); + }; + + const handlePublisherSelect = async (publisherName: string) => { + setPublisher(publisherName); + if (!publisherOptions.includes(publisherName)) { + try { + const token = localStorage.getItem('access_token'); + if (!token) return; + + const response = await fetch(`${API_BASE_URL}/api/publishers`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ name: publisherName }), + }); + + if (!response.ok) throw new Error(await response.text()); + + await fetchPublishers(''); + } catch (err) { + console.error("Error adding publisher:", err); + } + } + //setPublisherInput(''); + setShowPublisherDropdown(false); + }; + + const fetchPublishers = async (query: string) => { + const token = localStorage.getItem('access_token'); + const response = await fetch(`${API_BASE_URL}/api/publishers/search?name=${query}`, { + headers: { 'Authorization': `Bearer ${token}` }, + }); + const data: Publisher[] = await response.json(); + setPublisherOptions(data.map(p => p.name)); + }; + + const handleAddBook = async () => { + if (!title.trim() || !categoryName.trim() || authors.length === 0 || !publisher.trim() || !isbn.trim() || !language.trim() || releaseYear === null) { + setError('Wszystkie pola są wymagane.'); + return; + } + + setLoading(true); + setError(''); + + const token = localStorage.getItem('access_token'); + if (!token) { + setError('Brak tokena autoryzacyjnego.'); + setLoading(false); + return; + } + + const bookData: BookData = { + title, + categoryName, + authors: authors.map(a => a.trim()), + releaseYear: Number(releaseYear), + publisher, + isbn, + language, + }; + + if (image.trim()) { + bookData.image = image; + } + + try { + const response = await fetch(`${API_BASE_URL}/api/books`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(bookData), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Błąd ${response.status}: ${errorText}`); + } + + navigate('/librarian-dashboard'); + } catch (error) { + setError((error as Error).message || 'Wystąpił błąd podczas dodawania książki.'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+ Book Rider Logo +
+ +
+

Dodaj książkę

+ + setTitle(e.target.value)} + className="input-field mb-2 border-2 rounded w-full p-1.5" + /> + + + +
+ setShowDropdown(true)} + className="input-field border-2 rounded w-full p-1.5" + /> + {showDropdown && ( +
+ {authorOptions + .filter((author) => + author.toLowerCase().includes(authorInput.toLowerCase()) + ) + .map((author) => ( +
handleAuthorSelect(author)} + className="p-2 hover:bg-gray-200 cursor-pointer" + > + {author} +
+ ))} + {authorInput && !authorOptions.includes(authorInput) && ( +
handleAuthorSelect(authorInput)} + className="p-2 hover:bg-gray-200 cursor-pointer" + > + Dodaj "{authorInput}" +
+ )} +
+ )} +
+ +
+ {authors.map((author) => ( +
+ {author} + +
+ ))} +
+ + setReleaseYear(e.target.value)} + className="input-field mb-2 border-2 rounded w-full p-1.5" + /> + +
+ { + setPublisher(e.target.value); + setShowPublisherDropdown(true); + }} + onFocus={() => setShowPublisherDropdown(true)} + className="input-field border-2 rounded w-full p-1.5" + /> + {showPublisherDropdown && ( +
+ {publisherOptions + .filter((pub) => + pub.toLowerCase().includes(publisher.toLowerCase()) + ) + .map((pub) => ( +
handlePublisherSelect(pub)} + className="p-2 hover:bg-gray-200 cursor-pointer" + > + {pub} +
+ ))} + {publisher && !publisherOptions.includes(publisher) && ( +
handlePublisherSelect(publisher)} + className="p-2 hover:bg-gray-200 cursor-pointer" + > + Dodaj "{publisher}" +
+ )} +
+ )} +
+ + setIsbn(e.target.value)} + className="input-field mb-2 border-2 rounded w-full p-1.5" + /> + + + + setImage(e.target.value)} + className="input-field mb-2 border-2 rounded w-full p-1.5" + /> + + {error &&

{error}

} + + + +
+
+ ); + }; + +export default LibrarianAddBook; diff --git a/wa/src/Librarian/LibrarianHomePage.tsx b/wa/src/Librarian/LibrarianHomePage.tsx new file mode 100644 index 00000000..054807c3 --- /dev/null +++ b/wa/src/Librarian/LibrarianHomePage.tsx @@ -0,0 +1,700 @@ +import React, {useEffect, useState} from 'react'; +import {Link, useNavigate} from 'react-router-dom'; + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; + +const LibrarianHomePage: React.FC = () => { + + interface Book { + id: number; + title: string; + categoryName: string; + authorNames: string[]; + releaseYear: number; + publisherName: string; + isbn: string; + languageName: string; + image: string; + } + + interface Category { + name: string; + } + + interface Language { + name: string; + } + + interface Title { + name: string; + } + + interface Author { + name: string; + } + + interface Publisher { + name: string; + } + + interface Library { + id: number; + name: string; + } + + // Books + const [bookSearchInput, setBookSearchInput] = useState(''); + const [categoryInput, setCategoryInput] = useState(''); + const [languageInput, setLanguageInput] = useState(''); + const [publisherInput, setPublisherInput] = useState(''); + const [authorInput, setAuthorInput] = useState(''); + const [isbnInput, setIsbnInput] = useState(''); + const [releaseYearFrom, setReleaseYearFrom] = useState(''); + const [releaseYearTo, setReleaseYearTo] = useState(''); + const [searchResults, setBookSearchResults] = useState([]); + const [selectedBooks, setSelectedBooks] = useState([]); + + const [assignedLibrary, setAssignedLibrary] = useState(null); + const [isUserLibraryChecked, setIsUserLibraryChecked] = useState(false); + + const [categoryOptions, setCategoryOptions] = useState([]); + const [languageOptions, setLanguageOptions] = useState([]); + const [bookTitleOptions, setBookTitleOptions] = useState([]); + const [authorOptions, setAuthorOptions] = useState([]); + const [publisherOptions, setPublisherOptions] = useState([]); + + const [addBooksMessage, setAddBooksMessage] = useState<{ text: string; type: "success" | "error" | null }>({ text: "", type: null }); + const [deleteBooksMessage, setDeleteBooksMessage] = useState<{ text: string; type: "success" | "error" | null }>({ text: "", type: null }); + + const navigate = useNavigate(); + + useEffect(() => { + fetchAssignedLibrary(); + fetchDropdownData(); + }, []); + + useEffect(() => { + if (assignedLibrary !== null) { + fetchBooks(isUserLibraryChecked); + } + }, [isUserLibraryChecked, assignedLibrary]); + + useEffect(() => { + if (isUserLibraryChecked) { + setSelectedBooks([]); + } + }, [isUserLibraryChecked]); + + useEffect(() => { + if (addBooksMessage.type) { + const handleClick = () => { + setAddBooksMessage({ text: "", type: null }); + }; + + document.addEventListener("click", handleClick); + + return () => { + document.removeEventListener("click", handleClick); + }; + } + }, [addBooksMessage]); + + useEffect(() => { + if (deleteBooksMessage.type) { + const handleClick = () => { + setDeleteBooksMessage({ text: "", type: null }); + }; + + document.addEventListener("click", handleClick); + + return () => { + document.removeEventListener("click", handleClick); + }; + } + }, [deleteBooksMessage]); + + const fetchDropdownData = async () => { + const token = localStorage.getItem('access_token'); + + try { + const [ + categoriesRes, + languagesRes + ] = await Promise.all([ + fetch(`${API_BASE_URL}/api/categories`, {headers: {'Authorization': `Bearer ${token}`}}), + fetch(`${API_BASE_URL}/api/languages`, {headers: {'Authorization': `Bearer ${token}`}}), + ]); + + if (categoriesRes.ok) { + const categories: Category[] = await categoriesRes.json(); + setCategoryOptions(categories.map((c) => c.name)); + } + + if (languagesRes.ok) { + const languages: Language[] = await languagesRes.json(); + setLanguageOptions(languages.map((l) => l.name)); + } + + await fetchAuthors(''); + await fetchBookTitles(''); + await fetchPublishers(''); + + } catch (error) { + console.error("Error: ", error); + } + }; + + const fetchAssignedLibrary = async () => { + const token = localStorage.getItem('access_token'); + try { + const response = await fetch(`${API_BASE_URL}/api/libraries/assigned`, { + headers: { + 'Authorization': `Bearer ${token}`, + }, + }); + const data: Library = await response.json(); + setAssignedLibrary(data); + } catch (error) { + console.error("Error:", error); + } + }; + + const fetchBookTitles = async (query: string) => { + if (!query) return; + const token = localStorage.getItem('access_token'); + const response = await fetch(`${API_BASE_URL}/api/books/search-book-titles?title=${query}`, { + headers: { + 'Authorization': `Bearer ${token}`, + }, + }); + const data: Title[] = await response.json(); + setBookTitleOptions(data.map((b) => b.name)); + }; + + const fetchAuthors = async (query: string) => { + if (!query) return; + const token = localStorage.getItem('access_token'); + const response = await fetch(`${API_BASE_URL}/api/authors/search?name=${query}`, { + headers: { + 'Authorization': `Bearer ${token}`, + }, + }); + const data: Author[] = await response.json(); + setAuthorOptions(data.map((a) => a.name)); + }; + + const fetchPublishers = async (query: string) => { + if (!query) return; + const token = localStorage.getItem('access_token'); + + try { + const response = await fetch(`${API_BASE_URL}/api/publishers/search?name=${query}`, { + headers: {'Authorization': `Bearer ${token}`}, + }); + + if (response.ok) { + const data: Publisher[] = await response.json(); + setPublisherOptions(data.map((p) => p.name)); + } + } catch (error) { + console.error("Error:", error); + } + }; + + const fetchBooks = async (filterByLibrary: boolean) => { + const token = localStorage.getItem('access_token'); + if (!token) return; + + const queryParams = new URLSearchParams(); + + if (filterByLibrary && assignedLibrary?.name) { + queryParams.append("library", assignedLibrary.name); + } + + if (bookSearchInput) queryParams.append("title", bookSearchInput); + if (categoryInput) queryParams.append("category", categoryInput); + if (languageInput) queryParams.append("language", languageInput); + if (publisherInput) queryParams.append("publisher", publisherInput); + if (isbnInput) queryParams.append("isbn", isbnInput); + if (authorInput) queryParams.append("authorNames", authorInput); + if (releaseYearFrom) queryParams.append("releaseYearFrom", releaseYearFrom.toString()); + if (releaseYearTo) queryParams.append("releaseYearTo", releaseYearTo.toString()); + + queryParams.append("page", "0"); + queryParams.append("size", "20"); + + try { + const response = await fetch(`${API_BASE_URL}/api/books/search?${queryParams.toString()}`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`Error: ${response.statusText}`); + } + + const data = await response.json(); + setBookSearchResults(data.content); + } catch (error) { + console.error("Error: ", error); + } + }; + + const handleSearch = (reset: boolean = false) => { + if (reset) { + setBookSearchResults([]); + } + fetchBooks(isUserLibraryChecked); + }; + + const handleRedirectToAddBook = () => { + navigate('/add-book'); + }; + + const toggleBookSelection = (bookId: number) => { + setSelectedBooks((prevSelected) => + prevSelected.includes(bookId) + ? prevSelected.filter((id) => id !== bookId) + : [...prevSelected, bookId] + ); + }; + + const handleAddSelectedBooks = async () => { + const token = localStorage.getItem('access_token'); + if (!token) return; + + if (!assignedLibrary || !assignedLibrary.id) { + console.error("No library ID available."); + return; + } + + try { + const addRequests = selectedBooks.map(id => + fetch(`${API_BASE_URL}/api/books/add-existing/${id}?libraryId=${assignedLibrary.id}`, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + }, + }) + ); + + const responses = await Promise.all(addRequests); + + const allSuccessful = responses.every(response => response.ok); + + if (allSuccessful) { + setAddBooksMessage({ + text: "Pomyślnie dodano książki do biblioteki.", + type: "success" + }); + } else { + setAddBooksMessage({ + text: "Niektóre z książek już wcześniej zostały przypisane do Twojej biblioteki.", + type: "error" + }); + } + + setSelectedBooks([]); + } catch { + setAddBooksMessage({ + text: "Wystąpił błąd podczas dodawania książek.", + type: "error" + }); + } + }; + + const handleDeleteSelectedBooks = async () => { + const token = localStorage.getItem('access_token'); + if (!token) return; + + try { + const deleteRequests = selectedBooks.map(id => + fetch(`${API_BASE_URL}/api/books/my-library/${id}`, { + method: "DELETE", + headers: { + Authorization: `Bearer ${token}`, + }, + }) + ); + + const responses = await Promise.all(deleteRequests); + + const allSuccessful = responses.every(response => response.ok); + + if (allSuccessful) { + setBookSearchResults(prev => prev.filter(book => !selectedBooks.includes(book.id))); + + setSelectedBooks([]); + setDeleteBooksMessage({ + text: "Wybrane książki zostały usunięte z biblioteki.", + type: "success" + }); + } else { + setDeleteBooksMessage({ + text: "Niektórych książek nie udało się usunąć.", + type: "error" + }); + } + } catch { + setDeleteBooksMessage({ + text: "Wystąpił błąd podczas usuwania książek.", + type: "error" + }); + } + }; + + const handleLogout = () => { + localStorage.removeItem('username'); + localStorage.removeItem('email'); + localStorage.removeItem('access_token'); + localStorage.removeItem('role'); + + navigate('/'); + }; + + return ( +
+
+
+ Book Rider Logo +
+ {[ + {id: 'addBook', label: 'Książki', path: '/librarian-dashboard'}, + {id: 'orders', label: 'Wypożyczenia', path: '/orders'}, + {id: 'returns', label: 'Zwroty', path: '/returns'}, + {id: 'readers', label: 'Czytelnicy', path: '/readers'}, + {id: 'settings', label: 'Ustawienia', path: '/librarian-settings'}, + ].map(({id, label, path}) => ( + + {label} + + ))} + +
+ +
+
+

Wyszukaj książkę

+
+
+ { + setBookSearchInput(e.target.value); + fetchBookTitles(e.target.value); + }} + className="w-full p-2 rounded-lg border-2 outline-none bg-white text-[#3b4248] focus:outline-none focus:ring-2 focus:ring-[#3B576C]" + /> + {bookSearchInput && ( + + )} +
+ + {bookTitleOptions.map((option) => ( + + +
+ { + setPublisherInput(e.target.value); + fetchPublishers(e.target.value); + }} + className="w-full p-2 rounded-lg border-2 outline-none bg-white text-[#3b4248] focus:outline-none focus:ring-2 focus:ring-[#3B576C]" + /> + {publisherInput && ( + + )} +
+ + {publisherOptions.map((option) => ( + + +
+ { + setAuthorInput(e.target.value); + fetchAuthors(e.target.value); + }} + className="w-full p-2 rounded-lg border-2 outline-none bg-white text-[#3b4248] focus:outline-none focus:ring-2 focus:ring-[#3B576C]" + /> + {authorInput && ( + + )} +
+ + {authorOptions.map((option) => ( + + +
+ setCategoryInput(e.target.value)} + className="w-full p-2 rounded-lg border-2 outline-none bg-white text-[#3b4248] focus:outline-none focus:ring-2 focus:ring-[#3B576C]" + /> + {categoryInput && ( + + )} +
+ + {categoryOptions.map((option) => ( + + +
+ setLanguageInput(e.target.value)} + className="w-full p-2 rounded-lg border-2 outline-none bg-white text-[#3b4248] focus:outline-none focus:ring-2 focus:ring-[#3B576C]" + /> + {languageInput && ( + + )} +
+ + {languageOptions.map((option) => ( + + +
+ setIsbnInput(e.target.value)} + className="w-full p-2 rounded-lg border-2 outline-none bg-white text-[#3b4248] focus:outline-none focus:ring-2 focus:ring-[#3B576C]" + /> + {isbnInput && ( + + )} +
+ +
+ setReleaseYearFrom(e.target.value)} + className="w-full p-2 rounded-lg border-2 outline-none bg-white text-[#3b4248] focus:outline-none focus:ring-2 focus:ring-[#3B576C]" + /> + {releaseYearFrom && ( + + )} +
+ +
+ setReleaseYearTo(e.target.value)} + className="w-full p-2 rounded-lg border-2 outline-none bg-white text-[#3b4248] focus:outline-none focus:ring-2 focus:ring-[#3B576C]" + /> + {releaseYearTo && ( + + )} +
+ +
+ setIsUserLibraryChecked(!isUserLibraryChecked)} + className="cursor-pointer accent-[#3B576C]" + /> + +
+ +
+ +
+
+ + {searchResults.length > 0 ? ( +
    + {searchResults.map((book, index) => ( +
  • toggleBookSelection(book.id)} + className={`relative p-5 flex items-center gap-3 cursor-pointer rounded-lg ${ + selectedBooks.includes(book.id) ? "bg-gray-200" : "hover:bg-gray-100" + }`} + > + {book.title} +
    +

    + {book.title} ({book.releaseYear}) + {selectedBooks.includes(book.id) && ( + + )} +

    +

    Autor: {book.authorNames.join(", ")}

    +

    Kategoria: {book.categoryName}

    +

    Język: {book.languageName}

    +

    Wydawnictwo: {book.publisherName}

    +

    ISBN: {book.isbn}

    +

    ID książki: {book.id}

    +
    + +
    +
  • + ))} +
+ ) : ( +
+

Nie znaleziono książki.

+
+ )} + + {selectedBooks.length > 0 && ( +
+

+ Zaznaczone książki: {selectedBooks.length} +

+ + + + {isUserLibraryChecked ? ( + + ) : ( + + )} +
+ )} + + {addBooksMessage.type && ( +
+ {addBooksMessage.text} +
+ )} + + {deleteBooksMessage.type && ( +
+ {deleteBooksMessage.text} +
+ )} + +
+

Nie możesz znaleźć tego, czego szukasz?

+ +
+ +
+
+
+ ); +}; + +export default LibrarianHomePage; \ No newline at end of file diff --git a/wa/src/Librarian/LibrarianLogin.tsx b/wa/src/Librarian/LibrarianLogin.tsx new file mode 100644 index 00000000..381c2d26 --- /dev/null +++ b/wa/src/Librarian/LibrarianLogin.tsx @@ -0,0 +1,229 @@ +import React, { useState } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; + +const LibrarianLogin: React.FC = () => { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [libraryId, setLibraryId] = useState(''); + const [error, setError] = useState(null); + const [role, setRole] = useState('librarian'); + const navigate = useNavigate(); + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + + if (name === 'username') { + setUsername(value); + } else if (name === 'password') { + setPassword(value); + } else if (name === 'libraryId') { + setLibraryId(value); + } else if (name === 'role') { + setRole(value); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + + try { + const response = await fetch(`${API_BASE_URL}/api/auth/login/${role}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + username, + libraryId: Number(libraryId), + password, + }), + }); + + if (response.ok) { + const authHeader = response.headers.get('Authorization'); + if (authHeader) { + const token = authHeader.split(' ')[1]; + + localStorage.setItem('access_token', token); + localStorage.setItem('role', role); + localStorage.setItem('username', username); + + navigate('/librarian-dashboard'); + + } else { + setError('Authorization header missing'); + } + } else { + const errorData = await response.json(); + switch (errorData.code) { + case 401: + setError('Nieprawidłowa nazwa użytkownika, hasło lub identyfikator biblioteki.'); + break; + case 500: + setError('Wewnętrzny błąd serwera. Spróbuj ponownie później.'); + break; + case 400: + setError('Należy podać nazwę użytkownika, identyfikator biblioteki oraz hasło.'); + break; + default: + setError(errorData.message || 'Błąd logowania. Spróbuj ponownie.'); + break; + } + } + } catch (error) { + console.error('Login error: ', error); + setError('Podczas logowania wystąpił błąd'); + } + }; + + return ( +
+ + {/* Top Bar */} +
+
+
+ + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+
+

+ Logowanie bibliotekarza +

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + {error && ( +
+ +

{error}

+
+ )} + + +
+
+
+
+
+ ); +}; + +export default LibrarianLogin; diff --git a/wa/src/Librarian/LibrarianOrders.tsx b/wa/src/Librarian/LibrarianOrders.tsx new file mode 100644 index 00000000..9c2a398a --- /dev/null +++ b/wa/src/Librarian/LibrarianOrders.tsx @@ -0,0 +1,594 @@ +import React, {useState, useEffect } from 'react'; +import {Link, useNavigate} from 'react-router-dom'; + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; + +const LibrarianOrders: React.FC = () => { + interface Book { + id: number; + title: string; + categoryName: string; + authorNames: string[]; + releaseYear: number; + publisherName: string; + isbn: string; + languageName: string; + image: string; + } + + // Error messages + // const [message, setMessage] = useState(null); + // const [messageType, setMessageType] = useState<'success' | 'error' | null>(null); + + const [pendingMessage, setPendingMessage] = React.useState(''); + const [pendingMessageType, setPendingMessageType] = React.useState<'success' | 'error' | ''>(''); + + const [realizationMessage, setRealizationMessage] = React.useState(''); + const [realizationMessageType, setRealizationMessageType] = React.useState<'success' | 'error' | ''>(''); + + + // Orders + const [orderDetails, setOrderDetails] = useState([]); + const [showRejectionInput, setShowRejectionInput] = useState(false); + const [rejectionReason, setRejectionReason] = useState(''); + const [selectedOrderId, setSelectedOrderId] = useState(null); + const [customReason, setCustomReason] = useState(''); + const [handoverVisible, setHandoverVisible] = useState<{ [orderId: number]: boolean }>({}); + + // Active section + // const [currentStatus, setCurrentStatus] = useState<'PENDING' | 'IN_REALIZATION' | 'COMPLETED'>('PENDING'); + + interface OrderItem { + book: Book; + quantity: number; + } + + interface OrderDetails { + orderId: number; + userId: string; + libraryName: string; + pickupAddress: string; + destinationAddress: string; + isReturn: boolean; + status: string; + amount: number; + paymentStatus: string; + noteToDriver: string; + createdAt: string; + acceptedAt: string; + driverAssignedAt: string; + pickedUpAt: string; + deliveredAt: string; + orderItems: OrderItem[]; + driverId: string; + + displayStatus?: 'PENDING' | 'IN_REALIZATION' | 'COMPLETED'; + } + + const navigate = useNavigate(); + + // Orders ---------------------------------------------------------------------------------------------------------- + useEffect(() => { + fetchOrderDetails(); + }, []); + + useEffect(() => { + if (pendingMessageType || realizationMessageType) { + const handleClick = () => { + setPendingMessage(''); + setPendingMessageType(''); + setRealizationMessage(''); + setRealizationMessageType(''); + }; + + document.addEventListener("click", handleClick); + + return () => { + document.removeEventListener("click", handleClick); + }; + } + return undefined; + }, [pendingMessageType, realizationMessageType]); + + const fetchOrderDetails = async () => { + const token = localStorage.getItem('access_token'); + + try { + const [pendingRes, realizationRes, completedRes] = await Promise.all([ + fetch(`${API_BASE_URL}/api/orders/librarian/pending?page=0&size=10`, { + headers: { 'Authorization': `Bearer ${token}` }, + }), + fetch(`${API_BASE_URL}/api/orders/librarian/in-realization?page=0&size=10`, { + headers: { 'Authorization': `Bearer ${token}` }, + }), + fetch(`${API_BASE_URL}/api/orders/librarian/completed?page=0&size=10`, { + headers: { 'Authorization': `Bearer ${token}` }, + }), + ]); + + const pendingData = pendingRes.ok ? await pendingRes.json() : { content: [] }; + const realizationData = realizationRes.ok ? await realizationRes.json() : { content: [] }; + const completedData = completedRes.ok ? await completedRes.json() : { content: [] }; + + const pendingOrders: OrderDetails[] = pendingData.content.map((order: OrderDetails) => ({ + ...order, + displayStatus: 'PENDING', + })); + + const realizationOrders: OrderDetails[] = realizationData.content.map((order: OrderDetails) => ({ + ...order, + displayStatus: 'IN_REALIZATION', + })); + + const completedOrders: OrderDetails[] = completedData.content.map((order: OrderDetails) => ({ + ...order, + displayStatus: 'COMPLETED', + })); + + const combined = [...pendingOrders, ...realizationOrders, ...completedOrders]; + setOrderDetails(combined); + } catch (error) { + console.error('Error fetching orders:', error); + } + }; + + const handleAccept = async (orderId: number) => { + const token = localStorage.getItem('access_token'); + + try { + const response = await fetch(`${API_BASE_URL}/api/orders/${orderId}/accept`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${token}`, + }, + }); + + if (response.ok) { + setPendingMessage('Zamówienie zostało zatwierdzone.'); + setPendingMessageType('success'); + await fetchOrderDetails(); + } else { + setPendingMessage('Nie udało się zatwierdzić zamówienia.'); + setPendingMessageType('error'); + } + } catch (error) { + console.error('Błąd przy zatwierdzaniu zamówienia:', error); + } + }; + + const handleConfirmRejection = async (orderId: number) => { + const token = localStorage.getItem('access_token'); + const reasonToSend = rejectionReason === 'Inne' ? customReason : rejectionReason; + + if (!reasonToSend || reasonToSend.trim() === '') { + setPendingMessage('Proszę podać powód odrzucenia.'); + setPendingMessageType('error'); + return; + } + + try { + const response = await fetch(`${API_BASE_URL}/api/orders/${orderId}/decline`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + }, + body: JSON.stringify({ reason: reasonToSend }), + }); + + if (response.ok) { + setPendingMessage('Zamówienie zostało odrzucone.'); + setPendingMessageType('success'); + setShowRejectionInput(false); + setSelectedOrderId(null); + setRejectionReason(''); + setCustomReason(''); + await fetchOrderDetails(); + } else { + setPendingMessage('Błąd przy odrzucaniu zamówienia.'); + setPendingMessageType('error'); + } + } catch (err) { + console.error('Error declining order:', err); + } + }; + + const handleHandover = async (orderId: number, driverId: string) => { + const token = localStorage.getItem('access_token'); + + if (!driverId || driverId.trim() === "") { + setRealizationMessage("Proszę wprowadzić ID kierowcy."); + setRealizationMessageType("error"); + return; + } + + try { + const response = await fetch(`${API_BASE_URL}/api/orders/${orderId}/handover?driverId=${encodeURIComponent(driverId)}`, { + method: "PUT", + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + }, + }); + + if (response.ok) { + setRealizationMessage(`Zamówienie ${orderId} zostało przekazane pomyślnie!`); + setRealizationMessageType("success"); + await fetchOrderDetails(); + } else { + setRealizationMessage('Błąd przy przekazywaniu zamówienia.'); + setRealizationMessageType('error'); + } + } catch (err) { + console.error('Error handing over order:', err); + } + }; + + const handleLogout = () => { + localStorage.removeItem('username'); + localStorage.removeItem('email'); + localStorage.removeItem('access_token'); + localStorage.removeItem('role'); + + navigate('/'); + }; + + return ( +
+
+
+ Book Rider Logo +
+ {[ + {id: 'addBook', label: 'Książki', path: '/librarian-dashboard'}, + {id: 'orders', label: 'Wypożyczenia', path: '/orders'}, + {id: 'returns', label: 'Zwroty', path: '/returns'}, + {id: 'readers', label: 'Czytelnicy', path: '/readers'}, + {id: 'settings', label: 'Ustawienia', path: '/librarian-settings'}, + ].map(({id, label, path}) => ( + + {label} + + ))} + +
+ +
+
+

Oczekujące

+ + {/* PENDING */} +
+ {orderDetails + .filter((order) => order.displayStatus === 'PENDING') + .map((order) => ( +
+
+
+

Zamówienie + nr: {order.orderId}

+
+ +

+ ID użytkownika: {order.userId} +

+

+ Data + utworzenia: {new Date(order.createdAt).toLocaleString()} +

+ + {order.orderItems.map((item, index) => ( +
+
+ {item.book.title} +
+ +
+

{item.book.title}

+

+ Autor: {item.book.authorNames.join(', ')}

+

+ ISBN: {item.book.isbn}

+

+ Kategoria: {item.book.categoryName}

+

Rok + wydania: {item.book.releaseYear}

+

+ Wydawnictwo: {item.book.publisherName}

+

+ Język: {item.book.languageName}

+

ID + książki: {item.book.id}

+

Zamówiona + ilość: {item.quantity}

+
+
+ ))} + +
+ + + +
+ + {showRejectionInput && selectedOrderId === order.orderId && ( +
+

Wybierz przyczynę + odmowy:

+
+ + {['Brak w zbiorach biblioteki', 'Wszystkie egzemplarze zostały wypożyczone', 'Inne'].map(reason => ( + + ))} + + {rejectionReason === 'Inne' && ( + setCustomReason(e.target.value)} + /> + )} +
+ + {pendingMessage && ( +
+ {pendingMessage} +
+ )} + + +
+ )} +
+
+ ))} +
+
+ + {/* IN_REALIZATION */} +
+

W realizacji

+ +
+ {orderDetails + .filter((order) => order.displayStatus === 'IN_REALIZATION') + .map((order) => ( +
+
+
+

+ Zamówienie nr: {order.orderId} +

+
+ +

+ ID użytkownika: {order.userId} +

+

+ Data utworzenia:{' '} + {new Date(order.createdAt).toLocaleString()} +

+ + {order.orderItems.map((item, index) => ( +
+
+ {item.book.title} +
+ +
+

+ {item.book.title} +

+

+ Autor: {item.book.authorNames.join(', ')} +

+

+ ISBN: {item.book.isbn} +

+

+ Kategoria: {item.book.categoryName} +

+

+ Rok wydania: {item.book.releaseYear} +

+

+ Wydawnictwo: {item.book.publisherName} +

+

+ Język: {item.book.languageName} +

+

+ ID książki: {item.book.id} +

+

+ Zamówiona ilość: {item.quantity} +

+
+
+ ))} + + + + {handoverVisible[order.orderId] && ( + <> + { + const newDriverId = e.target.value; + setOrderDetails((prevOrders) => + prevOrders.map((o) => + o.orderId === order.orderId + ? { ...o, driverId: newDriverId } + : o + ) + ); + }} + + /> + + {realizationMessage && ( +
+ {realizationMessage} +
+ )} + + + + )} +
+
+ ))} +
+
+ +
+

Zrealizowane

+ {/* COMPLETED */} +
+ {orderDetails + .filter((order) => order.displayStatus === 'COMPLETED') + .map((order) => ( +
+
+
+

Zamówienie + nr: {order.orderId}

+
+ +

+ ID użytkownika: {order.userId} +

+

+ Data + utworzenia: {new Date(order.createdAt).toLocaleString()} +

+ + {order.orderItems.map((item, index) => ( +
+
+ {item.book.title} +
+ +
+

{item.book.title}

+

+ Autor: {item.book.authorNames.join(', ')}

+

+ ISBN: {item.book.isbn}

+

+ Kategoria: {item.book.categoryName}

+

Rok + wydania: {item.book.releaseYear}

+

+ Wydawnictwo: {item.book.publisherName}

+

+ Język: {item.book.languageName}

+

ID + książki: {item.book.id}

+

Zamówiona + ilość: {item.quantity}

+
+
+ ))} +
+
+ ))} +
+
+
+
+ ); +}; + +export default LibrarianOrders; \ No newline at end of file diff --git a/wa/src/Librarian/LibrarianReaders.tsx b/wa/src/Librarian/LibrarianReaders.tsx new file mode 100644 index 00000000..d19570d5 --- /dev/null +++ b/wa/src/Librarian/LibrarianReaders.tsx @@ -0,0 +1,273 @@ +import React, {useState} from 'react'; +import {Link, useNavigate} from 'react-router-dom'; + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; + +const LibrarianReaders: React.FC = () => { + // Readers + const [userId, setUserId] = useState(''); + const [cardId, setCardId] = useState(''); + const [firstName, setFirstName] = useState(''); + const [lastName, setLastName] = useState(''); + const [expirationDate, setExpirationDate] = useState('2025-01-01'); + const [libraryCardSearchId, setLibraryCardSearchId] = useState(''); + + // User + interface LibraryCardDetails { + userId: string; + cardId: string; + firstName: string; + lastName: string; + expirationDate: string; + } + + const [libraryCardDetails, setLibraryCardDetails] = useState(null); + + const navigate = useNavigate(); + + // Readers --------------------------------------------------------------------------------------------------------- + const handleCreateLibraryCard = async () => { + const token = localStorage.getItem('access_token'); + if (!token) { + console.error('No token found'); + return; + } + + try { + const response = await fetch(`${API_BASE_URL}/api/library-cards`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + userId, + cardId, + firstName, + lastName, + expirationDate, + }), + }); + + if (response.ok) { + alert('Library card created successfully'); + setUserId(''); + setCardId(''); + setFirstName(''); + setLastName(''); + setExpirationDate('2025-01-01'); + } else { + throw new Error('Error creating library card'); + } + } catch (error) { + console.error('Error creating library card: ', error); + } + }; + + const handleSearchLibraryCard = async () => { + const token = localStorage.getItem('access_token'); + if (!token) { + console.error("No token found"); + return; + } + + try { + const response = await fetch(`${API_BASE_URL}/api/library-cards/${libraryCardSearchId}`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + + if (response.ok) { + const data = await response.json(); + setLibraryCardDetails(data[0]); + } else { + throw new Error('Error searching for library card'); + } + } catch (error) { + console.error('Error searching for library card: ', error); + setLibraryCardDetails(null); + } + }; + + const handleLogout = () => { + localStorage.removeItem('username'); + localStorage.removeItem('email'); + localStorage.removeItem('access_token'); + localStorage.removeItem('role'); + + navigate('/'); + }; + + return ( +
+
+
+ Book Rider Logo +
+ {[ + {id: 'addBook', label: 'Książki', path: '/librarian-dashboard'}, + {id: 'orders', label: 'Wypożyczenia', path: '/orders'}, + {id: 'returns', label: 'Zwroty', path: '/returns'}, + {id: 'readers', label: 'Czytelnicy', path: '/readers'}, + {id: 'settings', label: 'Ustawienia', path: '/librarian-settings'}, + ].map(({id, label, path}) => ( + + {label} + + ))} + +
+ +
+
+

Czytelnicy

+ +
+

Dodaj nowego użytkownika + BookRider:

+
+
+ + setUserId(e.target.value)} + className="w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#3B576C]" + /> +
+
+ + setCardId(e.target.value)} + className="w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#3B576C]" + /> +
+
+ + setFirstName(e.target.value)} + className="w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#3B576C]" + /> +
+
+ + setLastName(e.target.value)} + className="w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#3B576C]" + /> +
+
+ + setExpirationDate(e.target.value)} + className="mb-5 w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#3B576C]" + /> +
+
+ +
+
+
+ + +
+ +
+

Wyszukaj użytkownika:

+
+
+ + setLibraryCardSearchId(e.target.value)} + className="mb-5 w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#3B576C]" + /> +
+
+ +
+
+
+ + {libraryCardDetails && ( +
+

+ Szczegóły wyszukiwanego konta: +

+

ID użytkownika: {libraryCardDetails.userId}

+

ID karty: {libraryCardDetails.cardId}

+

Imię: {libraryCardDetails.firstName}

+

Nazwisko: {libraryCardDetails.lastName}

+

Data ważności karty + bibliotecznej: {libraryCardDetails.expirationDate}

+
+ )} +
+ + {/*{activeSection === 'settings' && (*/} + {/*
*/} + {/*

Ustawienia

*/} + {/*
*/} + {/*)}*/} + +
+
+ ); +}; + +export default LibrarianReaders; \ No newline at end of file diff --git a/wa/src/Librarian/LibrarianReturns.tsx b/wa/src/Librarian/LibrarianReturns.tsx new file mode 100644 index 00000000..8e75a701 --- /dev/null +++ b/wa/src/Librarian/LibrarianReturns.tsx @@ -0,0 +1,374 @@ +import React, {useEffect, useState} from 'react'; +import {Link, useNavigate} from 'react-router-dom'; +import useWebSocketConnection from "../WebSocket/useWebSocketConnection.tsx"; +import {toast} from "react-toastify"; +import 'react-toastify/dist/ReactToastify.css'; + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; + + +const LibrarianReturns: React.FC = () => { + const [message, setMessage] = useState(null); + const [messageType, setMessageType] = useState<'success' | 'error' | null>(null); + + useWebSocketConnection(`librarian/orders/pending`, () => { + toast.info("📚 New order received!", { + position: "bottom-right", + }); + }); + + // Returns + interface RentalReturnItem { + id: number; + rentalId: number; + book: { + id: number; + title: string; + categoryName: string; + authorNames: string[]; + releaseYear: number; + publisherName: string; + isbn: string; + languageName: string; + image: string; + }; + returnedQuantity: number; + } + + interface RentalReturnDetails { + id: number; + orderId: number; + returnedAt: string; + status: 'IN_PROGRESS' | 'COMPLETED'; + rentalReturnItems: RentalReturnItem[]; + } + + const [returnType, setReturnType] = useState<'inPerson' | 'driver'>('inPerson'); + const [rentalReturnId, setRentalReturnId] = useState(null); + const [rentalReturnDetails, setRentalReturnDetails] = useState(null); + const [driverId, setDriverId] = useState(''); + // const [returnStatus, setReturnStatus] = useState<'IN_PROGRESS' | 'COMPLETED'>('IN_PROGRESS'); + + const navigate = useNavigate(); + + useEffect(() => { + if (messageType) { + const handleClick = () => { + setMessage(null); + setMessageType(null); + }; + + document.addEventListener("click", handleClick); + + return () => { + document.removeEventListener("click", handleClick); + }; + } + }, [messageType]); + + // Returns --------------------------------------------------------------------------------------------------------- + const completeInPersonReturn = async (rentalReturnId: number) => { + const token = localStorage.getItem('access_token'); + if (!token) return; + + try { + const response = await fetch(`${API_BASE_URL}/api/rental-returns/${rentalReturnId}/complete-in-person`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${token}`, + }, + }); + + if (response.ok) { + //setReturnStatus('COMPLETED'); + setMessage('Return completed successfully.'); + setMessageType('success'); + } else { + setMessage('Failed to complete the return.'); + setMessageType('error'); + } + } catch (error) { + console.error('Error:', error); + setMessage('Error completing the return.'); + setMessageType('error'); + } + }; + + const completeDriverReturn = async (rentalReturnId: number) => { + const token = localStorage.getItem('access_token'); + if (!token) return; + + try { + const response = await fetch(`${API_BASE_URL}/api/rental-returns/${rentalReturnId}/complete-delivery`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${token}`, + }, + }); + + if (response.ok) { + //setReturnStatus('COMPLETED'); + setMessage('Return completed successfully.'); + setMessageType('success'); + } else { + setMessage('Failed to complete the return.'); + setMessageType('error'); + } + } catch (error) { + console.error('Error:', error); + setMessage('Error completing the return.'); + setMessageType('error'); + } + }; + + const handleReturnOptionChange = (option: 'inPerson' | 'driver') => { + setReturnType(option); + setRentalReturnId(null); + setRentalReturnDetails(null); + }; + + const handleFetchReturnDetails = async () => { + const token = localStorage.getItem('access_token'); + + if (returnType === 'inPerson') { + if (!rentalReturnId || isNaN(rentalReturnId)) { + setMessage("Nieprawidłowe ID zwrotu"); + setMessageType("error"); + return; + } + } else if (returnType === 'driver') { + if (!driverId || driverId.trim() === "") { + setMessage("Nieprawidłowe ID kierowcy"); + setMessageType("error"); + return; + } + } else { + setMessage("Wybierz rodzaj zwrotu"); + setMessageType("error"); + return; + } + + try { + let url = ""; + if (returnType === 'inPerson') { + url = `${API_BASE_URL}/api/rental-returns/${rentalReturnId}`; + } else { + url = `${API_BASE_URL}/api/rental-returns/latest-by-driver/${driverId}`; + } + + const response = await fetch(url, { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (response.ok) { + const data = await response.json(); + + if (returnType === 'driver') { + if (data && data.id) { + setRentalReturnId(data.id); + } else { + setMessage("Nie znaleziono zwrotu dla tego kierowcy"); + setMessageType("error"); + setRentalReturnDetails(null); + return; + } + } + + setRentalReturnDetails(data); + setMessage(""); + } else { + throw new Error("Nie udało się pobrać danych zwrotu"); + } + } catch (error) { + console.error("Błąd podczas pobierania szczegółów zwrotu:", error); + setMessage("Wystąpił błąd podczas pobierania danych."); + setMessageType("error"); + setRentalReturnDetails(null); + } + }; + + const handleCompleteReturn = async () => { + if (!rentalReturnId) return; + + try { + if (returnType === 'inPerson') { + await completeInPersonReturn(rentalReturnId); + setMessage('Zwrot zakończony pomyślnie.'); + setMessageType('success'); + } else if (returnType === 'driver') { + await completeDriverReturn(rentalReturnId); + setMessage('Zwrot przez kierowcę zakończony pomyślnie.'); + setMessageType('success'); + } + } catch (error) { + console.error('Error:', error); + setMessage('Nie udało się zakończyć zwrotu.'); + setMessageType('error'); + } + }; + + const handleLogout = () => { + localStorage.removeItem('username'); + localStorage.removeItem('email'); + localStorage.removeItem('access_token'); + localStorage.removeItem('role'); + + navigate('/'); + }; + + return ( +
+
+
+ Book Rider Logo +
+ {[ + {id: 'addBook', label: 'Książki', path: '/librarian-dashboard'}, + {id: 'orders', label: 'Wypożyczenia', path: '/orders'}, + {id: 'returns', label: 'Zwroty', path: '/returns'}, + {id: 'readers', label: 'Czytelnicy', path: '/readers'}, + {id: 'settings', label: 'Ustawienia', path: '/librarian-settings'}, + ].map(({id, label, path}) => ( + + {label} + + ))} + +
+ +
+
+

Zwroty

+ +
+

Wybierz rodzaj zwrotu:

+
+ + +
+
+ +
+ + {(returnType === 'inPerson' || returnType === 'driver') && ( +
+

Wprowadź wymagane dane:

+
+ + {returnType === 'inPerson' && ( +
+ + setRentalReturnId(Number(e.target.value))} + className="w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#3B576C]" + /> +
+ )} + + {returnType === 'driver' && ( +
+ + setDriverId(e.target.value)} + className="w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#3B576C]" + /> +
+ )} + +
+ +
+
+
+ )} + + {rentalReturnDetails && ( +
+

Szczegóły zwrotu:

+ +
+ {rentalReturnDetails.rentalReturnItems.map((item) => ( +
+

{item.book.authorNames.join(', ')}

+

{item.book.title}

+

Ilość: {item.returnedQuantity}

+

ID książki: {item.book.id}

+

ISBN: {item.book.isbn}

+
+ ))} +
+ +
+ +
+
+ )} + + {message && ( +
+ {message} +
+ )} +
+
+
+ ); +}; + +export default LibrarianReturns; \ No newline at end of file diff --git a/wa/src/Librarian/LibrarianSettings.tsx b/wa/src/Librarian/LibrarianSettings.tsx new file mode 100644 index 00000000..bc6bcd53 --- /dev/null +++ b/wa/src/Librarian/LibrarianSettings.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import {Link, useNavigate} from 'react-router-dom'; + +// const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; + +const LibrarianSettings: React.FC = () => { + const navigate = useNavigate(); + + const handleLogout = () => { + localStorage.removeItem('username'); + localStorage.removeItem('email'); + localStorage.removeItem('access_token'); + localStorage.removeItem('role'); + + navigate('/'); + }; + + return ( +
+
+
+ Book Rider Logo +
+ {[ + {id: 'addBook', label: 'Książki', path: '/librarian-dashboard'}, + {id: 'orders', label: 'Wypożyczenia', path: '/orders'}, + {id: 'returns', label: 'Zwroty', path: '/returns'}, + {id: 'readers', label: 'Czytelnicy', path: '/readers'}, + {id: 'settings', label: 'Ustawienia', path: '/librarian-settings'}, + ].map(({id, label, path}) => ( + + {label} + + ))} + +
+ +
+
+

Ustawienia będą tutaj

+
+ +
+
+ ); +}; + +export default LibrarianSettings; \ No newline at end of file diff --git a/wa/src/LibraryAdmin/LibraryAdminAddLibrarian.tsx b/wa/src/LibraryAdmin/LibraryAdminAddLibrarian.tsx new file mode 100644 index 00000000..975be314 --- /dev/null +++ b/wa/src/LibraryAdmin/LibraryAdminAddLibrarian.tsx @@ -0,0 +1,124 @@ +import React, { useState } from 'react'; +import {Link, useNavigate} from "react-router-dom"; + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; + +const LibraryAdminHomePage: React.FC = () => { + const [newLibrarian, setNewLibrarian] = useState({ username: '', firstName: '', lastName: '' }); + const [message, setMessage] = useState(''); + + const navigate = useNavigate(); + + const addLibrarian = async () => { + const token = localStorage.getItem('access_token'); + try { + const res = await fetch(`${API_BASE_URL}/api/library-admins/librarians`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(newLibrarian), + }); + if (res.ok) { + setMessage('Dodano bibliotekarza.'); + setNewLibrarian({ username: '', firstName: '', lastName: '' }); + } else { + setMessage('Nie udało się dodać bibliotekarza.'); + } + } catch (err) { + console.error(err); + setMessage('Error adding librarian'); + } + }; + + const handleLogout = () => { + localStorage.removeItem('username'); + localStorage.removeItem('email'); + localStorage.removeItem('access_token'); + localStorage.removeItem('role'); + + navigate('/'); + }; + + return ( +
+
+
+ Book Rider Logo +
+ {[ + {id: 'librarian_search', label: 'Bibliotekarze', path: '/library-admin-dashboard'}, + {id: 'add_librarians', label: 'Dodaj', path: '/library-admin-add-librarian'}, + {id: 'settings', label: 'Ustawienia', path: '/library-admin-settings'}, + ].map(({id, label, path}) => ( + + {label} + + ))} + +
+
+
+

Dodaj bibliotekarza do Twojej biblioteki

+
+
+ +
+
+ setNewLibrarian({...newLibrarian, username: e.target.value})} + className="border border-gray-300 rounded p-2" + /> + setNewLibrarian({...newLibrarian, firstName: e.target.value})} + className="border border-gray-300 rounded p-2" + /> + setNewLibrarian({...newLibrarian, lastName: e.target.value})} + className="border border-gray-300 rounded p-2" + /> +
+
+ +
+
+ + {message &&

{message}

} +
+
+
+
+
+ ); +}; + +export default LibraryAdminHomePage; diff --git a/wa/src/LibraryAdmin/LibraryAdminAddLibrary.tsx b/wa/src/LibraryAdmin/LibraryAdminAddLibrary.tsx new file mode 100644 index 00000000..b7250c9f --- /dev/null +++ b/wa/src/LibraryAdmin/LibraryAdminAddLibrary.tsx @@ -0,0 +1,149 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; + +const LibraryAdminAddLibrary: React.FC = () => { + const navigate = useNavigate(); + + interface FormData { + libraryName: string; + addressLine: string; + city: string; + postalCode: string; + phoneNumber: string; + emailAddress: string; + } + + const [formData, setFormData] = useState({ + libraryName: '', + addressLine: '', + city: '', + postalCode: '', + phoneNumber: '', + emailAddress: '', + }); + + const handleLogout = () => { + localStorage.removeItem('access_token'); + localStorage.removeItem('role'); + + navigate('/'); + }; + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData({ ...formData, [name]: value }); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + const requestBody = { + street: formData.addressLine, + city: formData.city, + postalCode: formData.postalCode.toString(), + libraryName: formData.libraryName, + phoneNumber: formData.phoneNumber, + libraryEmail: formData.emailAddress, + }; + + const token = localStorage.getItem('access_token'); + + try { + const response = await fetch(`${API_BASE_URL}/api/library-requests`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + }, + body: JSON.stringify(requestBody), + }); + + if (response.ok) { + navigate('/processing-info'); + } else { + console.error('Error submitting request', response.statusText); + } + } catch (error) { + console.error('Error:', error); + } + }; + + const handleSettings = () => { + alert('Ustawienia'); + }; + + return ( +
+
+
+ Book Rider Logo +
+ + +
+ +
+
+

+ Złóż podanie o dodanie Twojej biblioteki do systemu BookRider +

+
+ {[ + {label: 'Nazwa biblioteki:', name: 'libraryName', maxLength: 70, required: true }, + { label: 'Ulica i nr budynku:', name: 'addressLine', maxLength: 70, required: true }, + { label: 'Miasto:', name: 'city', maxLength: 50, required: true }, + { label: 'Kod pocztowy:', name: 'postalCode', maxLength: 6, required: true, pattern: '\\d{2}-\\d{3}', title: 'XX-XXX' }, + { label: 'Numer telefonu:', name: 'phoneNumber', maxLength: 9, required: true, pattern: '\\d{9}', title: 'Podaj prawidłowy numer telefonu bez numeru kierunkowego' }, + { label: 'Adres e-mail:', name: 'emailAddress', maxLength: 50, required: true, type: 'email' }, + ].map((field, index) => ( +
+ + +
+ ))} +
+ +
+
+
+
+
+ ); +}; + +export default LibraryAdminAddLibrary; diff --git a/wa/src/LibraryAdmin/LibraryAdminHomePage.tsx b/wa/src/LibraryAdmin/LibraryAdminHomePage.tsx new file mode 100644 index 00000000..d4db28b2 --- /dev/null +++ b/wa/src/LibraryAdmin/LibraryAdminHomePage.tsx @@ -0,0 +1,222 @@ +import React, { useEffect, useState } from 'react'; +import {Link, useNavigate} from "react-router-dom"; + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; + +interface Librarian { + id: string; + username: string; + firstName: string; + lastName: string; +} + +const LibraryAdminHomePage: React.FC = () => { + const [usernameSearch, setUsernameSearch] = useState(''); + const [librarians, setLibrarians] = useState([]); + const [message, setMessage] = useState(''); + + const navigate = useNavigate(); + + useEffect(() => { + const token = localStorage.getItem('access_token'); + const fetchAll = async () => { + try { + const response = await fetch(`${API_BASE_URL}/api/library-admins/librarians`, { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + }, + }); + if (response.ok) { + const data = await response.json(); + setLibrarians(data); + setMessage(''); + } else { + setMessage('Nie udało się pobrać listy bibliotekarzy.'); + } + } catch (err) { + console.error(err); + setMessage('Error fetching librarian list'); + } + }; + + if (usernameSearch.trim() === '') { + fetchAll(); + } + }, [usernameSearch]); + + const fetchLibrarian = async () => { + const token = localStorage.getItem('access_token'); + if (!usernameSearch.trim()) return; + + try { + const response = await fetch(`${API_BASE_URL}/api/library-admins/librarians?username=${usernameSearch}`, { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (response.ok) { + const data = await response.json(); + setLibrarians(Array.isArray(data) ? data : [data]); + setMessage(''); + } else { + setLibrarians([]); + setMessage('Nie znaleziono bibliotekarza.'); + } + } catch (err) { + console.error(err); + setMessage('Error fetching librarian'); + } + }; + + const resetPassword = async (username: string) => { + const token = localStorage.getItem('access_token'); + try { + const res = await fetch(`${API_BASE_URL}/api/library-admins/librarians/reset-password/${username}`, { + method: 'PATCH', + headers: { + Authorization: `Bearer ${token}`, + }, + }); + setMessage(res.ok ? 'Hasło bibliotekarza zresetowano pomyślnie.' : 'Nie udało się zresetować hasła bibliotekarza.'); + } catch (err) { + console.error(err); + setMessage('Error resetting password'); + } + }; + + const deleteLibrarian = async (username: string) => { + const token = localStorage.getItem('access_token'); + try { + const res = await fetch(`${API_BASE_URL}/api/library-admins/librarians/${username}`, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${token}`, + }, + }); + if (res.ok) { + setLibrarians(librarians.filter(lib => lib.username !== username)); + setMessage('Usunięto bibliotekarza.'); + } else { + setMessage('Nie udało się usunąć bibliotekarza.'); + } + } catch (err) { + console.error(err); + setMessage('Error deleting librarian'); + } + }; + + const handleLogout = () => { + localStorage.removeItem('username'); + localStorage.removeItem('email'); + localStorage.removeItem('access_token'); + localStorage.removeItem('role'); + + navigate('/'); + }; + + return ( +
+
+
+ Book Rider Logo +
+ {[ + {id: 'librarian_search', label: 'Bibliotekarze', path: '/library-admin-dashboard'}, + {id: 'add_librarians', label: 'Dodaj', path: '/library-admin-add-librarian'}, + {id: 'settings', label: 'Ustawienia', path: '/library-admin-settings'}, + ].map(({id, label, path}) => ( + + {label} + + ))} + +
+
+
+

Wyszukaj bibliotekarza

+
+
+
+
+ setUsernameSearch(e.target.value)} + className="border border-gray-300 rounded p-2 w-full pr-10" + /> + {usernameSearch && ( + + )} + +
+
+ + {librarians.length > 0 && ( +
+

Bibliotekarze w Twojej bibliotece:

+
    + {librarians.map(lib => ( +
  • +
    +

    {lib.firstName} {lib.lastName}

    +

    Nazwa użytkownika:
    {lib.username}

    +
    +
    + + +
    +
  • + ))} +
+
+ )} + + {message &&

{message}

} +
+
+
+
+
+ ); +}; + +export default LibraryAdminHomePage; diff --git a/wa/src/LibraryAdmin/LibraryAdminLogin.tsx b/wa/src/LibraryAdmin/LibraryAdminLogin.tsx new file mode 100644 index 00000000..da5a20ba --- /dev/null +++ b/wa/src/LibraryAdmin/LibraryAdminLogin.tsx @@ -0,0 +1,252 @@ +import React, { useState } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; + +const LibraryAdminLogin: React.FC = () => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(null); + const [role, setRole] = useState('library_administrator'); + const [emailValid, setEmailValid] = useState(true); + const navigate = useNavigate(); + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + + if (name === 'email') { + setEmail(value); + + const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); + setEmailValid(isValid); + + if (!isValid) { + setError('Wprowadź poprawny adres email'); + } else { + setError(null); + } + } else if (name === 'password') { + setPassword(value); + } else if (name === 'role') { + setRole(value); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + + if (!emailValid) { + setError('Wprowadź poprawny adres email'); + return; + } + + try { + const response = await fetch(`${API_BASE_URL}/api/auth/login/${role}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + identifier: email, + password, + }), + }); + + if (response.ok) { + const authHeader = response.headers.get('Authorization'); + if (authHeader) { + const token = authHeader.split(' ')[1]; + + localStorage.setItem('access_token', token); + localStorage.setItem('role', role); + localStorage.setItem('email', email); + + const statusResponse = await fetch(`${API_BASE_URL}/api/library-requests/me?page=0&size=1`, { + headers: { + 'Authorization': `Bearer ${token}`, + }, + }); + + if (statusResponse.ok) { + const requests = await statusResponse.json(); + const status = requests[0]?.status; + + if (status === 'PENDING') { + navigate('/processing-info'); + } else if (status === 'APPROVED') { + navigate('/library-admin-dashboard'); + } else { + navigate('/add-library'); + } + } else { + navigate('/'); // Landing page + } + + } else { + setError('Authorization header missing'); + } + } else { + const errorData = await response.json(); + switch (errorData.code) { + case 401: + setError('Nieprawidłowy email lub hasło.'); + break; + case 500: + setError('Wewnętrzny błąd serwera. Spróbuj ponownie później.'); + break; + case 400: + setError('Należy podać adres email oraz hasło.'); + break; + default: + setError(errorData.message || 'Błąd logowania. Spróbuj ponownie.'); + break; + } + } + } catch (error) { + console.error('Login error: ', error); + setError('Podczas logowania wystąpił błąd'); + } + }; + + return ( +
+ + {/* Top Bar */} +
+
+
+ + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+
+

+ Logowanie administratora biblioteki +

+ +
+
+ + +
+ +
+ + +
+ + {error && ( +
+ +

{error}

+
+ )} + + +
+ +
+ + + +
+
+
+
+
+ ); +}; + +export default LibraryAdminLogin; diff --git a/wa/src/LibraryAdmin/LibraryAdminRegisterForm.tsx b/wa/src/LibraryAdmin/LibraryAdminRegisterForm.tsx new file mode 100644 index 00000000..f9484309 --- /dev/null +++ b/wa/src/LibraryAdmin/LibraryAdminRegisterForm.tsx @@ -0,0 +1,234 @@ +import React, { useState } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; + +interface FormData { + firstName: string; + lastName: string; + email: string; + password: string; + confirm_password: string; +} + +const RegistrationForm: React.FC = () => { + const [formData, setFormData] = useState({ + firstName: '', + lastName: '', + email: '', + password: '', + confirm_password: '', + }); + + const [error, setError] = useState(null); + const navigate = useNavigate(); + + const handleInputChange = (e: React.ChangeEvent): void => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + const isPasswordSafe = (password: string): boolean => { + const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#])[A-Za-z\d@$!%*?&#]{12,}$/; + return passwordRegex.test(password); + }; + + const handleSubmit = async (e: React.FormEvent): Promise => { + e.preventDefault(); + + if (formData.password !== formData.confirm_password) { + setError('Hasła nie pasują do siebie.'); + return; + } + + if (!isPasswordSafe(formData.password)) { + setError( + 'Hasło powinno mieć co najmniej 12 znaków, w tym wielkie i małe litery, cyfry i znaki specjalne.' + ); + return; + } + + setError(null); + + const requestBody = { + email: formData.email, + firstName: formData.firstName, + lastName: formData.lastName, + password: formData.password, + }; + + try { + const response = await fetch(`${API_BASE_URL}/api/auth/register/library_administrator`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + }); + + console.log(response.status); + + if (!response.ok) { + throw new Error('Registration failed'); + } + + navigate('/library-admin-login'); + } catch (error) { + setError('Podczas rejestracji nastąpił błąd.'); + console.error('Error:', error); + } + }; + + + return ( +
+ + {/* Top Bar */} + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+

Rejestracja
administratora biblioteki

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + {error &&

{error}

} + + +
+ +
+ + + +
+
+
+ ); +}; + +export default RegistrationForm; diff --git a/wa/src/LibraryAdmin/LibraryAdminSettings.tsx b/wa/src/LibraryAdmin/LibraryAdminSettings.tsx new file mode 100644 index 00000000..5d1f28ff --- /dev/null +++ b/wa/src/LibraryAdmin/LibraryAdminSettings.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import {Link, useNavigate} from 'react-router-dom'; + +// const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; + +const LibraryAdminSettings: React.FC = () => { + const navigate = useNavigate(); + + const handleLogout = () => { + localStorage.removeItem('username'); + localStorage.removeItem('email'); + localStorage.removeItem('access_token'); + localStorage.removeItem('role'); + + navigate('/'); + }; + + return ( +
+
+
+ Book Rider Logo +
+ {[ + {id: 'librarian_search', label: 'Bibliotekarze', path: '/library-admin-dashboard'}, + {id: 'add_librarians', label: 'Dodaj', path: '/library-admin-add-librarian'}, + {id: 'settings', label: 'Ustawienia', path: '/library-admin-settings'}, + ].map(({id, label, path}) => ( + + {label} + + ))} + +
+
+
+

Ustawienia

+
+
+
+ ); +}; +export default LibraryAdminSettings; \ No newline at end of file diff --git a/wa/src/SystemAdmin/SubmissionDetailsDriver.tsx b/wa/src/SystemAdmin/SubmissionDetailsDriver.tsx new file mode 100644 index 00000000..aa8ebebc --- /dev/null +++ b/wa/src/SystemAdmin/SubmissionDetailsDriver.tsx @@ -0,0 +1,297 @@ +import React, { useState, useEffect } from 'react'; +import { Link, useNavigate, useParams } from 'react-router-dom'; + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; + +interface DriverDocument { + documentType: string; + documentPhotoUrl: string; + expiryDate: string; +} + +interface DriverApplication { + id: number; + userEmail: string; + reviewerID: string | null; + status: string; + submittedAt: string; + reviewedAt: string | null; + rejectionReason: string | null; + driverDocuments: DriverDocument[]; +} + +const formatDate = (isoString: string) => { + return new Date(isoString).toLocaleString('pl-PL', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); +}; + +const formatDateDocument = (isoString: string) => { + return new Date(isoString).toLocaleDateString('pl-PL', { + year: 'numeric', + month: 'long', + day: 'numeric' + }); +}; + +const SubmissionDetailsDriver: React.FC = () => { + const [email, setEmail] = useState(null); + const { submissionId } = useParams(); + const [driverApplication, setDriverApplication] = useState(null); + const [error, setError] = useState(''); + const [formError, setFormError] = useState(''); + const [showRejectionInput, setShowRejectionInput] = useState(false); + const [selectedReason, setSelectedReason] = useState(''); + const [customRejectionReason, setCustomRejectionReason] = useState(''); + const navigate = useNavigate(); + + const getEmail = () => { + return localStorage.getItem('email'); + }; + + const handleLogout = () => { + localStorage.removeItem('access_token'); + localStorage.removeItem('role'); + navigate('/'); + }; + + useEffect(() => { + const fetchDriverApplication = async () => { + const token = localStorage.getItem('access_token'); + if (!token) return; + + const userEmail = getEmail(); + if (userEmail) { + setEmail(userEmail); + } + + try { + const response = await fetch(`${API_BASE_URL}/api/driver-applications/${submissionId}`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error('Błąd podczas pobierania danych.'); + } + const data: DriverApplication = await response.json(); + setDriverApplication(data); + } catch (error) { + setError(error instanceof Error ? error.message : 'Nieznany błąd.'); + } + }; + + fetchDriverApplication(); + }, [submissionId]); + + const handleAccept = async () => { + const token = localStorage.getItem('access_token'); + if (!token) return; + + try { + const response = await fetch(`${API_BASE_URL}/api/driver-applications/${submissionId}/status?status=APPROVED`, { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error('Błąd podczas zatwierdzania podania.'); + } + + setDriverApplication((prev) => prev ? { ...prev, status: 'APPROVED' } : prev); + + navigate('/system-admin-dashboard'); + } catch (error) { + setError(error instanceof Error ? error.message : 'Nieznany błąd.'); + } + }; + + const handleDecline = () => { + setShowRejectionInput(true); + }; + + const handleReasonChange = (e: React.ChangeEvent) => { + setSelectedReason(e.target.value); + setFormError(''); + if (e.target.value !== 'Inne') { + setCustomRejectionReason(''); + } + }; + + const handleCustomInputChange = (e: React.ChangeEvent) => { + setCustomRejectionReason(e.target.value); + setFormError(''); + }; + + const handleConfirmRejection = async () => { + const token = localStorage.getItem('access_token'); + if (!token) return; + + const finalRejectionReason = selectedReason === 'Inne' ? customRejectionReason : selectedReason; + + if (finalRejectionReason.trim()) { + try { + const response = await fetch(`${API_BASE_URL}/api/driver-applications/${submissionId}/status?status=REJECTED&rejectionReason=${encodeURIComponent(finalRejectionReason)}`, { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error('Błąd podczas odrzucania podania.'); + } + + setDriverApplication((prev) => prev ? { ...prev, status: 'REJECTED', rejectionReason: finalRejectionReason } : prev); + setShowRejectionInput(false); + setSelectedReason(''); + setCustomRejectionReason(''); + setFormError(''); + + navigate('/system-admin-dashboard'); + } catch (error) { + setError(error instanceof Error ? error.message : 'Nieznany błąd.'); + } + } else { + setFormError('Proszę podać powód odrzucenia.'); + } + }; + + if (error) { + return
{error}
; + } + + return ( +
+
+
+ Book Rider Logo +
+
+ {email && {email}} + +
+
+ +
+
+

+ Szczegóły podania nr: {driverApplication?.id} +

+ +
+

Email użytkownika: {driverApplication?.userEmail}

+

Status: {driverApplication?.status}

+

Data + złożenia: {driverApplication?.submittedAt ? formatDate(driverApplication.submittedAt) : "Brak danych"} +

+
+ +
+ + {driverApplication?.driverDocuments && driverApplication.driverDocuments.length > 0 && ( +
+

Dokument kierowcy

+ {driverApplication.driverDocuments.map((doc, index) => ( +
+

Typ dokumentu: {doc.documentType}

+

Data ważności: {formatDateDocument(doc.expiryDate)}

+
+ Zdjęcie dokumentu +
+
+ ))} +
+ )} + + {!showRejectionInput ? ( +
+ + +
+ ) : ( +
+ +
+ {["Nieprawidłowy dokument", "Zdjęcie dokumentu ma zbyt niską jakość", "Nieprawidłowe dane kierowcy", "Inne"].map((reason) => ( + + ))} + + {selectedReason === "Inne" && ( + + )} + + {formError && ( +
{formError}
+ )} + +
+ +
+
+
+ )} + + +
+ +
+ + + +
+
+
+
+ ); +}; + +export default SubmissionDetailsDriver; diff --git a/wa/src/SystemAdmin/SubmissionDetailsLibrary.tsx b/wa/src/SystemAdmin/SubmissionDetailsLibrary.tsx new file mode 100644 index 00000000..eb5cdc54 --- /dev/null +++ b/wa/src/SystemAdmin/SubmissionDetailsLibrary.tsx @@ -0,0 +1,304 @@ +import React, { useState, useEffect } from 'react'; +import { useNavigate, Link, useParams } from 'react-router-dom'; + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; + +interface Address { + id: number; + street: string; + city: string; + postalCode: string; + latitude: number; + longitude: number; +} + +interface LibraryRequest { + id: number; + creatorEmail: string; + reviewerId: string; + address: Address; + libraryName: string; + phoneNumber: string; + libraryEmail: string; + status: string; + submittedAt: string; + reviewedAt: string; + rejectionReason: string; +} + +const formatDate = (isoString: string) => { + return new Date(isoString).toLocaleString('pl-PL', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); +}; + +const SubmissionDetailsLibrary: React.FC = () => { + const [email, setEmail] = useState(null); + const { submissionId } = useParams(); + const [libraryRequest, setLibraryRequest] = useState(null); + const [error, setError] = useState(''); + const [showRejectionInput, setShowRejectionInput] = useState(false); + const [rejectionReason, setRejectionReason] = useState(''); + const [selectedReason, setSelectedReason] = useState(''); + const [validationError, setValidationError] = useState(''); + const navigate = useNavigate(); + + const getEmail = () => { + return localStorage.getItem('email'); + } + + const handleLogout = () => { + localStorage.removeItem('access_token'); + localStorage.removeItem('role'); + + navigate('/'); + }; + + useEffect(() => { + const fetchLibraryRequest = async () => { + const token = localStorage.getItem('access_token'); + if (!token) return; + + const userEmail = getEmail(); + if (userEmail) { + setEmail(userEmail); + } + + try { + const response = await fetch(`${API_BASE_URL}/api/library-requests/${submissionId}`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error('Failed to fetch library request'); + } + const data: LibraryRequest = await response.json(); + setLibraryRequest(data); + } catch (error) { + setError(error instanceof Error ? error.message : 'Unknown error'); + } + }; + + fetchLibraryRequest(); + }, [submissionId]); + + const handleAccept = async () => { + const token = localStorage.getItem('access_token'); + if (!token) return; + + try { + const response = await fetch(`${API_BASE_URL}/api/library-requests/${submissionId}/status?status=APPROVED`, { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error('Failed to accept the library request'); + } + + setLibraryRequest((prev) => prev ? { ...prev, status: 'APPROVED' } : prev); + + navigate('/system-admin-dashboard'); + } catch (error) { + setError(error instanceof Error ? error.message : 'Unknown error'); + } + }; + + const handleDecline = () => { + setShowRejectionInput(true); + }; + + const handleReasonChange = (e: React.ChangeEvent) => { + setSelectedReason(e.target.value); + if (e.target.value !== 'Inne') { + setRejectionReason(e.target.value); + } else { + setRejectionReason(''); + } + }; + + const handleInputChange = (e: React.ChangeEvent) => { + setRejectionReason(e.target.value); + }; + + const handleConfirmRejection = async () => { + if (!selectedReason) { + setValidationError('Proszę podać powód odrzucenia.'); + return; + } + + if (selectedReason === 'Inne' && !rejectionReason.trim()) { + setValidationError('Podaj przyczynę odmowy.'); + return; + } + + const token = localStorage.getItem('access_token'); + if (!token) return; + + try { + const response = await fetch(`${API_BASE_URL}/api/library-requests/${submissionId}/status?status=REJECTED&rejectionReason=${encodeURIComponent(rejectionReason)}`, { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error('Failed to reject the library request'); + } + + setLibraryRequest((prev) => prev ? { ...prev, status: 'REJECTED', rejectionReason } : prev); + setShowRejectionInput(false); + setSelectedReason(''); + setRejectionReason(''); + setValidationError(''); + + navigate('/system-admin-dashboard'); + } catch (error) { + setError(error instanceof Error ? error.message : 'Unknown error'); + } + }; + + if (error) { + return
Error: {error}
; + } + + return ( +
+
+
+ Book Rider Logo +
+
+
+ {email && {email}} + +
+
+
+ +
+
+

+ Szczegóły podania nr: {submissionId} +

+ +
+

Nazwa biblioteki: {libraryRequest?.libraryName || 'Brak nazwy'}

+

Email twórcy: {libraryRequest?.creatorEmail}

+

+ Adres: {libraryRequest?.address ? `${libraryRequest.address.street}, ${libraryRequest.address.city}, ${libraryRequest.address.postalCode}` : "Brak adresu"} +

+

Szerokość geograficzna: {libraryRequest?.address?.latitude || "Brak danych"} +

+

Długość geograficzna: {libraryRequest?.address?.longitude || "Brak danych"} +

+

Numer telefonu: {libraryRequest?.phoneNumber || "Brak numeru"}

+

Email biblioteki: {libraryRequest?.libraryEmail || "Brak adresu e-mail"}

+

Status: {libraryRequest?.status}

+

Data + złożenia: {libraryRequest?.submittedAt ? formatDate(libraryRequest.submittedAt) : "Brak danych"} +

+ {libraryRequest?.status === "Odrzucone" && ( +

Powód odrzucenia: {libraryRequest?.rejectionReason || "Brak powodu"}

+ )} +
+ + {!showRejectionInput && ( +
+ + +
+ )} + + {showRejectionInput && ( +
+ +
+ +

Wybierz powód odrzucenia:

+
+ {["Wprowadzono niepoprawne dane (nie możemy potwierdzić istnienia biblioteki)", + "Biblioteka została już dodana do systemu", + "Inne"].map((reason) => ( + + ))} + + {selectedReason === 'Inne' && ( + + )} + + {validationError && ( +
{validationError}
+ )} + +
+ +
+
+
+ )} + +
+ +
+ + + +
+
+
+
+ ); +}; + +export default SubmissionDetailsLibrary; diff --git a/wa/src/SystemAdmin/SystemAdminHomePage.tsx b/wa/src/SystemAdmin/SystemAdminHomePage.tsx new file mode 100644 index 00000000..513b52ef --- /dev/null +++ b/wa/src/SystemAdmin/SystemAdminHomePage.tsx @@ -0,0 +1,281 @@ +import React, {useEffect, useRef, useState} from 'react'; +import {useNavigate} from 'react-router-dom'; + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; + +interface DriverApplication { + id: number; + driverEmail: string; + status: string; + submittedAt: string; +} + +interface LibraryRequest { + id: number; + creatorEmail: string; + libraryName: string; + status: string; + submittedAt: string; +} + +const formatDate = (isoString: string) => { + return new Date(isoString).toLocaleString('pl-PL', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); +}; + +const SystemAdminDashboard: React.FC = () => { + const [email, setEmail] = useState(null); + const [activeSection, setActiveSection] = useState('librarySubmissions'); + const [driverApplications, setDriverApplications] = useState([]); + const [libraryRequests, setLibraryRequests] = useState([]); + const [driverPage, setDriverPage] = useState(0); + const [driverHasMore, setDriverHasMore] = useState(true); + const [libraryPage, setLibraryPage] = useState(0); + const [libraryHasMore, setLibraryHasMore] = useState(true); + const navigate = useNavigate(); + + const firstLoad = useRef(true); + + const getEmail = () => { + return localStorage.getItem('email'); + } + + const handleLogout = () => { + localStorage.removeItem('email'); + localStorage.removeItem('access_token'); + localStorage.removeItem('role'); + + navigate('/'); + }; + + useEffect(() => { + if (firstLoad.current) { + firstLoad.current = false; + return; + } + + const userEmail = getEmail(); + if (userEmail) { + setEmail(userEmail); + } + + if (activeSection === 'driverSubmissions') { + setDriverApplications([]); + setDriverPage(0); + setDriverHasMore(true); + fetchDriverApplications(0); + } else if (activeSection === 'librarySubmissions') { + setLibraryRequests([]); + setLibraryPage(0); + setLibraryHasMore(true); + fetchLibraryRequests(0); + } + }, [activeSection]); + + const fetchDriverApplications = async (page: number = 0) => { + const token = localStorage.getItem('access_token'); + if (!token) { + console.error("No token found."); + return; + } + + try { + const response = await fetch(`${API_BASE_URL}/api/driver-applications?statuses=PENDING,UNDER_REVIEW&page=${page}&size=1`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`Error: ${response.statusText}`); + } + + const data: DriverApplication[] = await response.json(); + + if (data.length > 0) { + setDriverApplications((prev) => [...prev, ...data]); + setDriverPage(page + 1); + } else { + setDriverHasMore(false); + } + } catch (error) { + console.error('Error:', error); + setDriverHasMore(false); + } + }; + + const fetchLibraryRequests = async (page: number) => { + const token = localStorage.getItem('access_token'); + if (!token) return; + + try { + const response = await fetch(`${API_BASE_URL}/api/library-requests?statuses=PENDING,UNDER_REVIEW&page=${page}&size=1`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) throw new Error(`Error:: ${response.statusText}`); + + const data: LibraryRequest[] = await response.json(); + + if (data.length > 0) { + setLibraryRequests((prev) => [...prev, ...data]); + setLibraryPage(page + 1); + } else { + setLibraryHasMore(false); + } + } catch (error) { + console.error('Error:', error); + setLibraryHasMore(false); + } + }; + + const updateStatusAndNavigate = async (id: number, type: 'driver' | 'library') => { + const token = localStorage.getItem('access_token'); + if (!token) return; + + const endpoint = type === 'driver' ? `/api/driver-applications/${id}/status?status=UNDER_REVIEW` : `/api/library-requests/${id}/status?status=UNDER_REVIEW`; + + try { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) throw new Error(`Error: ${response.statusText}`); + + navigate(type === 'driver' ? `/submissionDetailsDriver/${id}` : `/submissionDetailsLibrary/${id}`); + } catch (error) { + console.error('Error:', error); + } + }; + + return ( +
+
+
+ Book Rider Logo +
+
+ +
+ {email && ( + {email} + )} + +
+
+
+ +
+ + +
+ + +
+ {activeSection === 'driverSubmissions' && ( +
+
+ {driverApplications.length > 0 ? ( + driverApplications.map((application) => ( +
+

ID podania: {application.id}

+

Data wysłania: {formatDate(application.submittedAt)}

+

Utworzone przez: {application.driverEmail}

+ +
+ )) + ) : ( +

Brak podań.

+ )} +
+ {driverHasMore ? ( + + ) : ( +

Brak więcej podań do wyświetlenia.

+ )} +
+ )} + + {activeSection === 'librarySubmissions' && ( +
+
+ {libraryRequests.length > 0 ? ( + libraryRequests.map((request) => ( +
+

ID podania: {request.id}

+

Data wysłania: {formatDate(request.submittedAt)}

+

Utworzone przez: {request.creatorEmail}

+

Nazwa biblioteki: {request.libraryName}

+ +
+ )) + ) : ( +

Brak podań.

+ )} +
+ {libraryHasMore ? ( + + ) : ( +

Brak więcej podań do wyświetlenia.

+ )} +
+ )} +
+
+ ); +}; + +export default SystemAdminDashboard; diff --git a/wa/src/SystemAdmin/SystemAdminLogin.tsx b/wa/src/SystemAdmin/SystemAdminLogin.tsx new file mode 100644 index 00000000..f3d51e52 --- /dev/null +++ b/wa/src/SystemAdmin/SystemAdminLogin.tsx @@ -0,0 +1,223 @@ +import React, { useState } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; + +const SysAdminLogin: React.FC = () => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(null); + const [role, setRole] = useState('system_administrator'); + const [emailValid, setEmailValid] = useState(true); + const navigate = useNavigate(); + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + + if (name === 'email') { + setEmail(value); + + const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); + setEmailValid(isValid); + + if (!isValid) { + setError('Wprowadź poprawny adres email'); + } else { + setError(null); + } + } else if (name === 'password') { + setPassword(value); + } else if (name === 'role') { + setRole(value); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + + if (!emailValid) { + setError('Wprowadź poprawny adres email'); + return; + } + + try { + const response = await fetch(`${API_BASE_URL}/api/auth/login/${role}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + identifier: email, + password, + }), + }); + + if (response.ok) { + const authHeader = response.headers.get('Authorization'); + if (authHeader) { + const token = authHeader.split(' ')[1]; + + localStorage.setItem('access_token', token); + localStorage.setItem('role', role); + localStorage.setItem('email', email); + + navigate('/system-admin-dashboard'); + + } else { + setError('Authorization header missing'); + } + } else { + const errorData = await response.json(); + switch (errorData.code) { + case 401: + setError('Nieprawidłowy email lub hasło.'); + break; + case 500: + setError('Wewnętrzny błąd serwera. Spróbuj ponownie później.'); + break; + case 400: + setError('Należy podać adres email oraz hasło.'); + break; + default: + setError(errorData.message || 'Błąd logowania. Spróbuj ponownie.'); + break; + } + } + } catch (error) { + console.error('Login error: ', error); + setError('Podczas logowania wystąpił błąd'); + } + }; + + return ( +
+ + {/* Top Bar */} +
+
+
+ + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+
+

+ Logowanie
administratora systemów +

+
+
+ + +
+ +
+ + +
+ + {error && ( +
+ + + +

{error}

+
+ )} + + +
+
+
+
+
+ ); +}; + +export default SysAdminLogin; diff --git a/wa/src/Utils/ContactInfoPage.tsx b/wa/src/Utils/ContactInfoPage.tsx new file mode 100644 index 00000000..b40bd974 --- /dev/null +++ b/wa/src/Utils/ContactInfoPage.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import {Link} from "react-router-dom"; + +export const ContactInfoPage: React.FC = () => { + return ( +
+
+ {/* Big Book Logo Image */} + Book Rider Logo +

Kontakt

+

+ Informacje o sposobach kontaktu z zespołem BookRider. +

+ + + +
+
+ ); +}; + +export default ContactInfoPage; \ No newline at end of file diff --git a/wa/src/Utils/LegalInfoPage.tsx b/wa/src/Utils/LegalInfoPage.tsx new file mode 100644 index 00000000..5c7d4263 --- /dev/null +++ b/wa/src/Utils/LegalInfoPage.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import {Link} from "react-router-dom"; + +export const LegalInfoPage: React.FC = () => { + return ( +
+
+ {/* Big Book Logo Image */} + Book Rider Logo +

Informacje o kwestiach prawnych
związanych z systemem Book Rider

+ + + +
+
+ ); +}; + +export default LegalInfoPage; \ No newline at end of file diff --git a/wa/src/Utils/ProcessingInfoPage.tsx b/wa/src/Utils/ProcessingInfoPage.tsx new file mode 100644 index 00000000..7d6c4d2f --- /dev/null +++ b/wa/src/Utils/ProcessingInfoPage.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +const ProcessingPage: React.FC = () => { + return ( +
+
+ {/* Big Book Logo Image */} + Book Rider Logo +

Twoje zgłoszenie jest przetwarzane

+

+ Może to zająć od 1 do 7 dni roboczych.
Prosimy o cierpliwość. +

+ + + +
+
+ ); +}; + +export default ProcessingPage; diff --git a/wa/src/Utils/UserManualPage.tsx b/wa/src/Utils/UserManualPage.tsx new file mode 100644 index 00000000..1d4a2e5b --- /dev/null +++ b/wa/src/Utils/UserManualPage.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import {Link} from "react-router-dom"; + +export const UserManualPage: React.FC = () => { + return ( +
+
+ {/* Big Book Logo Image */} + Book Rider Logo +

Informacje o funkcjonowaniu systemu Book Rider

+ + + +
+
+ ); +}; + +export default UserManualPage; \ No newline at end of file diff --git a/wa/src/WebSocket/AppWebSocketNotification.tsx b/wa/src/WebSocket/AppWebSocketNotification.tsx new file mode 100644 index 00000000..677c6460 --- /dev/null +++ b/wa/src/WebSocket/AppWebSocketNotification.tsx @@ -0,0 +1,24 @@ +// // App.tsx (wrap this in all pages if needed) +// import useWebSocketConnection from "./useWebSocketConnection"; +// import { useToast } from "@/components/ui/use-toast"; +// +// const AppWebSocketNotification: React.FC = () => { +// const { toast } = useToast(); +// +// useWebSocketConnection({ +// channel: "librarian/orders/pending", +// onMessage: (message) => { +// // Show bottom-right notification +// toast({ +// title: "Nowe zamówienie", +// description: message, +// duration: 5000, +// className: "bottom-4 right-4 fixed", // depends on toast lib +// }); +// }, +// }); +// +// return null; // Just a hook, no visible content +// }; +// +// export default AppWebSocketNotification; diff --git a/wa/src/WebSocket/WebSocketProvider.tsx b/wa/src/WebSocket/WebSocketProvider.tsx new file mode 100644 index 00000000..acef20c3 --- /dev/null +++ b/wa/src/WebSocket/WebSocketProvider.tsx @@ -0,0 +1,43 @@ +import { useEffect, useRef } from "react"; + +interface UseWebSocketOptions { + channel: string; + onMessage: (msg: string) => void; +} + +const useWebSocketConnection = ({ channel, onMessage }: UseWebSocketOptions) => { + const wsRef = useRef(null); + + useEffect(() => { + const token = localStorage.getItem('access_token'); // ✅ For web + + if (!token) { + console.warn("No JWT token found in localStorage."); + return; + } + + const baseUrl = import.meta.env.VITE_API_BASE_URL || ""; + const domain = baseUrl.replace(/^https?:\/\//, ""); // Strip protocol + const wsUrl = `wss://${domain}/ws?token=${encodeURIComponent(token)}&channel=${encodeURIComponent(channel)}`; + + const ws = new WebSocket(wsUrl); + wsRef.current = ws; + + ws.onopen = () => console.log("WebSocket connected:", wsUrl); + ws.onmessage = (event) => { + console.log("WebSocket message:", event.data); + onMessage(event.data); + }; + ws.onerror = (error) => console.error("WebSocket error:", error); + ws.onclose = () => console.log("WebSocket closed"); + + return () => { + if (wsRef.current) { + wsRef.current.close(); + wsRef.current = null; + } + }; + }, [channel, onMessage]); +}; + +export default useWebSocketConnection; diff --git a/wa/src/WebSocket/useWebSocketConnection.tsx b/wa/src/WebSocket/useWebSocketConnection.tsx new file mode 100644 index 00000000..f3b7794f --- /dev/null +++ b/wa/src/WebSocket/useWebSocketConnection.tsx @@ -0,0 +1,47 @@ +import { useEffect, useRef } from "react"; + +const useWebSocketConnection = ( + channel: string, + onMessage: (msg: string) => void +) => { + const wsRef = useRef(null); + + useEffect(() => { + const jwtToken = localStorage.getItem('access_token'); + if (!jwtToken || wsRef.current) return; // 💡 Avoid reconnect if already connected + + const apiBaseUrl = import.meta.env.VITE_API_BASE_URL?.replace(/^https?:\/\//, '') || ""; + const wsUrl = `wss://${apiBaseUrl}/ws?token=${encodeURIComponent(jwtToken)}&channel=${encodeURIComponent(channel)}`; + const ws = new WebSocket(wsUrl); + + console.log(`✅ WebSocket connecting to: ${wsUrl}`); + + ws.onopen = () => { + console.log(`✅ WebSocket connected to: ${wsUrl}`); + }; + + ws.onmessage = (e) => { + onMessage(e.data); + }; + + ws.onerror = (err) => { + console.error("WebSocket error:", err); + }; + + ws.onclose = () => { + console.log("WebSocket closed"); + wsRef.current = null; + }; + + wsRef.current = ws; + + return () => { + if (wsRef.current) { + wsRef.current.close(); + wsRef.current = null; + } + }; + }, [channel, onMessage]); +}; + +export default useWebSocketConnection; diff --git a/wa/src/index.css b/wa/src/index.css new file mode 100644 index 00000000..c485c151 --- /dev/null +++ b/wa/src/index.css @@ -0,0 +1,31 @@ +@import url("https://fonts.googleapis.com/css?family=Be+Vietnam+Pro:400,700,600"); +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer components { + .all-\[unset\] { + all: unset; + } +} + +* { + -webkit-font-smoothing: antialiased; + box-sizing: border-box; +} + +html, +body { + margin: 0; + height: 100%; + font-family: 'Be Vietnam Pro', sans-serif !important; +} + +/*button:focus-visible {*/ +/* outline: 2px solid #4a90e2 !important;*/ +/* outline: -webkit-focus-ring-color auto 5px !important;*/ +/*}*/ + +a { + text-decoration: none; +} diff --git a/wa/src/main.tsx b/wa/src/main.tsx new file mode 100644 index 00000000..bef5202a --- /dev/null +++ b/wa/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/wa/src/tailwind.css b/wa/src/tailwind.css new file mode 100644 index 00000000..7ef0f8d6 --- /dev/null +++ b/wa/src/tailwind.css @@ -0,0 +1,13 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer components { + .all-\[unset\] { + all: unset; + } +} + +.peer:invalid { + outline: 2px solid red; +} \ No newline at end of file diff --git a/wa/src/vite-env.d.ts b/wa/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/wa/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/wa/tailwind.config.js b/wa/tailwind.config.js new file mode 100644 index 00000000..f65df7dc --- /dev/null +++ b/wa/tailwind.config.js @@ -0,0 +1,14 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + './src/**/*.{html,js,jsx,ts,tsx}', + ], + theme: { + extend: { + fontFamily: { + 'be-vietnam': ['Be_Vietnam_Pro', 'sans-serif'], + }, + }, + }, + plugins: [], +}; diff --git a/wa/tsconfig.app.json b/wa/tsconfig.app.json new file mode 100644 index 00000000..f867de0d --- /dev/null +++ b/wa/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/wa/tsconfig.json b/wa/tsconfig.json new file mode 100644 index 00000000..1ffef600 --- /dev/null +++ b/wa/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/wa/tsconfig.node.json b/wa/tsconfig.node.json new file mode 100644 index 00000000..abcd7f0d --- /dev/null +++ b/wa/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/wa/vite.config.ts b/wa/vite.config.ts new file mode 100644 index 00000000..8b0f57b9 --- /dev/null +++ b/wa/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/web_app/src/LibraryAdmin/LibraryAdminAddLibrarian.tsx b/web_app/src/LibraryAdmin/LibraryAdminAddLibrarian.tsx index 975be314..ccfd4905 100644 --- a/web_app/src/LibraryAdmin/LibraryAdminAddLibrarian.tsx +++ b/web_app/src/LibraryAdmin/LibraryAdminAddLibrarian.tsx @@ -112,7 +112,8 @@ const LibraryAdminHomePage: React.FC = () => {
- {message &&

{message}

} + {message &&

{message}

} +
diff --git a/web_app/src/LibraryAdmin/LibraryAdminLogin.tsx b/web_app/src/LibraryAdmin/LibraryAdminLogin.tsx index b4c3b9d5..d5e7900d 100644 --- a/web_app/src/LibraryAdmin/LibraryAdminLogin.tsx +++ b/web_app/src/LibraryAdmin/LibraryAdminLogin.tsx @@ -80,7 +80,7 @@ const LibraryAdminLogin: React.FC = () => { navigate('/add-library'); } } else { - navigate('/'); // Landing page + navigate('/'); // landing page } } else { diff --git a/web_app/src/SystemAdmin/SystemAdminHomePage.tsx b/web_app/src/SystemAdmin/SystemAdminHomePage.tsx index 97b83a15..5e08e861 100644 --- a/web_app/src/SystemAdmin/SystemAdminHomePage.tsx +++ b/web_app/src/SystemAdmin/SystemAdminHomePage.tsx @@ -44,14 +44,14 @@ const SystemAdminDashboard: React.FC = () => { const firstLoad = useRef(true); - useWebSocketNotification('system-administrator/library-requests/pending', () => { + useWebSocketNotification('administrator/library-requests', () => { toast.info("Otrzymano nowe zgłoszenie biblioteki!", { position: "bottom-right", }); console.log("New library request received!"); }); - useWebSocketNotification('system-administrator/driver-requests/pending', () => { + useWebSocketNotification('administrator/driver-applications', () => { toast.info("Otrzymano nowe zgłoszenie kierowcy!", { position: "bottom-right", });