Projet Gestion de Bibliothèque
Réalisé par : Hanae Chaiboub FS-202
les questions:
- 1. Créer les models des tables : Book, Author, Category, Loan, User, Profile, Card
- 2. Créer les migrations des models (7 tables + 1 table pivot book_category)
- 3. Créer les Controllers avec les méthodes CRUD (index, create, store, show, edit, update, destroy)
pour Book, Author, Category, Loan, User
- 4. Créer les vues pour chaque Controller : Index, Create, Show, Edit
- 5. Afficher pour chaque auteur la liste de ses livres avec catégories
- 6. Afficher la liste des livres par catégorie sélectionnée
- 7. Afficher les emprunts avec statut (En cours / Retourné)
- 8. Afficher le nombre d'emprunts pour chaque utilisateur
- 9. Afficher le nombre de livres pour chaque catégorie
- 10. Upload d'images de couverture et fichiers PDF pour les livres
Aperçu de l'application
Models
Book model
<?php
namespace App\Models; // Dossier où se trouve le modèle
use Illuminate\Database\Eloquent\Model; // Classe de base pour tous les modèles Eloquent
class Book extends Model
{
// $fillable : liste des champs que l'on peut remplir via un formulaire (mass assignment)
protected $fillable = ['titre', 'author_id', 'image', 'fichier_pdf'];
// Relation : Un livre appartient à UN seul auteur (Many-to-One)
// Clé étrangère : author_id dans la table books
public function author()
{
return $this->belongsTo(Author::class);
}
// Relation : Un livre peut avoir PLUSIEURS catégories (Many-to-Many)
// Utilise la table pivot 'book_category' pour stocker les associations
public function categories()
{
return $this->belongsToMany(Category::class);
}
// Relation : Un livre peut avoir PLUSIEURS emprunts (One-to-Many)
public function loans()
{
return $this->hasMany(Loan::class);
}
// Méthode personnalisée : vérifie si le livre est disponible
// Retourne true si aucun emprunt n'a une date_retour NULL (= pas encore retourné)
public function isAvailable()
{
return !$this->loans()->whereNull('date_retour')->exists();
}
}
Author model
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Author extends Model
{
// Seul le champ 'nom_complet' peut être rempli via un formulaire
protected $fillable = ['nom_complet'];
// Relation : Un auteur peut avoir PLUSIEURS livres (One-to-Many)
// Laravel cherche automatiquement la clé étrangère 'author_id' dans la table books
public function books()
{
return $this->hasMany(Book::class);
}
}
Category model
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Category extends Model
{
protected $fillable = ['libelle']; // 'libelle' = nom de la catégorie
// Relation Many-to-Many : Une catégorie peut contenir PLUSIEURS livres
// et un livre peut appartenir à PLUSIEURS catégories
// Table pivot utilisée : book_category
public function books()
{
return $this->belongsToMany(Book::class);
}
}
Loan model
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
// Modèle Loan (Emprunt) : représente un emprunt de livre par un utilisateur
class Loan extends Model
{
// Champs remplissables : qui a emprunté, quel livre, quand, et date de retour
// date_retour est nullable : NULL = emprunt en cours, une date = livre retourné
protected $fillable = ['user_id', 'book_id', 'date_emprunt', 'date_retour'];
// Relation : Un emprunt appartient à UN utilisateur (Many-to-One)
public function user()
{
return $this->belongsTo(User::class);
}
// Relation : Un emprunt concerne UN livre (Many-to-One)
public function book()
{
return $this->belongsTo(Book::class);
}
}
User model
<?php
namespace App\Models;
// On étend Authenticatable au lieu de Model car User gère l'authentification
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
protected $fillable = ['name', 'email', 'password'];
// $hidden : ces champs ne seront JAMAIS visibles dans les réponses JSON (sécurité)
protected $hidden = ['password', 'remember_token'];
// Relation One-to-One : Un utilisateur a UN seul profil
public function profile()
{
return $this->hasOne(Profile::class);
}
// Relation hasOneThrough : accéder à la carte DIRECTEMENT via le profil
// User -> Profile -> Card (raccourci sans passer par profile manuellement)
public function card()
{
return $this->hasOneThrough(Card::class, Profile::class);
}
// Relation One-to-Many : Un utilisateur peut faire PLUSIEURS emprunts
public function loans()
{
return $this->hasMany(Loan::class);
}
}
Profile model
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
// Modèle Profile : informations complémentaires d'un utilisateur
class Profile extends Model
{
protected $fillable = ['user_id', 'adresse', 'telephone'];
// Relation inverse : Ce profil appartient à UN utilisateur
public function user()
{
return $this->belongsTo(User::class);
}
// Relation One-to-One : Un profil a UNE seule carte de bibliothèque
public function card()
{
return $this->hasOne(Card::class);
}
}
Card model
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
// Modèle Card : carte de bibliothèque liée au profil
class Card extends Model
{
protected $fillable = ['profile_id', 'numero_carte'];
// Relation inverse : Cette carte appartient à UN profil
public function profile()
{
return $this->belongsTo(Profile::class);
}
}
Migrations
Authors migration
<?php
// Importation des classes nécessaires pour créer une migration
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
// Classe anonyme qui étend Migration
return new class extends Migration
{
// up() : exécutée quand on lance 'php artisan migrate'
public function up(): void
{
Schema::create('authors', function (Blueprint $table) {
$table->id(); // Colonne ID auto-incrémentée (clé primaire)
$table->string('nom_complet'); // Nom complet de l'auteur (VARCHAR)
$table->timestamps(); // Crée created_at et updated_at automatiquement
});
}
// down() : exécutée quand on lance 'php artisan migrate:rollback'
public function down(): void
{
Schema::dropIfExists('authors'); // Supprime la table si elle existe
}
};
Categories migration
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('libelle'); // Nom de la catégorie
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('categories');
}
};
Books migration
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('books', function (Blueprint $table) {
$table->id();
$table->string('titre'); // Titre du livre
// foreignId() crée une clé étrangère vers la table 'authors'
// constrained() ajoute automatiquement la contrainte de clé étrangère
// onDelete('cascade') : si l'auteur est supprimé, ses livres aussi
$table->foreignId('author_id')->constrained()->onDelete('cascade');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('books');
}
};
Books: ajout image et PDF migration
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
// Cette migration MODIFIE une table existante (Schema::table au lieu de Schema::create)
return new class extends Migration
{
public function up(): void
{
Schema::table('books', function (Blueprint $table) {
// nullable() : le champ peut être vide (pas obligatoire)
// after() : positionne la colonne après 'author_id' dans la table
$table->string('image')->nullable()->after('author_id');
$table->string('fichier_pdf')->nullable()->after('image');
});
}
public function down(): void
{
Schema::table('books', function (Blueprint $table) {
$table->dropColumn(['image', 'fichier_pdf']); // Supprime les 2 colonnes
});
}
};
book_category (table pivot) migration
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
// TABLE PIVOT : nécessaire pour la relation Many-to-Many entre Book et Category
// Convention Laravel : noms des 2 tables au singulier, ordre alphabétique = book_category
return new class extends Migration
{
public function up(): void
{
Schema::create('book_category', function (Blueprint $table) {
// Deux clés étrangères : chaque ligne associe un livre à une catégorie
$table->foreignId('book_id')->constrained()->onDelete('cascade');
$table->foreignId('category_id')->constrained()->onDelete('cascade');
// Clé primaire composée : empêche les doublons (même livre + même catégorie)
$table->primary(['book_id', 'category_id']);
});
}
public function down(): void
{
Schema::dropIfExists('book_category');
}
};
Profiles migration
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('profiles', function (Blueprint $table) {
$table->id();
// Clé étrangère vers la table users : chaque profil appartient à un seul user
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('adresse')->nullable(); // Adresse optionnelle
$table->string('telephone')->nullable(); // Téléphone optionnel
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('profiles');
}
};
Cards migration
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('cards', function (Blueprint $table) {
$table->id();
$table->foreignId('profile_id')->constrained()->onDelete('cascade');
$table->string('numero_carte')->unique(); // unique() : chaque carte a un numéro unique
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('cards');
}
};
Loans migration
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('loans', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade'); // Qui a emprunté
$table->foreignId('book_id')->constrained()->onDelete('cascade'); // Quel livre
$table->date('date_emprunt'); // Date de l'emprunt (obligatoire)
$table->date('date_retour')->nullable(); // Date de retour (NULL = pas encore retourné)
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('loans');
}
};
Controllers
BookController
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Book;
use App\Models\Author;
use App\Models\Category;
use Illuminate\Support\Facades\Storage; // Pour gérer les fichiers (upload/suppression)
class BookController extends Controller
{
// INDEX : Afficher la liste de tous les livres
public function index()
{
// with() = Eager Loading : charge les relations 'author' et 'categories' en même temps
// latest() = trie par date de création décroissante (les plus récents d'abord)
// paginate(10) = affiche 10 livres par page
$books = Book::with(['author', 'categories'])->latest()->paginate(10);
return view('books.index', compact('books')); // compact() envoie $books à la vue
}
// CREATE : Afficher le formulaire d'ajout
public function create()
{
// On récupère les auteurs et catégories pour les listes déroulantes du formulaire
$authors = Author::orderBy('nom_complet')->get();
$categories = Category::orderBy('libelle')->get();
return view('books.create', compact('authors', 'categories'));
}
// STORE : Traiter l'envoi du formulaire d'ajout
public function store(Request $request)
{
// Validation des données reçues du formulaire
// required = obligatoire, exists = doit exister dans la table, max = taille max
$validated = $request->validate([
'titre' => 'required|string|max:255',
'author_id' => 'required|exists:authors,id', // L'auteur doit exister en BDD
'categories' => 'nullable|array',
'categories.*' => 'exists:categories,id', // Chaque catégorie doit exister
'image' => 'nullable|image|mimes:jpeg,png,jpg,gif,webp|max:2048', // Max 2 Mo
'fichier_pdf' => 'nullable|mimes:pdf|max:10240', // Max 10 Mo
]);
$data = [
'titre' => $validated['titre'],
'author_id' => $validated['author_id'],
];
// Si un fichier image est envoyé, on le stocke dans storage/app/public/books/images
if ($request->hasFile('image')) {
$data['image'] = $request->file('image')->store('books/images', 'public');
}
// Si un fichier PDF est envoyé, on le stocke dans storage/app/public/books/pdfs
if ($request->hasFile('fichier_pdf')) {
$data['fichier_pdf'] = $request->file('fichier_pdf')->store('books/pdfs', 'public');
}
$book = Book::create($data); // Créer le livre en BDD
// sync() : associe le livre aux catégories sélectionnées dans la table pivot
if (!empty($validated['categories'])) {
$book->categories()->sync($validated['categories']);
}
// Redirige vers la liste avec un message de succès
return redirect()->route('books.index')->with('success', 'Livre ajouté avec succès.');
}
// SHOW : Afficher les détails d'un livre
public function show(Book $book) // Route Model Binding : Laravel trouve le livre automatiquement par son ID
{
// load() charge les relations après la récupération du modèle
$book->load(['author', 'categories', 'loans.user']);
return view('books.show', compact('book'));
}
// EDIT : Afficher le formulaire de modification
public function edit(Book $book)
{
$authors = Author::orderBy('nom_complet')->get();
$categories = Category::orderBy('libelle')->get();
$book->load('categories'); // Charge les catégories actuelles du livre
return view('books.edit', compact('book', 'authors', 'categories'));
}
// UPDATE : Traiter la modification du livre
public function update(Request $request, Book $book)
{
$validated = $request->validate([
'titre' => 'required|string|max:255',
'author_id' => 'required|exists:authors,id',
'categories' => 'nullable|array',
'categories.*' => 'exists:categories,id',
'image' => 'nullable|image|mimes:jpeg,png,jpg,gif,webp|max:2048',
'fichier_pdf' => 'nullable|mimes:pdf|max:10240',
]);
$data = [
'titre' => $validated['titre'],
'author_id' => $validated['author_id'],
];
// Si une nouvelle image est envoyée, on supprime l'ancienne avant de stocker la nouvelle
if ($request->hasFile('image')) {
if ($book->image) Storage::disk('public')->delete($book->image);
$data['image'] = $request->file('image')->store('books/images', 'public');
}
if ($request->hasFile('fichier_pdf')) {
if ($book->fichier_pdf) Storage::disk('public')->delete($book->fichier_pdf);
$data['fichier_pdf'] = $request->file('fichier_pdf')->store('books/pdfs', 'public');
}
$book->update($data); // Met à jour le livre en BDD
// sync() remplace toutes les catégories par celles sélectionnées
$book->categories()->sync($validated['categories'] ?? []);
return redirect()->route('books.index')->with('success', 'Livre mis à jour.');
}
// DESTROY : Supprimer un livre
public function destroy(Book $book)
{
// Supprimer les fichiers associés du stockage avant de supprimer le livre
if ($book->image) Storage::disk('public')->delete($book->image);
if ($book->fichier_pdf) Storage::disk('public')->delete($book->fichier_pdf);
$book->categories()->detach(); // Supprimer les associations dans la table pivot
$book->delete(); // Supprimer le livre de la BDD
return redirect()->route('books.index')->with('success', 'Livre supprimé.');
}
}
AuthorController
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Author;
class AuthorController extends Controller
{
// INDEX : Liste des auteurs avec le nombre de livres de chacun
public function index()
{
// withCount('books') ajoute un attribut 'books_count' à chaque auteur
// C'est une requête optimisée (sous-requête SQL) au lieu de charger tous les livres
$authors = Author::withCount('books')->latest()->paginate(10);
return view('authors.index', compact('authors'));
}
public function create()
{
return view('authors.create');
}
public function store(Request $request)
{
$request->validate(['nom_complet' => 'required|string|max:255']);
// only() : ne récupère que le champ 'nom_complet' du formulaire (sécurité)
Author::create($request->only('nom_complet'));
return redirect()->route('authors.index')->with('success', 'Auteur ajouté.');
}
public function show(Author $author)
{
// Charge les livres de l'auteur AVEC leurs catégories (relation imbriquée)
$author->load('books.categories');
return view('authors.show', compact('author'));
}
public function edit(Author $author)
{
return view('authors.edit', compact('author'));
}
public function update(Request $request, Author $author)
{
$request->validate(['nom_complet' => 'required|string|max:255']);
$author->update($request->only('nom_complet'));
return redirect()->route('authors.index')->with('success', 'Auteur mis à jour.');
}
public function destroy(Author $author)
{
$author->delete(); // Supprime l'auteur (et ses livres grâce au cascade)
return redirect()->route('authors.index')->with('success', 'Auteur supprimé.');
}
}
CategoryController
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Category;
class CategoryController extends Controller
{
// INDEX : Liste des catégories avec le nombre de livres dans chacune
public function index()
{
// withCount('books') : ajoute books_count à chaque catégorie
$categories = Category::withCount('books')->latest()->paginate(10);
return view('categories.index', compact('categories'));
}
public function create()
{
return view('categories.create');
}
public function store(Request $request)
{
// unique:categories = le libellé doit être unique dans la table categories
$request->validate(['libelle' => 'required|string|max:255|unique:categories']);
Category::create($request->only('libelle'));
return redirect()->route('categories.index')->with('success', 'Catégorie ajoutée.');
}
public function show(Category $category)
{
$category->load('books'); // Charge tous les livres de cette catégorie
return view('categories.show', compact('category'));
}
public function edit(Category $category)
{
return view('categories.edit', compact('category'));
}
public function update(Request $request, Category $category)
{
// unique:categories,libelle,$id : ignore la catégorie actuelle lors de la vérification d'unicité
$request->validate(['libelle' => 'required|string|max:255|unique:categories,libelle,'.$category->id]);
$category->update($request->only('libelle'));
return redirect()->route('categories.index')->with('success', 'Catégorie mise à jour.');
}
public function destroy(Category $category)
{
$category->books()->detach(); // Supprimer les liaisons dans la table pivot d'abord
$category->delete();
return redirect()->route('categories.index')->with('success', 'Catégorie supprimée.');
}
}
LoanController
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Loan;
use App\Models\Book;
use App\Models\User;
class LoanController extends Controller
{
// INDEX : Afficher tous les emprunts avec livre et utilisateur
public function index()
{
// 'book.author' : charge le livre ET son auteur en une seule requête (relation imbriquée)
$loans = Loan::with(['book.author', 'user'])->latest()->paginate(10);
return view('loans.index', compact('loans'));
}
public function create()
{
$users = User::orderBy('name')->get();
$books = Book::orderBy('titre')->get();
return view('loans.create', compact('users', 'books'));
}
public function store(Request $request)
{
$request->validate([
'user_id' => 'required|exists:users,id',
'book_id' => 'required|exists:books,id',
'date_emprunt' => 'required|date',
// after_or_equal : la date de retour doit être égale ou après la date d'emprunt
'date_retour' => 'nullable|date|after_or_equal:date_emprunt',
]);
Loan::create($request->all());
return redirect()->route('loans.index')->with('success', 'Emprunt enregistré.');
}
public function show(Loan $loan)
{
$loan->load(['book', 'user']);
return view('loans.show', compact('loan'));
}
public function edit(Loan $loan)
{
$users = User::orderBy('name')->get();
$books = Book::orderBy('titre')->get();
return view('loans.edit', compact('loan', 'users', 'books'));
}
public function update(Request $request, Loan $loan)
{
$request->validate([
'user_id' => 'required|exists:users,id',
'book_id' => 'required|exists:books,id',
'date_emprunt' => 'required|date',
'date_retour' => 'nullable|date|after_or_equal:date_emprunt',
]);
$loan->update($request->all());
return redirect()->route('loans.index')->with('success', 'Emprunt mis à jour.');
}
public function destroy(Loan $loan)
{
$loan->delete();
return redirect()->route('loans.index')->with('success', 'Emprunt supprimé.');
}
}
UserController
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\User;
use Illuminate\Support\Facades\Hash; // Pour le hashage sécurisé du mot de passe
class UserController extends Controller
{
// INDEX : Liste des utilisateurs avec nombre d'emprunts
public function index()
{
// withCount('loans') : ajoute loans_count à chaque utilisateur
$users = User::withCount('loans')->latest()->paginate(10);
return view('users.index', compact('users'));
}
public function create()
{
return view('users.create');
}
public function store(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users', // Email unique dans la table users
'password' => 'required|min:6|confirmed', // confirmed : nécessite un champ password_confirmation
]);
User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password), // JAMAIS stocker le mot de passe en clair !
]);
return redirect()->route('users.index')->with('success', 'Utilisateur créé.');
}
public function show(User $user)
{
// Charge le profil et les emprunts avec les livres associés
$user->load(['profile', 'loans.book']);
return view('users.show', compact('user'));
}
public function edit(User $user)
{
return view('users.edit', compact('user'));
}
public function update(Request $request, User $user)
{
$request->validate([
'name' => 'required|string|max:255',
// unique:users,email,$id : ignore l'utilisateur actuel lors de la vérification
'email' => 'required|email|unique:users,email,'.$user->id,
'password' => 'nullable|min:6|confirmed', // nullable : le mot de passe est optionnel lors de la modification
]);
$data = $request->only('name', 'email');
// filled() : vérifie si le champ n'est pas vide (différent de has())
if ($request->filled('password')) {
$data['password'] = Hash::make($request->password);
}
$user->update($data);
return redirect()->route('users.index')->with('success', 'Utilisateur mis à jour.');
}
public function destroy(User $user)
{
$user->delete();
return redirect()->route('users.index')->with('success', 'Utilisateur supprimé.');
}
}
Routes
web.php
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\BookController;
use App\Http\Controllers\AuthorController;
use App\Http\Controllers\CategoryController;
use App\Http\Controllers\LoanController;
use App\Http\Controllers\UserController;
use App\Models\Book;
use App\Models\Author;
use App\Models\Category;
use App\Models\Loan;
// Page d'accueil : affiche les statistiques et les livres récents
Route::get('/', function () {
return view('welcome', [
'booksCount' => Book::count(), // Nombre total de livres
'authorsCount' => Author::count(), // Nombre total d'auteurs
'categoriesCount' => Category::count(), // Nombre total de catégories
// Emprunts actifs = ceux qui n'ont pas encore de date de retour
'activeLoansCount' => Loan::whereNull('date_retour')->count(),
// Les 6 derniers livres ajoutés avec leurs relations
'recentBooks' => Book::with(['author','categories'])->latest()->take(6)->get(),
]);
});
// Route::resource() crée automatiquement les 7 routes CRUD :
// GET /books => index (liste)
// GET /books/create => create (formulaire d'ajout)
// POST /books => store (enregistrer)
// GET /books/{book} => show (détails)
// GET /books/{book}/edit => edit (formulaire de modification)
// PUT /books/{book} => update (mettre à jour)
// DELETE /books/{book} => destroy (supprimer)
Route::resource('books', BookController::class);
Route::resource('authors', AuthorController::class);
Route::resource('categories', CategoryController::class);
Route::resource('loans', LoanController::class);
Route::resource('users', UserController::class);
Vues
Layouts app.blade.php
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', 'Biblio')</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
/* Design System - Light Theme */
:root {
--bg-primary: #f8f9fc;
--text-primary: #1e1e2e;
--accent: #6366f1;
}
nav { background: linear-gradient(135deg, #4f46e5, #7c3aed); padding: 15px 30px; }
nav a { color: #fff; text-decoration: none; margin-right: 15px; font-family: 'Inter', sans-serif; }
nav a:hover { text-decoration: underline; }
h1 { color: #333; font-family: 'Inter', sans-serif; }
</style>
</head>
<body>
@include('partials.navbar')
@yield('content')
@include('partials.footer')
</body>
</html>
Liste des Livres
Route::resource('books', BookController::class);
// GET /books => BookController@index
@extends('layouts.app')
@section('content')
<div class="page-container">
<div class="page-header">
<h1>📚 Tous les Livres</h1>
<a href="{{ route('books.create') }}" class="btn btn-primary">+ Ajouter un Livre</a>
</div>
<table class="data-table">
<thead><tr>
<th>#</th><th>Couverture</th><th>Titre</th><th>Auteur</th>
<th>Catégories</th><th>PDF</th><th>Statut</th><th>Actions</th>
</tr></thead>
<tbody>
@foreach($books as $book)
<tr>
<td>{{ $book->id }}</td>
<td>
@if($book->image)
<img src="{{ asset('storage/'.$book->image) }}" width="50">
@endif
</td>
<td><strong>{{ $book->titre }}</strong></td>
<td>{{ $book->author->nom_complet }}</td>
<td>
@foreach($book->categories as $cat)
<span class="badge">{{ $cat->libelle }}</span>
@endforeach
</td>
<td>
@if($book->fichier_pdf)
<a href="{{ asset('storage/'.$book->fichier_pdf) }}">📄 PDF</a>
@endif
</td>
<td>{{ $book->isAvailable() ? '✅ Disponible' : '🔴 Emprunté' }}</td>
<td>
<a href="{{ route('books.show', $book) }}" class="btn-view">Voir</a>
<a href="{{ route('books.edit', $book) }}" class="btn-edit">Modifier</a>
<form action="{{ route('books.destroy', $book) }}" method="POST" style="display:inline;">
@csrf @method('DELETE')
<button type="submit" class="btn-danger">Supprimer</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
{{ $books->links() }}
</div>
@endsection
Ajouter un Livre
// GET /books/create => BookController@create
// POST /books => BookController@store
@extends('layouts.app')
@section('content')
<div class="page-container">
<h1>📖 Ajouter un Livre</h1>
<form action="{{ route('books.store') }}" method="POST" enctype="multipart/form-data">
@csrf
<label for="titre">Titre</label>
<input type="text" name="titre" class="form-control" required>
<label for="author_id">Auteur</label>
<select name="author_id" class="form-control">
<option value="">Choisissez un auteur</option>
@foreach($authors as $author)
<option value="{{ $author->id }}">{{ $author->nom_complet }}</option>
@endforeach
</select>
<label>Catégories</label>
@foreach($categories as $cat)
<label class="checkbox-label">
<input type="checkbox" name="categories[]" value="{{ $cat->id }}">
{{ $cat->libelle }}
</label>
@endforeach
<label for="image">Image de couverture</label>
<input type="file" name="image" accept="image/*">
<label for="fichier_pdf">Fichier PDF</label>
<input type="file" name="fichier_pdf" accept=".pdf">
<button type="submit" class="btn btn-primary">Ajouter le livre</button>
</form>
</div>
@endsection
Liste des Emprunts
Route::resource('loans', LoanController::class);
// GET /loans => LoanController@index
@extends('layouts.app')
@section('content')
<div class="page-container">
<div class="page-header">
<h1>📋 Liste des Emprunts</h1>
<a href="{{ route('loans.create') }}" class="btn btn-primary">+ Nouvel Emprunt</a>
</div>
<table class="data-table">
<thead><tr>
<th>#</th><th>Livre</th><th>Utilisateur</th>
<th>Date Emprunt</th><th>Date Retour</th><th>Statut</th><th>Actions</th>
</tr></thead>
<tbody>
@foreach($loans as $loan)
<tr>
<td>{{ $loan->id }}</td>
<td>{{ $loan->book->titre }}</td>
<td>{{ $loan->user->name }}</td>
<td>{{ $loan->date_emprunt }}</td>
<td>{{ $loan->date_retour ?? '—' }}</td>
<td>{{ $loan->date_retour ? '✅ Retourné' : '🔴 En cours' }}</td>
<td>
<a href="{{ route('loans.edit', $loan) }}" style="margin-right: 10px; text-decoration: none; padding: 5px 10px; border-radius: 5px; background-color: green; color: white;">Modifier</a>
<form action="{{ route('loans.destroy', $loan) }}" method="POST" style="display:inline;">
@csrf @method('DELETE')
<button type="submit" style="padding: 5px 10px; border-radius: 5px; background-color: red; color: white; border:none; cursor:pointer;">Supprimer</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
{{ $loans->links() }}
</div>
@endsection
Ajouter un Emprunt
// GET /loans/create => LoanController@create
// POST /loans => LoanController@store
@extends('layouts.app')
@section('content')
<div class="page-container">
<h1>📋 Ajouter un Emprunt</h1>
<form action="{{ route('loans.store') }}" method="POST">
@csrf
<label for="user_id">Utilisateur</label>
<select name="user_id" class="form-control">
<option value="">Choisissez un utilisateur</option>
@foreach($users as $user)
<option value="{{ $user->id }}">{{ $user->name }}</option>
@endforeach
</select>
<label for="book_id">Livre</label>
<select name="book_id" class="form-control">
<option value="">Choisissez un livre</option>
@foreach($books as $book)
<option value="{{ $book->id }}">{{ $book->titre }}</option>
@endforeach
</select>
<label for="date_emprunt">Date d'emprunt</label>
<input type="date" name="date_emprunt" class="form-control" required>
<label for="date_retour">Date de retour (optionnel)</label>
<input type="date" name="date_retour" class="form-control">
<button type="submit" class="btn btn-primary">Enregistrer l'emprunt</button>
</form>
</div>
@endsection
Liste des Auteurs (Nombre de livres par auteur)
Route::resource('authors', AuthorController::class);
// GET /authors => AuthorController@index
@extends('layouts.app')
@section('content')
<div class="page-container">
<div class="page-header">
<h1>✍️ Liste des Auteurs</h1>
<a href="{{ route('authors.create') }}" class="btn btn-primary">+ Ajouter</a>
</div>
<table class="data-table">
<thead><tr>
<th>#</th><th>Nom Complet</th><th>Nombre de Livres</th><th>Actions</th>
</tr></thead>
<tbody>
@foreach($authors as $author)
<tr>
<td>{{ $author->id }}</td>
<td>{{ $author->nom_complet }}</td>
<td>{{ $author->books_count }}</td>
<td>
<a href="{{ route('authors.show', $author) }}" style="margin-right: 10px; text-decoration: none; padding: 5px 10px; border-radius: 5px; background-color: #3b82f6; color: white;">Voir</a>
<a href="{{ route('authors.edit', $author) }}" style="margin-right: 10px; text-decoration: none; padding: 5px 10px; border-radius: 5px; background-color: green; color: white;">Modifier</a>
<form action="{{ route('authors.destroy', $author) }}" method="POST" style="display:inline;">
@csrf @method('DELETE')
<button type="submit" style="padding: 5px 10px; border-radius: 5px; background-color: red; color: white; border:none;">Supprimer</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
{{ $authors->links() }}
</div>
@endsection
Liste des Utilisateurs (Nombre d'emprunts par utilisateur)
Route::resource('users', UserController::class);
// GET /users => UserController@index
@extends('layouts.app')
@section('content')
<div class="page-container">
<div class="page-header">
<h1>👥 Liste des Utilisateurs</h1>
<a href="{{ route('users.create') }}" class="btn btn-primary">+ Ajouter</a>
</div>
<table class="data-table">
<thead><tr>
<th>#</th><th>Nom</th><th>Email</th>
<th>Nombre d'emprunts</th><th>Inscrit le</th><th>Actions</th>
</tr></thead>
<tbody>
@foreach($users as $user)
<tr>
<td>{{ $user->id }}</td>
<td>{{ $user->name }}</td>
<td>{{ $user->email }}</td>
<td>{{ $user->loans_count }}</td>
<td>{{ $user->created_at->format('d/m/Y') }}</td>
<td>
<a href="{{ route('users.show', $user) }}" style="margin-right: 10px; text-decoration: none; padding: 5px 10px; border-radius: 5px; background-color: #3b82f6; color: white;">Voir</a>
<a href="{{ route('users.edit', $user) }}" style="margin-right: 10px; text-decoration: none; padding: 5px 10px; border-radius: 5px; background-color: green; color: white;">Modifier</a>
<form action="{{ route('users.destroy', $user) }}" method="POST" style="display:inline;">
@csrf @method('DELETE')
<button type="submit" style="padding: 5px 10px; border-radius: 5px; background-color: red; color: white; border:none;">Supprimer</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
{{ $users->links() }}
</div>
@endsection
Liste des Catégories (Nombre de livres par catégorie)
Route::resource('categories', CategoryController::class);
// GET /categories => CategoryController@index
@extends('layouts.app')
@section('content')
<div class="page-container">
<div class="page-header">
<h1>🏷️ Liste des Catégories</h1>
<a href="{{ route('categories.create') }}" class="btn btn-primary">+ Ajouter</a>
</div>
<table class="data-table">
<thead><tr>
<th>#</th><th>Libellé</th><th>Nombre de Livres</th><th>Actions</th>
</tr></thead>
<tbody>
@foreach($categories as $category)
<tr>
<td>{{ $category->id }}</td>
<td>{{ $category->libelle }}</td>
<td>{{ $category->books_count }}</td>
<td>
<a href="{{ route('categories.show', $category) }}" style="margin-right: 10px; text-decoration: none; padding: 5px 10px; border-radius: 5px; background-color: #3b82f6; color: white;">Voir</a>
<a href="{{ route('categories.edit', $category) }}" style="margin-right: 10px; text-decoration: none; padding: 5px 10px; border-radius: 5px; background-color: green; color: white;">Modifier</a>
<form action="{{ route('categories.destroy', $category) }}" method="POST" style="display:inline;">
@csrf @method('DELETE')
<button type="submit" style="padding: 5px 10px; border-radius: 5px; background-color: red; color: white; border:none;">Supprimer</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
{{ $categories->links() }}
</div>
@endsection
Page d'accueil (Statistiques)
// Route page d'accueil
Route::get('/', function () {
return view('welcome', [
'booksCount' => Book::count(),
'authorsCount' => Author::count(),
'categoriesCount' => Category::count(),
'activeLoansCount' => Loan::whereNull('date_retour')->count(),
'recentBooks' => Book::with(['author','categories'])->latest()->take(6)->get(),
]);
});
@extends('layouts.app')
@section('content')
@include('partials.hero') {{-- Section hero avec titre et description --}}
@include('partials.stats') {{-- 4 cartes statistiques : Livres, Auteurs, Catégories, Emprunts actifs --}}
@include('partials.books') {{-- Grille des 6 derniers livres ajoutés --}}
@endsection