- {% if is_paginated %}
+
+ {% if is_paginated %}
-
{{ book.title }}
{{ book.author }}
@@ -94,4 +96,4 @@
{{ book.title }}
});
});
-{% endblock %}
+{% endblock %}
\ No newline at end of file
diff --git a/BookStation/reviews/__init__.py b/BookStation/reviews/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/BookStation/reviews/admin.py b/BookStation/reviews/admin.py
new file mode 100644
index 00000000..8c38f3f3
--- /dev/null
+++ b/BookStation/reviews/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/BookStation/reviews/apps.py b/BookStation/reviews/apps.py
new file mode 100644
index 00000000..80e01867
--- /dev/null
+++ b/BookStation/reviews/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class ReviewsConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'reviews'
diff --git a/BookStation/reviews/forms.py b/BookStation/reviews/forms.py
new file mode 100644
index 00000000..e3afda96
--- /dev/null
+++ b/BookStation/reviews/forms.py
@@ -0,0 +1,7 @@
+from django import forms
+from .models import Review
+
+class ReviewForm(forms.ModelForm):
+ class Meta:
+ model = Review
+ fields = ['rating', 'comment']
diff --git a/BookStation/reviews/migrations/0001_initial.py b/BookStation/reviews/migrations/0001_initial.py
new file mode 100644
index 00000000..560afb18
--- /dev/null
+++ b/BookStation/reviews/migrations/0001_initial.py
@@ -0,0 +1,32 @@
+# Generated by Django 5.2.1 on 2025-06-08 16:54
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Book',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(max_length=200)),
+ ('description', models.TextField()),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Review',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('content', models.TextField()),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('book', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='reviews.book')),
+ ],
+ ),
+ ]
diff --git a/BookStation/reviews/migrations/0002_alter_review_book_delete_book.py b/BookStation/reviews/migrations/0002_alter_review_book_delete_book.py
new file mode 100644
index 00000000..25bcb3d4
--- /dev/null
+++ b/BookStation/reviews/migrations/0002_alter_review_book_delete_book.py
@@ -0,0 +1,23 @@
+# Generated by Django 5.2.1 on 2025-06-08 18:19
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('books', '0001_initial'),
+ ('reviews', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='review',
+ name='book',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='books.book'),
+ ),
+ migrations.DeleteModel(
+ name='Book',
+ ),
+ ]
diff --git a/BookStation/reviews/migrations/__init__.py b/BookStation/reviews/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/BookStation/reviews/models.py b/BookStation/reviews/models.py
new file mode 100644
index 00000000..33cc0ab4
--- /dev/null
+++ b/BookStation/reviews/models.py
@@ -0,0 +1,11 @@
+from django.db import models
+from books.models import Book
+
+# class Book(models.Model):
+# title = models.CharField(max_length=200)
+# description = models.TextField()
+
+class Review(models.Model):
+ book = models.ForeignKey(Book, on_delete=models.CASCADE)
+ content = models.TextField()
+ created_at = models.DateTimeField(auto_now_add=True)
\ No newline at end of file
diff --git a/BookStation/reviews/templates/reviews/add_reviews.html b/BookStation/reviews/templates/reviews/add_reviews.html
new file mode 100644
index 00000000..dc5d8a68
--- /dev/null
+++ b/BookStation/reviews/templates/reviews/add_reviews.html
@@ -0,0 +1,198 @@
+{% extends 'base.html' %}
+{% load static %}
+
+{% block title %}Viết đánh giá - {{ book.title }}{% endblock %}
+
+{% block content %}
+
+
+
+
+ Viết đánh giá cho: {{ book.title }}
+
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/BookStation/reviews/templates/reviews/book_review.html b/BookStation/reviews/templates/reviews/book_review.html
new file mode 100644
index 00000000..8cac6c73
--- /dev/null
+++ b/BookStation/reviews/templates/reviews/book_review.html
@@ -0,0 +1,206 @@
+{% extends 'base.html' %}
+{% load static %}
+
+{% block title %}Đánh giá - {{ book.title }}{% endblock %}
+
+{% block content %}
+
+
+
Đánh giá cho sách: {{ book.title }}
+
+
+
+ {% if reviews %}
+ {% for review in reviews %}
+
+
+
+
+
+
{{ review.user.username }}
+
+
+ {% for i in "12345" %}
+ {% if forloop.counter <= review.rating %}
+
+ {% else %}
+
+ {% endif %}
+ {% endfor %}
+
+
+
+
+
+
+ {% endfor %}
+ {% else %}
+
Chưa có đánh giá nào cho sách này.
+ {% endif %}
+
+
+
+
+
+
+{% endblock %}
diff --git a/BookStation/reviews/tests.py b/BookStation/reviews/tests.py
new file mode 100644
index 00000000..7ce503c2
--- /dev/null
+++ b/BookStation/reviews/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/BookStation/reviews/urls.py b/BookStation/reviews/urls.py
new file mode 100644
index 00000000..76051ddf
--- /dev/null
+++ b/BookStation/reviews/urls.py
@@ -0,0 +1,9 @@
+from django.urls import path
+from . import views
+
+app_name = 'reviews'
+
+urlpatterns = [
+ path('add_reviews/
/', views.add_reviews, name='add_reviews'),
+ path('book_review//', views.book_review, name='book_review'),
+]
\ No newline at end of file
diff --git a/BookStation/reviews/views.py b/BookStation/reviews/views.py
new file mode 100644
index 00000000..6b7d2f30
--- /dev/null
+++ b/BookStation/reviews/views.py
@@ -0,0 +1,21 @@
+from django.shortcuts import render, get_object_or_404, redirect
+from .models import Review
+from books.models import Book
+
+def add_reviews(request, book_id):
+ book = get_object_or_404(Book, pk=book_id)
+
+ if request.method == 'POST':
+ content = request.POST.get('comment') # Tên khớp với name="comment" trong HTML
+
+ if content:
+ Review.objects.create(book=book, content=content)
+ return redirect('reviews:book_review', book_id=book.id)
+
+ return render(request, 'reviews/add_reviews.html', {'book': book})
+
+
+def book_review(request, book_id):
+ book = get_object_or_404(Book, pk=book_id)
+ reviews = Review.objects.filter(book=book).order_by('-created_at')
+ return render(request, 'reviews/book_review.html', {'book': book, 'reviews': reviews})
diff --git a/BookStation/static/css/analytics/analytics.css b/BookStation/static/css/analytics/analytics.css
new file mode 100644
index 00000000..0331d5ef
--- /dev/null
+++ b/BookStation/static/css/analytics/analytics.css
@@ -0,0 +1,199 @@
+/* Reset & base */
+body {
+ background-color: #f4f6f8;
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ margin: 0;
+ padding: 0;
+ color: #2e3a59;
+ line-height: 1.6;
+}
+
+.analytics-container {
+ max-width: 1200px;
+ margin: 32px auto;
+ padding: 0 20px 40px;
+ background: #fff;
+ border-radius: 12px;
+ box-shadow: 0 6px 18px rgba(0,0,0,0.1);
+}
+
+/* Header */
+.stats-header {
+ padding-bottom: 24px;
+ border-bottom: 1px solid #e1e8f0;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 12px;
+}
+
+.stats-header h1 {
+ font-size: 2.2rem;
+ font-weight: 700;
+ color: #1a2a6c;
+ margin: 0;
+}
+
+/* Nav buttons */
+.nav-button {
+ background-color: #1a73e8;
+ border: none;
+ color: white;
+ font-weight: 600;
+ padding: 10px 24px;
+ border-radius: 8px;
+ cursor: pointer;
+ text-decoration: none;
+ box-shadow: 0 3px 8px rgba(26, 115, 232, 0.4);
+ transition: background-color 0.3s ease;
+ display: inline-flex;
+ align-items: center;
+}
+
+.nav-button:hover {
+ background-color: #155db2;
+}
+
+.nav-button.active {
+ background-color: #155db2;
+}
+
+/* Stats grid */
+.stats-grid {
+ display: flex;
+ gap: 20px;
+ margin-top: 24px;
+ flex-wrap: wrap;
+}
+
+/* Stat cards */
+.stat-card {
+ background: #e9f0ff;
+ border-radius: 16px;
+ padding: 24px 28px;
+ flex: 1 1 240px;
+ box-shadow: 0 2px 10px rgba(26, 115, 232, 0.15);
+ transition: transform 0.3s ease;
+}
+
+.stat-card:hover {
+ transform: translateY(-4px);
+}
+
+.stat-label {
+ font-size: 0.9rem;
+ font-weight: 600;
+ color: #5271ff;
+ margin-bottom: 8px;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+.stat-number {
+ font-size: 2.4rem;
+ font-weight: 800;
+ color: #0b1d6b;
+}
+
+/* Alerts */
+.alert-danger {
+ background-color: #ffe5e5 !important;
+}
+
+.alert-warning {
+ background-color: #fff7e6 !important;
+}
+
+/* Section header */
+.section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 24px;
+ flex-wrap: wrap;
+ gap: 12px;
+}
+
+.section-header h2 {
+ font-size: 1.8rem;
+ font-weight: 700;
+ color: #1a2a6c;
+ margin: 0;
+}
+
+/* Link button in section header */
+.section-header .nav-button {
+ padding: 8px 20px;
+ font-size: 1rem;
+}
+
+/* Chart container */
+.chart-container {
+ background-color: #fff;
+ border-radius: 14px;
+ padding: 20px 24px;
+ box-shadow: 0 3px 12px rgba(0,0,0,0.06);
+ margin-top: 12px;
+}
+
+/* Table */
+.data-table-container h3 {
+ font-weight: 700;
+ font-size: 1.25rem;
+ margin-bottom: 12px;
+ color: #1a2a6c;
+}
+
+.data-table {
+ width: 100%;
+ border-collapse: separate;
+ border-spacing: 0 10px;
+ font-size: 1rem;
+}
+
+.data-table thead th {
+ background-color: #1a73e8;
+ color: white;
+ font-weight: 700;
+ padding: 14px 18px;
+ text-align: left;
+ border-radius: 12px 12px 0 0;
+}
+
+.data-table tbody tr {
+ background-color: #f9fbff;
+ border-radius: 12px;
+ box-shadow: 0 1px 4px rgba(0,0,0,0.05);
+ transition: background-color 0.3s ease;
+}
+
+.data-table tbody tr:hover {
+ background-color: #e6f0ff;
+}
+
+.data-table tbody td {
+ padding: 14px 18px;
+ border-bottom: none;
+ vertical-align: middle;
+ color: #3a3a3a;
+}
+
+/* Responsive */
+@media (max-width: 992px) {
+ .stats-header {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 16px;
+ }
+ .stats-grid {
+ flex-direction: column;
+ }
+}
+
+@media (max-width: 480px) {
+ .nav-button {
+ width: 100%;
+ text-align: center;
+ }
+}
diff --git a/BookStation/static/css/analytics/book_inventory_detail.css b/BookStation/static/css/analytics/book_inventory_detail.css
new file mode 100644
index 00000000..1ea1c68e
--- /dev/null
+++ b/BookStation/static/css/analytics/book_inventory_detail.css
@@ -0,0 +1,315 @@
+/* Reset & base */
+body {
+ background-color: #f4f6f8;
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ margin: 0;
+ padding: 0;
+ color: #2e3a59;
+ line-height: 1.6;
+}
+
+.analytics-container {
+ max-width: 1200px;
+ margin: 32px auto;
+ padding: 0 20px 40px;
+ background: #fff;
+ border-radius: 12px;
+ box-shadow: 0 6px 18px rgba(0,0,0,0.1);
+}
+
+/* Header */
+.inventory-header {
+ padding-bottom: 24px;
+ border-bottom: 1px solid #e1e8f0;
+}
+
+.stats-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 12px;
+}
+
+.stats-header h1 {
+ font-size: 2.2rem;
+ font-weight: 700;
+ color: #1a2a6c;
+ margin: 0;
+}
+
+.dashboard-btn {
+ background-color: #1a73e8;
+ border: none;
+ color: white;
+ font-weight: 600;
+ padding: 10px 24px;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: background-color 0.3s ease;
+ box-shadow: 0 3px 8px rgba(26, 115, 232, 0.4);
+ text-decoration: none;
+ display: inline-block;
+}
+
+.dashboard-btn:hover {
+ background-color: #155db2;
+}
+
+/* Stats grid */
+.stats-grid {
+ display: flex;
+ gap: 20px;
+ margin-top: 24px;
+ flex-wrap: wrap;
+}
+
+.stat-card {
+ background: #e9f0ff;
+ border-radius: 16px;
+ padding: 24px 28px;
+ flex: 1 1 240px;
+ box-shadow: 0 2px 10px rgba(26, 115, 232, 0.15);
+ transition: transform 0.3s ease;
+}
+.stat-card:hover {
+ transform: translateY(-4px);
+}
+
+.stat-label {
+ font-size: 0.9rem;
+ font-weight: 600;
+ color: #5271ff;
+ margin-bottom: 8px;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+.stat-number {
+ font-size: 2.4rem;
+ font-weight: 800;
+ color: #0b1d6b;
+}
+
+/* Toolbar */
+.toolbar {
+ margin-top: 32px;
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ gap: 16px;
+ flex-wrap: wrap;
+}
+
+.search-form {
+ display: flex;
+ gap: 12px;
+ flex-wrap: wrap;
+ width: 100%;
+}
+
+.search-form input[type="text"] {
+ flex-grow: 1;
+ min-width: 200px;
+ padding: 12px 16px;
+ border-radius: 12px;
+ border: 1.5px solid #c1c7d0;
+ font-size: 1rem;
+ transition: border-color 0.3s ease;
+}
+
+.search-form input[type="text"]:focus {
+ outline: none;
+ border-color: #1a73e8;
+ box-shadow: 0 0 6px rgba(26, 115, 232, 0.4);
+}
+
+.search-form select {
+ padding: 12px 16px;
+ border-radius: 12px;
+ border: 1.5px solid #c1c7d0;
+ font-size: 1rem;
+ background-color: #fff;
+ cursor: pointer;
+ transition: border-color 0.3s ease;
+}
+
+.search-form select:focus {
+ outline: none;
+ border-color: #1a73e8;
+ box-shadow: 0 0 6px rgba(26, 115, 232, 0.4);
+}
+
+.search-form button[type="submit"] {
+ background-color: #1a73e8;
+ color: white;
+ border: none;
+ border-radius: 12px;
+ padding: 12px 28px;
+ font-weight: 700;
+ cursor: pointer;
+ transition: background-color 0.3s ease;
+ box-shadow: 0 3px 8px rgba(26, 115, 232, 0.5);
+}
+
+.search-form button[type="submit"]:hover {
+ background-color: #155db2;
+}
+
+.search-form a.btn-success {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ background-color: #28a745;
+ padding: 12px 28px;
+ border-radius: 12px;
+ color: white;
+ font-weight: 700;
+ text-decoration: none;
+ box-shadow: 0 3px 8px rgba(40, 167, 69, 0.5);
+ transition: background-color 0.3s ease;
+}
+
+.search-form a.btn-success:hover {
+ background-color: #1e7e34;
+}
+
+/* Table */
+.inventory-table {
+ margin-top: 36px;
+ overflow-x: auto;
+}
+
+.data-table {
+ width: 100%;
+ border-collapse: separate;
+ border-spacing: 0 10px;
+ font-size: 1rem;
+}
+
+.data-table thead th {
+ background-color: #1a73e8;
+ color: white;
+ font-weight: 700;
+ padding: 14px 18px;
+ text-align: left;
+ border-radius: 12px 12px 0 0;
+}
+
+.data-table tbody tr {
+ background-color: #f9fbff;
+ border-radius: 12px;
+ box-shadow: 0 1px 4px rgba(0,0,0,0.05);
+ transition: background-color 0.3s ease;
+}
+
+.data-table tbody tr:hover {
+ background-color: #e6f0ff;
+}
+
+.data-table tbody td {
+ padding: 14px 18px;
+ border-bottom: none;
+ vertical-align: middle;
+ color: #3a3a3a;
+}
+
+/* Stock status row styling */
+.out-of-stock {
+ background-color: #ffe5e5 !important;
+}
+
+.low-stock {
+ background-color: #fff7e6 !important;
+}
+
+.status-badge {
+ padding: 6px 12px;
+ border-radius: 9999px;
+ font-size: 0.85rem;
+ font-weight: 600;
+ color: white;
+ display: inline-block;
+ user-select: none;
+}
+
+.status-out {
+ background-color: #dc3545;
+ box-shadow: 0 2px 8px rgba(220,53,69,0.4);
+}
+
+.status-low {
+ background-color: #ffc107;
+ color: #3a3a3a;
+ box-shadow: 0 2px 8px rgba(255,193,7,0.4);
+}
+
+.status-ok {
+ background-color: #28a745;
+ box-shadow: 0 2px 8px rgba(40,167,69,0.4);
+}
+
+/* Pagination */
+.pagination {
+ margin-top: 32px;
+ display: flex;
+ justify-content: flex-end;
+ gap: 12px;
+}
+
+.pagination .step-links a,
+.pagination .step-links span {
+ padding: 10px 18px;
+ border-radius: 8px;
+ font-weight: 600;
+ font-size: 0.95rem;
+ text-decoration: none;
+ cursor: pointer;
+ user-select: none;
+ transition: background-color 0.3s ease;
+}
+
+.pagination .step-links a {
+ background-color: #e9f0ff;
+ color: #1a73e8;
+ border: 1px solid #a9c0ff;
+}
+
+.pagination .step-links a:hover {
+ background-color: #c6dbff;
+}
+
+.pagination .step-links span.current {
+ background-color: #1a73e8;
+ color: white;
+ border: none;
+}
+
+/* Responsive */
+@media (max-width: 992px) {
+ .stats-header {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 16px;
+ }
+
+ .stats-grid {
+ flex-direction: column;
+ }
+
+ .search-form {
+ flex-direction: column;
+ gap: 16px;
+ }
+
+ .pagination {
+ justify-content: center;
+ }
+}
+
+@media (max-width: 480px) {
+ .dashboard-btn, .search-form button[type="submit"], .search-form a.btn-success {
+ width: 100%;
+ text-align: center;
+ }
+}
diff --git a/BookStation/static/css/book_detail/style.css b/BookStation/static/css/book_detail/style.css
new file mode 100644
index 00000000..e18bff59
--- /dev/null
+++ b/BookStation/static/css/book_detail/style.css
@@ -0,0 +1,265 @@
+.container {
+ max-width: 1100px;
+ margin: 40px auto;
+ padding: 0 20px;
+}
+
+.book-detail-card {
+ background-color: #fff;
+ padding: 30px;
+ border-radius: 12px;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.05);
+}
+
+.book-image {
+ max-width: 100%;
+ height: auto;
+ width: 100%; /* full width của cột */
+ border-radius: 8px;
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
+ object-fit: cover;
+}
+
+
+.book-title {
+ font-size: 2.2rem;
+ font-weight: 700;
+ color: #1e293b;
+ margin-bottom: 10px;
+}
+
+.book-author {
+ font-size: 1.2rem;
+ margin-bottom: 20px;
+ color: #4b5563;
+}
+
+.book-author a {
+ color: #64748b;
+ font-weight: 500;
+ text-decoration: none;
+}
+
+.book-info {
+ background-color: #f1f3f5;
+ border-radius: 10px;
+ padding: 15px;
+ margin-bottom: 20px;
+ font-size: 1rem;
+ color: #334155;
+}
+
+.info-item {
+ margin-bottom: 8px;
+}
+
+.price {
+ font-size: 1.8rem;
+ font-weight: bold;
+ color: #e53935;
+ margin-bottom: 20px;
+}
+
+.description {
+ background-color: #f1f3f5;
+ border-radius: 10px;
+ padding: 15px;
+ margin-bottom: 20px;
+ color: #374151;
+ font-size: 1rem;
+}
+
+.categories {
+ margin-bottom: 20px;
+}
+
+.category-tag {
+ background-color: #64748b;
+ color: white;
+ padding: 6px 12px;
+ border-radius: 999px;
+ text-decoration: none;
+ font-size: 0.9rem;
+ margin-right: 6px;
+ display: inline-block;
+}
+
+.quantity-container {
+ display: flex;
+ align-items: center;
+ margin-bottom: 12px;
+}
+
+.quantity-btn {
+ width: 36px;
+ height: 36px;
+ border: none;
+ background-color: #f0f0f0;
+ color: #444;
+ font-size: 18px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ box-shadow: 0 2px 5px rgb(0 0 0 / 0.1);
+ cursor: pointer;
+ transition: background-color 0.3s ease, color 0.3s ease, box-shadow 0.3s ease;
+ user-select: none;
+ margin: 0 6px;
+}
+
+.quantity-btn:hover:not(:disabled) {
+ background-color: #ffc107;
+ color: #fff;
+ box-shadow: 0 4px 10px rgb(255 193 7 / 0.6);
+}
+
+.quantity-btn:disabled {
+ cursor: not-allowed;
+ opacity: 0.5;
+ box-shadow: none;
+}
+
+
+.quantity-input {
+ width: 60px;
+ height: 36px;
+ text-align: center;
+ font-size: 18px;
+ font-weight: 600;
+ color: #333;
+ border: 1px solid #ccc;
+ border-radius: 6px;
+ background-color: #fafafa;
+ user-select: none; /* không cho chọn text */
+ pointer-events: none; /* vì input readonly, không cho thao tác */
+ margin: 0 8px;
+ box-shadow: inset 0 2px 4px rgb(0 0 0 / 0.1);
+ transition: border-color 0.3s ease;
+}
+
+.quantity-input:focus {
+ border-color: #ffc107;
+ outline: none;
+ background-color: #fff;
+}
+
+
+.btn-warning {
+ background-color: #facc15;
+ border: none;
+ color: #000;
+ padding: 10px 16px;
+ font-weight: 600;
+ border-radius: 6px;
+ margin-bottom: 20px;
+}
+
+.review-buttons {
+ display: flex;
+ gap: 12px;
+ margin-top: 20px;
+}
+
+.review-buttons .btn {
+ padding: 10px 16px;
+ font-weight: 500;
+ border-radius: 6px;
+ font-size: 0.95rem;
+}
+
+.book-detail-card .row {
+ display: flex;
+ align-items: flex-start; /* hoặc center nếu bạn muốn canh giữa theo chiều dọc */
+ flex-wrap: wrap;
+ gap: 20px;
+}
+
+.book-detail-card .col-md-4,
+.book-detail-card .col-md-8 {
+ flex: 0 0 auto;
+}
+
+.book-detail-card .col-md-4 {
+ width: 35%; /* hoặc dùng flex-basis */
+}
+
+.book-detail-card .col-md-8 {
+ width: 60%;
+}
+
+.review-buttons {
+ margin-top: 20px;
+ display: flex;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+
+.review-buttons .btn {
+ padding: 10px 18px;
+ font-size: 1rem;
+ font-weight: 500;
+ border-radius: 8px;
+ transition: all 0.3s ease;
+}
+
+.review-buttons .btn-primary {
+ background-color: #007bff;
+ border: none;
+}
+
+.review-buttons .btn-primary:hover {
+ background-color: #0056b3;
+}
+
+.review-buttons .btn-secondary {
+ background-color: #6c757d;
+ border: none;
+}
+
+.review-buttons .btn-secondary:hover {
+ background-color: #5a6268;
+}
+
+.btn-cart {
+ background-color: #ff8c00; /* màu cam chính */
+ color: #fff;
+ border: none;
+ padding: 10px 20px;
+ font-size: 1.05rem;
+ border-radius: 8px;
+ font-weight: 600;
+ transition: background-color 0.3s ease;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.btn-cart:hover {
+ background-color: #e67600; /* màu cam đậm khi hover */
+ color: #fff;
+ text-decoration: none;
+}
+
+.btn-cart:disabled {
+ background-color: #ffcba4;
+ cursor: not-allowed;
+ opacity: 0.6;
+}
+
+.book-stock {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 6px; /* Khoảng cách giữa icon và chữ */
+ margin-top: 10px;
+}
+
+.book-stock i {
+ color: #ffc107;
+}
+
+.book-stock .quantity {
+ color: #333;
+ font-weight: 500;
+}
diff --git a/BookStation/static/js/analytics.js b/BookStation/static/js/analytics.js
new file mode 100644
index 00000000..330c79af
--- /dev/null
+++ b/BookStation/static/js/analytics.js
@@ -0,0 +1,138 @@
+// Initialize revenue chart
+function initRevenueChart(ctx, data) {
+ return new Chart(ctx, {
+ type: 'line',
+ data: {
+ labels: data.labels,
+ datasets: [{
+ label: 'Doanh thu',
+ data: data.values,
+ backgroundColor: 'rgba(99, 102, 241, 0.1)',
+ borderColor: 'rgba(99, 102, 241, 1)',
+ borderWidth: 2,
+ tension: 0.4,
+ fill: true
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ interaction: {
+ intersect: false,
+ mode: 'index'
+ },
+ scales: {
+ y: {
+ beginAtZero: true,
+ ticks: {
+ callback: function(value) {
+ return new Intl.NumberFormat('vi-VN', {
+ style: 'currency',
+ currency: 'VND'
+ }).format(value);
+ }
+ }
+ }
+ },
+ plugins: {
+ tooltip: {
+ callbacks: {
+ label: function(context) {
+ return new Intl.NumberFormat('vi-VN', {
+ style: 'currency',
+ currency: 'VND'
+ }).format(context.parsed.y);
+ }
+ }
+ }
+ }
+ }
+ });
+}
+
+// Initialize category distribution chart
+function initCategoryChart(ctx, data) {
+ return new Chart(ctx, {
+ type: 'doughnut',
+ data: {
+ labels: data.labels,
+ datasets: [{
+ data: data.values,
+ backgroundColor: [
+ 'rgba(99, 102, 241, 0.8)',
+ 'rgba(16, 185, 129, 0.8)',
+ 'rgba(245, 158, 11, 0.8)',
+ 'rgba(239, 68, 68, 0.8)',
+ 'rgba(168, 85, 247, 0.8)',
+ 'rgba(59, 130, 246, 0.8)'
+ ]
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ position: 'bottom'
+ }
+ }
+ }
+ });
+}
+
+// Initialize top books chart
+function initTopBooksChart(ctx, data) {
+ return new Chart(ctx, {
+ type: 'bar',
+ data: {
+ labels: data.labels,
+ datasets: [{
+ label: 'Số lượng bán',
+ data: data.values,
+ backgroundColor: 'rgba(16, 185, 129, 0.8)',
+ borderColor: 'rgba(16, 185, 129, 1)',
+ borderWidth: 1
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ scales: {
+ y: {
+ beginAtZero: true,
+ ticks: {
+ stepSize: 1
+ }
+ }
+ },
+ plugins: {
+ legend: {
+ display: false
+ }
+ }
+ }
+ });
+}
+
+// Animate numbers
+function animateNumbers() {
+ document.querySelectorAll('.animate-number').forEach(element => {
+ const value = parseFloat(element.dataset.value);
+ const duration = 1000;
+ const start = 0;
+ const increment = value / (duration / 16);
+ let current = start;
+
+ const animate = () => {
+ current += increment;
+ if (current < value) {
+ element.textContent = Math.floor(current).toLocaleString('vi-VN');
+ requestAnimationFrame(animate);
+ } else {
+ element.textContent = value.toLocaleString('vi-VN');
+ }
+ };
+
+ animate();
+ });
+}
diff --git a/BookStation/static/js/book_detail/quantity-control.js b/BookStation/static/js/book_detail/quantity-control.js
new file mode 100644
index 00000000..2a1bfbfe
--- /dev/null
+++ b/BookStation/static/js/book_detail/quantity-control.js
@@ -0,0 +1,32 @@
+document.addEventListener("DOMContentLoaded", function () {
+ const maxStock = parseInt(document.getElementById('max-stock-value').dataset.maxStock);
+ const input = document.querySelector('.quantity-input');
+ const btnIncrease = document.querySelector('.increase-btn');
+ const btnDecrease = document.querySelector('.decrease-btn');
+
+ input.setAttribute("readonly", true);
+
+ window.increaseQuantity = function () {
+ let current = parseInt(input.value);
+ if (current < maxStock) {
+ input.value = current + 1;
+ }
+ updateButtons();
+ };
+
+ window.decreaseQuantity = function () {
+ let current = parseInt(input.value);
+ if (current > 1) {
+ input.value = current - 1;
+ }
+ updateButtons();
+ };
+
+ function updateButtons() {
+ const value = parseInt(input.value);
+ btnIncrease.disabled = value >= maxStock;
+ btnDecrease.disabled = value <= 1;
+ }
+
+ updateButtons();
+});
\ No newline at end of file
diff --git a/BookStation/templates/base.html b/BookStation/templates/base.html
index 8d012a8f..66b671cc 100644
--- a/BookStation/templates/base.html
+++ b/BookStation/templates/base.html
@@ -6,10 +6,11 @@
Book Store
+
-
-
-
+
+
+ {% block extra_css %}{% endblock %}