diff --git a/README.md b/README.md index 8a34402..3c94c22 100644 --- a/README.md +++ b/README.md @@ -26,14 +26,14 @@ ## Trạng thái dự án -| Tuần | Nội dung | -|--------|--------------------------------------| -| Tuần 1 | Khởi tạo dự án | +| Tuần | Nội dung | +|-----|----------| +| Tuần 1 | Khởi tạo dự án | | Tuần 2 | Xác định các actor và use case chính | -| Tuần 3 | C4 Diagrams, ADRs | -| Tuần 4 | FONT - END | -| Tuần 5 | FONT - END + BACK END | - +| Tuần 3 | C4 Diagrams, ADRs | +| Tuần 4 | FONT - END | +| Tuần 5 | BACK-END | +| Tuần 6 | BACK-END | --- diff --git a/backend/src/__init__.py b/backend/src/__init__.py index 0524095..906a45f 100644 --- a/backend/src/__init__.py +++ b/backend/src/__init__.py @@ -1,16 +1,41 @@ +import os from flask import Flask -from flask_sqlalchemy import SQLAlchemy -app=Flask(__name__) -app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:08032005@localhost/event' -app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False -db=SQLAlchemy(app) +from backend.src.extensions import db, jwt + +def create_app(): + BASE_DIR = os.path.abspath(os.path.dirname(__file__)) + TEMPLATE_DIR = os.path.join(BASE_DIR, '../../frontend/src/templates') + STATIC_DIR = os.path.join(BASE_DIR, '../../frontend/src') + + app = Flask( + __name__, + template_folder=TEMPLATE_DIR, + static_folder=STATIC_DIR, + static_url_path='' + ) + + app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:08032005@localhost/event2' + app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + app.config['JWT_SECRET_KEY'] = 'event_ticketing_super_secret' + + db.init_app(app) + jwt.init_app(app) -if __name__ == '__main__': with app.app_context(): - # Lệnh này sẽ xóa sạch các bảng cũ - db.drop_all() - # Lệnh này tạo lại các bảng dựa trên Model bạn đã khai báo - db.create_all() - print("--- Đã reset xong Database ticket! ---") + from backend.src import models + + from backend.src.routes.admin_routes import admin_bp, admin_api_bp + from backend.src.routes.organizer_routes import organizer_bp + from backend.src.routes.api_auth_routes import api_auth_bp + from backend.src.routes.main_routes import main_bp + + try: + app.register_blueprint(admin_bp) + app.register_blueprint(organizer_bp) + app.register_blueprint(api_auth_bp) + app.register_blueprint(main_bp) + app.register_blueprint(admin_api_bp) + except Exception as e: + pass - app.run(debug=True, port=8000) \ No newline at end of file + return app \ No newline at end of file diff --git a/backend/src/app.py b/backend/src/app.py index b654f37..0af3580 100644 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -1,32 +1,7 @@ -import os -from flask import Flask -# Import db từ models -from backend.src.models import db +from backend.src.__init__ import create_app -# Nạp các blueprint -from routes.admin_routes import admin_bp -from routes.organizer_routes import organizer_bp -from routes.api_auth_routes import api_auth_bp - -# Set up đường dẫn thư mục -BASE_DIR = os.path.abspath(os.path.dirname(__file__)) -TEMPLATE_DIR = os.path.join(BASE_DIR, '../../frontend/src/templates') -STATIC_DIR = os.path.join(BASE_DIR, '../../frontend/src') - -# Khởi tạo Flask -app = Flask(__name__, template_folder=TEMPLATE_DIR, static_folder=STATIC_DIR, static_url_path='') - -# CẤU HÌNH DATABASE CHUẨN XÁC NẰM Ở ĐÂY -app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:08032005@localhost/event' -app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False - -# Móc nối db vào app -db.init_app(app) - -# Đăng ký blueprint vào app chính -app.register_blueprint(admin_bp) -app.register_blueprint(organizer_bp) -app.register_blueprint(api_auth_bp) +app = create_app() if __name__ == '__main__': + app.run(debug=True, port=8000) \ No newline at end of file diff --git a/backend/src/extensions.py b/backend/src/extensions.py new file mode 100644 index 0000000..a91d766 --- /dev/null +++ b/backend/src/extensions.py @@ -0,0 +1,5 @@ +from flask_sqlalchemy import SQLAlchemy +from flask_jwt_extended import JWTManager + +db = SQLAlchemy() +jwt = JWTManager() \ No newline at end of file diff --git a/backend/src/models.py b/backend/src/models.py index 56b2fe9..d59aac4 100644 --- a/backend/src/models.py +++ b/backend/src/models.py @@ -1,41 +1,41 @@ +# File: backend/src/models.py import enum +import sys -from flask_sqlalchemy import SQLAlchemy from sqlalchemy import Column, Integer, String, Boolean, DateTime, DECIMAL, ForeignKey, Enum as SAEnum, Date from sqlalchemy.sql import func +from backend.src.extensions import db +class EventStatusEnum(enum.Enum): + PENDING = 0 + PUBLISHED = 1 + CANCELLED = 2 -db = SQLAlchemy() -# ========================================== -# 1. KHAI BÁO ENUM CHO CÁC TRẠNG THÁI & HẠNG -# ========================================== +class EventCategoryEnum(enum.Enum): + MUSIC = 0 + WORKSHOP = 1 + ENTERTAINMENT = 2 -class EventStatusEnum(enum.Enum): - PENDING = 0 # Chờ Admin duyệt - PUBLISHED = 1 # Đã đăng bán - CANCELLED = 2 # Hủy bỏ class SeatStatusEnum(enum.Enum): - AVAILABLE = 0 # Trống - LOCKED = 1 # Đang giữ chỗ - SOLD = 2 # Đã bán + AVAILABLE = 0 + LOCKED = 1 + SOLD = 2 + class OrderStatusEnum(enum.Enum): - PENDING = 0 # Đang chờ thanh toán - PAID = 1 # Đã thanh toán - FAILED = 2 # Thất bại/Hết hạn + PENDING = 0 + PAID = 1 + FAILED = 2 -class CustomerTierEnum(enum.Enum): - MEMBER = 0 # Thành viên mới - VIP = 1 # Khách quen - VVIP = 2 # Khách sộp +class CustomerTierEnum(enum.Enum): + MEMBER = 0 + VIP = 1 + VVIP = 2 -# ========================================== -# 2. CẤP ĐỘ 1: LỚP BASE USER -# ========================================== class User(db.Model): __tablename__ = 'user' @@ -46,10 +46,10 @@ class User(db.Model): phone_number = Column(String(15), unique=True) username = Column(String(50), nullable=False, unique=True) password = Column(String(255), nullable=False) - status = Column(Boolean, default=True) # True: Active, False: Inactive - avatar = Column(String(500), default="https://default-avatar.com/user.png") # Cột avatar ông đã có sẵn nè + status = Column(Boolean, default=True) + avatar = Column(String(500), default="https://default-avatar.com/user.png") dob = Column(Date, nullable=False) - type = Column(String(50)) # Cột phân loại Kế thừa + type = Column(String(50)) __mapper_args__ = { 'polymorphic_identity': 'user', @@ -57,10 +57,6 @@ class User(db.Model): } -# ========================================== -# 3. CẤP ĐỘ 2: CÁC ACTOR KẾ THỪA TỪ USER -# ========================================== - class Customer(User): __tablename__ = 'customer' id = Column(Integer, ForeignKey('user.id'), primary_key=True) @@ -73,6 +69,7 @@ class Customer(User): orders = db.relationship('Order', backref='customer', lazy=True) + class Organizer(User): __tablename__ = 'organizer' id = Column(Integer, ForeignKey('user.id'), primary_key=True) @@ -86,6 +83,7 @@ class Organizer(User): events = db.relationship('Event', backref='organizer', lazy=True) + class Admin(User): __tablename__ = 'admin' id = Column(Integer, ForeignKey('user.id'), primary_key=True) @@ -97,53 +95,59 @@ class Admin(User): } -# ========================================== -# 4. CÁC BẢNG NGHIỆP VỤ (Sự kiện, Vé, Đơn hàng) -# ========================================== - class Event(db.Model): __tablename__ = 'events' + id = Column(Integer, primary_key=True, autoincrement=True) organizer_id = Column(Integer, ForeignKey('organizer.id'), nullable=False) title = Column(String(200), nullable=False) - thumbnail = Column(String(500), nullable=True) - description = Column(db.Text, nullable=True) + thumbnail = Column(String(500)) + description = db.Column(db.Text) location = Column(String(255), nullable=False) start_time = Column(DateTime, nullable=False) + category = Column(SAEnum(EventCategoryEnum), default=EventCategoryEnum.MUSIC) status = Column(SAEnum(EventStatusEnum), default=EventStatusEnum.PENDING) ticket_types = db.relationship('TicketType', backref='event', lazy=True) seats = db.relationship('Seat', backref='event', lazy=True) + class TicketType(db.Model): __tablename__ = 'ticket_types' - id = Column(Integer, primary_key=True, autoincrement=True) + + id = Column(Integer, primary_key=True) event_id = Column(Integer, ForeignKey('events.id'), nullable=False) name = Column(String(50), nullable=False) price = Column(DECIMAL(10, 2), nullable=False) total_quantity = Column(Integer, nullable=False) __table_args__ = ( - db.CheckConstraint('price >= 0', name='check_price_positive'), - db.CheckConstraint('total_quantity >= 0', name='check_quantity_positive'), + db.CheckConstraint('price >= 0'), + db.CheckConstraint('total_quantity >= 0'), ) + class Seat(db.Model): __tablename__ = 'seats' - id = Column(Integer, primary_key=True, autoincrement=True) + + id = Column(Integer, primary_key=True) event_id = Column(Integer, ForeignKey('events.id'), nullable=False) ticket_type_id = Column(Integer, ForeignKey('ticket_types.id'), nullable=False) seat_name = Column(String(10), nullable=False) status = Column(SAEnum(SeatStatusEnum), default=SeatStatusEnum.AVAILABLE) - locked_until = Column(DateTime, nullable=True) + locked_until = Column(DateTime) + + __table_args__ = ( + db.UniqueConstraint('event_id', 'seat_name'), + ) - __table_args__ = (db.UniqueConstraint('event_id', 'seat_name', name='_event_seat_uc'),) class Order(db.Model): __tablename__ = 'orders' - id = Column(Integer, primary_key=True, autoincrement=True) + + id = Column(Integer, primary_key=True) customer_id = Column(Integer, ForeignKey('customer.id'), nullable=False) event_id = Column(Integer, ForeignKey('events.id'), nullable=False) total_amount = Column(DECIMAL(10, 2), nullable=False) @@ -155,12 +159,30 @@ class Order(db.Model): tickets = db.relationship('Ticket', backref='order', lazy=True) + class Ticket(db.Model): __tablename__ = 'tickets' - id = Column(Integer, primary_key=True, autoincrement=True) + + id = Column(Integer, primary_key=True) order_id = Column(Integer, ForeignKey('orders.id'), nullable=False) seat_id = Column(Integer, ForeignKey('seats.id'), nullable=False, unique=True) ticket_code = Column(String(100), nullable=False, unique=True) is_checked_in = Column(Boolean, default=False) +def create_db(): + # Trick báo cho Python biết là file models đã được load rồi, tránh import vòng + sys.modules['backend.src.models'] = sys.modules[__name__] + + from backend.src import create_app, db + + app = create_app() + + with app.app_context(): + db.drop_all() + db.create_all() + print("Database created successfully!") + + +if __name__ == "__main__": + create_db() \ No newline at end of file diff --git a/backend/src/routes/admin_routes.py b/backend/src/routes/admin_routes.py index 692007a..8672ba8 100644 --- a/backend/src/routes/admin_routes.py +++ b/backend/src/routes/admin_routes.py @@ -1,28 +1,78 @@ -from flask import Blueprint, render_template +from flask import Blueprint, render_template, jsonify, request +from backend.src.services import event_service,user_service -# khoi tao blueprint cho admin +# bp cho html admin_bp = Blueprint('admin', __name__) -@admin_bp.route('/') + +@admin_bp.route('/admin') def admin_dashboard(): return render_template('index_administrator.html') -@admin_bp.route('/login') -def login(): - return render_template('login.html') - -@admin_bp.route('/register') -def register(): - return render_template('register.html') @admin_bp.route('/manage-users') def manage_users(): return render_template('manage_users.html') + + @admin_bp.route('/approve-events') def approve_events(): return render_template('approve_events.html') + @admin_bp.route('/revenue-report') def revenue_report(): return render_template('revenue_report.html') + + +admin_api_bp = Blueprint('admin_api', __name__, url_prefix='/api/v1/admin') + + +@admin_api_bp.route('/events', methods=['GET']) +def get_admin_events(): + events, status_code = event_service.get_all_events_for_admin() + return jsonify(events), status_code + + +@admin_api_bp.route('/events//status', methods=['PUT']) +def update_admin_event_status(event_id): + data = request.get_json() + action = data.get('action') + if not action: + return jsonify({"status": "error", "message": "Thiếu hành động!"}), 400 + + result, status_code = event_service.update_event_status_by_id(event_id, action) + return jsonify(result), status_code + +#---user manage +@admin_api_bp.route('/users', methods=['GET']) +def get_admin_users(): + result, status_code = user_service.get_all_users() + return jsonify(result), status_code + +@admin_api_bp.route('/users', methods=['POST']) +def add_admin_user(): + data = request.get_json() + if not all(k in data for k in ("name", "email", "username", "password", "role", "dob")): + return jsonify({"status": "error", "message": "Vui lòng điền đủ thông tin"}), 400 + result, status_code = user_service.add_new_user_by_admin(data) + return jsonify(result), status_code + +@admin_api_bp.route('/users//status', methods=['PUT']) +def toggle_user_status_api(user_id): + result, status_code = user_service.toggle_user_status(user_id) + return jsonify(result), status_code + +@admin_api_bp.route('/users//role', methods=['PUT']) +def change_user_role_api(user_id): + data = request.get_json() + new_role = data.get('role') + if not new_role: + return jsonify({"status": "error", "message": "thiếu role mới"}), 400 + result, status_code = user_service.change_user_role(user_id, new_role) + return jsonify(result), status_code +@admin_api_bp.route('/users/', methods=['DELETE']) +def delete_admin_user(user_id): + result, status_code = user_service.delete_user(user_id) + return jsonify(result), status_code \ No newline at end of file diff --git a/backend/src/routes/api_auth_routes.py b/backend/src/routes/api_auth_routes.py index 187f7de..2f1013c 100644 --- a/backend/src/routes/api_auth_routes.py +++ b/backend/src/routes/api_auth_routes.py @@ -1,11 +1,13 @@ from flask import Blueprint, request, jsonify -from backend.src.services.auth_service import register_new_user - +from backend.src.services.auth_service import register_new_user,verify_login +from werkzeug.security import check_password_hash, generate_password_hash +from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity +import datetime api_auth_bp = Blueprint('api_auth', __name__, url_prefix='/api/v1/auth') @api_auth_bp.route('/register', methods=['POST']) def api_register(): - # 1. form data, lấy data từ text + data = { 'name': request.form.get('name'), 'email': request.form.get('email'), @@ -18,15 +20,36 @@ def api_register(): 'bank_account': request.form.get('bank_account') } - # Validate cơ bản (Bắt buộc phải có email, pass, username...) + # Bắt buộc phải có email, pass, username...) if not data['email'] or not data['password'] or not data['username']: return jsonify({"status": "error", "message": "Vui lòng điền đủ các trường bắt buộc!"}), 400 # 2. Hứng file ảnh (avatar) avatar_file = request.files.get('avatar') - # 3. Giao cho tầng Service xử lý nghiệp vụ + result, status_code = register_new_user(data, avatar_file) - # 4. Trả kết quả JSON về cho Frontend + + return jsonify(result), status_code + +@api_auth_bp.route('/login', methods=['POST']) +def api_login(): + + if request.is_json: + data = request.get_json() + username = data.get('username') + password = data.get('password') + else: + username = request.form.get('username') + password = request.form.get('password') + + + if not username or not password: + return jsonify({"status": "error", "message": "Vui lòng nhập đủ Username và Password!"}), 400 + + + result, status_code = verify_login(username, password) + + return jsonify(result), status_code \ No newline at end of file diff --git a/backend/src/routes/main_routes.py b/backend/src/routes/main_routes.py new file mode 100644 index 0000000..1fe44ff --- /dev/null +++ b/backend/src/routes/main_routes.py @@ -0,0 +1,16 @@ +from flask import Blueprint, render_template + +# blueprint cho khách và trang chính +main_bp = Blueprint('public', __name__) + +@main_bp.route('/') +def home(): + return render_template('landingpage.html') + +@main_bp.route('/login') +def login(): + return render_template('login.html') + +@main_bp.route('/register') +def register(): + return render_template('register.html') \ No newline at end of file diff --git a/backend/src/services/auth_service.py b/backend/src/services/auth_service.py index 340dc70..1159691 100644 --- a/backend/src/services/auth_service.py +++ b/backend/src/services/auth_service.py @@ -1,13 +1,15 @@ -from backend.src.models import db, Customer, Organizer -from werkzeug.security import generate_password_hash -from backend.src.utils import upload_avatar_to_cloudinary import datetime +from flask_jwt_extended import create_access_token +from werkzeug.security import generate_password_hash, check_password_hash + +from backend.src.models import db, Customer, Organizer,User +from backend.src.utils import upload_avatar_to_cloudinary + def register_new_user(data, avatar_file): from backend.src.models import User - # 1. Kiểm tra xem email hoặc username đã tồn tại chưa if User.query.filter_by(email=data['email']).first(): return {"status": "error", "message": "Email đã được sử dụng!"}, 400 @@ -22,20 +24,18 @@ def register_new_user(data, avatar_file): # 3. Mã hóa mật khẩu hashed_password = generate_password_hash(data['password']) - # 4. Tự động sinh ID và Lưu xuống Database + # 4. Tự động sinh ID và Lưu try: - # THUẬT TOÁN SINH ID 8 CHỮ SỐ Ở ĐÂY + prefix = 20000000 if data['role'] == 'organizer' else 10000000 - # Quét xem trong MySQL, thằng user nào đang có ID lớn nhất trong phân khúc này last_user = User.query.filter(User.id >= prefix, User.id < prefix + 10000000).order_by(User.id.desc()).first() - # Nếu đã có người, lấy ID đó + 1. Nếu chưa có ai, lấy 10000001 hoặc 20000001 new_id = (last_user.id + 1) if last_user else (prefix + 1) if data['role'] == 'organizer': new_user = Organizer( - id=new_id, # ÉP CÁI ID TỰ CHẾ VÀO ĐÂY + id=new_id, name=data['name'], email=data['email'], phone_number=data.get('phone_number', ''), @@ -49,7 +49,7 @@ def register_new_user(data, avatar_file): else: # Mặc định là Customer new_user = Customer( - id=new_id, # ÉP CÁI ID TỰ CHẾ VÀO ĐÂY LUÔN + id=new_id, name=data['name'], email=data['email'], phone_number=data.get('phone_number', ''), @@ -65,4 +65,37 @@ def register_new_user(data, avatar_file): except Exception as e: db.session.rollback() - return {"status": "error", "message": f"Lỗi hệ thống: {str(e)}"}, 500 \ No newline at end of file + return {"status": "error", "message": f"Lỗi hệ thống: {str(e)}"}, 500 + + +##-----login + +def verify_login(username, password): + from backend.src.models import User + + user = User.query.filter_by(username=username).first() + + if not user or not check_password_hash(user.password, password): + return {"status": "error", "message": "Sai tên đăng nhập hoặc mật khẩu!"}, 401 + + if not user.status: + return {"status": "error", "message": "Tài khoản của bạn đã bị khóa!"}, 403 + + user_identity = { + "id": user.id, + "username": user.username, + "role": user.type, # Cột 'type' chứa 'customer', 'organizer', hoặc 'admin' + "name": user.name, + "avatar": user.avatar + } + + # hết tg tồn tại của token ->login lại + access_token = create_access_token(identity=user_identity, expires_delta=datetime.timedelta(days=1)) + + # 5. Trả về Token kèm thông tin user + return { + "status": "success", + "message": "Đăng nhập thành công!", + "access_token": access_token, + "user": user_identity + }, 200 diff --git a/backend/src/services/event_service.py b/backend/src/services/event_service.py new file mode 100644 index 0000000..7187b67 --- /dev/null +++ b/backend/src/services/event_service.py @@ -0,0 +1,58 @@ +from backend.src.models import db, Event, Organizer, EventStatusEnum +from sqlalchemy import case + + +def get_all_events_for_admin(): + try: + #pending len dau + pending_priority = case( + (Event.status == EventStatusEnum.PENDING, 0), + else_=1 + ) + + #query + events_data = db.session.query(Event, Organizer.company_name) \ + .join(Organizer, Event.organizer_id == Organizer.id) \ + .order_by(pending_priority, Event.id.desc()).all() + + #json + event_list = [] + for event, company_name in events_data: + event_list.append({ + "id": event.id, + "id_str": f"#EV{event.id:03d}", + "title": event.title, + "thumbnail": event.thumbnail if event.thumbnail else "https://via.placeholder.com/400x300?text=No+Image", + "location": event.location, + "organizer_name": company_name or "Chưa cập nhật", + "start_time": event.start_time.strftime("%d/%m/%Y"), + "start_time_full": event.start_time.strftime("%d/%m/%Y %H:%M:%S"), + "description": event.description or "Chưa có mô tả", + "status_value": event.status.value # 0: Pending, 1: Published, 2: Cancelled + }) + + return {"status": "success", "data": event_list}, 200 + + except Exception as e: + return {"status": "error", "message": str(e)}, 500 + + +def update_event_status_by_id(event_id, action): + try: + event = Event.query.get(event_id) + if not event: + return {"status": "error", "message": "Không tìm thấy sự kiện"}, 404 + + if action == 'approve': + event.status = EventStatusEnum.PUBLISHED + elif action == 'reject': + event.status = EventStatusEnum.CANCELLED + else: + return {"status": "error", "message": "Hành động không hợp lệ"}, 400 + + db.session.commit() + return {"status": "success", "message": "Cập nhật trạng thái thành công!"}, 200 + + except Exception as e: + db.session.rollback() + return {"status": "error", "message": str(e)}, 500 \ No newline at end of file diff --git a/backend/src/services/user_service.py b/backend/src/services/user_service.py new file mode 100644 index 0000000..ceebe22 --- /dev/null +++ b/backend/src/services/user_service.py @@ -0,0 +1,135 @@ +from backend.src.models import db, User, Customer, Organizer, Admin +from werkzeug.security import generate_password_hash +from sqlalchemy import text +import datetime + + +def get_all_users(): + try: + users = User.query.order_by(User.id.desc()).all() + user_list = [] + for u in users: + user_list.append({ + "id": u.id, + "id_str": f"#{u.id}", # Đã sửa lại hiển thị đúng 100% ID thật từ DB + "name": u.name, + "email": u.email, + "phone_number": u.phone_number or "Chưa cập nhật", + "username": u.username, + "avatar": u.avatar if u.avatar else "https://ui-avatars.com/api/?name=" + u.name, + "status": u.status, + "role": u.type, + "dob": u.dob.strftime("%d/%m/%Y") if u.dob else "Chưa cập nhật" + }) + return {"status": "success", "data": user_list}, 200 + except Exception as e: + return {"status": "error", "message": f"Lỗi hệ thống: {str(e)}"}, 500 + + +def toggle_user_status(user_id): + try: + user = User.query.get(user_id) + if not user: + return {"status": "error", "message": "Không tìm thấy người dùng!"}, 404 + + user.status = not user.status + db.session.commit() + + action = "Mở khóa" if user.status else "Khóa" + return {"status": "success", "message": f"Đã {action.lower()} tài khoản thành công!"}, 200 + except Exception as e: + db.session.rollback() + return {"status": "error", "message": f"Lỗi hệ thống: {str(e)}"}, 500 + + +def change_user_role(user_id, new_role): + try: + user = User.query.get(user_id) + if not user: + return {"status": "error", "message": "Không tìm thấy người dùng!"}, 404 + + old_role = user.type + if old_role == new_role: + return {"status": "error", "message": "Người dùng đã ở quyền này rồi!"}, 400 + + db.session.execute(text(f"DELETE FROM {old_role} WHERE id = :id"), {"id": user_id}) + db.session.execute(text("UPDATE user SET type = :new_role WHERE id = :id"), + {"new_role": new_role, "id": user_id}) + + if new_role == 'customer': + db.session.execute(text("INSERT INTO customer (id, tier) VALUES (:id, 'MEMBER')"), {"id": user_id}) + elif new_role == 'organizer': + db.session.execute(text("INSERT INTO organizer (id, company_name, bank_account) VALUES (:id, '', '')"), + {"id": user_id}) + elif new_role == 'admin': + db.session.execute(text("INSERT INTO admin (id, access_level) VALUES (:id, 1)"), {"id": user_id}) + + db.session.commit() + return {"status": "success", "message": "Cập nhật quyền thành công!"}, 200 + except Exception as e: + db.session.rollback() + return {"status": "error", "message": f"Lỗi đổi quyền: {str(e)}"}, 500 + + +def add_new_user_by_admin(data): + try: + if User.query.filter_by(email=data['email']).first(): + return {"status": "error", "message": "Email đã tồn tại!"}, 400 + if User.query.filter_by(username=data['username']).first(): + return {"status": "error", "message": "Username đã tồn tại!"}, 400 + + role = data['role'] + prefix = 10000000 + if role == 'organizer': + prefix = 20000000 + elif role == 'admin': + prefix = 30000000 + + last_user = User.query.filter(User.id >= prefix, User.id < prefix + 10000000).order_by(User.id.desc()).first() + new_id = (last_user.id + 1) if last_user else (prefix + 1) + + hashed_password = generate_password_hash(data['password']) + dob_obj = datetime.datetime.strptime(data['dob'], '%Y-%m-%d') + avatar = data.get('avatar') or f"https://ui-avatars.com/api/?name={data['name']}&background=random" + + if role == 'organizer': + new_user = Organizer(id=new_id, name=data['name'], email=data['email'], phone_number=data['phone'], + username=data['username'], password=hashed_password, dob=dob_obj, avatar=avatar, + status=True) + elif role == 'admin': + new_user = Admin(id=new_id, name=data['name'], email=data['email'], phone_number=data['phone'], + username=data['username'], password=hashed_password, dob=dob_obj, avatar=avatar, + status=True) + else: + new_user = Customer(id=new_id, name=data['name'], email=data['email'], phone_number=data['phone'], + username=data['username'], password=hashed_password, dob=dob_obj, avatar=avatar, + status=True) + + db.session.add(new_user) + db.session.commit() + return {"status": "success", "message": "Thêm người dùng thành công!"}, 201 + except Exception as e: + db.session.rollback() + return {"status": "error", "message": f"Lỗi hệ thống: {str(e)}"}, 500 + + +# Hàm MỚI: Xóa người dùng +def delete_user(user_id): + try: + user = User.query.get(user_id) + if not user: + return {"status": "error", "message": "Không tìm thấy người dùng!"}, 404 + + role = user.type + # Xóa ở bảng con trước + db.session.execute(text(f"DELETE FROM {role} WHERE id = :id"), {"id": user_id}) + # Xóa ở bảng cha sau + db.session.execute(text("DELETE FROM user WHERE id = :id"), {"id": user_id}) + + db.session.commit() + return {"status": "success", "message": "Xóa tài khoản thành công!"}, 200 + except Exception as e: + db.session.rollback() + if "foreign key constraint" in str(e).lower(): + return {"status": "error", "message": "Không thể xóa người dùng đã có giao dịch/sự kiện!"}, 400 + return {"status": "error", "message": f"Lỗi hệ thống: {str(e)}"}, 500 \ No newline at end of file diff --git a/data.sql b/data.sql index ffd12eb..c980d14 100644 --- a/data.sql +++ b/data.sql @@ -1,13 +1,12 @@ -- ======================================================================= -- FILE: data.sql --- CÔNG DỤNG: Xóa sạch data cũ, tự động nạp hàng ngàn dòng data liên kết (ID 8 chữ số) --- HƯỚNG DẪN: Mở bằng MySQL Workbench, bôi đen toàn bộ và bấm chạy (⚡) -- ======================================================================= -USE event; -- Sửa lại chữ 'event' nếu database khác +USE event2; --- 1. Xóa sạch dữ liệu cũ (Tắt check khóa ngoại để không bị lỗi) +-- 1. XÓA DATA CŨ SET FOREIGN_KEY_CHECKS = 0; + TRUNCATE TABLE tickets; TRUNCATE TABLE orders; TRUNCATE TABLE seats; @@ -17,63 +16,120 @@ TRUNCATE TABLE admin; TRUNCATE TABLE organizer; TRUNCATE TABLE customer; TRUNCATE TABLE user; + SET FOREIGN_KEY_CHECKS = 1; --- 2. Đổi Delimiter để viết vòng lặp tự động (Stored Procedure) +-- 2. TẠO PROCEDURE DELIMITER $$ DROP PROCEDURE IF EXISTS GenerateTicketHubData$$ CREATE PROCEDURE GenerateTicketHubData() BEGIN - DECLARE i INT DEFAULT 1; + DECLARE i INT; DECLARE current_event_id INT; DECLARE current_order_id INT; - -- ================================================= - -- A. TẠO 200 KHÁCH HÀNG (Customer ID: 10000001 -> 10000200) - -- ================================================= + -- ========================= + -- A. CUSTOMER + -- ========================= SET i = 10000001; WHILE i <= 10000200 DO INSERT INTO user (id, name, email, phone_number, username, password, status, avatar, dob, type) - VALUES (i, CONCAT('Khách Hàng ', i), CONCAT('customer', i, '@gmail.com'), CONCAT('090', i, '1111'), CONCAT('khachhang', i), 'scrypt:32768:8:1$mockhash', 1, '', '2005-03-08', 'customer'); - INSERT INTO customer (id, tier) VALUES (i, 'MEMBER'); + VALUES ( + i, + CONCAT('Khách Hàng ', i), + CONCAT('customer', i, '@gmail.com'), + CONCAT('090', i, '1111'), + CONCAT('khachhang', i), + 'scrypt:32768:8:1$mockhash', + 1, + '', + '2005-03-08', + 'customer' + ); + + INSERT INTO customer (id, tier) + VALUES (i, 'MEMBER'); + SET i = i + 1; END WHILE; - -- ================================================= - -- B. TẠO 200 BAN TỔ CHỨC (Organizer ID: 20000001 -> 20000200) - -- ================================================= + -- ========================= + -- B. ORGANIZER + -- ========================= SET i = 20000001; WHILE i <= 20000200 DO INSERT INTO user (id, name, email, phone_number, username, password, status, avatar, dob, type) - VALUES (i, CONCAT('Ban Tổ Chức ', i), CONCAT('org', i, '@gmail.com'), CONCAT('091', i, '2222'), CONCAT('btc', i), 'scrypt:32768:8:1$mockhash', 1, '', '1995-01-01', 'organizer'); - INSERT INTO organizer (id, company_name, bank_account) VALUES (i, CONCAT('Công ty Sự Kiện ', i), CONCAT('VCB', i)); + VALUES ( + i, + CONCAT('Ban Tổ Chức ', i), + CONCAT('org', i, '@gmail.com'), + CONCAT('091', i, '2222'), + CONCAT('btc', i), + 'scrypt:32768:8:1$mockhash', + 1, + '', + '1995-01-01', + 'organizer' + ); + + INSERT INTO organizer (id, company_name, bank_account) + VALUES ( + i, + CONCAT('Công ty Sự Kiện ', i), + CONCAT('VCB', i) + ); + SET i = i + 1; END WHILE; - -- ================================================= - -- C. TẠO 10 ADMIN (Admin ID: 30000001 -> 30000010) - -- ================================================= + -- ========================= + -- C. ADMIN + -- ========================= SET i = 30000001; WHILE i <= 30000010 DO INSERT INTO user (id, name, email, phone_number, username, password, status, avatar, dob, type) - VALUES (i, CONCAT('Admin ', i), CONCAT('admin', i, '@gmail.com'), CONCAT('098', i, '3333'), CONCAT('admin', i), 'scrypt:32768:8:1$mockhash', 1, '', '2000-10-10', 'admin'); - INSERT INTO admin (id, access_level) VALUES (i, 1); + VALUES ( + i, + CONCAT('Admin ', i), + CONCAT('admin', i, '@gmail.com'), + CONCAT('098', i, '3333'), + CONCAT('admin', i), + 'scrypt:32768:8:1$mockhash', + 1, + '', + '2000-10-10', + 'admin' + ); + + INSERT INTO admin (id, access_level) + VALUES (i, 1); + SET i = i + 1; END WHILE; - -- ================================================= - -- D. TẠO 200 SỰ KIỆN (Event ID: 4001 -> 4200) - -- ================================================= + -- ========================= + -- D. EVENTS + -- ========================= SET i = 4001; WHILE i <= 4200 DO - INSERT INTO events (id, organizer_id, title, thumbnail, description, location, start_time, status) + INSERT INTO events ( + id, + organizer_id, + title, + thumbnail, + description, + location, + start_time, + status, + category + ) VALUES ( i, - FLOOR(20000001 + RAND() * 199), -- Gán đại 1 ông BTC bất kỳ (20000001-20000200) + FLOOR(20000001 + RAND() * 200), CONCAT('Đại Nhạc Hội TicketHub Vol.', i), - CASE -- Gắn 2 cái link ảnh Cloudinary + CASE WHEN i = 4001 THEN 'https://res.cloudinary.com/dmhnfoc9i/image/upload/v1774104712/1_lnmafr.jpg' WHEN i = 4002 THEN 'https://res.cloudinary.com/dmhnfoc9i/image/upload/v1774104712/2_lupigx.jpg' ELSE '' @@ -81,60 +137,89 @@ BEGIN CONCAT('Chi tiết sự kiện cực kỳ hoành tráng số ', i), 'Hội trường Đại học Mở TP.HCM', DATE_ADD(NOW(), INTERVAL FLOOR(5 + RAND() * 30) DAY), - 'PUBLISHED' + 'PUBLISHED', + ELT(FLOOR(1 + (RAND() * 3)), 'MUSIC', 'WORKSHOP', 'ENTERTAINMENT') ); + SET i = i + 1; END WHILE; - -- ================================================= - -- E. TẠO HẠNG VÉ & GHẾ (Liên kết Sự kiện -> Hạng vé -> Ghế) - -- ================================================= + -- ========================= + -- E. TICKET + SEAT + -- ========================= SET current_event_id = 4001; - WHILE current_event_id <= 4050 DO -- Cấp ghế cho 50 sự kiện đầu tiên - -- 1. Loại vé VIP - INSERT INTO ticket_types (event_id, name, price, total_quantity) VALUES (current_event_id, 'VIP', 1000000, 5); + + WHILE current_event_id <= 4050 DO + + -- VIP + INSERT INTO ticket_types (event_id, name, price, total_quantity) + VALUES (current_event_id, 'VIP', 1000000, 5); + SET @vip_type_id = LAST_INSERT_ID(); - -- Sinh 5 ghế VIP - INSERT INTO seats (event_id, ticket_type_id, seat_name, status) VALUES - (current_event_id, @vip_type_id, 'V1', 'AVAILABLE'), (current_event_id, @vip_type_id, 'V2', 'AVAILABLE'), - (current_event_id, @vip_type_id, 'V3', 'AVAILABLE'), (current_event_id, @vip_type_id, 'V4', 'AVAILABLE'), + + INSERT INTO seats (event_id, ticket_type_id, seat_name, status) + VALUES + (current_event_id, @vip_type_id, 'V1', 'AVAILABLE'), + (current_event_id, @vip_type_id, 'V2', 'AVAILABLE'), + (current_event_id, @vip_type_id, 'V3', 'AVAILABLE'), + (current_event_id, @vip_type_id, 'V4', 'AVAILABLE'), (current_event_id, @vip_type_id, 'V5', 'AVAILABLE'); - -- 2. Loại vé Thường (Standard) - INSERT INTO ticket_types (event_id, name, price, total_quantity) VALUES (current_event_id, 'Standard', 300000, 10); + -- STANDARD + INSERT INTO ticket_types (event_id, name, price, total_quantity) + VALUES (current_event_id, 'Standard', 300000, 5); + SET @std_type_id = LAST_INSERT_ID(); - -- Sinh 5 ghế thường - INSERT INTO seats (event_id, ticket_type_id, seat_name, status) VALUES - (current_event_id, @std_type_id, 'N1', 'AVAILABLE'), (current_event_id, @std_type_id, 'N2', 'AVAILABLE'), - (current_event_id, @std_type_id, 'N3', 'AVAILABLE'), (current_event_id, @std_type_id, 'N4', 'AVAILABLE'), + + INSERT INTO seats (event_id, ticket_type_id, seat_name, status) + VALUES + (current_event_id, @std_type_id, 'N1', 'AVAILABLE'), + (current_event_id, @std_type_id, 'N2', 'AVAILABLE'), + (current_event_id, @std_type_id, 'N3', 'AVAILABLE'), + (current_event_id, @std_type_id, 'N4', 'AVAILABLE'), (current_event_id, @std_type_id, 'N5', 'AVAILABLE'); SET current_event_id = current_event_id + 1; END WHILE; - -- ================================================= - -- F. TẠO ĐƠN HÀNG VÀ VÉ (Liên kết Customer -> Order -> Ticket -> Seat) - -- ================================================= - -- Đơn hàng 1: Khách 10000001 mua vé VIP sự kiện 4001 + -- ========================= + -- F. ORDER + TICKET + -- ========================= + + -- Order 1 INSERT INTO orders (customer_id, event_id, total_amount, status, created_at, updated_at) VALUES (10000001, 4001, 1000000, 'PAID', NOW(), NOW()); + SET current_order_id = LAST_INSERT_ID(); - -- Cập nhật ghế V1 thành Đã Bán - UPDATE seats SET status = 'SOLD' WHERE event_id = 4001 AND seat_name = 'V1'; + UPDATE seats + SET status = 'SOLD' + WHERE event_id = 4001 AND seat_name = 'V1'; + + SET @seat_v1_id = ( + SELECT id FROM seats + WHERE event_id = 4001 AND seat_name = 'V1' + LIMIT 1 + ); - -- Phát hành vé cho Khách 10000001 - SET @seat_v1_id = (SELECT id FROM seats WHERE event_id = 4001 AND seat_name = 'V1'); INSERT INTO tickets (order_id, seat_id, ticket_code, is_checked_in) VALUES (current_order_id, @seat_v1_id, 'TICKET-4001-V1-10000001', 0); - -- Đơn hàng 2: Khách 10000002 mua vé Thường sự kiện 4002 + -- Order 2 INSERT INTO orders (customer_id, event_id, total_amount, status, created_at, updated_at) VALUES (10000002, 4002, 300000, 'PAID', NOW(), NOW()); + SET current_order_id = LAST_INSERT_ID(); - UPDATE seats SET status = 'SOLD' WHERE event_id = 4002 AND seat_name = 'N1'; - SET @seat_n1_id = (SELECT id FROM seats WHERE event_id = 4002 AND seat_name = 'N1'); + UPDATE seats + SET status = 'SOLD' + WHERE event_id = 4002 AND seat_name = 'N1'; + + SET @seat_n1_id = ( + SELECT id FROM seats + WHERE event_id = 4002 AND seat_name = 'N1' + LIMIT 1 + ); INSERT INTO tickets (order_id, seat_id, ticket_code, is_checked_in) VALUES (current_order_id, @seat_n1_id, 'TICKET-4002-N1-10000002', 0); @@ -143,5 +228,11 @@ END$$ DELIMITER ; --- 3. Gọi thủ tục chạy 1 phát tự động nhồi ngàn dòng data -CALL GenerateTicketHubData(); \ No newline at end of file +-- 3. CHẠY PROCEDURE +CALL GenerateTicketHubData(); + + + +----test sự kiện: +--USE event2; +--UPDATE events SET status = 'PENDING' WHERE id <= 4010; \ No newline at end of file diff --git a/data_2.sql b/data_2.sql new file mode 100644 index 0000000..80a8e71 --- /dev/null +++ b/data_2.sql @@ -0,0 +1,43 @@ +UPDATE event +SET thumbnail = 'https://res.cloudinary.com/dmhnfoc9i/image/upload/v1774104712/2_lupigx.jpg' +WHERE id = 4181; + +UPDATE event SET thumbnail = 'https://res.cloudinary.com/dmhnfoc9i/image/upload/v1774104712/2_lupigx.jpg'; + +-- ---------------------------------------------------------------------- +-- 3. TẠO SƠ ĐỒ GHẾ (SEATS) CHO SỰ KIỆN 4181 (100 GHẾ) +-- ---------------------------------------------------------------------- +INSERT INTO seat (event_id, ticket_type_id, seat_name, seat_status) VALUES + -- ===== KHU VỰC VIP (Hàng A -> D) ===== + -- Hàng A + (4181, 102, 'A1', 'AVAILABLE'), (4181, 102, 'A2', 'AVAILABLE'), (4181, 102, 'A3', 'AVAILABLE'), (4181, 102, 'A4', 'AVAILABLE'), (4181, 102, 'A5', 'AVAILABLE'), + (4181, 102, 'A6', 'AVAILABLE'), (4181, 102, 'A7', 'AVAILABLE'), (4181, 102, 'A8', 'AVAILABLE'), (4181, 102, 'A9', 'AVAILABLE'), (4181, 102, 'A10', 'AVAILABLE'), + -- Hàng B + (4181, 102, 'B1', 'AVAILABLE'), (4181, 102, 'B2', 'SOLD'), (4181, 102, 'B3', 'SOLD'), (4181, 102, 'B4', 'AVAILABLE'), (4181, 102, 'B5', 'AVAILABLE'), + (4181, 102, 'B6', 'AVAILABLE'), (4181, 102, 'B7', 'AVAILABLE'), (4181, 102, 'B8', 'AVAILABLE'), (4181, 102, 'B9', 'AVAILABLE'), (4181, 102, 'B10', 'AVAILABLE'), + -- Hàng C + (4181, 102, 'C1', 'AVAILABLE'), (4181, 102, 'C2', 'AVAILABLE'), (4181, 102, 'C3', 'AVAILABLE'), (4181, 102, 'C4', 'HELD'), (4181, 102, 'C5', 'HELD'), + (4181, 102, 'C6', 'AVAILABLE'), (4181, 102, 'C7', 'AVAILABLE'), (4181, 102, 'C8', 'AVAILABLE'), (4181, 102, 'C9', 'AVAILABLE'), (4181, 102, 'C10', 'AVAILABLE'), + -- Hàng D + (4181, 102, 'D1', 'AVAILABLE'), (4181, 102, 'D2', 'AVAILABLE'), (4181, 102, 'D3', 'AVAILABLE'), (4181, 102, 'D4', 'AVAILABLE'), (4181, 102, 'D5', 'AVAILABLE'), + (4181, 102, 'D6', 'AVAILABLE'), (4181, 102, 'D7', 'AVAILABLE'), (4181, 102, 'D8', 'AVAILABLE'), (4181, 102, 'D9', 'AVAILABLE'), (4181, 102, 'D10', 'AVAILABLE'), + + -- ===== KHU VỰC STANDARD (Hàng E -> J) ===== + -- Hàng E + (4181, 101, 'E1', 'AVAILABLE'), (4181, 101, 'E2', 'AVAILABLE'), (4181, 101, 'E3', 'AVAILABLE'), (4181, 101, 'E4', 'AVAILABLE'), (4181, 101, 'E5', 'AVAILABLE'), + (4181, 101, 'E6', 'AVAILABLE'), (4181, 101, 'E7', 'AVAILABLE'), (4181, 101, 'E8', 'AVAILABLE'), (4181, 101, 'E9', 'AVAILABLE'), (4181, 101, 'E10', 'AVAILABLE'), + -- Hàng F + (4181, 101, 'F1', 'AVAILABLE'), (4181, 101, 'F2', 'AVAILABLE'), (4181, 101, 'F3', 'AVAILABLE'), (4181, 101, 'F4', 'AVAILABLE'), (4181, 101, 'F5', 'AVAILABLE'), + (4181, 101, 'F6', 'AVAILABLE'), (4181, 101, 'F7', 'AVAILABLE'), (4181, 101, 'F8', 'AVAILABLE'), (4181, 101, 'F9', 'AVAILABLE'), (4181, 101, 'F10', 'AVAILABLE'), + -- Hàng G + (4181, 101, 'G1', 'AVAILABLE'), (4181, 101, 'G2', 'AVAILABLE'), (4181, 101, 'G3', 'AVAILABLE'), (4181, 101, 'G4', 'AVAILABLE'), (4181, 101, 'G5', 'AVAILABLE'), + (4181, 101, 'G6', 'AVAILABLE'), (4181, 101, 'G7', 'SOLD'), (4181, 101, 'G8', 'SOLD'), (4181, 101, 'G9', 'AVAILABLE'), (4181, 101, 'G10', 'AVAILABLE'), + -- Hàng H + (4181, 101, 'H1', 'AVAILABLE'), (4181, 101, 'H2', 'AVAILABLE'), (4181, 101, 'H3', 'AVAILABLE'), (4181, 101, 'H4', 'AVAILABLE'), (4181, 101, 'H5', 'AVAILABLE'), + (4181, 101, 'H6', 'AVAILABLE'), (4181, 101, 'H7', 'AVAILABLE'), (4181, 101, 'H8', 'AVAILABLE'), (4181, 101, 'H9', 'AVAILABLE'), (4181, 101, 'H10', 'AVAILABLE'), + -- Hàng I + (4181, 101, 'I1', 'AVAILABLE'), (4181, 101, 'I2', 'AVAILABLE'), (4181, 101, 'I3', 'AVAILABLE'), (4181, 101, 'I4', 'AVAILABLE'), (4181, 101, 'I5', 'AVAILABLE'), + (4181, 101, 'I6', 'AVAILABLE'), (4181, 101, 'I7', 'AVAILABLE'), (4181, 101, 'I8', 'AVAILABLE'), (4181, 101, 'I9', 'AVAILABLE'), (4181, 101, 'I10', 'AVAILABLE'), + -- Hàng J + (4181, 101, 'J1', 'AVAILABLE'), (4181, 101, 'J2', 'AVAILABLE'), (4181, 101, 'J3', 'AVAILABLE'), (4181, 101, 'J4', 'AVAILABLE'), (4181, 101, 'J5', 'AVAILABLE'), + (4181, 101, 'J6', 'AVAILABLE'), (4181, 101, 'J7', 'AVAILABLE'), (4181, 101, 'J8', 'AVAILABLE'), (4181, 101, 'J9', 'AVAILABLE'), (4181, 101, 'J10', 'AVAILABLE'); \ No newline at end of file diff --git a/frontend/src/js/admin_events.js b/frontend/src/js/admin_events.js new file mode 100644 index 0000000..f225677 --- /dev/null +++ b/frontend/src/js/admin_events.js @@ -0,0 +1,241 @@ +// doi load xong html +document.addEventListener("DOMContentLoaded", function() { + loadEvents(); + + // xu ly click bang + document.getElementById('eventTableBody').addEventListener('click', function(e) { + if (e.target.closest('.view-event-btn')) { + const eventId = e.target.closest('.view-event-btn').dataset.id; + openEventDetailModal(eventId); + } + if (e.target.closest('.approve-btn')) { + const eventId = e.target.closest('.approve-btn').dataset.id; + updateEventStatus(eventId, 'approve'); + } + if (e.target.closest('.reject-btn')) { + const eventId = e.target.closest('.reject-btn').dataset.id; + updateEventStatus(eventId, 'reject'); + } + }); + + // loc theo trang thai + document.getElementById('statusFilter').addEventListener('change', function() { + currentPage = 1; + renderEventTable(); + }); + + // tim kiem + document.getElementById('searchInput').addEventListener('input', function() { + currentPage = 1; + renderEventTable(); + }); +}); + +let globalEvents = []; +let currentPage = 1; +const itemsPerPage = 10; + +// tai du lieu +async function loadEvents() { + const tableBody = document.getElementById('eventTableBody'); + tableBody.innerHTML = '
Đang tải dữ liệu...'; + + try { + const response = await fetch('/api/v1/admin/events'); + const result = await response.json(); + + if (response.ok) { + globalEvents = result.data; + renderEventTable(); + } else { + tableBody.innerHTML = 'lỗi tải dữ liệu'; + } + } catch (error) { + console.error("lỗi", error); + } +} + +// ve bang +function renderEventTable() { + const tableBody = document.getElementById('eventTableBody'); + tableBody.innerHTML = ''; + + const statusValue = document.getElementById('statusFilter').value; + const searchText = document.getElementById('searchInput').value.toLowerCase().trim(); + + // loc data + const filteredEvents = globalEvents.filter(ev => { + const matchStatus = statusValue === 'all' || ev.status_value.toString() === statusValue; + const matchSearch = searchText === '' || + ev.title.toLowerCase().includes(searchText) || + ev.organizer_name.toLowerCase().includes(searchText); + return matchStatus && matchSearch; + }); + + if (filteredEvents.length === 0) { + tableBody.innerHTML = 'không tìm thấy sự kiện nào phù hợp'; + renderPagination(0); + return; + } + + // tinh toan phan trang + const totalPages = Math.ceil(filteredEvents.length / itemsPerPage); + if (currentPage > totalPages) currentPage = totalPages; + + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const paginatedEvents = filteredEvents.slice(startIndex, endIndex); + + // render data + paginatedEvents.forEach(event => { + let statusBadge = ''; + let actionButtons = ``; + + if (event.status_value === 0) { + statusBadge = ' Chờ duyệt'; + actionButtons += ` + + + `; + } else if (event.status_value === 1) { + statusBadge = ' Đã duyệt'; + } else if (event.status_value === 2) { + statusBadge = ' Từ chối'; + } + + const rowHTML = ` + + ${event.id_str} + +
${event.title}
+
${event.location}
+ +
${event.organizer_name}
+
${event.start_time}
+ ${statusBadge} + ${actionButtons} + + `; + tableBody.insertAdjacentHTML('beforeend', rowHTML); + }); + + // ve nut chuyen trang + renderPagination(totalPages); +} + +// ve thanh phan trang +function renderPagination(totalPages) { + const paginationContainer = document.getElementById('paginationContainer'); + if (!paginationContainer) return; + + if (totalPages <= 1) { + paginationContainer.innerHTML = ''; + return; + } + + let html = ''; + const prevDisabled = currentPage === 1 ? 'disabled' : ''; + html += `
  • Trước
  • `; + + let startPage = Math.max(1, currentPage - 2); + let endPage = Math.min(totalPages, startPage + 4); + + if (endPage - startPage < 4) { + startPage = Math.max(1, endPage - 4); + } + + if (startPage > 1) { + html += `
  • 1
  • `; + if (startPage > 2) html += `
  • ...
  • `; + } + + for (let i = startPage; i <= endPage; i++) { + if (i === currentPage) { + html += `
  • ${i}
  • `; + } else { + html += `
  • ${i}
  • `; + } + } + + if (endPage < totalPages) { + if (endPage < totalPages - 1) html += `
  • ...
  • `; + html += `
  • ${totalPages}
  • `; + } + + const nextDisabled = currentPage === totalPages ? 'disabled' : ''; + html += `
  • Sau
  • `; + + paginationContainer.innerHTML = html; +} + +// doi trang +window.changePage = function(page, event) { + event.preventDefault(); + currentPage = page; + renderEventTable(); +} + +// mo popup +function openEventDetailModal(eventId) { + const event = globalEvents.find(ev => ev.id == eventId); + if (!event) return; + + document.getElementById('modalEventTitle').innerText = event.title; + document.getElementById('modalEventThumbnail').src = event.thumbnail || 'https://via.placeholder.com/400x300?text=No+Image'; + document.getElementById('modalOrganizerName').innerText = event.organizer_name; + document.getElementById('modalLocation').innerText = event.location; + document.getElementById('modalTime').innerText = event.start_time_full; + document.getElementById('modalDescription').innerHTML = event.description || "chưa có mô tả"; + + document.getElementById('modalEventPrice').innerText = "liên hệ"; + document.getElementById('modalEventCapacity').innerText = "n/a"; + + const statusEl = document.getElementById('modalEventStatus'); + const actionArea = document.getElementById('modalActionButtons'); + actionArea.innerHTML = ''; + + if (event.status_value === 0) { + statusEl.className = 'badge bg-warning text-dark mb-2'; + statusEl.innerText = 'đang chờ duyệt'; + actionArea.innerHTML = ` + + + `; + } else if (event.status_value === 1) { + statusEl.className = 'badge bg-success mb-2'; + statusEl.innerText = 'đã duyệt'; + } else { + statusEl.className = 'badge bg-danger mb-2'; + statusEl.innerText = 'Đã từ chối'; + } + + const modal = new bootstrap.Modal(document.getElementById('eventDetailModal')); + modal.show(); +} + +// cap nhat trang thai +async function updateEventStatus(eventId, action) { + const isApprove = action === 'approve'; + if (!confirm(`bạn có chắc muốn ${isApprove ? 'Phê duyệt' : 'Từ chối'} sự kiện này`)) return; + + try { + const response = await fetch(`/api/v1/admin/events/${eventId}/status`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: action }) + }); + const result = await response.json(); + + if (response.ok) { + const modalEl = document.getElementById('eventDetailModal'); + const modalInstance = bootstrap.Modal.getInstance(modalEl); + if (modalInstance) modalInstance.hide(); + + loadEvents(); + } else { + alert("lỗi: " + result.message); + } + } catch (error) { + console.error("lỗi", error); + } +} \ No newline at end of file diff --git a/frontend/src/js/admin_users.js b/frontend/src/js/admin_users.js new file mode 100644 index 0000000..835d2bc --- /dev/null +++ b/frontend/src/js/admin_users.js @@ -0,0 +1,266 @@ +document.addEventListener("DOMContentLoaded", function() { + loadusers(); + + document.getElementById('userTableBody').addEventListener('click', function(e) { + if (e.target.closest('.view-user-btn')) { + const userid = e.target.closest('.view-user-btn').dataset.id; + opendetailmodal(userid); + } + if (e.target.closest('.toggle-status-btn')) { + const userid = e.target.closest('.toggle-status-btn').dataset.id; + toggleuserstatus(userid); + } + if (e.target.closest('.change-role-btn')) { + const userid = e.target.closest('.change-role-btn').dataset.id; + openchangerolemodal(userid); + } + if (e.target.closest('.delete-user-btn')) { + const userid = e.target.closest('.delete-user-btn').dataset.id; + deleteuser(userid); + } + }); + + document.getElementById('roleFilter').addEventListener('change', function() { + currentpage = 1; + renderusertable(); + }); + + document.getElementById('searchInput').addEventListener('input', function() { + currentpage = 1; + renderusertable(); + }); + + document.getElementById('btnSubmitAddUser').addEventListener('click', addnewuser); + document.getElementById('btnSubmitChangeRole').addEventListener('click', submitchangerole); +}); + +let globalusers = []; +let currentpage = 1; +const itemsperpage = 10; + +async function loadusers() { + const tablebody = document.getElementById('userTableBody'); + tablebody.innerHTML = '
    '; + + try { + const response = await fetch('/api/v1/admin/users'); + const result = await response.json(); + if (response.ok) { + globalusers = result.data; + renderusertable(); + } else { + tablebody.innerHTML = 'Lỗi tải dữ liệu.'; + } + } catch (error) { + console.error("Lỗi:", error); + } +} + +function renderusertable() { + const tablebody = document.getElementById('userTableBody'); + tablebody.innerHTML = ''; + + const rolevalue = document.getElementById('roleFilter').value; + const searchtext = document.getElementById('searchInput').value.toLowerCase().trim(); + + const filteredusers = globalusers.filter(u => { + const matchrole = rolevalue === 'all' || u.role === rolevalue; + const matchsearch = searchtext === '' || + u.name.toLowerCase().includes(searchtext) || + u.email.toLowerCase().includes(searchtext) || + u.phone_number.includes(searchtext); + return matchrole && matchsearch; + }); + + if (filteredusers.length === 0) { + tablebody.innerHTML = 'Không tìm thấy người dùng.'; + renderpagination(0); + return; + } + + const totalpages = Math.ceil(filteredusers.length / itemsperpage); + if (currentpage > totalpages) currentpage = totalpages; + + const startindex = (currentpage - 1) * itemsperpage; + const paginatedusers = filteredusers.slice(startindex, startindex + itemsperpage); + + paginatedusers.forEach(u => { + let rolebadge = ''; + if (u.role === 'admin') rolebadge = 'Quản trị viên'; + else if (u.role === 'organizer') rolebadge = 'Ban tổ chức'; + else rolebadge = 'Khách hàng'; + + let statusbadge = u.status + ? ' Hoạt động' + : ' Bị khóa'; + + let lockicon = u.status ? 'fa-unlock text-success' : 'fa-lock text-danger'; + + const rowhtml = ` + + ${u.id_str} + +
    + +
    +
    ${u.name}
    +
    ${u.email}
    +
    +
    + + ${rolebadge} + ${statusbadge} + + + + + + + + `; + tablebody.insertAdjacentHTML('beforeend', rowhtml); + }); + + renderpagination(totalpages); +} + +function renderpagination(totalpages) { + const container = document.getElementById('paginationContainer'); + if (totalpages <= 1) { container.innerHTML = ''; return; } + + let html = ''; + const prevdisabled = currentpage === 1 ? 'disabled' : ''; + html += `
  • Trước
  • `; + + let maxpages = 5; + let startpage = currentpage - Math.floor(maxpages / 2); + let endpage = currentpage + Math.floor(maxpages / 2); + + if (startpage < 1) { + startpage = 1; + endpage = Math.min(totalpages, maxpages); + } + if (endpage > totalpages) { + endpage = totalpages; + startpage = Math.max(1, totalpages - maxpages + 1); + } + + if (startpage > 1) html += `
  • ...
  • `; + + for (let i = startpage; i <= endpage; i++) { + if (i === currentpage) { + html += `
  • ${i}
  • `; + } else { + html += `
  • ${i}
  • `; + } + } + + if (endpage < totalpages) html += `
  • ...
  • `; + + const nextdisabled = currentpage === totalpages ? 'disabled' : ''; + html += `
  • Sau
  • `; + + container.innerHTML = html; +} + +window.changepage = function(page, event) { + event.preventDefault(); + if(page < 1 || page > Math.ceil(globalusers.length / itemsperpage)) return; + currentpage = page; + renderusertable(); +} + +function opendetailmodal(userid) { + const u = globalusers.find(x => x.id == userid); + if(!u) return; + document.getElementById('detailAvatar').src = u.avatar; + document.getElementById('detailName').innerText = u.name; + document.getElementById('detailRole').innerText = u.role.toUpperCase(); + document.getElementById('detailId').innerText = u.id_str; + document.getElementById('detailUsername').innerText = u.username; + document.getElementById('detailEmail').innerText = u.email; + document.getElementById('detailPhone').innerText = u.phone_number; + document.getElementById('detailDob').innerText = u.dob; + document.getElementById('detailStatus').innerText = u.status ? "Hoạt động" : "Bị khóa"; + + new bootstrap.Modal(document.getElementById('detailUserModal')).show(); +} + +async function toggleuserstatus(userid) { + if(!confirm("Bạn có chắc chắn muốn thay đổi trạng thái tài khoản này?")) return; + try { + const response = await fetch(`/api/v1/admin/users/${userid}/status`, { method: 'PUT' }); + const result = await response.json(); + if(response.ok) { + alert(result.message); + loadusers(); + } else alert("Lỗi: " + result.message); + } catch (e) { console.error(e); } +} + +async function deleteuser(userid) { + if(!confirm("Bạn có chắc chắn muốn XÓA VĨNH VIỄN người dùng này không? Hành động này không thể hoàn tác!")) return; + try { + const response = await fetch(`/api/v1/admin/users/${userid}`, { method: 'DELETE' }); + const result = await response.json(); + if(response.ok) { + alert(result.message); + loadusers(); + } else { + alert("Lỗi: " + result.message); + } + } catch (e) { console.error(e); } +} + +function openchangerolemodal(userid) { + document.getElementById('changeRoleUserId').value = userid; + new bootstrap.Modal(document.getElementById('changeRoleModal')).show(); +} + +async function submitchangerole() { + const userid = document.getElementById('changeRoleUserId').value; + const newrole = document.getElementById('newRoleSelect').value; + if(!confirm("Xác nhận đổi quyền người dùng này?")) return; + + try { + const response = await fetch(`/api/v1/admin/users/${userid}/role`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ role: newrole }) + }); + const result = await response.json(); + if(response.ok) { + alert(result.message); + bootstrap.Modal.getInstance(document.getElementById('changeRoleModal')).hide(); + loadusers(); + } else alert("Lỗi: " + result.message); + } catch (e) { console.error(e); } +} + +async function addnewuser() { + const data = { + name: document.getElementById('addName').value, + username: document.getElementById('addUsername').value, + email: document.getElementById('addEmail').value, + phone: document.getElementById('addPhone').value, + password: document.getElementById('addPassword').value, + dob: document.getElementById('addDob').value, + role: document.getElementById('addRole').value, + avatar: document.getElementById('addAvatar').value + }; + + try { + const response = await fetch('/api/v1/admin/users', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + const result = await response.json(); + if(response.ok) { + alert(result.message); + bootstrap.Modal.getInstance(document.getElementById('addUserModal')).hide(); + document.getElementById('addUserForm').reset(); + loadusers(); + } else alert("Lỗi: " + result.message); + } catch (e) { console.error(e); } +} \ No newline at end of file diff --git a/frontend/src/js/main.js b/frontend/src/js/main.js index 4f06b2e..3da56c3 100644 --- a/frontend/src/js/main.js +++ b/frontend/src/js/main.js @@ -1,37 +1,174 @@ -// doi load xong html roi moi bat js + document.addEventListener("DOMContentLoaded", function() { - // tim the form trong trang - const authForm = document.querySelector("form"); + // logic đăng nhập + const loginForm = document.getElementById("loginForm"); + if (loginForm) { + loginForm.addEventListener("submit", async function(e) { + e.preventDefault(); // chặn reload trang + + // lấy dữ liệu form + const formData = new FormData(this); + const data = Object.fromEntries(formData.entries()); + + // đổi trạng thái nút + const btnSubmit = this.querySelector('button[type="submit"]'); + const originalText = btnSubmit.innerHTML; + btnSubmit.innerHTML = ' Đang xử lý...'; + btnSubmit.disabled = true; + + try { + // gọi api login + const response = await fetch('/api/v1/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); - if (authForm) { - authForm.addEventListener("submit", function(event) { + const result = await response.json(); - // check do dai mat khau - const passwordInputs = authForm.querySelectorAll("input[type='password']"); - if (passwordInputs.length > 0) { - const password = passwordInputs[0].value; - if (password.length < 6) { - alert("loi: mat khau phai co it nhat 6 ky tu"); - event.preventDefault(); // chan form khong cho gui - return; + if (response.ok) { + // lưu token và user + localStorage.setItem('ticketHubToken', result.access_token); + localStorage.setItem('ticketHubUser', JSON.stringify(result.user)); + + alert(result.message); + + // chuyển hướng theo role + if (result.user.role === 'admin') { + window.location.href = '/admin'; // chuyển trang admin + } else if (result.user.role === 'organizer') { + window.location.href = '/organizer'; // chuyển trang btc + } else { + window.location.href = '/'; // chuyển trang chủ + } + } else { + alert("lỗi: " + result.message); } + } catch (error) { + console.error("lỗi:", error); + alert("không thể kết nối đến máy chủ"); + } finally { + btnSubmit.innerHTML = originalText; + btnSubmit.disabled = false; } + }); + } - // check dinh dang so dien thoai neu co - const phoneInput = authForm.querySelector("input[type='tel']"); - if (phoneInput) { - const phone = phoneInput.value; - const phoneRegex = /^(0[3|5|7|8|9])+([0-9]{8})$/; - if (!phoneRegex.test(phone)) { - alert("loi: so dien thoai khong hop le, nhap lai di"); - event.preventDefault(); // chan form - return; + // logic đăng ký + const registerForm = document.getElementById("registerForm"); + if (registerForm) { + + // kiểm tra độ dài mk + registerForm.addEventListener("submit", function(event) { + const passwordInputs = this.querySelectorAll("input[type='password']"); + if (passwordInputs.length > 0) { + const password = passwordInputs[0].value; + if (password.length < 6) { + alert("mật khẩu phải từ 6 kí tự"); + event.preventDefault(); + event.stopImmediatePropagation(); // dừng thực thi + return; + } + } + }); + + // ẩn hiện form btc + const roleSelect = document.getElementById('roleSelect'); + const organizerFields = document.getElementById('organizerFields'); + const companyInput = document.getElementById('companyInput'); + + if(roleSelect && organizerFields) { + roleSelect.addEventListener('change', function() { + if (this.value === 'organizer') { + organizerFields.style.display = 'block'; + companyInput.setAttribute('required', 'required'); + } else { + organizerFields.style.display = 'none'; + companyInput.removeAttribute('required'); } + }); + } + + // xem trước avatar + const avatarInput = document.getElementById('avatarInput'); + const avatarPreview = document.getElementById('avatarPreview'); + if (avatarInput && avatarPreview) { + avatarInput.addEventListener('change', function(e) { + const file = e.target.files[0]; + if (file) { + avatarPreview.src = URL.createObjectURL(file); + } + }); + } + + // xử lý gửi form + registerForm.addEventListener('submit', async function(e) { + e.preventDefault(); + + // kiểm tra mk khớp + const pwd = document.getElementById('pwd').value; + const confirmPwd = document.getElementById('confirm_pwd').value; + if (pwd !== confirmPwd) { + alert("mật khẩu xác nhận không khớp"); + return; + } + + const btnSubmit = document.getElementById('btnSubmit'); + const originalText = btnSubmit.innerHTML; + btnSubmit.innerHTML = ' Đang xử lý...'; + btnSubmit.disabled = true; + + const formData = new FormData(this); + + try { + const response = await fetch('/api/v1/auth/register', { + method: 'POST', + body: formData + }); + + const result = await response.json(); + + if (response.ok) { + alert(result.message + "\ntạo tài khoản thành công! " ); + window.location.href = '/login'; + } else { + alert("lỗi: " + result.message); + } + } catch (error) { + console.error('lỗi:', error); + alert("không thể kết nối đến máy chủ"); + } finally { + btnSubmit.innerHTML = originalText; + btnSubmit.disabled = false; } + }); + } + + // xử lý giao diện header + const token = localStorage.getItem('ticketHubToken'); + const userDataStr = localStorage.getItem('ticketHubUser'); + const headerAuthArea = document.getElementById('headerAuthArea'); + + if (token && userDataStr && headerAuthArea) { + // parse thông tin user + const user = JSON.parse(userDataStr); + + // render html user + headerAuthArea.innerHTML = ` + Xin chào, ${user.name}! + Avatar + + `; + + // xử lý nút đăng xuất + document.getElementById('btnLogout').addEventListener('click', function() { + // xóa local storage + localStorage.removeItem('ticketHubToken'); + localStorage.removeItem('ticketHubUser'); - // bao console la da ok - console.log("form ok, chuan bi day len server"); + // reload lại trang + window.location.reload(); }); } }); \ No newline at end of file diff --git a/frontend/src/templates/approve_events.html b/frontend/src/templates/approve_events.html index 0cbe166..5ecf152 100644 --- a/frontend/src/templates/approve_events.html +++ b/frontend/src/templates/approve_events.html @@ -23,9 +23,8 @@
    -
    - Tổng Quan + Tổng Quan Duyệt Sự Kiện Quản Lý Người Dùng Báo Cáo Doanh Thu @@ -40,15 +39,15 @@

    Duyệt & Quản Lý Sự Kiện

    - +
    - + + + +
    @@ -66,75 +65,12 @@

    Duyệt & Quản Lý Sự Kiện

    Hành Động - - - #EV102 - -
    EDM Festival 2026
    -
    SVĐ Quân Khu 7
    - - -
    SpaceSpeakers
    - - -
    31/12/2026
    -
    19:00 - 23:30
    - - Chờ duyệt - - - - - - - - - #EV098 - -
    Khóa Học AI & Data
    -
    Hội trường OU
    - - -
    Đại học Mở HCM
    - - -
    15/06/2026
    -
    08:00 - 17:00
    - - Đã duyệt - - - - - - - - #EV095 - -
    Hội chợ Hàng Rong
    -
    Phố đi bộ
    - - -
    Tổ chức ABC
    - - -
    10/05/2026
    -
    Cả ngày
    - - Từ chối - - - - +
    @@ -142,10 +78,72 @@

    Duyệt & Quản Lý Sự Kiện

    © 2026 TicketHub Admin Dashboard.
    + + + + + \ No newline at end of file diff --git a/frontend/src/templates/landingpage.html b/frontend/src/templates/landingpage.html new file mode 100644 index 0000000..7380cb2 --- /dev/null +++ b/frontend/src/templates/landingpage.html @@ -0,0 +1,55 @@ + + + + + + Trang chủ - TicketHub + + + + + + + +
    +

    +

    đây là trang chủ tạm thời. danh sách sự kiện sẽ được hiển thị ở đây sau.

    + +
    +
    +
    +

    Khách Hàng

    +

    Có thể xem và mua vé tại đây.

    +
    +
    +
    +
    +

    Ban Tổ Chức

    +

    Có thể tạo sự kiện mới.

    + Vào trang Quản lý +
    +
    +
    +
    +

    Quản Trị Viên

    +

    Duyệt sự kiện và quản lý hệ thống.

    + Vào Admin Dashboard +
    +
    +
    +
    + + + + + \ No newline at end of file diff --git a/frontend/src/templates/login.html b/frontend/src/templates/login.html index 3cdf75d..38e3ef9 100644 --- a/frontend/src/templates/login.html +++ b/frontend/src/templates/login.html @@ -25,16 +25,16 @@

    Đăng Nhập

    -
    +
    - +
    - +
    - +
    Quên mật khẩu? diff --git a/frontend/src/templates/manage_users.html b/frontend/src/templates/manage_users.html index e5f026e..ad15381 100644 --- a/frontend/src/templates/manage_users.html +++ b/frontend/src/templates/manage_users.html @@ -5,121 +5,193 @@ Quản Lý Người Dùng - TicketHub + + - - - +
    + -
    -

    Quản Lý Người Dùng

    +
    +

    Quản Lý Người Dùng

    -
    -
    -
    -
    - - -
    -
    -
    - -
    -
    - +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    -
    - - - - - - - - - - - - - - - - - - +
    +
    IDNgười DùngVai Trò (Role)Trạng TháiHành Động
    #US001 -
    - avatar -
    -
    Thanh Mẫn
    -
    man@gmail.com
    -
    -
    -
    Khách hàng (VIP) Hoạt động - - - -
    + + + + + + + + + + + +
    IDNgười DùngVai Trò (Role)Trạng TháiHành Động
    + +
    +
    +
    - - #US002 - -
    - avatar -
    -
    Ánh Entertainment
    -
    anh@btc.com
    -
    -
    - - Ban tổ chức - Bị khóa - - - - - - - - + + -
    - © 2026 TicketHub Admin Dashboard. + + + + - - + + \ No newline at end of file diff --git a/frontend/src/templates/register.html b/frontend/src/templates/register.html index 2878758..0efab68 100644 --- a/frontend/src/templates/register.html +++ b/frontend/src/templates/register.html @@ -106,79 +106,53 @@
    Thông tin Doa
    + + - - \ No newline at end of file + + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c56f9dd..b05f808 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,14 +4,17 @@ click==8.3.1 cloudinary==1.44.1 colorama==0.4.6 Flask==3.1.3 +Flask-JWT-Extended==4.7.1 Flask-SQLAlchemy==3.1.1 greenlet==3.3.2 itsdangerous==2.2.0 Jinja2==3.1.6 MarkupSafe==3.0.3 +PyJWT==2.12.1 PyMySQL==1.1.2 six==1.17.0 SQLAlchemy==2.0.48 typing_extensions==4.15.0 urllib3==2.6.3 +utils==1.0.2 Werkzeug==3.1.6 diff --git a/test/add_user.py b/test/add_user.py new file mode 100644 index 0000000..5d757ca --- /dev/null +++ b/test/add_user.py @@ -0,0 +1,34 @@ +from backend.src import create_app +from backend.src.models import db, User +from werkzeug.security import generate_password_hash + +def create_admin(): + app = create_app() + + with app.app_context(): + existing = User.query.filter_by(username="admin5").first() + if existing: + print("️ Admin đã tồn tại!") + return + + admin = User( + id=30000011, + name="Admin Test", + email="admin@test.com", + username="admin5", + password=generate_password_hash("123456"), + phone_number="0989999999", + status=1, + avatar="", + dob="2000-01-01", + type="admin" + ) + + db.session.add(admin) + db.session.commit() + + print("✅ Tạo admin thành công!") + + +if __name__ == "__main__": + create_admin() \ No newline at end of file diff --git a/weekly-reports/week-6.md b/weekly-reports/week-6.md new file mode 100644 index 0000000..23dc7bb --- /dev/null +++ b/weekly-reports/week-6.md @@ -0,0 +1,54 @@ +# Báo cáo Tuần 6 + +**Tuần:** 6 (23/03/2026 - 29/03/2026) +**Nhóm:** 10 +**Đề tài:** A3 - Hệ thống bán vé sự kiện (Event Ticketing) +**Nhóm trưởng:** Nguyễn Thanh Mẫn - 2351050100 + +--- + +## 1. Công việc đã hoàn thành + +| Thành viên | MSSV | Công việc | Link Commit/PR | +|------------|------|-----------|----------------| +| Nguyễn Thanh Mẫn | 2351050100 | Tiếp tục thực hiện giao diện của các trang ADMIN,tạo database schem |https://github.com/mantks0803/EventTicketing_A3/tree/feature/event_approval_revenue_report_ui| +| Trần Minh Ánh | 2351010012 | Tiếp tục thực hiện các trang giao diện của phân quyền người dùng |https://github.com/mantks0803/EventTicketing_A3/tree/minhanh| +| Hoàng Ngọc Luyến | 2351010120 | Tiếp tục thực hiện các trang giao diện của phân quyền đơn vị tổ chức | https://github.com/mantks0803/EventTicketing_A3/tree/HaNoLu | + +--- + +## 2. Tiến độ tổng thể + +| Hạng mục | Trạng thái | % | +|----------|------------|---| +| Phân tích yêu cầu | Hoàn thành | 100% | +| Thiết kế kiến trúc | Đang làm | 100% | +| Backend API | Đang làm | 40% | +| Frontend UI | Đang làm | 90% | +| Docker | Chưa bắt đầu | 0% | +| Testing | Chưa bắt đầu | 0% | + +**Tổng tiến độ: 60%** + +--- + +## 3. Kế hoạch tuần tới (Tuần 7) + +*Mục tiêu: Hoàn thành toàn diện back-end, API,tích hợp docker, RabitMQ* + +| Thành viên | Công việc dự kiến | +|------------|-------------------| +| Nguyễn Thanh Mẫn | Thực hiện back-end, API,Docker,Redis, RabitMQ| +| Trần Minh Ánh | Thực hiện back-end, API,Docker,Redis,RabitMQ| +| Hoàng Ngọc Luyến |Thực hiện back-end, API,Docker,Redis,RabitMQ| + +--- + +## 4. Khó khăn / Cần hỗ trợ + +- [ ] . + +--- + +*Ngày nộp: 29/03/2026* +*Xác nhận của Nhóm trưởng: Nguyễn Thanh Mẫn* \ No newline at end of file