diff --git a/BookStation/BookStation/settings.py b/BookStation/BookStation/settings.py index dbcf92a7..2614e2bf 100644 --- a/BookStation/BookStation/settings.py +++ b/BookStation/BookStation/settings.py @@ -42,7 +42,8 @@ 'orders', 'home', 'staff', - + 'reviews', + 'analytics', ] AUTH_USER_MODEL = 'accounts.Users' EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' diff --git a/BookStation/BookStation/urls.py b/BookStation/BookStation/urls.py index 8ad39e6f..f3b4a618 100644 --- a/BookStation/BookStation/urls.py +++ b/BookStation/BookStation/urls.py @@ -28,6 +28,9 @@ path('orders/', include('orders.urls')), path('',include('home.urls')), path('staff/', include('staff.urls')), + path('reviews/', include('reviews.urls')), + path('analytics/', include('analytics.urls')), + ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) if settings.DEBUG: diff --git a/BookStation/analytics/__init__.py b/BookStation/analytics/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/BookStation/analytics/admin.py b/BookStation/analytics/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/BookStation/analytics/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/BookStation/analytics/apps.py b/BookStation/analytics/apps.py new file mode 100644 index 00000000..258d4ddc --- /dev/null +++ b/BookStation/analytics/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AnalyticsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'analytics' diff --git a/BookStation/analytics/migrations/__init__.py b/BookStation/analytics/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/BookStation/analytics/models.py b/BookStation/analytics/models.py new file mode 100644 index 00000000..71a83623 --- /dev/null +++ b/BookStation/analytics/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/BookStation/analytics/templates/analytics/book_inventory_detail.html b/BookStation/analytics/templates/analytics/book_inventory_detail.html new file mode 100644 index 00000000..27468316 --- /dev/null +++ b/BookStation/analytics/templates/analytics/book_inventory_detail.html @@ -0,0 +1,114 @@ +{% extends 'base.html' %} +{% load static %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+
+
+

Thống Kê BookStation

+ Dashboard +
+ +
+
+
Tổng Số Sách
+
{{ total_books }}
+
+
+
Hết Hàng
+
{{ out_of_stock }}
+
+
+
Sắp Hết
+
{{ low_stock }}
+
+
+ + +
+
+ + + + + Xuất Excel +
+
+
+ + +
+ + + + + + + + + + + + + + {% for book in page_obj %} + + + + + + + + + + {% endfor %} + +
Tên SáchTác GiảNXBTồn KhoĐã BánGiá ($)Trạng Thái
{{ book.title }}{{ book.author.name }}{{ book.publisher.name }}{{ book.stock }}{{ book.total_sold|default:"0" }}{{ book.price }} + {% if book.stock == 0 %} + Hết hàng + {% elif book.stock <= 5 %} + Sắp hết + {% else %} + Còn hàng + {% endif %} +
+ + + +
+
+{% endblock %} \ No newline at end of file diff --git a/BookStation/analytics/templates/analytics/dashboard.html b/BookStation/analytics/templates/analytics/dashboard.html new file mode 100644 index 00000000..6d44ce64 --- /dev/null +++ b/BookStation/analytics/templates/analytics/dashboard.html @@ -0,0 +1,109 @@ +{% extends 'base.html' %} +{% load static %} + +{% block extra_css %} + + +{% endblock %} + +{% block content %} +
+
+

Thống Kê BookStation

+ + + +
+ + +
+
+

Doanh Thu & Đơn Hàng

+
+
+
+
Tổng Doanh Thu
+
${{ total_revenue|floatformat:2 }}
+
+
+
Tổng Đơn Hàng
+
{{ total_orders }}
+
+
+
Khách Hàng Mới (30 ngày)
+
{{ new_customers }}
+
+
+ + +
+ +
+ + +
+

Chi Tiết Doanh Thu Theo Tháng

+ + + + + + + + + + {% for month in revenue_by_month %} + + + + + + {% endfor %} + +
ThángDoanh ThuSố Đơn Hàng
{{ month.month|date:"m/Y" }}${{ month.revenue|floatformat:2 }}{{ month.order_count }}
+
+
+ + +
+ + +
+
+
Tổng Số Sách
+
{{ total_books }}
+
+
+
Sách Hết Hàng
+
{{ out_of_stock_books.count }}
+
+
+
Sách Sắp Hết
+
{{ low_stock_books.count }}
+
+
+ + +
+

Top 10 Sách Bán Chạy

+ +
+
+
+ +{% block extra_js %} +// ...existing chart.js code... +{% endblock %} +{% endblock %} diff --git a/BookStation/analytics/tests.py b/BookStation/analytics/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/BookStation/analytics/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/BookStation/analytics/urls.py b/BookStation/analytics/urls.py new file mode 100644 index 00000000..9069566f --- /dev/null +++ b/BookStation/analytics/urls.py @@ -0,0 +1,9 @@ +from django.urls import path +from . import views + +app_name = 'analytics' + +urlpatterns = [ + path('dashboard/', views.analytics_dashboard, name='dashboard'), + path('books/inventory/', views.book_inventory_detail, name='book_inventory_detail'), +] \ No newline at end of file diff --git a/BookStation/analytics/views.py b/BookStation/analytics/views.py new file mode 100644 index 00000000..8e00f1b9 --- /dev/null +++ b/BookStation/analytics/views.py @@ -0,0 +1,148 @@ +from django.shortcuts import render +from django.db.models import Sum, Count, F, DecimalField, Q +from django.db.models.functions import TruncMonth +from django.utils import timezone +from datetime import timedelta +from books.models import Book +from orders.models import Order, OrderItem +import pandas as pd +from django.http import HttpResponse +from django.core.paginator import Paginator +from accounts.models import Users +def get_revenue_stats(): + """Thống kê doanh thu""" + # Tính tổng doanh thu từ các đơn hàng đã hoàn thành + total_revenue = OrderItem.objects.filter( + order__status='completed' + ).aggregate( + total=Sum(F('quantity') * F('price'), output_field=DecimalField()) + )['total'] or 0 + + # Thống kê doanh thu theo tháng + revenue_by_month = OrderItem.objects.filter( + order__status='completed' + ).annotate( + month=TruncMonth('order__created_at') + ).values('month').annotate( + revenue=Sum(F('quantity') * F('price'), output_field=DecimalField()), + order_count=Count('order__id', distinct=True) + ).order_by('-month')[:6] + + return { + 'total_revenue': total_revenue, + 'revenue_by_month': revenue_by_month + } + +def get_customer_order_stats(): + """Thống kê khách hàng và đơn hàng""" + # Thống kê khách hàng + total_customers = Users.objects.count() # Changed from User to Users + new_customers = Users.objects.filter( # Changed from User to Users + date_joined__gte=timezone.now() - timedelta(days=30) + ).count() + + # Thống kê đơn hàng + orders = Order.objects.all() + total_orders = orders.count() + orders_by_status = orders.values('status').annotate(count=Count('id')) + + # Top khách hàng theo doanh số + top_customers = Users.objects.annotate( # Changed from User to Users + total_spent=Sum(F('order__items__quantity') * F('order__items__price'), + output_field=DecimalField()) + ).order_by('-total_spent')[:5] + + return { + 'total_customers': total_customers, + 'new_customers': new_customers, + 'total_orders': total_orders, + 'orders_by_status': orders_by_status, + 'top_customers': top_customers + } + +def get_book_inventory_stats(): + """Thống kê tình trạng sách""" + # Thống kê tổng quan về sách + total_books = Book.objects.count() + out_of_stock = Book.objects.filter(stock=0) + low_stock = Book.objects.filter(stock__gt=0, stock__lte=5) + + # Top sách bán chạy + best_selling = Book.objects.annotate( + total_sold=Sum('orderitem__quantity') + ).order_by('-total_sold')[:10] + + return { + 'total_books': total_books, + 'out_of_stock_books': out_of_stock, + 'low_stock_books': low_stock, + 'best_selling_books': best_selling + } + +def analytics_dashboard(request): + """View chính cho dashboard analytics""" + context = { + **get_revenue_stats(), + **get_customer_order_stats(), + **get_book_inventory_stats() + } + return render(request, 'analytics/dashboard.html', context) + +def book_inventory_detail(request): + """Thống kê chi tiết về sách và kho""" + # Xử lý tìm kiếm + search_query = request.GET.get('search', '') + sort_by = request.GET.get('sort', '-stock') # Mặc định sắp xếp theo tồn kho giảm dần + + books = Book.objects.annotate( + total_sold=Sum('orderitem__quantity') + ).select_related('author', 'publisher') + + # Tìm kiếm + if search_query: + books = books.filter( + Q(title__icontains=search_query) | + Q(author__name__icontains=search_query) | + Q(publisher__name__icontains=search_query) + ) + + # Sắp xếp + books = books.order_by(sort_by) + + # Phân trang + paginator = Paginator(books, 20) # 20 items mỗi trang + page_number = request.GET.get('page') + page_obj = paginator.get_page(page_number) + + # Xuất Excel + if request.GET.get('export') == 'excel': + # Tạo DataFrame + data = [] + for book in books: + data.append({ + 'Tên Sách': book.title, + 'Tác Giả': book.author.name, + 'NXB': book.publisher.name, + 'Tồn Kho': book.stock, + 'Đã Bán': book.total_sold or 0, + 'Giá': book.price + }) + + df = pd.DataFrame(data) + + # Tạo response Excel + response = HttpResponse(content_type='application/vnd.ms-excel') + response['Content-Disposition'] = 'attachment; filename="book_inventory.xlsx"' + df.to_excel(response, index=False) + return response + + context = { + 'page_obj': page_obj, + 'search_query': search_query, + 'sort_by': sort_by, + 'total_books': books.count(), + 'out_of_stock': books.filter(stock=0).count(), + 'low_stock': books.filter(stock__gt=0, stock__lte=5).count(), + } + + return render(request, 'analytics/book_inventory_detail.html', context) \ No newline at end of file diff --git a/BookStation/books/templates/books/book_detail.html b/BookStation/books/templates/books/book_detail.html index 7af5775c..1470f6e5 100644 --- a/BookStation/books/templates/books/book_detail.html +++ b/BookStation/books/templates/books/book_detail.html @@ -5,191 +5,12 @@ {% block title %}Chi tiết sách - BookStation{% endblock %} {% block content %} - - - - +{% comment %} {% endcomment %} + +{% comment %} - + {% endcomment %} -
@@ -202,7 +23,9 @@

{{ book.title }}

-

Tác giả: {{ book.author.name }}

+

+ Tác giả: {{ book.author.name }} +

@@ -211,9 +34,6 @@

{{ book.title }}

Ngày xuất bản: {{ book.publication_date|date:"d/m/Y" }}
-
- Tồn kho: {{ book.stock }} quyển -
Giá: ${{ book.price }}
@@ -226,103 +46,52 @@

{{ book.title }}

{% endif %} -
Thể loại:
{% for category in book.categories.all %} - {{ category.name }} + {{ category.name }} {% endfor %}
- - -
-
- {% csrf_token %} - -
-
-
- -
- -
- - - -
- -
- Số lượng không hợp lệ! -
- - -
-
-
+
+ + Còn lại: {{ book.stock }} quyển + {% if book.stock == 0 %} + Tạm thời hết hàng + {% endif %} +
+ + {% comment %}
+ {% csrf_token %} +
+ + + +
+ +
{% endcomment %} +
+ {% csrf_token %} +
+ + + +
+ +
+
- diff --git a/BookStation/books/templates/books/book_form.html b/BookStation/books/templates/books/book_form.html index d007caba..1470f6e5 100644 --- a/BookStation/books/templates/books/book_form.html +++ b/BookStation/books/templates/books/book_form.html @@ -1,122 +1,98 @@ -{% comment %} {% extends 'base.html' %} {% endcomment %} +{% extends 'base.html'%} {% load static %} +{% load i18n %} -{% block title %} - {% if book %}Sửa sách - {{ book.title }}{% else %}Thêm sách mới{% endif %} - BookStation -{% endblock %} +{% block title %}Chi tiết sách - BookStation{% endblock %} {% block content %} -
- +{% comment %} {% endcomment %} + +{% comment %} + + {% endcomment %} -
-
-
-
-

- {% if book %}Sửa sách - {{ book.title }}{% else %}Thêm sách mới{% endif %} -

-
- {% csrf_token %} +
+
+
+ +
+ {{ book.title }} +
+ + +
+

{{ book.title }}

+

+ Tác giả: {{ book.author.name }} +

+ +
+
+ Nhà xuất bản: {{ book.publisher.name }} +
+
+ Ngày xuất bản: {{ book.publication_date|date:"d/m/Y" }} +
+
- {% for field in form %} -
- - - {% if field.field.widget.input_type == 'file' %} - {% if book and book.cover %} -
- Current cover -
- {% endif %} - {% endif %} +
Giá: ${{ book.price }}
- {{ field }} - - {% if field.help_text %} -
{{ field.help_text }}
- {% endif %} - - {% if field.errors %} -
- {% for error in field.errors %} - {{ error }} - {% endfor %} -
- {% endif %} -
- {% endfor %} + + {% if book.description %} +
+ Mô tả:
+ {{ book.description }} +
+ {% endif %} -
- - - Hủy - -
- +
+ Thể loại:
+ {% for category in book.categories.all %} + {{ category.name }} + {% endfor %} +
+
+ + Còn lại: {{ book.stock }} quyển + {% if book.stock == 0 %} + Tạm thời hết hàng + {% endif %} +
+ + {% comment %}
+ {% csrf_token %} +
+ + + +
+ +
{% endcomment %} +
+ {% csrf_token %} +
+ + + +
+ +
+
- -{% block extra_css %} - -{% endblock %} - -{% block extra_js %} -{% endblock %} + {% endblock %} \ No newline at end of file diff --git a/BookStation/books/templates/books/book_list.html b/BookStation/books/templates/books/book_list.html index dadd1835..6926ace7 100644 --- a/BookStation/books/templates/books/book_list.html +++ b/BookStation/books/templates/books/book_list.html @@ -11,15 +11,9 @@ - {% load i18n %} + {% load i18n %} -
-

Danh sách sách

+

Danh sách sách

+
{% for book in books %}
@@ -98,12 +92,13 @@

{{ book.title }}

- +
{% endfor %}
-
{% if is_paginated %} + + {% if is_paginated %} - {{ book.title }} + + {{ book.title }} +

{{ 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 }} +

+ +
+
+
+ {% csrf_token %} + + +
+
+ {% for i in "12345" %} + + + {% endfor %} +
+
+ +
+ +
+ +
+ + + Quay lại + +
+
+
+
+
+
+ + +{% 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 %} +
+
+
+ +
+ {% for i in "12345" %} + {% if forloop.counter <= review.rating %} + + {% else %} + + {% endif %} + {% endfor %} +
+
+

{{ review.content }}

+
+ +
+
+
+ {% 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 %}