24 Commits

Author SHA1 Message Date
RKaraMos_EVO 24849c6414 feat(front): promoCode validation + estimation du total via API 2026-06-14 16:47:24 +02:00
RKaraMos_EVO d7dfec49fd feat(front): ajouter champ promoCode pour commandes 2026-06-14 16:44:55 +02:00
AISSI-JUDE-CHRIST 0d9f8dc539 Créer un abonnement 2026-06-14 16:12:47 +02:00
Riad KARA-MOSTEFA 4197f99116 Merge pull request 'Api java' (#8) from api_java into main
Reviewed-on: #8
2026-06-14 15:28:23 +02:00
AISSI-JUDE-CHRIST 0d37efd227 merge retours livres admin 2026-06-14 00:10:02 +02:00
Adrien DICK 1cf4bed83b Merge pull request ' Faire une commande' (#7) from Commande into main
Reviewed-on: #7
2026-06-13 11:34:02 +02:00
Adrien DICK 0ba4e5599e Faire une commande 2026-06-13 05:32:59 -04:00
AISSI-JUDE-CHRIST 587e81816c Gérer les retours de livres par l'admin 2026-06-13 01:11:27 +02:00
AISSI-JUDE-CHRIST 8359f21e07 Merge 2026-06-13 00:48:12 +02:00
Riad KARA-MOSTEFA e3c02a6fc8 Merge pull request 'Api java' (#6) from api_java into main
Reviewed-on: #6
2026-06-12 23:34:30 +02:00
AISSI-JUDE-CHRIST bfd1f600de Gerer un avis 2026-06-12 22:58:43 +02:00
AISSI-JUDE-CHRIST 52213fce0b re Correction page login 2026-06-12 21:37:12 +02:00
AISSI-JUDE-CHRIST 7117aba838 Correction page login 2026-06-12 21:35:40 +02:00
AISSI-JUDE-CHRIST 010ec9c11a reservation livres 2026-06-11 23:08:21 +02:00
AISSI-JUDE-CHRIST de47c293b8 correction affiche livre 2026-06-11 22:36:47 +02:00
AISSI-JUDE-CHRIST 4ae227d79d ajout authentification admin/user 2026-06-11 22:23:39 +02:00
Adrien DICK 8f3451adc0 Merge pull request 'Gestion client' (#5) from GestionClient into main
Reviewed-on: #5
2026-06-11 16:49:36 +02:00
Riad KARA-MOSTEFA 9916b02ef6 Merge pull request 'Api java' (#4) from api_java into main
Reviewed-on: #4
2026-06-11 14:28:28 +02:00
Adrien DICK e33acdf151 Ajouter et Retirer des points de fidélité 2026-06-11 07:59:03 -04:00
Adrien DICK ccafc24f72 Ajouter un client 2026-06-11 07:27:37 -04:00
Adrien DICK c811c373d5 Merge pull request ' Voir le detail d'un livre' (#3) from DetailLivre into main
Reviewed-on: #3
2026-06-11 11:55:29 +02:00
Adrien DICK 1f9a1e9af8 Voir le detail d'un livre 2026-06-11 05:52:26 -04:00
Adrien DICK e438373370 Merge pull request ' Enregistrer un livre' (#2) from EnregistrerLivre into main
Reviewed-on: #2
2026-06-11 11:35:23 +02:00
Adrien DICK 2c0f30f0a2 Enregistrer un livre 2026-06-11 05:15:30 -04:00
22 changed files with 1117 additions and 16 deletions
+30 -3
View File
@@ -1,19 +1,46 @@
import { Routes, Route } from 'react-router-dom'; import { Routes, Route, Navigate } from 'react-router-dom';
import Layout from './components/Layout'; import Layout from './components/Layout';
import Home from './pages/Home'; import Home from './pages/Home';
import Books from './pages/Books'; import Books from './pages/Books';
import Orders from './pages/Orders'; import Orders from './pages/Orders';
import Profile from './pages/Profile'; import Profile from './pages/Profile';
import NotFound from './pages/NotFound'; import NotFound from './pages/NotFound';
import AddBook from './pages/AddBook';
import BookDetail from './pages/BookDetail';
import Customers from './pages/Customers';
import Login from './pages/Login';
import Reservations from './pages/Reservations';
import Returns from './pages/Returns';
import Subscription from './pages/Subscription';
import { useAuth } from './context/AuthContext';
function RequireAuth({ children }) {
const { user } = useAuth();
return user ? children : <Navigate to="/login" replace />;
}
function RequireAdmin({ children }) {
const { user } = useAuth();
if (!user) return <Navigate to="/login" replace />;
if (user.role !== 'admin') return <Navigate to="/" replace />;
return children;
}
export default function App() { export default function App() {
return ( return (
<Routes> <Routes>
<Route path="/login" element={<Login />} />
<Route path="/" element={<Layout />}> <Route path="/" element={<Layout />}>
<Route index element={<Home />} /> <Route index element={<Home />} />
<Route path="books" element={<Books />} /> <Route path="books" element={<Books />} />
<Route path="orders" element={<Orders />} /> <Route path="books/:bookId" element={<RequireAuth><BookDetail /></RequireAuth>} />
<Route path="profile" element={<Profile />} /> <Route path="books/new" element={<RequireAdmin><AddBook /></RequireAdmin>} />
<Route path="orders" element={<RequireAuth><Orders /></RequireAuth>} />
<Route path="reservations" element={<RequireAuth><Reservations /></RequireAuth>} />
<Route path="profile" element={<RequireAuth><Profile /></RequireAuth>} />
<Route path="customers" element={<RequireAdmin><Customers /></RequireAdmin>} />
<Route path="returns" element={<RequireAdmin><Returns /></RequireAdmin>} />
<Route path="subscription" element={<RequireAuth><Subscription /></RequireAuth>} />
<Route path="*" element={<NotFound />} /> <Route path="*" element={<NotFound />} />
</Route> </Route>
</Routes> </Routes>
+17
View File
@@ -0,0 +1,17 @@
import client from './client';
export function getBooks(page = 0, size = 20) {
return client.get('/api/books', { params: { page, size } });
}
export function registerBook(book) {
return client.post('/api/books', book);
}
export function getBookById(id) {
return client.get(`/api/books/${id}`);
}
export function reserveBook(bookId, reservation) {
return client.post(`/api/books/${bookId}/reservations`, reservation);
}
-5
View File
@@ -1,5 +0,0 @@
import client from './client';
export function getBooks(page = 0, size = 20) {
return client.get('/api/books', { params: { page, size } });
}
+17
View File
@@ -0,0 +1,17 @@
import client from './client';
export function registerCustomer(customer) {
return client.post('/api/customers', customer);
}
export function findCustomerByPhone(phoneNumber) {
return client.get(`/api/customers/phone/${encodeURIComponent(phoneNumber)}`);
}
export function addLoyaltyPoints(customerId, points) {
return client.post(`/api/customers/${customerId}/loyalty/add`, null, { params: { points } });
}
export function subtractLoyaltyPoints(customerId, points) {
return client.post(`/api/customers/${customerId}/loyalty/subtract`, null, { params: { points } });
}
+5
View File
@@ -0,0 +1,5 @@
import client from './client';
export function createOrder(order) {
return client.post('/api/orders', order);
}
+24 -5
View File
@@ -1,7 +1,16 @@
import { Link } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import '../styles/navbar.css'; import '../styles/navbar.css';
export default function Navbar() { export default function Navbar() {
const { user, logout } = useAuth();
const navigate = useNavigate();
function handleLogout() {
logout();
navigate('/login');
}
return ( return (
<nav className="navbar"> <nav className="navbar">
<Link to="/" className="navbar__logo">Biblio</Link> <Link to="/" className="navbar__logo">Biblio</Link>
@@ -9,13 +18,23 @@ export default function Navbar() {
<ul className="navbar__links"> <ul className="navbar__links">
<li><Link to="/">Accueil</Link></li> <li><Link to="/">Accueil</Link></li>
<li><Link to="/books">Catalogue</Link></li> <li><Link to="/books">Catalogue</Link></li>
<li><Link to="/orders">Commandes</Link></li> {user && <li><Link to="/orders">Commandes</Link></li>}
<li><Link to="/profile">Mon compte</Link></li> {user?.role === 'user' && <li><Link to="/reservations">Mes réservations</Link></li>}
{user?.role === 'user' && <li><Link to="/subscription">Mon abonnement</Link></li>}
{user && <li><Link to="/profile">Mon compte</Link></li>}
{user?.role === 'admin' && <li><Link to="/customers">Clients</Link></li>}
{user?.role === 'admin' && <li><Link to="/returns">Retours</Link></li>}
</ul> </ul>
<div className="navbar__actions"> <div className="navbar__actions">
<button className="btn-ghost">Connexion</button> {user ? (
<button className="btn-cta">S'inscrire</button> <>
<span>{user.username} ({user.role})</span>
<button onClick={handleLogout}>Déconnexion</button>
</>
) : (
<Link to="/login"><button>Connexion</button></Link>
)}
</div> </div>
</nav> </nav>
); );
+39
View File
@@ -0,0 +1,39 @@
import { createContext, useContext, useState } from 'react';
const USERS = [
{ username: 'admin', password: 'admin', role: 'admin' },
{ username: 'alice', password: 'bob', role: 'user' },
];
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(() => {
const saved = localStorage.getItem('auth');
return saved ? JSON.parse(saved) : null;
});
function login(username, password) {
const found = USERS.find(u => u.username === username && u.password === password);
if (!found) return false;
const { password: _, ...safe } = found;
setUser(safe);
localStorage.setItem('auth', JSON.stringify(safe));
return true;
}
function logout() {
setUser(null);
localStorage.removeItem('auth');
}
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
return useContext(AuthContext);
}
@@ -0,0 +1,42 @@
import { createContext, useContext, useState } from 'react';
const ReservationContext = createContext(null);
export function ReservationProvider({ children }) {
const [reservations, setReservations] = useState(() => {
const saved = localStorage.getItem('reservations');
return saved ? JSON.parse(saved) : [];
});
function addReservation(book, phoneNumber) {
const reservation = {
reservationId: crypto.randomUUID(),
bookId: book.isbn,
bookTitle: book.title,
bookAuthor: book.author,
phoneNumber,
status: 'CONFIRMED',
reservedAt: new Date().toISOString(),
};
const updated = [...reservations, reservation];
setReservations(updated);
localStorage.setItem('reservations', JSON.stringify(updated));
return reservation;
}
function cancelReservation(reservationId) {
const updated = reservations.filter(r => r.reservationId !== reservationId);
setReservations(updated);
localStorage.setItem('reservations', JSON.stringify(updated));
}
return (
<ReservationContext.Provider value={{ reservations, addReservation, cancelReservation }}>
{children}
</ReservationContext.Provider>
);
}
export function useReservations() {
return useContext(ReservationContext);
}
+36
View File
@@ -0,0 +1,36 @@
import { createContext, useContext, useState } from 'react';
const ReturnContext = createContext(null);
export function ReturnProvider({ children }) {
const [returns, setReturns] = useState(() => {
const saved = localStorage.getItem('returns');
return saved ? JSON.parse(saved) : [];
});
function addReturn(bookId, bookTitle, customerPhone, reason) {
const bookReturn = {
returnId: crypto.randomUUID(),
bookId,
bookTitle,
customerPhone,
reason,
status: 'PROCESSED',
returnedAt: new Date().toISOString(),
};
const updated = [...returns, bookReturn];
setReturns(updated);
localStorage.setItem('returns', JSON.stringify(updated));
return bookReturn;
}
return (
<ReturnContext.Provider value={{ returns, addReturn }}>
{children}
</ReturnContext.Provider>
);
}
export function useReturns() {
return useContext(ReturnContext);
}
+40
View File
@@ -0,0 +1,40 @@
import { createContext, useContext, useState } from 'react';
const ReviewContext = createContext(null);
export function ReviewProvider({ children }) {
const [reviews, setReviews] = useState(() => {
const saved = localStorage.getItem('reviews');
return saved ? JSON.parse(saved) : [];
});
function addReview(bookId, bookTitle, username, rating, comment) {
const review = {
reviewId: crypto.randomUUID(),
bookId,
bookTitle,
username,
rating,
comment,
createdAt: new Date().toISOString(),
};
const updated = [...reviews, review];
setReviews(updated);
localStorage.setItem('reviews', JSON.stringify(updated));
return review;
}
function getReviewsByBook(bookId) {
return reviews.filter(r => String(r.bookId) === String(bookId));
}
return (
<ReviewContext.Provider value={{ reviews, addReview, getReviewsByBook }}>
{children}
</ReviewContext.Provider>
);
}
export function useReviews() {
return useContext(ReviewContext);
}
@@ -0,0 +1,50 @@
import { createContext, useContext, useState } from 'react';
const SubscriptionContext = createContext(null);
const PLANS = [
{ id: 'basic', name: 'Basic', price: 9.99, description: '2 livres par mois' },
{ id: 'standard', name: 'Standard', price: 14.99, description: '5 livres par mois' },
{ id: 'premium', name: 'Premium', price: 24.99, description: 'Livres illimités par mois' },
];
export { PLANS };
export function SubscriptionProvider({ children }) {
const [subscription, setSubscription] = useState(() => {
const saved = localStorage.getItem('subscription');
return saved ? JSON.parse(saved) : null;
});
function subscribe(planId, phoneNumber) {
const plan = PLANS.find(p => p.id === planId);
const newSubscription = {
subscriptionId: crypto.randomUUID(),
planId,
planName: plan.name,
price: plan.price,
phoneNumber,
status: 'ACTIVE',
startDate: new Date().toISOString(),
};
setSubscription(newSubscription);
localStorage.setItem('subscription', JSON.stringify(newSubscription));
return newSubscription;
}
function cancelSubscription() {
const updated = { ...subscription, status: 'CANCELLED' };
setSubscription(updated);
localStorage.setItem('subscription', JSON.stringify(updated));
}
return (
<SubscriptionContext.Provider value={{ subscription, subscribe, cancelSubscription, PLANS }}>
{children}
</SubscriptionContext.Provider>
);
}
export function useSubscription() {
return useContext(SubscriptionContext);
}
+16 -1
View File
@@ -1,6 +1,11 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import { AuthProvider } from './context/AuthContext';
import { ReservationProvider } from './context/ReservationContext';
import { ReviewProvider } from './context/ReviewContext';
import { ReturnProvider } from './context/ReturnContext';
import { SubscriptionProvider } from './context/SubscriptionContext';
import App from './App'; import App from './App';
import './styles/global.css'; import './styles/global.css';
@@ -8,7 +13,17 @@ const root = ReactDOM.createRoot(document.getElementById('root'));
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<BrowserRouter> <BrowserRouter>
<App /> <AuthProvider>
<ReservationProvider>
<ReviewProvider>
<ReturnProvider>
<SubscriptionProvider>
<App />
</SubscriptionProvider>
</ReturnProvider>
</ReviewProvider>
</ReservationProvider>
</AuthProvider>
</BrowserRouter> </BrowserRouter>
</React.StrictMode> </React.StrictMode>
); );
+105
View File
@@ -0,0 +1,105 @@
import { useState } from 'react';
import { registerBook } from '../api/books';
const CATEGORIES = ['FICTION', 'NON_FICTION', 'SCIENCE_FICTION', 'FANTASY', 'MYSTERY', 'THRILLER', 'ROMANCE', 'BIOGRAPHY', 'HISTORY', 'POETRY', 'CHILDRENS', 'YOUNG_ADULT', 'SCIENCE', 'PHILOSOPHY', 'SELF_HELP', 'TRAVEL', 'COOKING', 'ART', 'RELIGION', 'REFERENCE'];
const initialForm = {
isbn: '', title: '', author: '', publisher: '', publicationDate: '', price: '', quantity: '', categories: [], description: '', language: '',
};
export default function AddBook() {
const [form, setForm] = useState(initialForm);
const [message, setMessage] = useState(null);
const [submitting, setSubmitting] = useState(false);
function handleChange(e) {
const { name, value } = e.target;
setForm((prev) => ({ ...prev, [name]: value }));
}
function handleCategoryChange(e) {
setForm((prev) => ({ ...prev, categories: e.target.value ? [e.target.value] : [] }));
}
function handleSubmit(e) {
e.preventDefault();
setSubmitting(true);
setMessage(null);
const payload = {
...form,
isbn: Number(form.isbn),
price: Number(form.price),
quantity: Number(form.quantity),
};
registerBook(payload)
.then((response) => {
setMessage({ type: 'success', text: `Livre créé (id : ${response.data})` });
setForm(initialForm);
})
.catch((error) => {
console.error(error);
const status = error.response?.status;
if (status === 406) setMessage({ type: 'error', text: 'Ce livre existe déjà.' });
else if (status === 400) setMessage({ type: 'error', text: 'Données invalides, vérifie les champs.' });
else setMessage({ type: 'error', text: 'Erreur lors de la création.' });
})
.finally(() => setSubmitting(false));
}
return (
<main>
<h1>Ajouter un livre</h1>
<form onSubmit={handleSubmit}>
<table>
<tr>
<td>ISBN</td>
<td><input name="isbn" type="number" value={form.isbn} onChange={handleChange} required /></td>
</tr>
<tr>
<td>Titre</td>
<td><input name="title" value={form.title} onChange={handleChange} required /></td>
</tr>
<tr>
<td>Auteur</td>
<td><input name="author" value={form.author} onChange={handleChange} required /></td>
</tr>
<tr>
<td>Éditeur</td>
<td><input name="publisher" value={form.publisher} onChange={handleChange} required /></td>
</tr>
<tr>
<td>Date de publication</td>
<td><input name="publicationDate" type="date" value={form.publicationDate} onChange={handleChange} required /></td>
</tr>
<tr>
<td>Prix</td>
<td><input name="price" type="number" step="0.01" value={form.price} onChange={handleChange} required /></td>
</tr>
<tr>
<td>Quantité</td>
<td><input name="quantity" type="number" value={form.quantity} onChange={handleChange} required /></td>
</tr>
<tr>
<td>Langue</td>
<td><input name="language" value={form.language} onChange={handleChange} required /></td>
</tr>
<tr>
<td>Catégorie</td>
<td><select value={form.categories[0] || ''} onChange={handleCategoryChange} required><option value=""> choisir </option>{CATEGORIES.map((c) => <option key={c} value={c}>{c}</option>)}</select></td>
</tr>
<tr>
<td>Description</td>
<td><textarea name="description" value={form.description} onChange={handleChange} /></td>
</tr>
</table>
<button type="submit" disabled={submitting}>
{submitting ? 'Envoi…' : 'Ajouter le livre'}
</button>
</form>
{message && <p>{message.text}</p>}
</main>
);
}
+158
View File
@@ -0,0 +1,158 @@
import { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { getBookById } from '../api/books';
import { useAuth } from '../context/AuthContext';
import { useReservations } from '../context/ReservationContext';
import { useReviews } from '../context/ReviewContext';
import { useSubscription } from '../context/SubscriptionContext';
export default function BookDetail() {
const { user } = useAuth();
const { subscription } = useSubscription();
const { addReservation, reservations } = useReservations();
const { addReview, getReviewsByBook } = useReviews();
const [rating, setRating] = useState(5);
const [comment, setComment] = useState('');
const [reviewStatus, setReviewStatus] = useState(null);
const { bookId } = useParams();
const [book, setBook] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [phoneNumber, setPhoneNumber] = useState('');
const [reservationStatus, setReservationStatus] = useState(null);
useEffect(() => { getBookById(bookId).then((response) => setBook(response.data)).catch((err) => { console.error(err); setError('Livre introuvable.'); }).finally(() => setLoading(false)); }, [bookId]);
function handleReservation(e) {
e.preventDefault();
addReservation(book, phoneNumber);
setReservationStatus({ success: true, message: 'Réservation effectuée avec succès !' });
setPhoneNumber('');
}
function handleReview(e) {
e.preventDefault();
addReview(book.isbn, book.title, user.username, Number(rating), comment);
setReviewStatus({ success: true, message: 'Avis publié avec succès !' });
setComment('');
setRating(5);
}
if (loading) return <main><p>Chargement</p></main>;
if (error) return <main><p>{error}</p><Link to="/books"> Retour au catalogue</Link></main>;
return (
<main>
<Link to="/books"> Retour au catalogue</Link>
<h1>{book.title}</h1>
<p>Auteur : {book.author}</p>
<p>ISBN : {book.isbn}</p>
<p>Éditeur : {book.publisher}</p>
<p>Publié le : {book.publicationDate}</p>
<p>Prix : {book.price} </p>
<p>Stock : {book.quantity}</p>
<p>Langue : {book.language}</p>
<p>Catégories : {book.categories?.join(', ')}</p>
{book.description && <p>{book.description}</p>}
<section>
<h2>Avis des lecteurs</h2>
{getReviewsByBook(book.isbn).length === 0 ? (
<p>Aucun avis pour le moment.</p>
) : (
<ul>
{getReviewsByBook(book.isbn).map(r => (
<li key={r.reviewId}>
<strong>{r.username}</strong> {'*'.repeat(r.rating)}
<br />
{r.comment}
<br />
<small>{new Date(r.createdAt).toLocaleDateString('fr-FR')}</small>
</li>
))}
</ul>
)}
{user?.role === 'user' && (
<form onSubmit={handleReview}>
<h3>Laisser un avis</h3>
<label>
Note :
<select value={rating} onChange={e => setRating(e.target.value)}>
<option value={1}>*</option>
<option value={2}>**</option>
<option value={3}>***</option>
<option value={4}>****</option>
<option value={5}>*****</option>
</select>
</label>
<label>
Commentaire :
<textarea value={comment} onChange={e => setComment(e.target.value)} required />
</label>
<button type="submit">Publier l'avis</button>
</form>
)}
{reviewStatus && (
<p style={{ color: reviewStatus.success ? 'green' : 'red' }}>
{reviewStatus.message}
</p>
)}
</section>
{user?.role === 'user' && (
<section>
<h2>Réserver ce livre</h2>
{!subscription || subscription.status !== 'ACTIVE' ? (
<p>
Vous devez avoir un abonnement actif pour réserver un livre.{' '}
<Link to="/subscription">S'abonner</Link>
</p>
) : (() => {
const QUOTAS = { basic: 2, standard: 5, premium: Infinity };
const quota = QUOTAS[subscription.planId];
const thisMonthStart = new Date();
thisMonthStart.setDate(1);
thisMonthStart.setHours(0, 0, 0, 0);
const usedThisMonth = reservations.filter(r =>
new Date(r.reservedAt) >= thisMonthStart
).length;
const remaining = quota - usedThisMonth;
if (remaining <= 0) {
return (
<p style={{ color: 'orange' }}>
Vous avez atteint votre quota de {quota} réservation(s) ce mois-ci.
<Link to="/subscription"> Passer au plan supérieur</Link>
</p>
);
}
return (
<>
<p>Réservations restantes ce mois : <strong>{remaining === Infinity ? 'illimitées' : remaining}</strong></p>
<form onSubmit={handleReservation}>
<label>
Numéro de téléphone :
<input
type="tel"
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value)}
placeholder="0612345678"
required
/>
</label>
<button type="submit">Réserver</button>
</form>
{reservationStatus && (
<p style={{ color: reservationStatus.success ? 'green' : 'red' }}>
{reservationStatus.message}
</p>
)}
</>
);
})()}
</section>
)}
</main>
);
}
+7 -1
View File
@@ -1,7 +1,10 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { getBooks } from '../api/books'; import { getBooks } from '../api/books';
import { Link } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
export default function Books() { export default function Books() {
const { user } = useAuth();
const [books, setBooks] = useState([]); const [books, setBooks] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
@@ -27,10 +30,13 @@ export default function Books() {
return ( return (
<main> <main>
<h1>Catalogue</h1> <h1>Catalogue</h1>
{user?.role === 'admin' && <Link to="/books/new">+ Ajouter un livre</Link>}
<ul> <ul>
{books.map((book) => ( {books.map((book) => (
<li key={book.isbn}> <li key={book.isbn}>
<strong>{book.title}</strong> - {book.author} <Link to={`/books/${book.isbn}`}>
<strong>{book.title}</strong> - {book.author}
</Link>
</li> </li>
))} ))}
</ul> </ul>
+109
View File
@@ -0,0 +1,109 @@
import { useState } from 'react';
import { registerCustomer, findCustomerByPhone, addLoyaltyPoints, subtractLoyaltyPoints } from '../api/customers';
const initialForm = { firstName: '', lastName: '', phoneNumber: '' };
export default function Customers() {
const [form, setForm] = useState(initialForm);
const [registerMsg, setRegisterMsg] = useState(null);
const [submitting, setSubmitting] = useState(false);
const [pointsInput, setPointsInput] = useState('');
function handleChange(e) {
const { name, value } = e.target;
setForm((prev) => ({ ...prev, [name]: value }));
}
function handleRegister(e) {
e.preventDefault();
setSubmitting(true);
setRegisterMsg(null);
registerCustomer(form)
.then((response) => {
setRegisterMsg(`Client créé (id : ${response.data})`);
setForm(initialForm);
})
.catch((error) => {
console.error(error);
setRegisterMsg('Erreur lors de la création du client.');
})
.finally(() => setSubmitting(false));
}
const [phone, setPhone] = useState('');
const [customer, setCustomer] = useState(null);
const [searchError, setSearchError] = useState(null);
function handleSearch(e) {
e.preventDefault();
setSearchError(null);
setCustomer(null);
findCustomerByPhone(phone)
.then((response) => setCustomer(response.data))
.catch((error) => {
console.error(error);
setSearchError('Aucun client trouvé avec ce numéro.');
});
}
function applyLoyalty(operation) {
const points = Number(pointsInput);
const action = operation === 'add' ? addLoyaltyPoints : subtractLoyaltyPoints;
action(customer.id, points)
.then((response) => {
setCustomer((prev) => ({ ...prev, loyaltyPoints: response.data }));
setPointsInput('');
})
.catch((error) => {
console.error(error);
if (error.response?.status === 400) setSearchError('Pas assez de points pour ce retrait.');
else setSearchError('Erreur lors de la mise à jour des points.');
});
}
return (
<main>
<h1>Gestion des clients</h1>
<section>
<h2>Enregistrer un client</h2>
<form onSubmit={handleRegister}>
<label>Prénom
<input name="firstName" value={form.firstName} onChange={handleChange} required />
</label>
<label>Nom
<input name="lastName" value={form.lastName} onChange={handleChange} required />
</label>
<label>Téléphone
<input name="phoneNumber" value={form.phoneNumber} onChange={handleChange} required />
</label>
<button type="submit" disabled={submitting}>
{submitting ? 'Envoi…' : 'Créer le client'}
</button>
</form>
{registerMsg && <p>{registerMsg}</p>}
</section>
<section>
<h2>Rechercher un client</h2>
<form onSubmit={handleSearch}>
<label>Téléphone
<input value={phone} onChange={(e) => setPhone(e.target.value)} required />
</label>
<button type="submit">Rechercher</button>
</form>
{searchError && <p>{searchError}</p>}
{customer && (
<div>
<p>{customer.firstName} {customer.lastName}</p>
<p>Téléphone : {customer.phoneNumber}</p>
<p><strong>Points de fidélité : {customer.loyaltyPoints}</strong></p>
<input type="number"value={pointsInput}onChange={(e) => setPointsInput(e.target.value)}placeholder="Nombre de points"/>
<button type="button" onClick={() => applyLoyalty('add')}>Ajouter</button>
<button type="button" onClick={() => applyLoyalty('subtract')}>Retirer</button>
</div>
)}
</section>
</main>
);
}
+57
View File
@@ -0,0 +1,57 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
export default function Login() {
const { login } = useAuth();
const navigate = useNavigate();
const [form, setForm] = useState({ username: '', password: '' });
const [error, setError] = useState(null);
function handleSubmit(e) {
e.preventDefault();
const ok = login(form.username, form.password);
if (ok) {
navigate('/');
} else {
setError('Identifiants incorrects.');
}
}
return (
<main>
<h1>Connexion</h1>
<form onSubmit={handleSubmit}>
<div>
<label>
Nom d'utilisateur :
<input
type="text"
value={form.username}
onChange={e => setForm(f => ({ ...f, username: e.target.value }))}
required
/>
</label>
</div>
<div>
<label>
Mot de passe :
<input
type="password"
value={form.password}
onChange={e => setForm(f => ({ ...f, password: e.target.value }))}
required
/>
</label>
</div>
<button type="submit">Se connecter</button>
{error && <p style={{ color: 'red' }}>{error}</p>}
</form>
<p>Comptes disponibles :</p>
<ul>
<li><strong>admin</strong> / admin (administrateur)</li>
<li><strong>alice (utilisateur)</strong> / bob (utilisateur)</li>
</ul>
</main>
);
}
+158 -1
View File
@@ -1,3 +1,160 @@
import { useState } from 'react';
import { createOrder } from '../api/orders';
import { getBookById } from '../api/books';
export default function Orders() { export default function Orders() {
return <main><h1>Mes commandes</h1></main>; const [customerId, setCustomerId] = useState('');
const [paymentMethod, setPaymentMethod] = useState('CREDIT_CARD');
const [address, setAddress] = useState({ street: '', city: '', postalCode: '', country: '' });
const [lines, setLines] = useState([{ bookId: '', quantity: 1 }]);
const [promoCode, setPromoCode] = useState('');
const [message, setMessage] = useState(null);
const [submitting, setSubmitting] = useState(false);
function handleAddressChange(e) {
const { name, value } = e.target;
setAddress((prev) => ({ ...prev, [name]: value }));
}
function handleLineChange(index, field, value) {
setLines((prev) => prev.map((line, i) => (i === index ? { ...line, [field]: value } : line)));
}
function addLine() {
setLines((prev) => [...prev, { bookId: '', quantity: 1 }]);
}
function removeLine(index) {
const [promoValid, setPromoValid] = useState(true);
const [estimate, setEstimate] = useState(null);
const [estimating, setEstimating] = useState(false);
const [estimateError, setEstimateError] = useState(null);
setLines((prev) => prev.filter((_, i) => i !== index));
}
function handleSubmit(e) {
e.preventDefault();
setSubmitting(true);
setMessage(null);
const payload = {
customerId,
paymentMethod,
address,
orderLineDtos: lines.map((line) => ({
bookId: Number(line.bookId),
quantity: Number(line.quantity),
})),
...(promoCode ? { promoCode } : {}),
};
createOrder(payload)
.then((response) => setMessage('Commande créée'))
.catch((error) => {
console.error(error);
setMessage(error.response?.data?.message || 'Erreur lors de la commande.');
})
.finally(() => setSubmitting(false));
}
return (
<main>
<h1>Passer une commande</h1>
<form onSubmit={handleSubmit}>
<label>ID client
<input value={customerId} onChange={(e) => setCustomerId(e.target.value)} required />
</label>
<h2>Livres</h2>
{lines.map((line, index) => (
<div key={index}>
<input type="number" placeholder="ISBN du livre" value={line.bookId}
onChange={(e) => handleLineChange(index, 'bookId', e.target.value)} required />
<input type="number" placeholder="Quantité" value={line.quantity}
onChange={(e) => handleLineChange(index, 'quantity', e.target.value)} required />
function validatePromo(code) {
if (!code) return true;
// simple format: PROMO10 => 10% , PROMO5 => 5%
return /^PROMO\d{1,2}$/.test(code);
}
async function handleEstimate() {
setEstimating(true);
setEstimate(null);
setEstimateError(null);
try {
const ids = lines.map((l) => Number(l.bookId)).filter((n) => !Number.isNaN(n) && n > 0);
if (ids.length === 0) {
setEstimateError('Aucun ISBN valide');
return;
}
const unique = Array.from(new Set(ids));
const responses = await Promise.all(unique.map((id) => getBookById(id).then((r) => r.data)));
const priceMap = new Map();
unique.forEach((id, idx) => priceMap.set(id, responses[idx].prix ?? responses[idx].price ?? 0));
let total = 0;
for (const l of lines) {
const id = Number(l.bookId);
const qty = Number(l.quantity) || 0;
total += (priceMap.get(id) || 0) * qty;
}
let discounted = total;
if (promoCode) {
const m = promoCode.match(/^PROMO(\d{1,2})$/);
if (m) {
const pct = Math.max(0, Math.min(100, Number(m[1])));
discounted = total * (1 - pct / 100);
}
}
setEstimate({ total, discounted });
} catch (err) {
setEstimateError('Erreur lors de l\'estimation');
console.error(err);
} finally {
setEstimating(false);
}
}
{lines.length > 1 && (
<button type="button" onClick={() => removeLine(index)}>Retirer</button>
)}
</div>
))}
<button type="button" onClick={addLine}>+ Ajouter un livre</button>
<h2>Adresse de livraison</h2>
<input name="street" placeholder="Rue" value={address.street} onChange={handleAddressChange} required />
<input name="city" placeholder="Ville" value={address.city} onChange={handleAddressChange} required />
<input name="postalCode" placeholder="Code postal" value={address.postalCode} onChange={handleAddressChange} required />
<input name="country" placeholder="Pays" value={address.country} onChange={handleAddressChange} required />
<h2>Paiement</h2>
<label>Code promo
<input value={promoCode} onChange={(e) => { setPromoCode(e.target.value); setPromoValid(validatePromo(e.target.value)); }} placeholder="Code promo (optionnel)" />
</label>
{!promoValid && <div style={{ color: 'red' }}>Format de code promo invalide (ex: PROMO10)</div>}
<select value={paymentMethod} onChange={(e) => setPaymentMethod(e.target.value)}>
<option value="CREDIT_CARD">Carte bancaire</option>
</select>
<div style={{ marginTop: 8 }}>
<button type="button" onClick={handleEstimate} disabled={estimating}>
{estimating ? 'Estimation…' : 'Estimer le total'}
</button>
{estimateError && <div style={{ color: 'red' }}>{estimateError}</div>}
{estimate && (
<div>
<div>Total estimé : {estimate.total.toFixed(2)} </div>
<div>Après promo : {estimate.discounted.toFixed(2)} </div>
</div>
)}
</div>
<button type="submit" disabled={submitting}>
{submitting ? 'Envoi…' : 'Valider la commande'}
</button>
</form>
{message && <p>{message}</p>}
</main>
);
} }
+37
View File
@@ -0,0 +1,37 @@
import { useReservations } from '../context/ReservationContext';
import { Link } from 'react-router-dom';
export default function Reservations() {
const { reservations, cancelReservation } = useReservations();
if (reservations.length === 0) {
return (
<main>
<h1>Mes réservations</h1>
<p>Aucune réservation pour le moment.</p>
<Link to="/books"> Retour au catalogue</Link>
</main>
);
}
return (
<main>
<h1>Mes réservations</h1>
<ul>
{reservations.map(r => (
<li key={r.reservationId}>
<strong>{r.bookTitle}</strong> {r.bookAuthor}
<br />
Téléphone : {r.phoneNumber}
<br />
Réservé le : {new Date(r.reservedAt).toLocaleDateString('fr-FR')}
<br />
Statut : {r.status}
<br />
<button onClick={() => cancelReservation(r.reservationId)}>Annuler</button>
</li>
))}
</ul>
</main>
);
}
+96
View File
@@ -0,0 +1,96 @@
import { useState } from 'react';
import { useReturns } from '../context/ReturnContext';
import { getBooks } from '../api/books';
import { useEffect } from 'react';
export default function Returns() {
const { returns, addReturn } = useReturns();
const [books, setBooks] = useState([]);
const [form, setForm] = useState({ bookId: '', customerPhone: '', reason: '' });
const [message, setMessage] = useState(null);
useEffect(() => {
getBooks(0, 100).then(res => setBooks(res.data.content)).catch(console.error);
}, []);
function handleChange(e) {
const { name, value } = e.target;
setForm(f => ({ ...f, [name]: value }));
}
function handleSubmit(e) {
e.preventDefault();
const book = books.find(b => String(b.isbn) === String(form.bookId));
const bookTitle = book ? book.title : form.bookId;
addReturn(form.bookId, bookTitle, form.customerPhone, form.reason);
setMessage({ success: true, text: 'Retour enregistré avec succès !' });
setForm({ bookId: '', customerPhone: '', reason: '' });
}
return (
<main>
<h1>Gestion des retours</h1>
<section>
<h2>Enregistrer un retour</h2>
<form onSubmit={handleSubmit}>
<label>
Livre :
<select name="bookId" value={form.bookId} onChange={handleChange} required>
<option value="">-- Choisir un livre --</option>
{books.map(b => (
<option key={b.isbn} value={b.isbn}>{b.title} {b.author}</option>
))}
</select>
</label>
<label>
Téléphone du client :
<input
name="customerPhone"
type="tel"
value={form.customerPhone}
onChange={handleChange}
placeholder="0612345678"
required
/>
</label>
<label>
Motif :
<textarea
name="reason"
value={form.reason}
onChange={handleChange}
placeholder="Motif du retour..."
required
/>
</label>
<button type="submit">Enregistrer le retour</button>
</form>
{message && <p style={{ color: message.success ? 'green' : 'red' }}>{message.text}</p>}
</section>
<section>
<h2>Historique des retours</h2>
{returns.length === 0 ? (
<p>Aucun retour enregistré.</p>
) : (
<ul>
{returns.map(r => (
<li key={r.returnId}>
<strong>{r.bookTitle}</strong>
<br />
Client : {r.customerPhone}
<br />
Motif : {r.reason}
<br />
Statut : {r.status}
<br />
<small>{new Date(r.returnedAt).toLocaleDateString('fr-FR')}</small>
</li>
))}
</ul>
)}
</section>
</main>
);
}
+74
View File
@@ -0,0 +1,74 @@
import { useState } from 'react';
import { useSubscription } from '../context/SubscriptionContext';
export default function Subscription() {
const { subscription, subscribe, cancelSubscription, PLANS } = useSubscription();
const [selectedPlan, setSelectedPlan] = useState('basic');
const [phoneNumber, setPhoneNumber] = useState('');
const [message, setMessage] = useState(null);
function handleSubscribe(e) {
e.preventDefault();
subscribe(selectedPlan, phoneNumber);
setMessage({ success: true, text: 'Abonnement créé avec succès !' });
setPhoneNumber('');
}
function handleCancel() {
cancelSubscription();
setMessage({ success: false, text: 'Abonnement annulé.' });
}
return (
<main>
<h1>Mon abonnement</h1>
{subscription && subscription.status === 'ACTIVE' ? (
<section>
<h2>Abonnement actif</h2>
<p>Plan : <strong>{subscription.planName}</strong></p>
<p>Prix : <strong>{subscription.price} / mois</strong></p>
<p>Téléphone : {subscription.phoneNumber}</p>
<p>Depuis le : {new Date(subscription.startDate).toLocaleDateString('fr-FR')}</p>
<p>Statut : <strong style={{ color: 'green' }}>{subscription.status}</strong></p>
<button onClick={handleCancel}>Annuler l'abonnement</button>
</section>
) : (
<section>
<h2>Choisir un abonnement</h2>
<form onSubmit={handleSubscribe}>
<div>
{PLANS.map(plan => (
<label key={plan.id} style={{ display: 'block', marginBottom: '8px' }}>
<input
type="radio"
name="plan"
value={plan.id}
checked={selectedPlan === plan.id}
onChange={() => setSelectedPlan(plan.id)}
/>
{' '}<strong>{plan.name}</strong> , {plan.price} € / mois , {plan.description}
</label>
))}
</div>
<label>
Numéro de téléphone :
<input
type="tel"
value={phoneNumber}
onChange={e => setPhoneNumber(e.target.value)}
placeholder="0612345678"
required
/>
</label>
<button type="submit">S'abonner</button>
</form>
</section>
)}
{message && (
<p style={{ color: message.success ? 'green' : 'red' }}>{message.text}</p>
)}
</main>
);
}