@@ -131,7 +108,7 @@ const ProfileSection = ({
role="navigation"
aria-label="Main navigation"
>
- {NAV_ITEMS.map((item) => (
+ {filteredNavItems.map((item) => (
@@ -27,54 +38,176 @@ const CourseItem = ({ course }) => (
);
-// SỬA LẠI PURCHASE CARD
const PurchaseCard = ({ purchase, isExpanded, onToggle }) => {
const formatDate = (dateString) => {
const date = new Date(dateString);
- return date.toLocaleString("en-US", {
- day: "numeric",
- month: "long",
+ return date.toLocaleString("vi-VN", {
+ // Đổi sang vi-VN cho biên lai thân thiện hơn
+ day: "2-digit",
+ month: "2-digit",
year: "numeric",
- hour: "numeric",
- minute: "numeric",
- hour12: true,
+ hour: "2-digit",
+ minute: "2-digit",
+ hour12: false,
});
};
+ const getStatusColor = (status) => {
+ switch (status?.toLowerCase()) {
+ case "completed":
+ return "#2ecc71";
+ case "pending":
+ return "#f1c40f";
+ case "failed":
+ return "#e74c3c";
+ case "cancelled":
+ return "#95a5a6";
+ default:
+ return "#34495e";
+ }
+ };
+
+ const hasCourses = purchase.courses && purchase.courses.length > 0;
+ const isPending = purchase.transaction.status === "pending";
+
+ // --- HÀM TẠO LINK VIETQR ---
+ const generateQRLink = () => {
+ if (!purchase) return "";
+ const amount = purchase.amount;
+ const addInfo = encodeURIComponent(purchase.description);
+ const accountName = encodeURIComponent(ADMIN_BANK_INFO.accountName);
+ return `https://img.vietqr.io/image/${ADMIN_BANK_INFO.bankId}-${ADMIN_BANK_INFO.accountNumber}-${ADMIN_BANK_INFO.template}.png?amount=${amount}&addInfo=${addInfo}&accountName=${accountName}`;
+ };
+
+ // --- HÀM XỬ LÝ TẢI BIÊN LAI (PDF) ---
+ const handleDownloadReceipt = async () => {
+ // 1. Tạo phần tử HTML tạm thời chứa nội dung biên lai
+ const receiptElement = document.createElement("div");
+
+ // Style cho biên lai đẹp mắt (giống hóa đơn giấy)
+ receiptElement.style.cssText = `
+ position: absolute; left: -9999px; top: 0;
+ width: 600px; padding: 40px;
+ font-family: 'Arial', sans-serif;
+ background: white; color: #333;
+ border: 1px solid #ddd;
+ `;
+
+ // Nội dung HTML của biên lai
+ receiptElement.innerHTML = `
+
+
BIÊN LAI THANH TOÁN ĐIỆN TỬ
+
F-Learning Platform
+
+
+
+
+ Mã giao dịch (Transaction ID):
+ ${purchase.gatewayTransactionId || purchase.paymentId}
+
+
+ Mã đơn hàng (Order Code):
+ ${purchase.transaction.orderCode || "N/A"}
+
+
+ Ngày thanh toán:
+ ${formatDate(purchase.paymentDate)}
+
+
+ Phương thức:
+ ${purchase.paymentMethod}
+
+
+ Trạng thái:
+ HOÀN THÀNH
+
+
+
+
+
Chi tiết khóa học
+
+ ${purchase.courses
+ .map(
+ (course) => `
+
+ ${course.title}
+ $${course.price?.toFixed(
+ 2
+ )}
+
+ `
+ )
+ .join("")}
+
+
+
+
+
Tổng tiền: ${purchase.amount?.toLocaleString(
+ "vi-VN"
+ )} VND
+
+
+
+
Cảm ơn bạn đã tin tưởng và sử dụng F-Learning!
+
Đây là biên lai điện tử có giá trị xác thực giao dịch.
+
+ `;
+
+ document.body.appendChild(receiptElement);
+
+ try {
+ // 2. Chụp ảnh phần tử HTML bằng html2canvas
+ const canvas = await html2canvas(receiptElement, { scale: 2 });
+ const imgData = canvas.toDataURL("image/png");
+
+ // 3. Tạo PDF bằng jspdf
+ const pdf = new jsPDF("p", "mm", "a4");
+ const pdfWidth = pdf.internal.pageSize.getWidth();
+ const pdfHeight = (canvas.height * pdfWidth) / canvas.width;
+
+ pdf.addImage(imgData, "PNG", 0, 10, pdfWidth, pdfHeight);
+ pdf.save(`Bien_lai_${purchase.transaction.orderCode || "GD"}.pdf`);
+ } catch (error) {
+ console.error("Lỗi khi tạo biên lai:", error);
+ alert("Không thể tải biên lai. Vui lòng thử lại.");
+ } finally {
+ // 4. Dọn dẹp phần tử tạm
+ document.body.removeChild(receiptElement);
+ }
+ };
+
return (
+ {/* HEADER CARD */}
{formatDate(purchase.paymentDate)}
- {/* SỬA 1: HIỂN THỊ TỔNG SỐ COURSE ĐỘNG */}
-
-
- {purchase.totalCourses} Course
- {purchase.totalCourses > 1 ? "s" : ""}
-
-
- {/* SỬA 2: SỬA LỖI HIỂN THỊ TIỀN TỆ (NẾU CÓ) */}
+ {purchase.totalCourses > 0 && (
+
+
+ {purchase.totalCourses} Course
+ {purchase.totalCourses > 1 ? "s" : ""}
+
+ )}
- {/* Giả sử backend trả về 'amount' là VND */}
{purchase.amount?.toLocaleString("vi-VN")} VND
-
{
/>
{purchase.paymentMethod}
-
-
+
{
-
onToggle(purchase.paymentId)}
- aria-label="Toggle purchase details"
- >
- {isExpanded ? "↑" : "↓"}
-
+
+ {hasCourses || isPending ? (
+
onToggle(purchase.paymentId)}
+ aria-label="Toggle purchase details"
+ >
+ {isExpanded ? "↑" : "↓"}
+
+ ) : (
+
+ )}
- {/* SỬA 3: KIỂM TRA MẢNG 'courses' */}
- {isExpanded && purchase.courses && purchase.courses.length > 0 && (
+ {/* EXPANDED CONTENT */}
+ {isExpanded && (
- {/* CỘT 1: Danh sách khóa học (Giữ nguyên) */}
+ {/* CỘT TRÁI: KHÓA HỌC */}
- {purchase.courses.map((course) => (
-
- ))}
+ {hasCourses ? (
+ purchase.courses.map((course) => (
+
+ ))
+ ) : (
+
+ {isPending
+ ? "Khóa học sẽ hiển thị sau khi hệ thống xác nhận thanh toán."
+ : "Không tìm thấy thông tin khóa học."}
+
+ )}
- {/* ======================================= */}
- {/* BƯỚC 1: THÊM KHỐI HÀNH ĐỘNG NÀY VÀO */}
+ {/* CỘT PHẢI: QR CODE HOẶC ACTIONS */}
-
Tuỳ chọn giao dịch
-
alert("Đang phát triển tính năng tải biên lai!")}
- >
- Tải biên lai
-
-
alert("Đang phát triển tính năng hỗ trợ!")}
- >
- Liên hệ hỗ trợ
-
+ {isPending ? (
+ // === HIỂN THỊ QR CODE ===
+
+
+ Quét mã để thanh toán
+
+
+
+
+
+ Sử dụng App Ngân hàng hoặc Ví điện tử để quét mã. Hệ thống
+ sẽ tự động cập nhật sau vài phút.
+
+
+ ) : (
+ // === CÁC TRẠNG THÁI KHÁC ===
+ <>
+
+ Tuỳ chọn giao dịch
+
+ {purchase.transaction.status === "completed" && (
+
+ Tải biên lai
+
+ )}
+
alert("Đang phát triển tính năng hỗ trợ!")}
+ >
+ Liên hệ hỗ trợ
+
+ >
+ )}
- {/* ======================================= */}
)}
@@ -139,7 +326,6 @@ const PurchaseCard = ({ purchase, isExpanded, onToggle }) => {
);
};
-// Component PurchaseHistory không cần thay đổi logic
const PurchaseHistory = () => {
const location = useLocation();
const [purchases, setPurchases] = useState([]);
@@ -151,20 +337,16 @@ const PurchaseHistory = () => {
totalPages: 1,
hasNext: false,
hasPrev: false,
+ totalTransactions: 0,
});
const fetchPurchases = async (page = 1) => {
try {
setLoading(true);
setError(null);
- // Gọi service (đã được sửa ở backend)
const response = await getPurchaseHistory(page);
- console.log("Purchase history response:", response.data.data);
-
- // Dữ liệu mới đã có 'courses' (mảng) và 'totalCourses' (số)
setPurchases(response.data.data);
setPagination(response.data.pagination);
-
if (response.data.data.length > 0) {
setExpandedId(response.data.data[0].paymentId);
}
@@ -178,7 +360,7 @@ const PurchaseHistory = () => {
};
useEffect(() => {
- fetchPurchases(1); // Fetch trang đầu tiên khi tải
+ fetchPurchases(1);
}, []);
const handlePageChange = (newPage) => {
@@ -197,13 +379,10 @@ const PurchaseHistory = () => {
Purchase History
-
{loading && (
Loading purchase history...
)}
-
{error &&
{error}
}
-
{!loading && !error && (
<>
@@ -222,7 +401,6 @@ const PurchaseHistory = () => {
)}
-
{purchases.length > 0 && pagination.totalPages > 1 && (
{
)}
-
{purchases.length > 0 && (
diff --git a/FrontEnd/src/components/ShoppingCart/CheckoutPage.jsx b/FrontEnd/src/components/ShoppingCart/CheckoutPage.jsx
index 6c611a3..fa93915 100644
--- a/FrontEnd/src/components/ShoppingCart/CheckoutPage.jsx
+++ b/FrontEnd/src/components/ShoppingCart/CheckoutPage.jsx
@@ -166,13 +166,16 @@ export default function CheckoutPage() {
if (selectedDiscount) {
if (selectedDiscount.type === "percent") {
// Percent discount applies to ORIGINAL price of each individual course
- let courseDiscount = course.originalPrice * (selectedDiscount.value / 100);
+ let courseDiscount =
+ course.originalPrice * (selectedDiscount.value / 100);
// Check if discount applies to this specific course
const appliesToThisCourse =
!selectedDiscount.applyCourses ||
selectedDiscount.applyCourses.length === 0 ||
- selectedDiscount.applyCourses.some((dc) => dc._id === course._id || dc === course._id);
+ selectedDiscount.applyCourses.some(
+ (dc) => dc._id === course._id || dc === course._id
+ );
if (appliesToThisCourse) {
discountAmount += courseDiscount;
@@ -188,7 +191,10 @@ export default function CheckoutPage() {
// Apply maximum discount limit if set
if (selectedDiscount && selectedDiscount.maximumDiscount > 0) {
- discountAmount = Math.min(discountAmount, selectedDiscount.maximumDiscount);
+ discountAmount = Math.min(
+ discountAmount,
+ selectedDiscount.maximumDiscount
+ );
}
const total = Math.max(0, subtotal - discountAmount);
@@ -221,8 +227,7 @@ export default function CheckoutPage() {
// 3. Tạo data gửi đi
const paymentData = {
description: `Thanh toan cho ${courseIds.length} khoa hoc`,
- // price: totalPrice, // <-- Dùng tổng tiền đã tính
- price: 2000, // <-- Tạm thời đặt 2000 để test PayOS
+ price: totalPrice, // <-- Dùng tổng tiền đã tính
packageType: "COURSE_PURCHASE",
courseIds: courseIds, // <-- Dùng mảng ID
cancelUrl: `${window.location.origin}/cart`, // <-- URL khi huỷ
diff --git a/FrontEnd/src/components/WatchCourse/CourseInfo.jsx b/FrontEnd/src/components/WatchCourse/CourseInfo.jsx
index 5ad15f4..dcb2b3a 100644
--- a/FrontEnd/src/components/WatchCourse/CourseInfo.jsx
+++ b/FrontEnd/src/components/WatchCourse/CourseInfo.jsx
@@ -311,7 +311,7 @@ const CourseInfo = ({
{renderContent()}
{deleteModalOpen && (
-
+
Confirm Delete
Are you sure you want to delete this comment?
diff --git a/FrontEnd/src/components/header/HeaderRight.jsx b/FrontEnd/src/components/header/HeaderRight.jsx
index 8604996..6bc7ba9 100644
--- a/FrontEnd/src/components/header/HeaderRight.jsx
+++ b/FrontEnd/src/components/header/HeaderRight.jsx
@@ -1,6 +1,11 @@
-import { faCog, faSignOutAlt, faUser } from "@fortawesome/free-solid-svg-icons";
+import {
+ faCog,
+ faSignOutAlt,
+ faUser,
+ faTachometerAlt,
+} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { faTachometerAlt } from "@fortawesome/free-solid-svg-icons";
+import { MessageCircle } from "lucide-react";
import React from "react";
import { Link, useNavigate, useLocation } from "react-router-dom";
import { useDispatch } from "react-redux";
@@ -21,6 +26,9 @@ function HeaderRight({ user: currentUser }) {
navigate("/login");
};
+ // Helper xác định có phải student không để code gọn hơn
+ const isStudent = currentUser?.role === "student";
+
return (
<>
@@ -29,16 +37,38 @@ function HeaderRight({ user: currentUser }) {
-
-
-
-
-
-
+
+ {/* 2. Chỉ hiển thị WishList nếu là student */}
+ {isStudent &&
}
+
+ {/* 3. Logic đổi Cart thành Chat */}
+ {isStudent ? (
+ // Nếu là Student -> Hiện giỏ hàng
+
+
+
+
+
+ ) : (
+ // Nếu KHÔNG phải Student -> Hiện nút Chat
+
+
+
+
+
+ )}
>
) : (
<>>
)}
+
+ {/* Phần Dropdown Avatar giữ nguyên */}
{currentUser ? (
)}
-
+
{/* Instructor sees Dashboard ONLY when NOT in instructor area */}
{currentUser.role === "instructor" && !isInInstructorArea && (
@@ -84,7 +114,7 @@ function HeaderRight({ user: currentUser }) {
)}
-
+
{/* Student sees Profile and Settings */}
{currentUser.role === "student" && (
<>
@@ -107,16 +137,16 @@ function HeaderRight({ user: currentUser }) {
>
)}
-
+
{/* Divider only if there are items above */}
- {(currentUser.role === "admin" ||
- currentUser.role === "student" ||
+ {(currentUser.role === "admin" ||
+ currentUser.role === "student" ||
(currentUser.role === "instructor" && !isInInstructorArea)) && (
)}
-
+
{
@@ -110,6 +111,7 @@ function NotificationSocketHandler() {
),
{
+ containerId: "socket-notification",
// Xử lý khi click vào toàn bộ hộp thông báo
onClick: () => {
// Nếu backend có gửi kèm link thì ưu tiên dùng link đó
@@ -129,15 +131,40 @@ function NotificationSocketHandler() {
dispatch(updateUnreadCount(count));
};
+ const onAccountBanned = (data) => {
+ console.warn("⚠️ TÀI KHOẢN ĐÃ BỊ KHÓA:", data.reason);
+
+ // a. Hiển thị thông báo lỗi nghiêm trọng
+ toast.error(
+ `Tài khoản của bạn đã bị KHÓA vĩnh viễn. Lý do: ${data.reason}`,
+ {
+ position: "top-center", // Hiện ở giữa trên cùng cho dễ thấy
+ autoClose: 10000, // Hiện lâu (10s)
+ containerId: "global_toast", // Dùng container toàn cục
+ }
+ );
+
+ // b. Thực hiện đăng xuất (Xóa Redux state & LocalStorage)
+ dispatch(logout());
+
+ // c. Ngắt kết nối socket
+ socket.disconnect();
+
+ // d. Chuyển hướng về trang đăng nhập
+ navigate("/login");
+ };
+
// Đăng ký lắng nghe sự kiện
socket.on("new_notification", onNewNotification);
socket.on("unread_notification_count", onUnreadCount);
+ socket.on("account_banned", onAccountBanned);
// Cleanup khi unmount/logout
return () => {
console.log("Ngắt kết nối Socket Notification...");
socket.off("new_notification", onNewNotification);
socket.off("unread_notification_count", onUnreadCount);
+ socket.off("account_banned", onAccountBanned);
socket.disconnect();
};
}