From a9d862cc1f29d9d7c9c201ca6c39d926a632e2ee Mon Sep 17 00:00:00 2001 From: TaTuan-no1 Date: Sat, 7 Jun 2025 07:07:35 +0700 Subject: [PATCH 1/4] gha --- BookStation/books/templates/authors/author_confirm_delete.html | 1 - 1 file changed, 1 deletion(-) diff --git a/BookStation/books/templates/authors/author_confirm_delete.html b/BookStation/books/templates/authors/author_confirm_delete.html index 58d5ba3..64e9630 100644 --- a/BookStation/books/templates/authors/author_confirm_delete.html +++ b/BookStation/books/templates/authors/author_confirm_delete.html @@ -14,7 +14,6 @@ -
From dfadadeb448d000175cb421aaff0460de6f0793e Mon Sep 17 00:00:00 2001 From: TaTuan-no1 Date: Tue, 10 Jun 2025 23:39:07 +0700 Subject: [PATCH 2/4] newest --- BookStation/BookStation/settings.py | 1 + BookStation/books/forms.py | 6 +- BookStation/orders/models.py | 3 + BookStation/staff/forms.py | 21 + .../templates/staff/accounts/add_users.html | 178 +++----- .../templates/staff/accounts/change_user.html | 167 ++++++-- .../templates/staff/accounts/list_users.html | 141 +++++-- .../staff/templates/staff/books/add_book.html | 139 ++++--- .../templates/staff/books/view_author.html | 159 ++++++-- .../templates/staff/books/view_book.html | 379 ++++++++++++------ .../templates/staff/dashboard_staff.html | 226 ++++++----- .../templates/staff/orders/change_order.html | 156 ++++++- .../templates/staff/orders/delete_order.html | 10 - .../staff/orders/delete_orderitem.html | 10 - .../templates/staff/orders/view_order.html | 42 +- .../staff/orders/view_orderitem.html | 41 +- BookStation/staff/urls.py | 21 +- BookStation/staff/views.py | 354 ++++++++++++++-- .../static/css/staff/dashboard_staff.css | 116 ++++++ 19 files changed, 1589 insertions(+), 581 deletions(-) delete mode 100644 BookStation/staff/templates/staff/orders/delete_order.html delete mode 100644 BookStation/staff/templates/staff/orders/delete_orderitem.html create mode 100644 BookStation/static/css/staff/dashboard_staff.css diff --git a/BookStation/BookStation/settings.py b/BookStation/BookStation/settings.py index 2614e2b..f861851 100644 --- a/BookStation/BookStation/settings.py +++ b/BookStation/BookStation/settings.py @@ -37,6 +37,7 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'widget_tweaks', 'accounts', 'books', 'orders', diff --git a/BookStation/books/forms.py b/BookStation/books/forms.py index 1c8ada9..564e170 100644 --- a/BookStation/books/forms.py +++ b/BookStation/books/forms.py @@ -35,4 +35,8 @@ class Meta: class CategoryForm(forms.ModelForm): class Meta: model = Category - fields = ['name', 'description'] \ No newline at end of file + fields = ['name', 'description'] + +class BookImportForm(forms.Form): + json_file = forms.FileField(label="File JSON chứa thông tin sách", required=True) + image_zip = forms.FileField(label="File ZIP chứa ảnh bìa sách", required=True) \ No newline at end of file diff --git a/BookStation/orders/models.py b/BookStation/orders/models.py index 4b8f9f9..7cd64df 100644 --- a/BookStation/orders/models.py +++ b/BookStation/orders/models.py @@ -1,6 +1,7 @@ from django.db import models from django.conf import settings from books.models import Book +from decimal import Decimal class Order(models.Model): STATUS_CHOICES = [ @@ -41,4 +42,6 @@ def __str__(self): return f"{self.quantity} x {self.book.title}" def subtotal(self): + if self.price is None: + return Decimal('0.00') # Trả về 0 nếu price là None return self.quantity * self.price diff --git a/BookStation/staff/forms.py b/BookStation/staff/forms.py index 3f2bdd8..8f55298 100644 --- a/BookStation/staff/forms.py +++ b/BookStation/staff/forms.py @@ -2,5 +2,26 @@ from accounts.models import Users from django import forms from books.models import Book +from django import forms +from orders.forms import OrderForm +from orders.models import Order +from orders.forms import OrderItemFormSet as BaseOrderItemFormSet + +class StaffOrderForm(OrderForm): + class Meta(OrderForm.Meta): + fields = ['status', 'note'] # Thêm status + widgets = { + 'status': forms.Select(attrs={'class': 'w-full p-2 border rounded'}), + 'note': forms.Textarea(attrs={'rows': 3, 'class': 'w-full p-2 border rounded'}), + } +class StaffOrderItemFormSet(BaseOrderItemFormSet): + def clean(self): + super().clean() + for form in self.forms: + if not form.cleaned_data.get('DELETE'): + book = form.cleaned_data.get('book') + quantity = form.cleaned_data.get('quantity') + if book and quantity and quantity > book.stock: + raise forms.ValidationError(f"Sách {book.title} chỉ còn {book.stock} cuốn.") diff --git a/BookStation/staff/templates/staff/accounts/add_users.html b/BookStation/staff/templates/staff/accounts/add_users.html index 2a94d62..0576fd2 100644 --- a/BookStation/staff/templates/staff/accounts/add_users.html +++ b/BookStation/staff/templates/staff/accounts/add_users.html @@ -1,127 +1,79 @@ +{% extends 'staff/dashboard_staff.html' %} {% load static %} - - - - - - Thêm người dùng - BookStation - - - - - -
-
+{% load widget_tweaks %} {# <-- Thêm dòng này #} -
-
- - Quản trị nhân viên - -

Thêm người dùng mới

-

Tạo tài khoản mới cho nhân viên hoặc khách hàng

-
- - {% if messages %} -
- {% for message in messages %} -
- - {{ message }} -
- {% endfor %} -
- {% endif %} - -
- {% csrf_token %} - -
-
- - - {% if form.first_name.errors %} -
- {{ form.first_name.errors|striptags }} -
- {% endif %} -
- -
- - - {% if form.last_name.errors %} -
- {{ form.last_name.errors|striptags }} -
- {% endif %} -
- -
- - - {% if form.username.errors %} -
- {{ form.username.errors|striptags }} -
- {% endif %} -
+{% block content %} +
+

+ ➕ Thêm người dùng mới +

-
- - - {% if form.email.errors %} -
- {{ form.email.errors|striptags }} -
- {% endif %} + {# Hiển thị thông báo (từ Django messages framework) #} + {% if messages %} +
+ {% for message in messages %} +
+ {{ message }}
+ {% endfor %} +
+ {% endif %} -
- - + + {% csrf_token %} - {% if form.phone.errors %} -
- {{ form.phone.errors|striptags }} -
- {% endif %} -
+ {# Hiển thị lỗi tổng quát của form (non-field errors) #} + {% if form.non_field_errors %} + + {% endif %} -
- - - {% if form.password1.errors %} -
- {{ form.password1.errors|striptags }} -
+ {# Lặp qua từng trường trong form để hiển thị #} + {% for field in form %} +
+
+ + {# Sử dụng |add_class để thêm Tailwind classes #} + {# Thêm placeholder và icon nếu bạn muốn, sử dụng |attr:"placeholder:Giá trị" #} + {% render_field field class="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" %} -
- - - {% if form.password2.errors %} -
- {{ form.password2.errors|striptags }} -
- {% endif %} -
+ {% if field.help_text %} +

{{ field.help_text }}

+ {% endif %} + {% if field.errors %} +
+ {% for error in field.errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %}
+ {% endfor %} - - - -
-
- - -
+
- - +{% endblock content %} \ No newline at end of file diff --git a/BookStation/staff/templates/staff/accounts/change_user.html b/BookStation/staff/templates/staff/accounts/change_user.html index 7dcfc57..471d483 100644 --- a/BookStation/staff/templates/staff/accounts/change_user.html +++ b/BookStation/staff/templates/staff/accounts/change_user.html @@ -1,26 +1,141 @@ - - - - - Chỉnh sửa người dùng - - -

Chỉnh sửa người dùng: {{ user.username }}

- -

Thông tin cá nhân

-
- {% csrf_token %} - {{ form.as_p }} - -
- -

Đổi mật khẩu

-
- {% csrf_token %} - {{ password_form.as_p }} - -
- -

⬅ Quay lại danh sách người dùng

- - +{% extends 'staff/dashboard_staff.html' %} +{% load static %} +{% load widget_tweaks %} + +{% block content %} +
+

+ Chỉnh sửa người dùng: {{ user.username }} +

+ + {# Hiển thị thông báo (từ Django messages framework) #} + {% if messages %} +
+ {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} +
+ {% endif %} + + {# Form chỉnh sửa thông tin cá nhân #} +
+

+ Thông tin cá nhân +

+
+ {% csrf_token %} + {# Thêm một trường ẩn để xác định form nào đang được submit #} + + + {# Hiển thị lỗi tổng quát của form #} + {% if form.non_field_errors %} + + {% endif %} + + {% for field in form %} +
+ + {% render_field field class="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" %} + + {% if field.help_text %} +

{{ field.help_text }}

+ {% endif %} + {% if field.errors %} +
+ {% for error in field.errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %} +
+ {% endfor %} + +
+ +
+
+
+ + {# Form đổi mật khẩu #} +
+

+ Đổi mật khẩu +

+
+ {% csrf_token %} + {# Thêm một trường ẩn để xác định form nào đang được submit #} + + + {# Hiển thị lỗi tổng quát của form #} + {% if password_form.non_field_errors %} + + {% endif %} + + {% for field in password_form %} +
+ + {% render_field field class="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" %} + + {% if field.help_text %} +

{{ field.help_text }}

+ {% endif %} + {% if field.errors %} +
+ {% for error in field.errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %} +
+ {% endfor %} + +
+ +
+
+
+ + +
+{% endblock content %} \ No newline at end of file diff --git a/BookStation/staff/templates/staff/accounts/list_users.html b/BookStation/staff/templates/staff/accounts/list_users.html index d81c189..805ca83 100644 --- a/BookStation/staff/templates/staff/accounts/list_users.html +++ b/BookStation/staff/templates/staff/accounts/list_users.html @@ -1,47 +1,106 @@ - - - - Danh sách người dùng - - -

📋 Danh sách người dùng

+{% extends 'staff/dashboard_staff.html' %} +{% load static %} - -
- - -
+{% block content %} +
+

+ 📋 Danh sách người dùng +

-
+ {# Đã sửa action của form để trỏ đúng URL #} +
+
+ {# Giữ nguyên input HTML gốc của bạn. 'name="q"' là quan trọng để tìm kiếm hoạt động. #} + +
+ +
+
+ +
- - - - - - - - - {% for u in users %} - - - - - - - - {% empty %} - - {% endfor %} -
IDUsernameHọ tênEmailThao tác
{{ u.id }}{{ u.username }}{{ u.first_name }} {{ u.last_name }}{{ u.email }} - {% if can_change_user %} - ✏️ Sửa +
+ + + + + + + + + + + + {% for u in users %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
IDUsernameHọ tênEmailThao tác
{{ u.id }}{{ u.username }}{{ u.first_name }} {{ u.last_name }}{{ u.email }} + {% if can_change_user %} {# Giữ nguyên biến 'can_change_user' như trong code gốc của bạn #} + + ✏️ Sửa + + {% else %} + + {% endif %} +
+ Không tìm thấy người dùng nào. +
+
+ + {# Pagination (phân trang) #} + {# ĐÃ BỎ ĐIỀU KIỆN 'if users.has_other_pages' ĐỂ LUÔN HIỆN THÔNG TIN TRANG #} +
+
+ {% if users.has_previous %} + + Trước + + {% else %} + + Trước + + {% endif %} +
+ +
+ Trang {{ users.number }} trên {{ users.paginator.num_pages }} +
+ +
+ {% if users.has_next %} + + Tiếp + {% else %} - ❌ + + Tiếp + {% endif %} -
Không tìm thấy người dùng nào.
+
+
+ {# Kết thúc phần phân trang #} -
- ⬅️ Về dashboard - - +{# #} +{# ⬅️ Về dashboard#} +{# #} +
+{% endblock content %} \ No newline at end of file diff --git a/BookStation/staff/templates/staff/books/add_book.html b/BookStation/staff/templates/staff/books/add_book.html index 57c2908..c41db0a 100644 --- a/BookStation/staff/templates/staff/books/add_book.html +++ b/BookStation/staff/templates/staff/books/add_book.html @@ -1,70 +1,95 @@ +{% extends 'staff/dashboard_staff.html' %} {# Giả định bạn có một base template chung #} {% load static %} +{% load widget_tweaks %} {# Vẫn cần nếu các trường khác dùng render_field #} - - - - - Thêm sách mới - BookStation - - - -
- +{% block content %} +
+

+ Thêm sách mới +

-
-
-
-
-

📘 Thêm sách mới

+
+ {% csrf_token %} - - {% csrf_token %} - {% for field in form %} -
-
- - - - - +{% endblock content %} \ No newline at end of file diff --git a/BookStation/staff/templates/staff/books/view_author.html b/BookStation/staff/templates/staff/books/view_author.html index e3a169a..8591041 100644 --- a/BookStation/staff/templates/staff/books/view_author.html +++ b/BookStation/staff/templates/staff/books/view_author.html @@ -1,48 +1,123 @@ +{% extends 'staff/dashboard_staff.html' %} {% load static %} - -{% block title %}Danh sách tác giả - BookStation{% endblock %} +{% load widget_tweaks %} {% block content %} -
-

Danh sách tác giả

+
+

+ ✍️ Danh sách tác giả +

+ + {# Form tìm kiếm #} +
+
+ +
+ +
+
+ +{# #} +{# Thêm tác giả#} +{# #} +
+ + {% if authors %} +
+ + + + + + + + + + + + {% for author in authors %} + + + + + + + + {% endfor %} + +
ẢnhTên tác giảQuốc tịchNgày sinhThao tác
+ {% if author.image %} + {{ author.name }} + {% else %} +
+ Không ảnh +
+ {% endif %} +
{{ author.name }}{{ author.nationality }}{{ author.birth_date|date:"d/m/Y" }} + + ✏️ Sửa + +
+ {% csrf_token %} + +
+
+
+ + {# Pagination (phân trang) #} +
+
+ {% if authors.has_previous %} + + Trước + + {% else %} + + Trước + + {% endif %} +
- - - - - - - - - - - - {% for author in authors %} - - - - - - - - {% empty %} - - - - {% endfor %} - -
ẢnhTên tác giảQuốc tịchNgày sinhThao tác
- {% if author.image %} - {{ author.name }} +
+ Trang {{ authors.number }} trên {{ authors.paginator.num_pages }} +
+ +
+ {% if authors.has_next %} + + Tiếp + {% else %} - Không có ảnh + + Tiếp + {% endif %} -
{{ author.name }}{{ author.nationality }}{{ author.birth_date|date:"d/m/Y" }} - ✏️ Sửa -
- {% csrf_token %} - -
-
Không có tác giả nào.
-
-{% endblock %} +
+
+ {# Kết thúc phần phân trang #} + + {% else %} +
+

Không tìm thấy tác giả nào.

+ + Thêm tác giả mới + +
+ {% endif %} +
+{% endblock content %} \ No newline at end of file diff --git a/BookStation/staff/templates/staff/books/view_book.html b/BookStation/staff/templates/staff/books/view_book.html index 76f45e5..25f498d 100644 --- a/BookStation/staff/templates/staff/books/view_book.html +++ b/BookStation/staff/templates/staff/books/view_book.html @@ -1,134 +1,251 @@ -{##} -{##} -{##} -{# #} -{# 📚 Danh sách sách#} -{# #} -{##} -{##} -{#

📚 Danh sách sách

#} +{#{% extends 'staff/dashboard_staff.html' %}#} +{#{% load static %}#} +{#{% load widget_tweaks %}#} {##} -{# {% if books %}#} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# {% for b in books %}#} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# {% endfor %}#} -{# #} -{#
IDTiêu đềTác giảThao tác
{{ b.id }}{{ b.title }}{{ b.author }}#} -{#
#} -{# {% csrf_token %}#} -{# #} -{#
#} +{#{% block content %}#} +{#
#} +{#

#} +{# 📚 Danh sách sách#} +{#

#} {##} -{# ✏️ Sửa#} -{#
#} -{# {% else %}#} -{#

Không có sách nào trong hệ thống.

#} -{# {% endif %}#} -{##} -{##} - - - - - 📚 Danh sách sách - - - -

📚 Danh sách sách

+{#
#} +{#
#} + {# Sửa lỗi ô tìm kiếm không nhập được: Thêm 'id' và đảm bảo cú pháp đúng #} +{# {% render_field request.GET.q|attr:"id:id_q"|attr:"name:q"|attr:"placeholder:Tìm kiếm sách theo tiêu đề hoặc tác giả..." class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" value=query %}#} +{#
#} +{# #} +{#
#} +{#
#} +{# #} +{#
#} +{##} +{# {% if books %}#} +{#
#} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{# {% for b in books %}#} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{# {% endfor %}#} +{# #} +{#
STTẢnhTiêu đềTác giảGiáTồn khoThao tác
#} +{# {{ forloop.counter0|add:books.start_index }}#} +{# #} + {# Đảm bảo đây là tên trường ảnh đúng trong Book model của bạn #} +{# {% if b.cover_image %}#} +{# {{ b.title }}#} +{# {% else %}#} +{#
#} +{# Không ảnh#} +{#
#} +{# {% endif %}#} +{#
{{ b.title }}{{ b.author }}{{ b.price|floatformat:"0" }} đ{{ b.stock }}#} +{# #} +{# ✏️ Sửa#} +{# #} +{#
#} +{# {% csrf_token %}#} +{# #} +{#
#} +{#
#} +{#
#} +{##} + {# Pagination (phân trang) #} + {# ĐÃ BỎ ĐIỀU KIỆN 'if books.has_other_pages' ĐỂ LUÔN HIỆN THÔNG TIN TRANG #} +{#
#} +{#
#} +{# {% if books.has_previous %}#} +{# #} +{# Trước#} +{# #} +{# {% else %}#} +{# #} +{# Trước#} +{# #} +{# {% endif %}#} +{#
#} +{##} +{#
#} +{# Trang {{ books.number }} trên {{ books.paginator.num_pages }}#} +{#
#} +{##} +{#
#} +{# {% if books.has_next %}#} +{# #} +{# Tiếp #} +{# #} +{# {% else %}#} +{# #} +{# Tiếp #} +{# #} +{# {% endif %}#} +{#
#} +{#
#} + {# Kết thúc phần phân trang #} +{##} +{# {% else %}#} +{#
#} +{#

Không có sách nào trong hệ thống.

#} +{# #} +{# Thêm sách mới#} +{# #} +{#
#} +{# {% endif %}#} +{#
#} +{#{% endblock content %}#} +{% extends 'staff/dashboard_staff.html' %} +{% load static %} +{% load widget_tweaks %} + +{% block content %} +
+

+ 📚 Danh sách sách +

+ + {# Đã sửa action của form để trỏ đúng URL của list_book_view #} +
+
+ {# Giữ nguyên cách sử dụng render_field của bạn, đảm bảo có name="q" và id. #} +{# {% render_field request.GET.q|attr:"id:id_q"|attr:"name:q"|attr:"placeholder:Tìm kiếm sách theo tiêu đề hoặc tác giả..." #} +{# class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" value=query %}#} + +
+ +
+
+ +
+ + {% if books %} +
+ + + + + + + + + + + + + + {% for b in books %} + + + + + + + + + + {% endfor %} + +
STTẢnhTiêu đềTác giảGiáTồn khoThao tác
+ {{ forloop.counter0|add:books.start_index }} + + {# Đảm bảo đây là tên trường ảnh đúng trong Book model của bạn #} + {% if b.cover_image %} + {{ b.title }} + {% else %} +
+ Không ảnh +
+ {% endif %} +
{{ b.title }}{{ b.author }}{{ b.price|floatformat:"0" }} đ{{ b.stock }} {# Thêm flexbox để các nút nằm ngang #} + + ✏️ Sửa + +
{# Thêm inline để form không xuống dòng khi có flex #} + {% csrf_token %} + +
+
+
+ + {# Pagination (phân trang) #} + {# ĐÃ BỎ ĐIỀU KIỆN 'if books.has_other_pages' ĐỂ LUÔN HIỆN THÔNG TIN TRANG #} +
+
+ {% if books.has_previous %} + + Trước + + {% else %} + + Trước + + {% endif %} +
+ +
+ Trang {{ books.number }} trên {{ books.paginator.num_pages }} +
+ +
+ {% if books.has_next %} + + Tiếp + + {% else %} + + Tiếp + + {% endif %} +
+
+ {# Kết thúc phần phân trang #} + + {% else %} +
+

Không có sách nào trong hệ thống.

- {% if books %} - - - - - - - - - - - {% for b in books %} - - - - - - - {% endfor %} - -
STT Tiêu đềTác giảThao tác
{{ forloop.counter }}{{ b.title }}{{ b.author }} -
- {% csrf_token %} - -
- ✏️ Sửa -
- {% else %} -

Không có sách nào trong hệ thống.

- {% endif %} - - +
+ {% endif %} +
+{% endblock content %} \ No newline at end of file diff --git a/BookStation/staff/templates/staff/dashboard_staff.html b/BookStation/staff/templates/staff/dashboard_staff.html index 3d016d5..db55751 100644 --- a/BookStation/staff/templates/staff/dashboard_staff.html +++ b/BookStation/staff/templates/staff/dashboard_staff.html @@ -1,110 +1,154 @@ +{% load static %} - 📊 Trang nhân viên + + BookStation - Dashboard + + + + - .dashboard { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 20px; - max-width: 800px; - margin: 30px auto; - } +
+ + +
+
+ +

BookStation Dashboard

+
+
+ +
+ {% block content %} +
+

Chào mừng đến với Dashboard

+

Sử dụng thanh điều hướng bên trái để quản lý các mục khác nhau.

+
+ {% endblock content %} +
+
+
+ + - + \ No newline at end of file diff --git a/BookStation/staff/templates/staff/orders/change_order.html b/BookStation/staff/templates/staff/orders/change_order.html index 566549b..4adbe6b 100644 --- a/BookStation/staff/templates/staff/orders/change_order.html +++ b/BookStation/staff/templates/staff/orders/change_order.html @@ -1,10 +1,146 @@ - - - - - Title - - - - - \ No newline at end of file +{% extends 'base.html' %} +{% block content %} +
+

Chỉnh sửa đơn hàng #{{ order.pk }}

+ + {% if messages %} +
+ {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} +
+ {% endif %} + +
+ {% csrf_token %} + + +
+

Thông tin khách hàng

+

Tên: {{ customer.username }}

+

Email: {{ customer.email|default:"Không có" }}

+

Phương thức thanh toán: {{ order.get_payment_method_display }}

+
+ + +
+

Thông tin đơn hàng

+
+ {% if order_form.status %} +
+ + {{ order_form.status }} +
+ {% else %} +
+ + {{ order.get_status_display }} +
+ {% endif %} +
+ + {{ order_form.note }} +
+
+
+ + +
+

Danh sách sách

+ {{ formset.management_form }} +
+ {% for form in formset %} +
+ {{ form.id }} + {{ form.DELETE }} +
+ + {{ form.book }} +
+
+ + {{ form.quantity }} +
+
+ + {{ form.instance.price|default:"0.00" }} +
+
+ + {{ form.instance.subtotal|default:"0.00" }} +
+
+ +
+
+ {% endfor %} +
+ + + +
+ + +
+

Tổng tiền: {{ total_amount|default:"0.00" }} VNĐ

+
+ + +
+ +
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/BookStation/staff/templates/staff/orders/delete_order.html b/BookStation/staff/templates/staff/orders/delete_order.html deleted file mode 100644 index 566549b..0000000 --- a/BookStation/staff/templates/staff/orders/delete_order.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - Title - - - - - \ No newline at end of file diff --git a/BookStation/staff/templates/staff/orders/delete_orderitem.html b/BookStation/staff/templates/staff/orders/delete_orderitem.html deleted file mode 100644 index 566549b..0000000 --- a/BookStation/staff/templates/staff/orders/delete_orderitem.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - Title - - - - - \ No newline at end of file diff --git a/BookStation/staff/templates/staff/orders/view_order.html b/BookStation/staff/templates/staff/orders/view_order.html index 566549b..39769c1 100644 --- a/BookStation/staff/templates/staff/orders/view_order.html +++ b/BookStation/staff/templates/staff/orders/view_order.html @@ -1,10 +1,48 @@ +{% load static %} - + - Title + 🧾 Danh sách đơn hàng +

🧾 Danh sách đơn hàng

+ +{% if orders %} + + + + + + + + + + + + + {% for order in orders %} + + + + + + + + + {% endfor %} + +
STTKhách hàngNgày tạoTrạng tháiGhi chúThao tác
{{ forloop.counter }}{{ order.customer.username }}{{ order.created_at|date:"d/m/Y H:i" }}{{ order.get_status_display }}{{ order.note|default:"Không có ghi chú" }} + ✏️ Chỉnh sửa toàn bộ +
+ {% csrf_token %} + +
+
+{% else %} +

Không có đơn hàng nào trong hệ thống.

+{% endif %} + \ No newline at end of file diff --git a/BookStation/staff/templates/staff/orders/view_orderitem.html b/BookStation/staff/templates/staff/orders/view_orderitem.html index 566549b..c0eba67 100644 --- a/BookStation/staff/templates/staff/orders/view_orderitem.html +++ b/BookStation/staff/templates/staff/orders/view_orderitem.html @@ -1,10 +1,45 @@ - + - Title + Chi tiết đơn hàng #{{ order.id }} + +

📦 Chi tiết đơn hàng #{{ order.id }}

+

Khách hàng: {{ order.customer.username }}

+

Ngày đặt: {{ order.created_at }}

+

Trạng thái: {{ order.get_status_display }}

+

Thanh toán: {{ order.get_payment_method_display }}

+

Ghi chú: {{ order.note|default:"Không có" }}

+ +

📚 Sách trong đơn

+ + + + + + + + + + + + {% for item in order.items.all %} + + + + + + + + {% endfor %} + +
STTTên sáchSố lượngĐơn giáThành tiền
{{ forloop.counter }}{{ item.book.title }}{{ item.quantity }}{{ item.price }}₫{{ item.subtotal }}₫
+ +

💰 Tổng cộng: {{ order.total_amount }}₫

+ +

← Quay lại danh sách đơn hàng

- \ No newline at end of file + diff --git a/BookStation/staff/urls.py b/BookStation/staff/urls.py index 4e2be85..f58e3ba 100644 --- a/BookStation/staff/urls.py +++ b/BookStation/staff/urls.py @@ -3,13 +3,16 @@ urlpatterns = [ - # User + path('staff_dashboard/', views.staff_dashboard, name='staff_dashboard'), + + # User path('add_user/', views.add_user_view, name='add_user'), path('change//', views.edit_user_view, name='change_user'), path('list/users', views.list_user_view, name='list_user'), # Book path('list/books', views.list_book_view, name='view_list_book'), + path('books/addbook', views.import_books, name="import_books"), path('books/change//', views.book_change, name='book_change'), path('books//delete/', views.book_delete, name='book_delete'), path('books/add/', views.book_add, name='book_add'), @@ -21,13 +24,25 @@ # Publisher path('list/publishers/', views.list_publisher, name='list_publisher'), path('publisher/add/', views.add_publisher, name='add_publisher'), - path('publisher//edit/', views.edit_publisher, name='change_publisher'), - path('publisher//delete/', views.delete_publisher, name='delete_publisher'), + path('publisher/edit//', views.edit_publisher, name='change_publisher'), + path('publisher/delete//', views.delete_publisher, name='delete_publisher'), # Category path('list/categories/', views.list_category, name='list_category'), path('categories/add/', views.add_category, name='add_category'), path('categories/edit//', views.edit_category, name='change_category'), path('categories/delete//', views.delete_category, name='delete_category'), + # # Order + # path('orders/', views.list_order_view, name='list_order'), + # path('orders/add/', views.add_order_view, name='add_order'), + # path('orders/edit//', views.edit_order_view, name='edit_order'), + # path('orders//delete/', views.delete_order_view, name='delete_order'), + # + # # OrderItem + # path('order_item/detal/', views.order_item_detail, name='order_item_detal'), + # path('order-items/add/', views.add_order_item_view, name='add_order_item'), + # # path('order-items/edit//', views.ed, name='edit_order_item'), + # path('order-items//delete/', views.delete_order_item_view, name='delete_order_item'), + ] diff --git a/BookStation/staff/views.py b/BookStation/staff/views.py index 9f8f22a..4f3ba6a 100644 --- a/BookStation/staff/views.py +++ b/BookStation/staff/views.py @@ -1,17 +1,22 @@ from django.contrib.auth.decorators import login_required, permission_required from django.shortcuts import render, redirect, get_object_or_404 +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from accounts.forms import CustomUserCreationForm from accounts.forms import EditProfile -from books.forms import BookForm, AuthorForm, CategoryForm, PublisherForm +from books.forms import BookForm, AuthorForm, CategoryForm, PublisherForm, BookImportForm from books.models import Book, Author , Category, Publisher from accounts.models import Users from django.contrib import messages from django.contrib.auth.forms import SetPasswordForm from django.contrib.auth import update_session_auth_hash from unidecode import unidecode -from orders.forms import OrderForm -from orders.models import Order - +from django.contrib.auth import get_user_model +from django.db.models import Q +from django.forms import modelformset_factory +from django.forms import inlineformset_factory +from django.core.files.base import ContentFile +import json, zipfile, os +from django.conf import settings @login_required @@ -19,10 +24,12 @@ def staff_dashboard(request): can_add_user = request.user.has_perm('accounts.add_users') can_change_user = request.user.has_perm('accounts.change_users') can_view_user = request.user.has_perm('accounts.view_users') - can_add_order = request.user.has_perm('orders.add_order') can_add_book = request.user.has_perm('books.add_book') can_add_category = request.user.has_perm('books.add_category') can_add_publisher = request.user.has_perm('books.add_publisher') + can_change_publisher = request.user.has_perm('books.change_publisher') + can_view_publisher = request.user.has_perm('books.view_publisher') + can_delete_publisher = request.user.has_perm('books.delete_publisher') can_add_author = request.user.has_perm('books.add_author') can_delete_book = request.user.has_perm('books.delete_book') can_change_book = request.user.has_perm('books.change_book') @@ -31,24 +38,91 @@ def staff_dashboard(request): can_delete_author = request.user.has_perm('books.delete_author') can_view_category = request.user.has_perm('books.view_category') can_delete_category = request.user.has_perm('books.delete_category') + can_add_order = request.user.has_perm('orders.add_order') + can_add_orderitem = request.user.has_perm('orders.add_orderitem') + can_delete_order = request.user.has_perm('orders.delete_order') + can_delete_orderitem = request.user.has_perm('orders.delete_orderitem') + can_view_order = request.user.has_perm('orders.view_orders') + can_view_orderitem = request.user.has_perm('orders.view_orderitem') + can_change_order = request.user.has_perm('orders.change_order') + can_view_orderitem = request.user.has_perm('orders.view_orderitem') + can_change_orderitem = request.user.has_perm('orders.change_orderitem') + + return render(request, 'staff/dashboard_staff.html', { 'can_add_user': can_add_user, 'can_change_user': can_change_user, 'can_view_user': can_view_user, - 'can_add_order': can_add_order, + 'can_add_book': can_add_book, + 'can_change_book': can_change_book, + 'can_delete_book': can_delete_book, + 'can_add_category': can_add_category, + 'can_view_category': can_view_category, + 'can_delete_category': can_delete_category, + 'can_add_publisher': can_add_publisher, + 'can_change_publisher': can_change_publisher, + 'can_view_publisher': can_view_publisher, + 'can_delete_publisher': can_delete_publisher, + 'can_add_author': can_add_author, - 'can_delete_book' : can_delete_book, - 'can_change_book' : can_change_book, 'can_view_author': can_view_author, 'can_delete_author': can_delete_author, - 'can_view_category': can_view_category, - # 'books':Book.objects.all(), + + 'can_add_order': can_add_order, + 'can_change_orderitem': can_change_orderitem, + 'can_view_orderitem': can_view_orderitem, + 'can_add_orderitem': can_add_orderitem, + 'can_delete_orderitem': can_delete_orderitem, + + }) +def import_books(request): + if request.method == "POST": + form = BookImportForm(request.POST, request.FILES) + if form.is_valid(): + # Lấy file JSON và ZIP + json_file = request.FILES['json_file'] + zip_file = request.FILES['image_zip'] + + # Đọc nội dung JSON + books_data = json.load(json_file) + + # Giải nén file ảnh vào bộ nhớ tạm + with zipfile.ZipFile(zip_file) as zf: + for book_item in books_data: + title = book_item.get("title") + author_name = book_item.get("author") + category_name = book_item.get("category") + publisher_name = book_item.get("publisher") + image_name = book_item.get("image") # ví dụ "book123.jpg" + + # Tạo hoặc lấy các liên kết + author, _ = Author.objects.get_or_create(name=author_name) + category, _ = Category.objects.get_or_create(name=category_name) + publisher, _ = Publisher.objects.get_or_create(name=publisher_name) + + # Lấy ảnh từ zip + image_file = zf.read(image_name) + + # Tạo book và gán ảnh + book = Book( + title=title, + author=author, + category=category, + publisher=publisher, + ) + book.cover.save(image_name, ContentFile(image_file), save=True) + + return redirect('view_list_book') + else: + form = BookImportForm() + return redirect('book_add') + @login_required @permission_required('accounts.add_users', raise_exception=True) def add_user_view(request): @@ -99,48 +173,95 @@ def edit_user_view(request, user_id): @login_required @permission_required('accounts.view_users', raise_exception=True) def list_user_view(request): - query = request.GET.get('q', '').strip().lower() - all_users = Users.objects.filter(is_staff=False, is_superuser=False) - filtered_users = [] + User = get_user_model() # Lấy User model hiện tại của dự án + query = request.GET.get('q', '').strip() # Không cần .lower() ở đây + + # Bắt đầu với tất cả người dùng không phải staff/superuser + # Bạn có thể điều chỉnh filter này tùy theo định nghĩa người dùng của bạn + users_queryset = User.objects.filter(is_staff=False, is_superuser=False) + if query: - normalized_query = unidecode(query) - for user in all_users: - if any( - normalized_query in unidecode((getattr(user, field) or '').lower()) - for field in ['username', 'first_name', 'last_name', 'email'] - ): - filtered_users.append(user) - else: - filtered_users = all_users + # Chuyển đổi query về dạng không dấu và chữ thường một lần + normalized_query = unidecode(query).lower() + + # Xây dựng các điều kiện tìm kiếm bằng Q objects + # Sử dụng __icontains để tìm kiếm không phân biệt chữ hoa/thường + # unidecode() cho phép tìm kiếm không dấu + search_conditions = ( + Q(username__icontains=normalized_query) | + Q(first_name__icontains=normalized_query) | + Q(last_name__icontains=normalized_query) | + Q(email__icontains=normalized_query) + ) + # Nếu muốn tìm kiếm không dấu chính xác hơn, bạn có thể áp dụng unidecode cho trường + # trong database nếu bạn đang sử dụng PostgreSQL với django-unaccent, + # nhưng cách này là phổ biến và đủ cho đa số trường hợp. + + users_queryset = users_queryset.filter(search_conditions) + + # Sắp xếp queryset để phân trang hoạt động ổn định + users_queryset = users_queryset.order_by('id') + + # Thiết lập phân trang + paginator = Paginator(users_queryset, 10) # 10 người dùng mỗi trang + + page_number = request.GET.get('page') + try: + users = paginator.page(page_number) + except PageNotAnInteger: + # Nếu page không phải số nguyên, hiển thị trang đầu tiên + users = paginator.page(1) + except EmptyPage: + # Nếu page vượt quá số trang có sẵn, hiển thị trang cuối cùng + users = paginator.page(paginator.num_pages) + + # Kiểm tra quyền thay đổi người dùng can_change_user = request.user.has_perm('accounts.change_users') + return render(request, 'staff/accounts/list_users.html', { - 'users': filtered_users, + 'users': users, # Đây là đối tượng Page đã được phân trang 'can_change_user': can_change_user, + 'query': query, # Quan trọng: Truyền query vào context để giữ lại trong liên kết phân trang }) - @login_required @permission_required('books.view_book', raise_exception=True) def list_book_view(request): - query = request.GET.get('q', '').strip().lower() - all_books = Book.objects.all() - filtered_books = [] + query = request.GET.get('q', '').strip() + books_queryset = Book.objects.all() if query: - normalized_query = unidecode(query) - for book in all_books: - if any( - normalized_query in unidecode((getattr(book, field) or '').lower()) - for field in ['title', 'author'] - ): - filtered_books.append(book) - else: - filtered_books = all_books + normalized_query = unidecode(query).lower() + search_conditions = ( + Q(title__icontains=normalized_query) | + Q(author__name__icontains=normalized_query) | + Q(publisher__name__icontains=normalized_query) + ) + + books_queryset = books_queryset.filter(search_conditions) + + books_queryset = books_queryset.order_by('id') # Hoặc theo title, tùy ý + + # Thiết lập phân trang + paginator = Paginator(books_queryset, 10) # 10 sách mỗi trang + + page_number = request.GET.get('page') + try: + books = paginator.page(page_number) + except PageNotAnInteger: + # Nếu page không phải số nguyên, hiển thị trang đầu tiên + books = paginator.page(1) + except EmptyPage: + # Nếu page vượt quá số trang có sẵn, hiển thị trang cuối cùng + books = paginator.page(paginator.num_pages) + + context = { + 'books': books, # Đây là đối tượng Page đã được phân trang + 'query': query, # Quan trọng: Truyền query vào context để giữ lại trong liên kết phân trang + } + return render(request, 'staff/books/view_book.html', context) - return render(request, 'staff/books/view_book.html', { - 'books': all_books, - }) @login_required @permission_required('books.add_book', raise_exception=True) @@ -184,8 +305,43 @@ def book_delete(request, pk): @login_required @permission_required('books.view_author', raise_exception=True) def view_list_author(request): - authors = Author.objects.all() - return render(request, 'staff/books/view_author.html', {'authors': authors}) + # Lấy từ khóa tìm kiếm từ request.GET, mặc định là chuỗi rỗng + query = request.GET.get('q', '').strip() + + # Bắt đầu với tất cả các tác giả + authors_queryset = Author.objects.all() + + # Nếu có từ khóa tìm kiếm, lọc queryset + if query: + # Chuyển đổi từ khóa tìm kiếm về dạng không dấu và chữ thường để tìm kiếm linh hoạt hơn + normalized_query = unidecode(query).lower() + + # Xây dựng các điều kiện tìm kiếm bằng Q objects + # Tìm kiếm theo tên hoặc quốc tịch (không phân biệt chữ hoa/thường, không dấu) + search_conditions = ( + Q(name__icontains=normalized_query) | + Q(nationality__icontains=normalized_query) + ) + authors_queryset = authors_queryset.filter(search_conditions) + authors_queryset = authors_queryset.order_by('id') + paginator = Paginator(authors_queryset, 10) + page_number = request.GET.get('page') + try: + authors = paginator.page(page_number) + except PageNotAnInteger: + # Nếu page_number không phải số nguyên, hiển thị trang đầu tiên + authors = paginator.page(1) + except EmptyPage: + # Nếu page_number vượt quá số trang có sẵn, hiển thị trang cuối cùng + authors = paginator.page(paginator.num_pages) + + # Chuẩn bị context để truyền dữ liệu sang template + context = { + 'authors': authors, # Đối tượng Page đã được phân trang (chứa các tác giả của trang hiện tại) + 'query': query, # Truyền lại từ khóa tìm kiếm để giữ nó trên ô tìm kiếm và các liên kết phân trang + } + # Render template 'staff/authors/list_authors.html' + return render(request, 'staff/books/view_author.html', context) @login_required @@ -314,3 +470,119 @@ def edit_publisher(request, pk): form = PublisherForm(instance=publisher) return render(request, 'staff/books/change_publisher.html', {'form': form, 'title': 'Edit Publisher'}) + +# @permission_required('orders.view_order', raise_exception=True) +# def list_order_view(request): +# query = request.GET.get('q') +# if query: +# orders = Order.objects.filter( +# Q(user__username__icontains=query) | +# Q(status__icontains=query) +# ).order_by('-created_at') +# else: +# orders = Order.objects.all().order_by('-created_at') +# return render(request, 'staff/orders/view_order.html', {'orders': orders, 'query': query}) +# +# +# +# @permission_required('orders.add_order', raise_exception=True) +# def add_order_view(request): +# if request.method == 'POST': +# form = OrderForm(request.POST) +# if form.is_valid(): +# form.save() +# messages.success(request, 'Đã thêm đơn hàng mới.') +# return redirect('view_orders') +# else: +# form = OrderForm() +# return render(request, 'staff/orders/add_order.html', {'form': form}) +# +# +# @permission_required('orders.change_order', raise_exception=True) +# def edit_order_view(request, order_id): +# order = get_object_or_404(Order, pk=order_id) +# +# if request.method == 'POST': +# order_form = OrderForm(request.POST, instance=order) +# formset = OrderItemFormSet(request.POST, instance=order) +# if order_form.is_valid() and formset.is_valid(): +# order_form.save() +# # Gán price cho OrderItem mới hoặc sửa +# for form in formset: +# if form.cleaned_data and not form.cleaned_data.get('DELETE'): +# instance = form.save(commit=False) +# if not instance.price or instance.book.price != instance.price: # Cập nhật price nếu cần +# instance.price = instance.book.price or Decimal('0.00') +# instance.save() +# formset.save() +# messages.success(request, "Đơn hàng đã được cập nhật thành công.") +# return redirect('staff:list_order') # Cập nhật URL nếu khác +# else: +# messages.error(request, "Vui lòng sửa các lỗi bên dưới.") +# else: +# order_form = OrderForm(instance=order) +# formset = OrderItemFormSet(instance=order) +# +# total_amount = order.total_amount() +# customer = order.customer +# +# return render(request, 'staff/orders/change_order.html', { +# 'order_form': order_form, +# 'formset': formset, +# 'order': order, +# 'total_amount': total_amount, +# 'customer': customer, +# }) +# +# @permission_required('orders.delete_order', raise_exception=True) +# def delete_order_view(request, order_id): +# order = get_object_or_404(Order, id=order_id) +# if request.method == 'POST': +# order.delete() +# messages.success(request, 'Đã xóa đơn hàng.') +# return redirect('view_orders') +# return render(request, '', {'order': order}) +# +# +# @permission_required('orders.add_orderitem', raise_exception=True) +# def add_order_item_view(request): +# if request.method == 'POST': +# form = OrderItemFormSet(request.POST) +# if form.is_valid(): +# form.save() +# messages.success(request, 'Đã thêm sản phẩm vào đơn hàng.') +# return redirect('staff:list_order_item') +# else: +# form = OrderItemFormSet() +# return render(request, 'staff/orders/add_orderitem.html', {'form': form}) +# +# +# @permission_required('orders.view_order', raise_exception=True) +# def order_item_detail(request, order_id): +# order = get_object_or_404(Order, pk=order_id) +# return render(request, 'staff/orders/view_orderitem.html', {'order': order}) +# +# @login_required +# @permission_required('orders.change_orderitem', raise_exception=True) +# def edit_order_item_view(request, item_id): +# item = get_object_or_404(OrderItem, id=item_id) +# if request.method == 'POST': +# form = OrderItemFormSet(request.POST, instance=item) +# if form.is_valid(): +# form.save() +# messages.success(request, 'Đã cập nhật sản phẩm trong đơn hàng.') +# return redirect('staff:list_order_item') +# else: +# form = OrderItemFormSet(instance=item) +# return render(request, 'staff/orders/change_orderitem.html', {'form': form, 'item': item}) +# +# +# @permission_required('orders.delete_orderitem', raise_exception=True) +# def delete_order_item_view(request, item_id): +# item = get_object_or_404(OrderItem, id=item_id) +# if request.method == 'POST': +# item.delete() +# messages.success(request, 'Đã xóa sản phẩm khỏi đơn hàng.') +# return redirect('list_order_item') +# return render(request, '', {'item': item}) +# diff --git a/BookStation/static/css/staff/dashboard_staff.css b/BookStation/static/css/staff/dashboard_staff.css new file mode 100644 index 0000000..84de549 --- /dev/null +++ b/BookStation/static/css/staff/dashboard_staff.css @@ -0,0 +1,116 @@ +/* General reset and base styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-color: #f4f6f9; +} + +/* Sidebar styles */ +.sidebar { + background-color: #2c3e50; + color: #ecf0f1; + height: 100vh; + position: fixed; + top: 0; + left: 0; + padding: 20px; + overflow-y: auto; + transition: all 0.3s ease; + box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1); +} + +.sidebar h4 { + font-size: 1.5rem; + margin-bottom: 30px; + text-align: center; + color: #1abc9c; +} + +.sidebar ul { + list-style: none; +} + +.sidebar ul li { + margin-bottom: 15px; +} + +.sidebar ul li a { + color: #ecf0f1; + text-decoration: none; + font-size: 1.1rem; + display: flex; + align-items: center; + padding: 10px 15px; + border-radius: 5px; + transition: background-color 0.3s ease, transform 0.2s ease; +} + +.sidebar ul li a:hover { + background-color: #34495e; + transform: translateX(5px); +} + +.sidebar ul li a::before { + content: ''; + margin-right: 10px; +} + +/* Main content styles */ +.content { + margin-left: 250px; /* Match sidebar width */ + padding: 30px; + min-height: 100vh; + background-color: #ffffff; + border-radius: 10px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); + transition: margin-left 0.3s ease; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .sidebar { + width: 200px; + transform: translateX(-100%); + z-index: 1000; + } + + .sidebar.active { + transform: translateX(0); + } + + .content { + margin-left: 0; + } + + /* Toggle button for mobile */ + .sidebar::before { + content: '☰'; + position: absolute; + top: 20px; + right: -40px; + background-color: #2c3e50; + color: #ecf0f1; + padding: 10px; + border-radius: 0 5px 5px 0; + cursor: pointer; + } +} + +/* Smooth transitions for all elements */ +a, button { + transition: all 0.3s ease; +} + +/* Additional utility classes */ +.container-fluid { + padding: 0; +} + +.row { + margin: 0; +} \ No newline at end of file From ea0eb91a2cf09b25399f988375f93ef3d689c358 Mon Sep 17 00:00:00 2001 From: TaTuan-no1 Date: Wed, 11 Jun 2025 02:01:31 +0700 Subject: [PATCH 3/4] huhu --- BookStation/books/forms.py | 3 - .../0002_alter_author_birth_date.py | 17 + BookStation/books/models.py | 2 +- BookStation/staff/forms.py | 5 + .../staff/templates/staff/books/add_book.html | 344 ++++++++++++++---- BookStation/staff/test.py | 0 BookStation/staff/urls.py | 3 +- BookStation/staff/views.py | 114 +++--- 8 files changed, 369 insertions(+), 119 deletions(-) create mode 100644 BookStation/books/migrations/0002_alter_author_birth_date.py create mode 100644 BookStation/staff/test.py diff --git a/BookStation/books/forms.py b/BookStation/books/forms.py index 564e170..7f39ca1 100644 --- a/BookStation/books/forms.py +++ b/BookStation/books/forms.py @@ -37,6 +37,3 @@ class Meta: model = Category fields = ['name', 'description'] -class BookImportForm(forms.Form): - json_file = forms.FileField(label="File JSON chứa thông tin sách", required=True) - image_zip = forms.FileField(label="File ZIP chứa ảnh bìa sách", required=True) \ No newline at end of file diff --git a/BookStation/books/migrations/0002_alter_author_birth_date.py b/BookStation/books/migrations/0002_alter_author_birth_date.py new file mode 100644 index 0000000..9b2eb21 --- /dev/null +++ b/BookStation/books/migrations/0002_alter_author_birth_date.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.1 on 2025-06-10 17:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("books", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="author", + name="birth_date", + field=models.DateField(blank=True, null=True), + ), + ] diff --git a/BookStation/books/models.py b/BookStation/books/models.py index 229c888..63fe239 100644 --- a/BookStation/books/models.py +++ b/BookStation/books/models.py @@ -30,7 +30,7 @@ def __str__(self): class Book(models.Model): title = models.CharField(max_length=200) - price = models.DecimalField(max_digits=6, decimal_places=2) + price = models.DecimalField(max_digits=8, decimal_places=4) description = models.TextField(blank=True) stock = models.PositiveIntegerField(default=0) #tồn kho cover_image = models.ImageField(upload_to='book_covers/', blank=True, null=True) diff --git a/BookStation/staff/forms.py b/BookStation/staff/forms.py index 8f55298..ca95e75 100644 --- a/BookStation/staff/forms.py +++ b/BookStation/staff/forms.py @@ -8,6 +8,11 @@ from orders.forms import OrderItemFormSet as BaseOrderItemFormSet +class BookImportForm(forms.Form): + json_file = forms.FileField(label="File JSON chứa thông tin sách", required=True) + image_zip = forms.FileField(label="File ZIP chứa ảnh bìa sách", required=True) + + class StaffOrderForm(OrderForm): class Meta(OrderForm.Meta): fields = ['status', 'note'] # Thêm status diff --git a/BookStation/staff/templates/staff/books/add_book.html b/BookStation/staff/templates/staff/books/add_book.html index c41db0a..719d057 100644 --- a/BookStation/staff/templates/staff/books/add_book.html +++ b/BookStation/staff/templates/staff/books/add_book.html @@ -1,95 +1,297 @@ -{% extends 'staff/dashboard_staff.html' %} {# Giả định bạn có một base template chung #} +{#{% extends 'staff/dashboard_staff.html' %} {# Giả định bạn có một base template chung#} +{#{% load static %}#} +{#{% load widget_tweaks %} {# Vẫn cần nếu các trường khác dùng render_field#} +{##} +{#{% block content %}#} +{#
#} +{#

#} +{# Thêm sách mới#} +{#

#} +{##} +{#
#} +{# {% csrf_token %}#} +{# {% for field in form %}#} +{#
#} +{# #} +{##} +{# ĐIỀU CHỈNH LẠI DÒNG NÀY: Xử lý riêng trường 'categories' bằng cách render thủ công từng checkbox#} +{# {% if field.name == 'categories' %}#} +{#
#} +{# {% for choice in field %}#} +{#
#} +{# RENDER THỦ CÔNG THẺ INPUT CHECKBOX VÀ GÁN CLASS TRỰC TIẾP#} +{# #} +{# #} +{#
#} +{# {% endfor %}#} +{#
#} +{# {% elif field.field.widget.input_type == 'checkbox' %} {# Xử lý các checkbox thông thường khác (không phải categories)#} +{#
#} +{# {% render_field field class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" %}#} +{# #} +{#
#} +{# {% else %} {# Xử lý các trường input/textarea/select thông thường#} +{# {% render_field field class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" %}#} +{# {% endif %}#} +{##} +{# {% if field.help_text %}#} +{#

{{ field.help_text }}

#} +{# {% endif %}#} +{##} +{# {% if field.errors %}#} +{#
#} +{# {% for error in field.errors %}#} +{#

{{ error }}

#} +{# {% endfor %}#} +{#
#} +{# {% endif %}#} +{#
#} +{# {% endfor %}#} +{##} +{#
#} +{# #} +{# Hủy#} +{# #} +{# #} +{#
#} +{# #} +{# {% csrf_token %}#} +{#
#} +{# #} +{# #} +{#
#} +{#
#} +{# #} +{# #} +{#
#} +{#
#} +{# #} +{#
#} +{#
#} +{# #} +{#
#} +{#{% endblock content %}#} +{#{% extends 'staff/dashboard_staff.html' %}#} +{#{% load static %}#} +{#{% load widget_tweaks %}#} +{##} +{#{% block content %}#} +{#
#} +{#

#} +{# Thêm sách mới#} +{#

#} +{##} + {# FORM THÊM SÁCH THỦ CÔNG #} +{#
#} +{# {% csrf_token %}#} +{# {% for field in form %}#} +{#
#} +{# #} +{##} +{# {% if field.name == 'categories' %}#} +{#
#} +{# {% for choice in field %}#} +{#
#} +{# #} +{# #} +{#
#} +{# {% endfor %}#} +{#
#} +{# {% elif field.field.widget.input_type == 'checkbox' %}#} +{#
#} +{# {% render_field field class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" %}#} +{# #} +{#
#} +{# {% else %}#} +{# {% render_field field class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" %}#} +{# {% endif %}#} +{##} +{# {% if field.help_text %}#} +{#

{{ field.help_text }}

#} +{# {% endif %}#} +{##} +{# {% if field.errors %}#} +{#
#} +{# {% for error in field.errors %}#} +{#

{{ error }}

#} +{# {% endfor %}#} +{#
#} +{# {% endif %}#} +{#
#} +{# {% endfor %}#} +{##} +{#
#} +{# #} +{# Hủy#} +{# #} +{# #} +{#
#} +{#
#} +{##} +{#
#} +{##} + {# FORM NHẬP SÁCH TỪ TỆP ZIP #} +{#
#} +{# {% csrf_token %}#} +{#
#} +{# #} +{# #} +{#
#} +{#
#} +{# #} +{#
#} +{#
#} +{#
#} +{#{% endblock content %}#} +{% extends 'staff/dashboard_staff.html' %} {% load static %} -{% load widget_tweaks %} {# Vẫn cần nếu các trường khác dùng render_field #} +{% load widget_tweaks %} {% block content %} -
-

- Thêm sách mới -

+
+

+ Thêm sách mới +

-
- {% csrf_token %} - - {% for field in form %} -
- - - {# ĐIỀU CHỈNH LẠI DÒNG NÀY: Xử lý riêng trường 'categories' bằng cách render thủ công từng checkbox #} - {% if field.name == 'categories' %} -
- {% for choice in field %} -
- {# RENDER THỦ CÔNG THẺ INPUT CHECKBOX VÀ GÁN CLASS TRỰC TIẾP #} - - -
- {% endfor %} -
- {% elif field.field.widget.input_type == 'checkbox' %} {# Xử lý các checkbox thông thường khác (không phải categories) #} -
- {% render_field field class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" %} - -
- {% else %} {# Xử lý các trường input/textarea/select thông thường #} - {% render_field field class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" %} + {# FORM 1: THÊM SÁCH THỦ CÔNG #} + + {% csrf_token %} + {% for field in form %} +
+ - {% if field.help_text %} -

{{ field.help_text }}

- {% endif %} + {% if field.name == 'categories' %} +
+ {% for choice in field %} +
+ + +
+ {% endfor %} +
+ {% elif field.field.widget.input_type == 'checkbox' %} +
+ {% render_field field class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" %} + +
+ {% else %} + {% render_field field class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" %} + {% endif %} - {% if field.errors %} -
- {% for error in field.errors %} -

{{ error }}

- {% endfor %} -
- {% endif %} -
- {% endfor %} + {% if field.help_text %} +

{{ field.help_text }}

+ {% endif %} -
- - Hủy - - + {% if field.errors %} +
+ {% for error in field.errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %}
- + {% endfor %} + +
+ + Hủy + + +
+ + +
+ +

Hoặc nhập sách từ file

+ + +
{% csrf_token %}
- +
+
+ +
+
+ + {# FORM 3: NHẬP TỪ FILE ZIP ẢNH #} +
+ {% csrf_token %}
- +
- -
-{% endblock content %} \ No newline at end of file +
+{% endblock content %} +x \ No newline at end of file diff --git a/BookStation/staff/test.py b/BookStation/staff/test.py new file mode 100644 index 0000000..e69de29 diff --git a/BookStation/staff/urls.py b/BookStation/staff/urls.py index f58e3ba..4ecd1d4 100644 --- a/BookStation/staff/urls.py +++ b/BookStation/staff/urls.py @@ -12,7 +12,8 @@ path('list/users', views.list_user_view, name='list_user'), # Book path('list/books', views.list_book_view, name='view_list_book'), - path('books/addbook', views.import_books, name="import_books"), + path('books/addbookjson', views.import_books_json, name="import_books"), + path('addimage', views.import_book_images, name='add_image'), path('books/change//', views.book_change, name='book_change'), path('books//delete/', views.book_delete, name='book_delete'), path('books/add/', views.book_add, name='book_add'), diff --git a/BookStation/staff/views.py b/BookStation/staff/views.py index 4f3ba6a..24c5a27 100644 --- a/BookStation/staff/views.py +++ b/BookStation/staff/views.py @@ -3,7 +3,7 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from accounts.forms import CustomUserCreationForm from accounts.forms import EditProfile -from books.forms import BookForm, AuthorForm, CategoryForm, PublisherForm, BookImportForm +from books.forms import BookForm, AuthorForm, CategoryForm, PublisherForm from books.models import Book, Author , Category, Publisher from accounts.models import Users from django.contrib import messages @@ -16,7 +16,12 @@ from django.forms import inlineformset_factory from django.core.files.base import ContentFile import json, zipfile, os +from django.utils.dateparse import parse_date from django.conf import settings +from .forms import BookImportForm +from django.http import HttpResponse +from pathlib import Path + @login_required @@ -81,48 +86,68 @@ def staff_dashboard(request): }) -def import_books(request): - if request.method == "POST": - form = BookImportForm(request.POST, request.FILES) - if form.is_valid(): - # Lấy file JSON và ZIP - json_file = request.FILES['json_file'] - zip_file = request.FILES['image_zip'] +def import_books_json(request): + if request.method == 'POST' and request.FILES.get('json_file'): + json_file = request.FILES['json_file'] + try: + data = json.load(json_file) - # Đọc nội dung JSON - books_data = json.load(json_file) + for book_data in data: + # Tạo hoặc lấy Author + author_name = book_data.get('author', 'Unknown') + author_obj, _ = Author.objects.get_or_create(name=author_name) - # Giải nén file ảnh vào bộ nhớ tạm - with zipfile.ZipFile(zip_file) as zf: - for book_item in books_data: - title = book_item.get("title") - author_name = book_item.get("author") - category_name = book_item.get("category") - publisher_name = book_item.get("publisher") - image_name = book_item.get("image") # ví dụ "book123.jpg" - - # Tạo hoặc lấy các liên kết - author, _ = Author.objects.get_or_create(name=author_name) - category, _ = Category.objects.get_or_create(name=category_name) - publisher, _ = Publisher.objects.get_or_create(name=publisher_name) - - # Lấy ảnh từ zip - image_file = zf.read(image_name) - - # Tạo book và gán ảnh - book = Book( - title=title, - author=author, - category=category, - publisher=publisher, - ) - book.cover.save(image_name, ContentFile(image_file), save=True) + # Tạo hoặc lấy Publisher + publisher_name = book_data.get('publisher', 'Unknown') + publisher_obj, _ = Publisher.objects.get_or_create(name=publisher_name) - return redirect('view_list_book') - else: - form = BookImportForm() - return redirect('book_add') + # Tạo sách + book = Book.objects.create( + title=book_data['title'], + price=book_data['price'], + description=book_data.get('description', ''), + stock=book_data.get('stock', 0), + cover_image=book_data.get('cover_image', ''), + publication_date=book_data.get('publication_date', None), + author=author_obj, + publisher=publisher_obj, + ) + + # Gán Category (theo tên thay vì ID) + for cat_name in book_data.get('categories', []): + category_obj, _ = Category.objects.get_or_create(name=cat_name) + book.categories.add(category_obj) + + book.save() + + messages.success(request, " Đã import dữ liệu thành công!") + return redirect('') + except json.JSONDecodeError: + messages.error(request, " File JSON không hợp lệ.") + except Exception as e: + messages.error(request, f"⚠ Có lỗi xảy ra: {str(e)}") + return render(request, 'staff/books/add_book.html') + + + +def import_book_images(request): + if request.method == 'POST' and request.FILES.get('image_zip'): + zip_file = request.FILES['image_zip'] + try: + with zipfile.ZipFile(zip_file) as zf: + for filename in zf.namelist(): + name = Path(filename).name # VD: "book_1.jpg" + + # Tìm sách theo tên ảnh đã lưu trong DB + book = Book.objects.filter(cover_image=name).first() + if book: + with zf.open(filename) as f: + book.cover_image.save(name, ContentFile(f.read()), save=True) + messages.success(request, "✔️ Đã nhập ảnh sách thành công.") + except Exception as e: + messages.error(request, f"❌ Lỗi khi xử lý ZIP: {e}") + return render(request, 'staff/books/add_book.html') @login_required @permission_required('accounts.add_users', raise_exception=True) def add_user_view(request): @@ -269,13 +294,16 @@ def book_add(request): if request.method == 'POST': form = BookForm(request.POST, request.FILES) if form.is_valid(): - book = form.save(commit=False) book = form.save() - form.save_m2m() + messages.success(request, f"✔️ Đã thêm sách: {book.title}") return redirect('view_list_book') + else: + messages.error(request, '❌ Form không hợp lệ.') else: - form = BookForm() - return render(request, 'staff/books/add_book.html', {'form': form, 'title': 'Add Book'}) + form = BookForm() # ✅ phải có phần này + + return render(request, 'staff/books/add_book.html', {'form': form}) + @login_required From 9f0e8c85642962b194094e2d819a75284e2c0dc2 Mon Sep 17 00:00:00 2001 From: TaTuan-no1 Date: Wed, 11 Jun 2025 15:38:12 +0700 Subject: [PATCH 4/4] hehe --- BookStation/BookStation/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/BookStation/BookStation/settings.py b/BookStation/BookStation/settings.py index f861851..7b2ce2e 100644 --- a/BookStation/BookStation/settings.py +++ b/BookStation/BookStation/settings.py @@ -38,6 +38,7 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'widget_tweaks', + 'django.contrib.humanize', 'accounts', 'books', 'orders',