diff --git a/FrontEnd/package.json b/FrontEnd/package.json index 37d6d82..b188088 100644 --- a/FrontEnd/package.json +++ b/FrontEnd/package.json @@ -26,6 +26,8 @@ "dayjs": "^1.11.13", "face-api.js": "^0.22.2", "firebase": "^11.9.1", + "html2canvas": "^1.4.1", + "jspdf": "^3.0.3", "jwt-decode": "^4.0.0", "lucide-react": "^0.513.0", "react": "^19.1.0", diff --git a/FrontEnd/src/App.js b/FrontEnd/src/App.js index fd8dee1..b015f06 100644 --- a/FrontEnd/src/App.js +++ b/FrontEnd/src/App.js @@ -1,20 +1,16 @@ import React, { useEffect } from "react"; -import AppRouter from "./routes"; // Import AppRouter từ thư mục routes (routes/index.js) -import { ToastContainer } from "react-toastify"; // Chỉ import Container, không cần hàm toast nữa +import AppRouter from "./routes"; +import { ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; import "./App.css"; import { useDispatch, useSelector } from "react-redux"; -import { fetchCurrentUser } from "./store/authSlice"; // Import action để lấy thông tin người dùng hiện tại - -// === QUAN TRỌNG: Import component dịch vụ xử lý socket vừa tạo === -// Đảm bảo đường dẫn này đúng với nơi bạn tạo file ở bước trước +import { fetchCurrentUser } from "./store/authSlice"; import NotificationSocketHandler from "./utils/NotificationSocketHandler"; function App() { const dispatch = useDispatch(); const { isAuthenticated } = useSelector((state) => state.auth); - // 1. Giữ nguyên logic lấy thông tin user khi load trang useEffect(() => { if (isAuthenticated) { dispatch(fetchCurrentUser()); @@ -23,14 +19,14 @@ function App() { return ( <> - {/* Router điều hướng trang */} - {/* Container chứa Toast (Popup thông báo) */} - {/* Nó cần nằm ở root để hiển thị đè lên mọi thứ */} + {/* === 1. TOAST CŨ (Dành cho thông báo hệ thống chung) === */} + {/* Không set containerId -> Nó sẽ nhận các toast mặc định */} - {/* === COMPONENT XỬ LÝ SOCKET === */} - {/* Chỉ render (và kết nối socket) khi user đã đăng nhập */} + {/* === 2. TOAST MỚI (Dành riêng cho thông báo Socket/Facebook) === */} + {/* Bọc trong div riêng như bạn muốn để dễ chỉnh CSS vị trí nếu cần */} +
+ +
+ {isAuthenticated && } ); diff --git a/FrontEnd/src/assets/PurchaseHistory/PurchaseHistory.css b/FrontEnd/src/assets/PurchaseHistory/PurchaseHistory.css index 50626b8..ecd0d36 100644 --- a/FrontEnd/src/assets/PurchaseHistory/PurchaseHistory.css +++ b/FrontEnd/src/assets/PurchaseHistory/PurchaseHistory.css @@ -343,6 +343,43 @@ background-color: #e9ecef; } +.flearning-qr-container { + display: flex; + flex-direction: column; + align-items: center; + background-color: #fff; + padding: 15px; + border-radius: 12px; + border: 1px solid #eee; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.flearning-qr-wrapper { + width: 100%; + max-width: 280px; /* Giới hạn chiều rộng QR */ + border-radius: 8px; + overflow: hidden; + border: 1px solid #e0e0e0; + background: #fff; +} + +.flearning-qr-image { + width: 100%; + height: auto; + display: block; + object-fit: contain; +} + +.flearning-qr-note { + font-size: 13px; + color: #666; + text-align: center; + margin-top: 15px; + margin-bottom: 0; + line-height: 1.5; + font-style: italic; +} + @media (max-width: 768px) { .flearning-purchase-details-grid { grid-template-columns: 1fr; diff --git a/FrontEnd/src/assets/Toast/CustomToast.css b/FrontEnd/src/assets/Toast/CustomToast.css index 77661ea..6429e9d 100644 --- a/FrontEnd/src/assets/Toast/CustomToast.css +++ b/FrontEnd/src/assets/Toast/CustomToast.css @@ -246,11 +246,11 @@ } /* ============================================================ - Toast (React-Toastify) + Toast (Socket / Global Notification) ============================================================ */ -/* Toastify overrides */ -.Toastify__toast { +/* Toastify overrides - Áp dụng cho wrapper mới */ +.socket-toast-wrapper .Toastify__toast { border-radius: var(--fb-radius) !important; box-shadow: var(--fb-shadow) !important; background: var(--fb-bg) !important; @@ -265,7 +265,7 @@ overflow: visible !important; } -.Toastify__toast-body { +.socket-toast-wrapper .Toastify__toast-body { padding: 0 !important; margin: 0 !important; width: 100%; @@ -273,11 +273,11 @@ align-items: flex-start; } -.Toastify__close-button { +.socket-toast-wrapper .Toastify__close-button { display: none !important; } -.Toastify__progress-bar { +.socket-toast-wrapper .Toastify__progress-bar { height: 3px !important; bottom: 0 !important; background-color: var(--fb-primary) !important; @@ -287,7 +287,7 @@ } /* Custom toast wrapper */ -.fb-toast-wrapper { +.socket-toast-wrapper .fb-toast-wrapper { width: 100%; background: var(--fb-bg); padding: 12px 16px; @@ -298,18 +298,18 @@ position: relative; } -.toast-header { +.socket-toast-wrapper .toast-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; } -.toast-title { +.socket-toast-wrapper .toast-title { font-size: 15px; font-weight: 600; color: var(--fb-muted); } -.toast-close-btn { +.socket-toast-wrapper .toast-close-btn { background: transparent; border: none; border-radius: 50%; @@ -323,24 +323,24 @@ transition: background-color var(--fb-transition); padding: 0; } -.toast-close-btn:hover { +.socket-toast-wrapper .toast-close-btn:hover { background-color: #e4e6eb; color: var(--fb-text); } -.toast-content-wrapper { +.socket-toast-wrapper .toast-content-wrapper { display: flex; align-items: center; gap: 12px; position: relative; } -.toast-avatar-container { +.socket-toast-wrapper .toast-avatar-container { position: relative; width: 56px; height: 56px; flex-shrink: 0; } -.toast-avatar { +.socket-toast-wrapper .toast-avatar { width: 100%; height: 100%; border-radius: 50%; @@ -348,7 +348,7 @@ border: 1px solid rgba(0, 0, 0, 0.1); } -.toast-action-icon { +.socket-toast-wrapper .toast-action-icon { position: absolute; bottom: -2px; right: -2px; @@ -362,29 +362,29 @@ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); z-index: 2; } -.toast-action-icon.like { +.socket-toast-wrapper .toast-action-icon.like { background-color: var(--fb-danger); } -.toast-action-icon.comment { +.socket-toast-wrapper .toast-action-icon.comment { background-color: var(--fb-success); } -.toast-action-icon.system { +.socket-toast-wrapper .toast-action-icon.system { background-color: var(--fb-primary); } -.toast-action-icon svg { +.socket-toast-wrapper .toast-action-icon svg { width: 12px; height: 12px; color: #fff; fill: #fff; } -.toast-text-content { +.socket-toast-wrapper .toast-text-content { display: flex; flex-direction: column; flex-grow: 1; padding-right: 12px; } -.toast-message { +.socket-toast-wrapper .toast-message { margin: 0; font-size: 15px; line-height: 1.3333; @@ -395,17 +395,17 @@ -webkit-box-orient: vertical; overflow: hidden; } -.toast-message strong { +.socket-toast-wrapper .toast-message strong { font-weight: 600; color: var(--fb-text); } -.toast-time { +.socket-toast-wrapper .toast-time { font-size: 13px; color: var(--fb-primary); font-weight: 500; margin-top: 4px; } -.toast-read-dot { +.socket-toast-wrapper .toast-read-dot { width: 10px; height: 10px; background-color: var(--fb-primary); diff --git a/FrontEnd/src/assets/WatchCourse/CourseInfo.css b/FrontEnd/src/assets/WatchCourse/CourseInfo.css index 1dfe56c..c4e8c4f 100644 --- a/FrontEnd/src/assets/WatchCourse/CourseInfo.css +++ b/FrontEnd/src/assets/WatchCourse/CourseInfo.css @@ -48,7 +48,7 @@ } .ci-tab-button.ci-tab-active::after { - content: ''; + content: ""; position: absolute; bottom: -1px; left: 0; @@ -148,7 +148,7 @@ } .ci-notes-list li::before { - content: '•'; + content: "•"; position: absolute; left: 0; color: #ff6b6b; @@ -169,7 +169,7 @@ .ci-code-block code { color: #fff; - font-family: 'Courier New', Courier, monospace; + font-family: "Courier New", Courier, monospace; font-size: 0.875rem; line-height: 1.5; } @@ -284,7 +284,7 @@ background: #fafbfc; border-radius: 8px; padding: 20px 24px; - box-shadow: 0 2px 8px rgba(0,0,0,0.03); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03); } .ci-comments-header { font-size: 1.3rem; @@ -354,7 +354,8 @@ font-weight: 500; color: #ff9800; } -.ci-comment-pagination button.active, .ci-comment-pagination button:hover:not(:disabled) { +.ci-comment-pagination button.active, +.ci-comment-pagination button:hover:not(:disabled) { background: #ff9800; color: #fff; border: 1px solid #ff9800; @@ -372,12 +373,12 @@ border-radius: 12px; padding: 18px 22px 16px 22px; border: 1.5px solid #ff9800; - box-shadow: 0 2px 8px rgba(64,148,247,0.06); + box-shadow: 0 2px 8px rgba(64, 148, 247, 0.06); position: relative; transition: box-shadow 0.2s, border 0.2s; } .ci-comment-item:hover { - box-shadow: 0 4px 16px rgba(64,148,247,0.13); + box-shadow: 0 4px 16px rgba(64, 148, 247, 0.13); border: 1.5px solid #ff9800; } .ci-comment-avatar { @@ -388,7 +389,7 @@ background: #e6e6e6; flex-shrink: 0; border: 2px solid #fff; - box-shadow: 0 1px 4px rgba(0,0,0,0.07); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.07); } .ci-comment-main { flex: 1; @@ -444,7 +445,7 @@ border-radius: 6px; transition: background 0.18s, border 0.18s, color 0.18s; font-weight: 500; - box-shadow: 0 1px 2px rgba(64,148,247,0.04); + box-shadow: 0 1px 2px rgba(64, 148, 247, 0.04); } .ci-comment-actions button:hover { background: #ff9800; @@ -796,43 +797,55 @@ .objective-item { padding: 0.625rem; } -} +} -.ci-modal-overlay { +.ci-modal-overlay-duc { position: fixed; - top: 0; left: 0; right: 0; bottom: 0; - background: rgba(0,0,0,0.3); + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.6); /* Semi-transparent dark background */ display: flex; - align-items: center; - justify-content: center; - z-index: 2020; + align-items: center; /* Vertically center */ + justify-content: center; /* Horizontally center */ + z-index: 1000; /* Ensure it sits on top of everything */ + backdrop-filter: blur(3px); /* Optional: blurs the content behind */ + animation: fadeIn 0.2s ease-out; } -.ci-modal { + +/* 2. The Modal Box */ +.ci-modal-overlay-duc .ci-modal { + position: relative; + display: block; + background-color: #fff; + padding: 24px; + border-radius: 12px; background: #fff; - padding: 24px 32px; - border-radius: 8px; - box-shadow: 0 2px 16px rgba(0,0,0,0.2); - min-width: 300px; - text-align: center; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); + width: 90%; /* Responsive width for mobile */ + max-width: 400px; /* Maximum width for desktop */ + text-align: center; /* Center the text */ + animation: slideUp 0.3s ease-out; } -.ci-modal-actions { +.ci-modal-overlay-duc .ci-modal-actions { margin-top: 16px; display: flex; justify-content: space-around; gap: 16px; } -.ci-modal-actions button { +.ci-modal-overlay-duc .ci-modal-actions button { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; } -.ci-modal-actions button:first-child { +.ci-modal-overlay-duc .ci-modal-actions button:first-child { background: #e74c3c; color: #fff; } -.ci-modal-actions button:last-child { +.ci-modal-overlay-duc .ci-modal-actions button:last-child { background: #ccc; color: #222; -} \ No newline at end of file +} diff --git a/FrontEnd/src/components/CourseDetails/PricingCard.jsx b/FrontEnd/src/components/CourseDetails/PricingCard.jsx index 2281b58..f8b154a 100644 --- a/FrontEnd/src/components/CourseDetails/PricingCard.jsx +++ b/FrontEnd/src/components/CourseDetails/PricingCard.jsx @@ -238,8 +238,7 @@ const ActionButtons = ({ course }) => { try { const paymentData = { description: `TT khoa hoc ${course._id.slice(-6)}`, - // price: course.currentPrice, - price: 2000, // <-- Tạm thời đặt 2000 để test PayOS + price: course.currentPrice, packageType: "COURSE_PURCHASE", courseIds: [course._id], cancelUrl: window.location.href, diff --git a/FrontEnd/src/components/CourseList/ProfileSection.jsx b/FrontEnd/src/components/CourseList/ProfileSection.jsx index 3bc52b6..bb131a6 100644 --- a/FrontEnd/src/components/CourseList/ProfileSection.jsx +++ b/FrontEnd/src/components/CourseList/ProfileSection.jsx @@ -1,8 +1,8 @@ -import React, { useState, useEffect } from "react"; +import React, { useState } from "react"; import PropTypes from "prop-types"; import { Link } from "react-router-dom"; import { FaBars, FaTimes } from "react-icons/fa"; -import { getProfile } from "../../services/profileService"; +import { useSelector } from "react-redux"; import "../../assets/CourseList/ProfileSection.css"; const DEFAULT_PROFILE_IMAGE = "/images/defaultImageUser.png"; @@ -17,8 +17,6 @@ const NAV_ITEMS = [ { path: "/profile/settings", label: "Settings" }, ]; -// Cache for profile data - const ProfileSection = ({ activePath, wrapperBackground = "#FFEEE8", @@ -26,48 +24,15 @@ const ProfileSection = ({ children, }) => { const [mobileMenuOpen, setMobileMenuOpen] = useState(false); - const [profileData, setProfileData] = useState({ - firstName: "", - lastName: "", - biography: "", - userImage: DEFAULT_PROFILE_IMAGE, - }); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(""); - - // Fetch profile data - const fetchProfileData = async () => { - try { - setIsLoading(true); - setError(""); - - const response = await getProfile(); - const data = response.data.data; - - const newProfileData = { - firstName: data.firstName || "", - lastName: data.lastName || "", - biography: data.biography || "", - userImage: data.userImage || DEFAULT_PROFILE_IMAGE, - }; - - setProfileData(newProfileData); - } catch (error) { - console.error("Error fetching profile:", error); - setError("Failed to load profile data"); - setProfileData((prev) => ({ - ...prev, - userImage: DEFAULT_PROFILE_IMAGE, - })); - } finally { - setIsLoading(false); - } - }; - // Luôn fetch profile khi mount - useEffect(() => { - fetchProfileData(); - }, []); + // Lấy trực tiếp currentUser từ Redux Store + const { currentUser } = useSelector((state) => state.auth); + + // === 1. HÀM VIẾT HOA CHỮ CÁI ĐẦU === + const capitalizeFirstLetter = (string) => { + if (!string) return ""; + return string.charAt(0).toUpperCase() + string.slice(1); + }; // Get mobile header title const getMobileHeaderTitle = () => { @@ -75,10 +40,23 @@ const ProfileSection = ({ return currentItem ? currentItem.label : "Profile"; }; - const displayName = - profileData.firstName || profileData.lastName - ? `${profileData.firstName} ${profileData.lastName}`.trim() - : "User"; + // Xử lý dữ liệu hiển thị từ currentUser + const displayName = currentUser + ? `${currentUser.firstName} ${currentUser.lastName}`.trim() + : "User"; + + const userImage = currentUser?.userImage || DEFAULT_PROFILE_IMAGE; + const userRole = currentUser?.role || "student"; + + // Logic lọc Menu: Nếu không phải student -> Chỉ hiện Message & Settings + const filteredNavItems = + userRole === "student" + ? NAV_ITEMS + : NAV_ITEMS.filter( + (item) => + item.path === "/profile/message" || + item.path === "/profile/settings" + ); const profileContent = ( <> @@ -96,14 +74,10 @@ const ProfileSection = ({
- {isLoading ? ( -
Loading...
- ) : error ? ( -
{error}
- ) : ( + {currentUser ? ( <> {displayName}

{displayName}

-

{profileData.biography || "Student"}

+ {/* === 2. ÁP DỤNG HÀM Ở ĐÂY === */} +

{capitalizeFirstLetter(userRole)}

+ ) : ( +
Please login...
)}
@@ -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 */} - - Course - {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 && ( + + Course + {purchase.totalCourses} Course + {purchase.totalCourses > 1 ? "s" : ""} + + )} Amount - {/* Giả sử backend trả về 'amount' là VND */} {purchase.amount?.toLocaleString("vi-VN")} VND - { /> {purchase.paymentMethod} - - + Status {
- + + {hasCourses || isPending ? ( + + ) : ( +
+ )}
- {/* 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
- - + {isPending ? ( + // === HIỂN THỊ QR CODE === +
+
+ Quét mã để thanh toán +
+
+ VietQR Payment +
+

+ 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" && ( + + )} + + + )}
- {/* ======================================= */}
)} @@ -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)) && (

  • )} - +