From 9290f29109423afa4d2a5e04dd0922fbc20841cd Mon Sep 17 00:00:00 2001 From: pauldungca Date: Sun, 12 Oct 2025 15:06:47 +0800 Subject: [PATCH] schedule module for capstone instructor done! --- src/components/Instructor/Adviser-Enroll.jsx | 1312 ++++---- src/components/Instructor/FinalDefense.jsx | 2335 +++++++-------- .../Instructor/InstructorDashboard.jsx | 428 ++- src/components/Instructor/ManuScript.jsx | 717 +++-- src/components/Instructor/OralDefense.jsx | 2665 +++++++++-------- src/components/Instructor/Schedule.jsx | 2 +- src/components/Instructor/TitleDefense.jsx | 2125 +++++++------ 7 files changed, 5398 insertions(+), 4186 deletions(-) diff --git a/src/components/Instructor/Adviser-Enroll.jsx b/src/components/Instructor/Adviser-Enroll.jsx index 3968bd2..7c69e75 100644 --- a/src/components/Instructor/Adviser-Enroll.jsx +++ b/src/components/Instructor/Adviser-Enroll.jsx @@ -1,4 +1,3 @@ - import React, { useState, useRef } from "react"; import * as XLSX from "xlsx"; import { saveAs } from "file-saver"; @@ -18,31 +17,30 @@ import { // Assuming this file contains generic Bootstrap/CSS overrides, keeping import for now import "../Style/Instructor/Enroll-Member.css"; import { openAddAdviser } from "../../services/instructor/addAdviser"; - - + const MySwal = withReactContent(Swal); - + const Adviser = () => { const [importedData, setImportedData] = useState([]); const [searchTerm, setSearchTerm] = useState(""); const [openDropdown, setOpenDropdown] = useState(null); const [selectedRows, setSelectedRows] = useState([]); const [isSelectionMode, setIsSelectionMode] = useState(false); - + const tableWrapperRef = useRef(null); - + // --- Utility Functions for Dropdown --- const handleToggleDropdown = (index) => { setOpenDropdown(openDropdown === index ? null : index); }; - + // --- Utility Functions for Selection --- const handleToggleSelect = (id) => { setSelectedRows((prev) => prev.includes(id) ? prev.filter((rowId) => rowId !== id) : [...prev, id] ); }; - + const handleSelectAll = (e) => { if (e.target.checked) { const allIds = filteredData.map((row) => row.id); @@ -51,19 +49,19 @@ const Adviser = () => { setSelectedRows([]); } }; - + // Function to start the selection mode const handleStartSelection = () => { setIsSelectionMode(true); setSelectedRows([]); }; - + // Function to cancel selection mode const handleCancelSelection = () => { setIsSelectionMode(false); setSelectedRows([]); }; - + const handleDeleteSelected = () => { if (selectedRows.length === 0) { MySwal.fire({ @@ -73,7 +71,7 @@ const Adviser = () => { }); return; } - + MySwal.fire({ title: `Delete ${selectedRows.length} Advisers?`, text: "This will remove the advisers from the list before final upload.", @@ -84,238 +82,325 @@ const Adviser = () => { confirmButtonText: "Yes, delete them!", }).then((result) => { if (result.isConfirmed) { - const updatedData = importedData.filter((row) => !selectedRows.includes(row.id)); - setImportedData(updatedData); - - handleCancelSelection(); - - MySwal.fire("Deleted!", `${selectedRows.length} advisers removed.`, "success"); + const newRow = { id: uuidv4(), ...result.value }; // includes role + setImportedData((prev) => [...prev, newRow]); + MySwal.fire("Added!", "New record added successfully.", "success"); } }); }; - + // --- Add Adviser (Updated to match Add Student modal design) --- + // --- Add Adviser / Guest Panel (UI with role toggle) --- const handleAddAdviser = async () => { MySwal.fire({ title: "", html: ` -
-
- - - - - Add Adviser -
+
+
+ + + + + Add Adviser +
+
+ + +
+
+ +
- -
- -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
+
+ + +
+
+ +
- `, +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ `, showConfirmButton: false, showCancelButton: false, width: "460px", - customClass: { - popup: 'custom-swal-popup', - }, + customClass: { popup: "custom-swal-popup" }, + didOpen: () => { const popup = Swal.getPopup(); - const adviserIdInput = popup.querySelector("#user_id"); - adviserIdInput.setAttribute("maxlength", "9"); - - // πŸ”Ή Create inline validation message element - const validationMessage = document.createElement("div"); - validationMessage.style.color = "red"; - validationMessage.style.fontSize = "0.8rem"; - validationMessage.style.marginTop = "0.4rem"; - validationMessage.style.textAlign = "left"; - validationMessage.style.display = "none"; - adviserIdInput.parentNode.appendChild(validationMessage); - - adviserIdInput.addEventListener("input", (e) => { - let value = e.target.value; - - // Auto-remove non-numeric characters - const cleanedValue = value.replace(/[^0-9]/g, ""); - if (value !== cleanedValue) { - e.target.value = cleanedValue; - validationMessage.textContent = "Only numbers are allowed for Adviser ID."; - validationMessage.style.display = "block"; - } else { - validationMessage.style.display = "none"; - } - // Enforce max length of 9 digits - if (cleanedValue.length > 9) { - e.target.value = cleanedValue.slice(0, 9); - validationMessage.textContent = "Adviser ID can only be 9 digits long."; - validationMessage.style.display = "block"; - } - }); - // Cancel button functionality - popup.querySelector('#cancel-btn').onclick = () => { - Swal.close(); - }; - - // Enroll button functionality - popup.querySelector('#enroll-btn').onclick = () => { - Swal.clickConfirm(); - }; - - // Hover effects - popup.querySelector('#cancel-btn').addEventListener('mouseenter', (e) => { - e.target.style.backgroundColor = '#f8f8f8'; - }); - popup.querySelector('#cancel-btn').addEventListener('mouseleave', (e) => { - e.target.style.backgroundColor = '#fff'; - }); - popup.querySelector('#enroll-btn').addEventListener('mouseenter', (e) => { - e.target.style.backgroundColor = '#2a0203'; - e.target.style.borderColor = '#2a0203'; - }); - popup.querySelector('#enroll-btn').addEventListener('mouseleave', (e) => { - e.target.style.backgroundColor = '#3B0304'; - e.target.style.borderColor = '#3B0304'; + const adviserOnly = popup.querySelector("#adviser-only"); + const radios = popup.querySelectorAll('input[name="roleType"]'); + const adviserIdInput = popup.querySelector("#user_id"); + const passwordInput = popup.querySelector("#password"); + + // --- Adviser ID constraints (numeric only, max 9) --- + adviserIdInput.setAttribute("maxlength", "9"); + const validationMsg = document.createElement("div"); + validationMsg.style.color = "red"; + validationMsg.style.fontSize = "0.8rem"; + validationMsg.style.marginTop = "0.4rem"; + validationMsg.style.textAlign = "left"; + validationMsg.style.display = "none"; + adviserIdInput.parentNode.appendChild(validationMsg); + + adviserIdInput.addEventListener("input", (e) => { + const cleaned = e.target.value.replace(/[^0-9]/g, ""); + if (e.target.value !== cleaned) { + e.target.value = cleaned; + validationMsg.textContent = + "Only numbers are allowed for Adviser ID."; + validationMsg.style.display = "block"; + } else { + validationMsg.style.display = "none"; + } + if (cleaned.length > 9) { + e.target.value = cleaned.slice(0, 9); + validationMsg.textContent = "Adviser ID can only be 9 digits long."; + validationMsg.style.display = "block"; + } }); - - // Add focus effects to inputs - const inputs = popup.querySelectorAll('input'); - inputs.forEach(input => { - input.addEventListener('focus', (e) => { - e.target.style.borderColor = '#3B0304'; - e.target.style.boxShadow = '0 0 0 2px rgba(59, 3, 4, 0.1)'; + + // --- Toggle UI when role changes --- + const applyRole = () => { + const role = + popup.querySelector('input[name="roleType"]:checked')?.value ?? + "adviser"; + + if (role === "adviser") { + adviserOnly.style.display = "block"; + } else { + adviserOnly.style.display = "none"; + // clear & hide any validation residuals + validationMsg.style.display = "none"; + } + }; + radios.forEach((r) => r.addEventListener("change", applyRole)); + applyRole(); // initial + + // Buttons + popup.querySelector("#cancel-btn").onclick = () => Swal.close(); + popup.querySelector("#enroll-btn").onclick = () => Swal.clickConfirm(); + + // Minor focus styling + popup.querySelectorAll("input").forEach((inp) => { + inp.addEventListener("focus", (e) => { + e.target.style.borderColor = "#3B0304"; + e.target.style.boxShadow = "0 0 0 2px rgba(59, 3, 4, 0.1)"; }); - input.addEventListener('blur', (e) => { - e.target.style.borderColor = '#888'; - e.target.style.boxShadow = 'none'; + inp.addEventListener("blur", (e) => { + e.target.style.borderColor = "#888"; + e.target.style.boxShadow = "none"; }); }); }, + + /*preConfirm: () => { + const role = + document.querySelector('input[name="roleType"]:checked')?.value ?? + "adviser"; + + const payload = { + role, + user_id: document.getElementById("user_id")?.value || "", + password: document.getElementById("password")?.value || "", + first_name: document.getElementById("first_name")?.value || "", + last_name: document.getElementById("last_name")?.value || "", + middle_name: document.getElementById("middle_name")?.value || "", + }; + + // Minimal validation per role (you can expand later) + if (role === "adviser") { + if ( + !payload.user_id || + !payload.password || + !payload.first_name || + !payload.last_name + ) { + MySwal.showValidationMessage( + "Please fill Adviser ID, Password, First Name, and Last Name." + ); + return false; + } + if (/\d/.test(payload.first_name) || /\d/.test(payload.last_name)) { + MySwal.showValidationMessage("Names cannot contain numbers."); + return false; + } + } else { + // guest panel: only names required + if (!payload.first_name || !payload.last_name) { + MySwal.showValidationMessage( + "Please fill First Name and Last Name." + ); + return false; + } + if (/\d/.test(payload.first_name) || /\d/.test(payload.last_name)) { + MySwal.showValidationMessage("Names cannot contain numbers."); + return false; + } + } + + return payload; // delivered to .then + },*/ preConfirm: () => { - const user_id = document.getElementById("user_id").value; - const password = document.getElementById("password").value; - const first_name = document.getElementById("first_name").value; - const last_name = document.getElementById("last_name").value; - - if (!user_id || !password || !first_name || !last_name) { + const role = ( + document.querySelector("input[name='roleType']:checked")?.value || + "adviser" + ).toLowerCase(); + + const user_id = document.getElementById("user_id")?.value?.trim() || ""; + const password = + document.getElementById("password")?.value?.trim() || ""; + const first_name = + document.getElementById("first_name")?.value?.trim() || ""; + const last_name = + document.getElementById("last_name")?.value?.trim() || ""; + const middle_name = + document.getElementById("middle_name")?.value?.trim() || ""; + + // Shared validations + if (!first_name || !last_name) { MySwal.showValidationMessage( - 'Please fill out all required fields (Adviser ID, Password, First Name, Last Name).' + "Please fill out First Name and Last Name." ); return false; } - - // Number check if (/\d/.test(first_name) || /\d/.test(last_name)) { - MySwal.showValidationMessage('Numbers in First Name or Last Name are not allowed.'); + MySwal.showValidationMessage( + "Numbers in First Name or Last Name are not allowed." + ); return false; } - + + // Adviser-only requirements + if (role === "adviser") { + if (!user_id || !password) { + MySwal.showValidationMessage( + "Adviser ID and Password are required for Advisers." + ); + return false; + } + } + return { - user_id, - password, + role, // <-- keep this + user_id, // may be empty for guest + password, // may be empty for guest first_name, last_name, - middle_name: document.getElementById("middle_name").value, + middle_name, }; }, }).then((result) => { - if (result.isConfirmed) { - const newAdviser = { - id: uuidv4(), - ...result.value, - }; - setImportedData((prev) => [...prev, newAdviser]); - MySwal.fire("Added!", "New adviser added successfully.", "success"); - } + if (!result.isConfirmed) return; + + // You said β€œfunctionality later”, so we only push into the temporary list for now + const incoming = result.value; // { role, user_id, password, first_name, last_name, middle_name } + + const newRow = { + id: uuidv4(), + user_id: incoming.user_id, + password: incoming.password, + first_name: incoming.first_name, + last_name: incoming.last_name, + middle_name: incoming.middle_name, + role: incoming.role, // "adviser" | "guest" + }; + + setImportedData((prev) => [...prev, newRow]); + MySwal.fire( + "Added!", + `${ + incoming.role === "guest" ? "Guest panelist" : "Adviser" + } added successfully.`, + "success" + ); }); }; - + // --- Core Functionality (Import/Download/Upload) --- const handleDownload = () => { - const wsData = [ - [ - "PASTE HERE : FULL NAME (LastN, FirstN, MiddleI)", - "", // Spacer - "ID Number", - "Password", - "Last Name", - "First Name", - "Middle Initial", - ], - ["", "", "", "", "", "", ""], - ]; - - const ws = XLSX.utils.aoa_to_sheet(wsData); - - // βœ… Column width setup - ws["!cols"] = [ - { wch: 45 }, // A - Full Name - { wch: 5 }, // B - Spacer - { wch: 15 }, // C - ID - { wch: 15 }, // D - Password - { wch: 18 }, // E - Last Name - { wch: 18 }, // F - First Name - { wch: 15 }, // G - Middle Initial - ]; - - // βœ… Apply parsing formulas (rows 2–500) - for (let row = 2; row <= 500; row++) { - // Last Name (E) - ws[`E${row}`] = { - t: "s", - f: `=IF(A${row}="","",TRIM(LEFT(A${row},FIND(",",A${row})-1)))`, - }; + const wsData = [ + [ + "PASTE HERE : FULL NAME (LastN, FirstN, MiddleI)", + "", // Spacer + "ID Number", + "Password", + "Last Name", + "First Name", + "Middle Initial", + ], + ["", "", "", "", "", "", ""], + ]; + + const ws = XLSX.utils.aoa_to_sheet(wsData); + + // βœ… Column width setup + ws["!cols"] = [ + { wch: 45 }, // A - Full Name + { wch: 5 }, // B - Spacer + { wch: 15 }, // C - ID + { wch: 15 }, // D - Password + { wch: 18 }, // E - Last Name + { wch: 18 }, // F - First Name + { wch: 15 }, // G - Middle Initial + ]; - // First Name (F) - ws[`F${row}`] = { - t: "s", - f: `=IF(A${row}="","", + // βœ… Apply parsing formulas (rows 2–500) + for (let row = 2; row <= 500; row++) { + // Last Name (E) + ws[`E${row}`] = { + t: "s", + f: `=IF(A${row}="","",TRIM(LEFT(A${row},FIND(",",A${row})-1)))`, + }; + + // First Name (F) + ws[`F${row}`] = { + t: "s", + f: `=IF(A${row}="","", TRIM( IF( ISERROR(FIND(",",A${row},FIND(",",A${row})+1)), @@ -328,12 +413,12 @@ const Adviser = () => { ) ) )`, - }; + }; - // Middle Initial (G) - ws[`G${row}`] = { - t: "s", - f: `=IF(A${row}="","", + // Middle Initial (G) + ws[`G${row}`] = { + t: "s", + f: `=IF(A${row}="","", IF( ISERROR(FIND(",",A${row},FIND(",",A${row})+1)), IF( @@ -344,203 +429,211 @@ const Adviser = () => { TRIM(SUBSTITUTE(MID(A${row},FIND(",",A${row},FIND(",",A${row})+1)+1,LEN(A${row})),".","")) ) )`, - }; - } - - // βœ… Workbook setup - const wb = XLSX.utils.book_new(); - XLSX.utils.book_append_sheet(wb, ws, "Adviser_Template"); - ws["!ref"] = "A1:G500"; - wb.Workbook = { CalcPr: { fullCalcOnLoad: true } }; - - // βœ… Save file - const wbout = XLSX.write(wb, { bookType: "xlsx", type: "array" }); - saveAs( - new Blob([wbout], { type: "application/octet-stream" }), - "adviser_template.xlsx" - ); -}; + }; + } -const handleImport = (event) => { - const file = event.target.files[0]; - if (!file) return; - - const reader = new FileReader(); - reader.onload = (e) => { - const data = new Uint8Array(e.target.result); - const workbook = XLSX.read(data, { type: "array" }); - const sheetName = workbook.SheetNames[0]; - const worksheet = workbook.Sheets[sheetName]; - const jsonData = XLSX.utils.sheet_to_json(worksheet, { defval: "" }); - - // βœ… Process imported Excel rows - const processedData = jsonData - .filter((row) => row["ID Number"] || row["Last Name"]) // skip empty - .map((row) => ({ - id: uuidv4(), - user_id: row["ID Number"] ? String(row["ID Number"]).trim() : "", - password: row["Password"] ? String(row["Password"]).trim() : "", - first_name: row["First Name"] ? String(row["First Name"]).trim() : "", - last_name: row["Last Name"] ? String(row["Last Name"]).trim() : "", - middle_name: row["Middle Initial"] ? String(row["Middle Initial"]).trim() : "", - })); - - // βœ… Validation: numeric characters in names - const invalidRow = processedData.find( - (r) => /\d/.test(r.first_name) || /\d/.test(r.last_name) + // βœ… Workbook setup + const wb = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(wb, ws, "Adviser_Template"); + ws["!ref"] = "A1:G500"; + wb.Workbook = { CalcPr: { fullCalcOnLoad: true } }; + + // βœ… Save file + const wbout = XLSX.write(wb, { bookType: "xlsx", type: "array" }); + saveAs( + new Blob([wbout], { type: "application/octet-stream" }), + "adviser_template.xlsx" ); - if (invalidRow) { - MySwal.fire({ - title: "Invalid Name Format", - text: "Names cannot contain numbers.", - icon: "warning", - confirmButtonColor: "#3B0304", - }); + }; + + const handleImport = (event) => { + const file = event.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + const data = new Uint8Array(e.target.result); + const workbook = XLSX.read(data, { type: "array" }); + const sheetName = workbook.SheetNames[0]; + const worksheet = workbook.Sheets[sheetName]; + const jsonData = XLSX.utils.sheet_to_json(worksheet, { defval: "" }); + + // βœ… Process imported Excel rows + const processedData = jsonData + .filter((row) => row["ID Number"] || row["Last Name"]) // skip empty + .map((row) => ({ + id: uuidv4(), + user_id: row["ID Number"] ? String(row["ID Number"]).trim() : "", + password: row["Password"] ? String(row["Password"]).trim() : "", + first_name: row["First Name"] ? String(row["First Name"]).trim() : "", + last_name: row["Last Name"] ? String(row["Last Name"]).trim() : "", + middle_name: row["Middle Initial"] + ? String(row["Middle Initial"]).trim() + : "", + })); + + // βœ… Validation: numeric characters in names + const invalidRow = processedData.find( + (r) => /\d/.test(r.first_name) || /\d/.test(r.last_name) + ); + if (invalidRow) { + MySwal.fire({ + title: "Invalid Name Format", + text: "Names cannot contain numbers.", + icon: "warning", + confirmButtonColor: "#3B0304", + }); + return; + } + + // βœ… Validation: duplicate IDs + const idCounts = processedData.reduce((acc, r) => { + const id = r.user_id; + if (id) acc[id] = (acc[id] || 0) + 1; + return acc; + }, {}); + const duplicateId = Object.keys(idCounts).find((id) => idCounts[id] > 1); + if (duplicateId) { + MySwal.fire({ + title: "Duplicate ID", + text: `Adviser ID "${duplicateId}" is duplicated. Import cancelled.`, + icon: "warning", + confirmButtonColor: "#3B0304", + }); + return; + } + + // βœ… Import ready + setImportedData(processedData); + setSelectedRows([]); + setSearchTerm(""); + }; + + reader.readAsArrayBuffer(file); + }; + + const handleUpload = async () => { + if (importedData.length === 0) { + MySwal.fire( + "No Data", + "Please add advisers/guest panels first.", + "warning" + ); return; } - // βœ… Validation: duplicate IDs - const idCounts = processedData.reduce((acc, r) => { - const id = r.user_id; - if (id) acc[id] = (acc[id] || 0) + 1; - return acc; - }, {}); - const duplicateId = Object.keys(idCounts).find((id) => idCounts[id] > 1); - if (duplicateId) { - MySwal.fire({ - title: "Duplicate ID", - text: `Adviser ID "${duplicateId}" is duplicated. Import cancelled.`, - icon: "warning", - confirmButtonColor: "#3B0304", - }); + // 1) Build academic year options + const currentYear = new Date().getFullYear(); + const maxFutureYears = 1; + const yearOptions = []; + for (let i = 0; i <= maxFutureYears; i++) { + const start = currentYear + i; + yearOptions.push(`${start}-${start + 1}`); + } + + // 2) Ask for year + const { value: selectedYear } = await MySwal.fire({ + title: ` +
+ Select Academic Year +
`, + html: ` +
+ +
`, + showCancelButton: true, + confirmButtonColor: "#3B0304", + cancelButtonColor: "#999", + confirmButtonText: "Confirm", + cancelButtonText: "Cancel", + focusConfirm: false, + width: "350px", + background: "#fff", + customClass: { popup: "custom-swal-popup" }, + didOpen: () => { + const select = document.getElementById("year-select"); + if (select) { + select.style.width = "100%"; + select.style.display = "block"; + } + }, + preConfirm: () => { + const year = document.getElementById("year-select").value; + if (!year) { + MySwal.showValidationMessage("Please select a year first."); + return false; + } + return year; + }, + }); + + if (!selectedYear) { + MySwal.fire("Cancelled", "Enrollment was cancelled.", "info"); return; } - // βœ… Import ready - setImportedData(processedData); - setSelectedRows([]); - setSearchTerm(""); - }; + // 3) Build rows with correct user_roles (3 = Adviser, 5 = Guest Panel) + const normalizeRole = (v) => + (v ?? "adviser").toString().trim().toLowerCase().replace(/\s+/g, ""); - reader.readAsArrayBuffer(file); -}; + try { + const rows = importedData.map((r) => { + const norm = normalizeRole(r.role); + const isGuest = norm.includes("guest"); // matches "guest", "guestpanel", etc. - - const handleUpload = async () => { - if (importedData.length === 0) { - MySwal.fire("No Data", "Please import advisers first.", "warning"); - return; - } - - // 1️⃣ Generate year options dynamically - const currentYear = new Date().getFullYear(); -var maxFutureYears = 1; // You can adjust how many future years to include -const yearOptions = []; - -for (let i = 0; i <= maxFutureYears; i++) { - const startYear = currentYear + i; - const endYear = startYear + 1; - yearOptions.push(`${startYear}-${endYear}`); -} - -// 2️⃣ SweetAlert2 prompt for year selection (auto-select current) -const { value: selectedYear } = await MySwal.fire({ - title: ` -
- Select Academic Year -
- `, - html: ` -
- -
- `, - showCancelButton: true, - confirmButtonColor: "#3B0304", - cancelButtonColor: "#999", - confirmButtonText: "Confirm", - cancelButtonText: "Cancel", - focusConfirm: false, - width: "350px", - background: "#fff", - customClass: { - popup: "custom-swal-popup", - }, - didOpen: () => { - const select = document.getElementById("year-select"); - if (select) { - select.style.width = "100%"; - select.style.display = "block"; - } - }, - preConfirm: () => { - const year = document.getElementById("year-select").value; - if (!year) { - MySwal.showValidationMessage("Please select a year first."); - return false; + return { + user_id: isGuest ? r.user_id || null : (r.user_id ?? "").trim(), + password: isGuest ? r.password || null : (r.password ?? "").trim(), + first_name: (r.first_name ?? "").trim(), + last_name: (r.last_name ?? "").trim(), + middle_name: (r.middle_name ?? "").trim(), + user_roles: isGuest ? 5 : 3, // βœ… correct mapping + year: selectedYear, + }; + }); + + const { error } = await supabase.from("user_credentials").insert(rows); + if (error) throw error; + + MySwal.fire( + "Success", + `Saved ${rows.length} record(s) for ${selectedYear}.`, + "success" + ); + setImportedData([]); + setSelectedRows([]); + } catch (err) { + console.error("Upload error:", err); + MySwal.fire("Error", err.message || "Upload failed.", "error"); } - return year; // Returns selected full string (e.g. "2025-2026") - }, -}); - - -// 3️⃣ Handle cancel -if (!selectedYear) { - MySwal.fire("Cancelled", "Enrollment was cancelled.", "info"); - return; -} - -try { - const dataToInsert = importedData.map((row) => ({ - user_id: row.user_id, - password: row.password, - first_name: row.first_name, - last_name: row.last_name, - middle_name: row.middle_name, - user_roles: 3, // Advisers - year: selectedYear, // πŸ†• Save academic year - })); - - const { error } = await supabase.from("user_credentials").insert(dataToInsert); - if (error) throw error; - - MySwal.fire("Success", `Advisers enrolled for ${selectedYear}!`, "success"); - setImportedData([]); - setSelectedRows([]); -} catch (err) { - console.error("Upload error:", err.message); - MySwal.fire("Error", err.message, "error"); -} - }; - + }; + // --- Edit Row (Updated to match Add Adviser modal design) --- const handleEditRow = (row, index) => { setOpenDropdown(null); - + MySwal.fire({ title: "", html: ` @@ -607,79 +700,85 @@ try { showCancelButton: false, width: "460px", customClass: { - popup: 'custom-swal-popup', + popup: "custom-swal-popup", }, didOpen: () => { const popup = Swal.getPopup(); - - const adviserIdInput = popup.querySelector("#user_id"); - adviserIdInput.setAttribute("maxlength", "9"); - - // πŸ”Ή Create inline validation message element - const validationMessage = document.createElement("div"); - validationMessage.style.color = "red"; - validationMessage.style.fontSize = "0.8rem"; - validationMessage.style.marginTop = "0.4rem"; - validationMessage.style.textAlign = "left"; - validationMessage.style.display = "none"; - adviserIdInput.parentNode.appendChild(validationMessage); - - adviserIdInput.addEventListener("input", (e) => { - let value = e.target.value; - - // Auto-remove non-numeric characters - const cleanedValue = value.replace(/[^0-9]/g, ""); - if (value !== cleanedValue) { - e.target.value = cleanedValue; - validationMessage.textContent = "Only numbers are allowed for Adviser ID."; - validationMessage.style.display = "block"; - } else { - validationMessage.style.display = "none"; - } - // Enforce max length of 9 digits - if (cleanedValue.length > 9) { - e.target.value = cleanedValue.slice(0, 9); - validationMessage.textContent = "Adviser ID can only be 9 digits long."; - validationMessage.style.display = "block"; - } - }); + const adviserIdInput = popup.querySelector("#user_id"); + adviserIdInput.setAttribute("maxlength", "9"); + + // πŸ”Ή Create inline validation message element + const validationMessage = document.createElement("div"); + validationMessage.style.color = "red"; + validationMessage.style.fontSize = "0.8rem"; + validationMessage.style.marginTop = "0.4rem"; + validationMessage.style.textAlign = "left"; + validationMessage.style.display = "none"; + adviserIdInput.parentNode.appendChild(validationMessage); + + adviserIdInput.addEventListener("input", (e) => { + let value = e.target.value; + + // Auto-remove non-numeric characters + const cleanedValue = value.replace(/[^0-9]/g, ""); + if (value !== cleanedValue) { + e.target.value = cleanedValue; + validationMessage.textContent = + "Only numbers are allowed for Adviser ID."; + validationMessage.style.display = "block"; + } else { + validationMessage.style.display = "none"; + } + + // Enforce max length of 9 digits + if (cleanedValue.length > 9) { + e.target.value = cleanedValue.slice(0, 9); + validationMessage.textContent = + "Adviser ID can only be 9 digits long."; + validationMessage.style.display = "block"; + } + }); // Cancel button functionality - popup.querySelector('#cancel-btn').onclick = () => { + popup.querySelector("#cancel-btn").onclick = () => { Swal.close(); }; - + // Save button functionality - popup.querySelector('#save-btn').onclick = () => { + popup.querySelector("#save-btn").onclick = () => { Swal.clickConfirm(); }; - + // Hover effects - popup.querySelector('#cancel-btn').addEventListener('mouseenter', (e) => { - e.target.style.backgroundColor = '#f8f8f8'; - }); - popup.querySelector('#cancel-btn').addEventListener('mouseleave', (e) => { - e.target.style.backgroundColor = '#fff'; - }); - popup.querySelector('#save-btn').addEventListener('mouseenter', (e) => { - e.target.style.backgroundColor = '#2a0203'; - e.target.style.borderColor = '#2a0203'; + popup + .querySelector("#cancel-btn") + .addEventListener("mouseenter", (e) => { + e.target.style.backgroundColor = "#f8f8f8"; + }); + popup + .querySelector("#cancel-btn") + .addEventListener("mouseleave", (e) => { + e.target.style.backgroundColor = "#fff"; + }); + popup.querySelector("#save-btn").addEventListener("mouseenter", (e) => { + e.target.style.backgroundColor = "#2a0203"; + e.target.style.borderColor = "#2a0203"; }); - popup.querySelector('#save-btn').addEventListener('mouseleave', (e) => { - e.target.style.backgroundColor = '#3B0304'; - e.target.style.borderColor = '#3B0304'; + popup.querySelector("#save-btn").addEventListener("mouseleave", (e) => { + e.target.style.backgroundColor = "#3B0304"; + e.target.style.borderColor = "#3B0304"; }); - + // Add focus effects to inputs - const inputs = popup.querySelectorAll('input'); - inputs.forEach(input => { - input.addEventListener('focus', (e) => { - e.target.style.borderColor = '#3B0304'; - e.target.style.boxShadow = '0 0 0 2px rgba(59, 3, 4, 0.1)'; + const inputs = popup.querySelectorAll("input"); + inputs.forEach((input) => { + input.addEventListener("focus", (e) => { + e.target.style.borderColor = "#3B0304"; + e.target.style.boxShadow = "0 0 0 2px rgba(59, 3, 4, 0.1)"; }); - input.addEventListener('blur', (e) => { - e.target.style.borderColor = '#888'; - e.target.style.boxShadow = 'none'; + input.addEventListener("blur", (e) => { + e.target.style.borderColor = "#888"; + e.target.style.boxShadow = "none"; }); }); }, @@ -688,20 +787,22 @@ try { const password = document.getElementById("password").value; const first_name = document.getElementById("first_name").value; const last_name = document.getElementById("last_name").value; - + if (!user_id || !password || !first_name || !last_name) { MySwal.showValidationMessage( - 'Please fill out all required fields (Adviser ID, Password, First Name, Last Name).' + "Please fill out all required fields (Adviser ID, Password, First Name, Last Name)." ); return false; } - + // Number check if (/\d/.test(first_name) || /\d/.test(last_name)) { - MySwal.showValidationMessage('Numbers in First Name or Last Name are not allowed.'); + MySwal.showValidationMessage( + "Numbers in First Name or Last Name are not allowed." + ); return false; } - + return { user_id, password, @@ -719,10 +820,10 @@ try { } }); }; - + const handleDeleteRow = (index) => { setOpenDropdown(null); - + MySwal.fire({ title: "Are you sure?", text: "This will remove the adviser from the list.", @@ -736,27 +837,27 @@ try { const updatedData = importedData.filter((_, i) => i !== index); const deletedId = importedData[index].id; setSelectedRows((prev) => prev.filter((id) => id !== deletedId)); - + setImportedData(updatedData); MySwal.fire("Deleted!", "Adviser removed.", "success"); } }); }; - + const handleCancel = () => { setImportedData([]); setSelectedRows([]); setSearchTerm(""); MySwal.fire("Cancelled", "Import cancelled.", "info"); }; - + // --- Filtering --- const filteredData = importedData.filter((row) => { const userId = String(row.user_id ?? "").toLowerCase(); const firstName = String(row.first_name ?? "").toLowerCase(); const lastName = String(row.last_name ?? "").toLowerCase(); const middleName = String(row.middle_name ?? "").toLowerCase(); - + return ( userId.includes(searchTerm.toLowerCase()) || firstName.includes(searchTerm.toLowerCase()) || @@ -764,9 +865,11 @@ try { middleName.includes(searchTerm.toLowerCase()) ); }); - - const isAllSelected = filteredData.length > 0 && filteredData.every((row) => selectedRows.includes(row.id)); - + + const isAllSelected = + filteredData.length > 0 && + filteredData.every((row) => selectedRows.includes(row.id)); + // --- Render --- return (
@@ -810,14 +913,17 @@ try { background-color: #f8f9fa; } `} - +
-
+
Enroll Β» Advisers
- +
- +
{/* Top Control Buttons */}
@@ -847,12 +953,16 @@ try { borderRadius: "6px", transition: "background-color 0.2s", }} - onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = "#f0f0f0")} - onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = "white")} + onMouseEnter={(e) => + (e.currentTarget.style.backgroundColor = "#f0f0f0") + } + onMouseLeave={(e) => + (e.currentTarget.style.backgroundColor = "white") + } > Download - +
- +
{importedData.length > 0 && ( <> @@ -891,12 +1011,14 @@ try { cursor: "pointer", transition: "opacity 0.2s", }} - onMouseEnter={(e) => (e.currentTarget.style.opacity = "0.9")} + onMouseEnter={(e) => + (e.currentTarget.style.opacity = "0.9") + } onMouseLeave={(e) => (e.currentTarget.style.opacity = "1")} > Save & Enroll ({importedData.length}) - + )} - + {/* Add Adviser Button */}
- + {importedData.length === 0 ? (
- NOTE: Please download the template to input the required adviser information. Once completed, import the file to proceed with enrolling the advisers into the system. + NOTE: Please download the template to input the + required adviser information. Once completed, import the file to + proceed with enrolling the advisers into the system.
) : ( // --- Enrollment Table Section --- -
+
{/* Table Controls (Search and Delete Selected) */}
@@ -974,9 +1109,17 @@ try { height: "38px", }} /> - +
- + {/* Conditional Delete Buttons */} {!isSelectionMode ? ( @@ -1011,8 +1158,12 @@ try { fontWeight: "500", transition: "background-color 0.2s", }} - onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = "#f0f0f0")} - onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = "transparent")} + onMouseEnter={(e) => + (e.currentTarget.style.backgroundColor = "#f0f0f0") + } + onMouseLeave={(e) => + (e.currentTarget.style.backgroundColor = "transparent") + } > Cancel @@ -1026,75 +1177,183 @@ try { borderRadius: "6px", backgroundColor: "transparent", fontWeight: "500", - cursor: selectedRows.length === 0 ? "not-allowed" : "pointer", - color: selectedRows.length === 0 ? "#A0A0A0" : "#3B0304", - border: `1.5px solid ${selectedRows.length === 0 ? "#B2B2B2" : "#3B0304"}`, + cursor: + selectedRows.length === 0 ? "not-allowed" : "pointer", + color: + selectedRows.length === 0 ? "#A0A0A0" : "#3B0304", + border: `1.5px solid ${ + selectedRows.length === 0 ? "#B2B2B2" : "#3B0304" + }`, transition: "all 0.2s", }} - onMouseEnter={(e) => (selectedRows.length > 0 ? (e.currentTarget.style.backgroundColor = "#f0f0f0") : null)} - onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = "transparent")} + onMouseEnter={(e) => + selectedRows.length > 0 + ? (e.currentTarget.style.backgroundColor = "#f0f0f0") + : null + } + onMouseLeave={(e) => + (e.currentTarget.style.backgroundColor = "transparent") + } > Delete Selected
)}
- + {/* Table Body with Scrolling */} -
+
{/* Checkbox Header (Conditional) */} {isSelectionMode && ( - )} - - - - - - - + + + + + + + {filteredData.map((row, index) => { const isSelected = selectedRows.includes(row.id); return ( - + {/* Checkbox Cell (Conditional) */} {isSelectionMode && ( )} - - - - - - + + + + + + - + )} @@ -1119,5 +1383,5 @@ try { ); }; - -export default Adviser; \ No newline at end of file + +export default Adviser; diff --git a/src/components/Instructor/FinalDefense.jsx b/src/components/Instructor/FinalDefense.jsx index 72332e2..9d996b8 100644 --- a/src/components/Instructor/FinalDefense.jsx +++ b/src/components/Instructor/FinalDefense.jsx @@ -2,1223 +2,1206 @@ import { useState, useEffect, useRef } from "react"; import Swal from "sweetalert2"; import withReactContent from "sweetalert2-react-content"; -import { FaCalendarAlt, FaEllipsisV, FaSearch, FaTrash, FaFileExport, FaPen, FaPlus } from "react-icons/fa"; +import { + FaCalendarAlt, + FaEllipsisV, + FaSearch, + FaTrash, + FaFileExport, + FaPen, + FaPlus, +} from "react-icons/fa"; import { supabase } from "../../supabaseClient"; import jsPDF from "jspdf"; const MySwal = withReactContent(Swal); const FinalDefense = () => { - const [advisers, setAdvisers] = useState([]); - const [accounts, setAccounts] = useState([]); - const [schedules, setSchedules] = useState([]); - const [openDropdown, setOpenDropdown] = useState(null); - const [search, setSearch] = useState(""); - const [isDeleteMode, setIsDeleteMode] = useState(false); - const [selectedSchedules, setSelectedSchedules] = useState([]); - const dropdownRef = useRef(null); - - const verdictMap = { - 1: "Pending", - 2: "Accepted", - 3: "Re-Oral", - 4: "Not-Accepted", - - }; - - // Close dropdown when clicking outside - useEffect(() => { - const handleClickOutside = (event) => { - if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { - setOpenDropdown(null); - } - }; - - document.addEventListener("mousedown", handleClickOutside); - return () => { - document.removeEventListener("mousedown", handleClickOutside); - }; - }, []); - - // fetch advisers, accounts, schedules - useEffect(() => { - const fetchData = async () => { - const { data: accData, error: accError } = await supabase - .from("user_credentials") - .select("*"); - if (accError) return console.error("Error fetching accounts:", accError); - - if (accData) { - setAccounts(accData); - setAdvisers(accData.filter((a) => a.user_roles === 3)); // advisers - } - - const { data: schedData, error: schedError } = await supabase - .from("user_final_sched") - .select( - ` - *, - manager:manager_id ( group_name ) - ` - ); - - if (schedError) return console.error("Error fetching schedules:", schedError); - if (schedData) setSchedules(schedData); - }; - fetchData(); - }, []); - - // Sort schedules by verdict priority: Pending -> Re-Oral -> Not-Accepted -> Accepted - const sortedSchedules = [...schedules].sort((a, b) => { - const priorityOrder = {1: 1, 2: 2, 3: 3, 4: 4}; // Pending -> Re-Oral -> Not-Accepted -> Accepted - return priorityOrder[a.verdict] - priorityOrder[b.verdict]; - }); - - // search by team - const filteredSchedules = sortedSchedules.filter((s) => - s.manager?.group_name?.toLowerCase().includes(search.toLowerCase()) - ); - - // Check if team already has a schedule - const isTeamAlreadyScheduled = (teamName) => { - return schedules.some(schedule => - schedule.manager?.group_name === teamName - ); - }; - - // Get teams that are already scheduled - const getScheduledTeams = () => { - return schedules.map(schedule => schedule.manager?.group_name).filter(Boolean); - }; - - // Check if time conflict exists (same time or within 1 hour gap) - const hasTimeConflict = (selectedDate, selectedTime, excludeScheduleId = null) => { - if (!selectedDate || !selectedTime) return false; - - const selectedDateTime = new Date(`${selectedDate}T${selectedTime}`); - - return schedules.some(schedule => { - if (schedule.id === excludeScheduleId) return false; // Exclude current schedule when updating - if (schedule.date !== selectedDate) return false; // Different dates, no conflict - - const scheduleDateTime = new Date(`${schedule.date}T${schedule.time}`); - const timeDiff = Math.abs(selectedDateTime - scheduleDateTime) / (1000 * 60 * 60); // Difference in hours - - return timeDiff < 1; // Conflict if less than 1 hour gap - }); + const [accounts, setAccounts] = useState([]); + const [advisersOnly, setAdvisersOnly] = useState([]); // role 3 only, for Adviser select + const [schedules, setSchedules] = useState([]); + const [openDropdown, setOpenDropdown] = useState(null); + const [search, setSearch] = useState(""); + const [isDeleteMode, setIsDeleteMode] = useState(false); + const [selectedSchedules, setSelectedSchedules] = useState([]); + const dropdownRef = useRef(null); + + // 1: Pending, 2: Accepted, 3: Re-Oral, 4: Not-Accepted + const verdictMap = { + 1: "Pending", + 2: "Accepted", + 3: "Re-Oral", + 4: "Not-Accepted", + }; + + useEffect(() => { + const handleClickOutside = (e) => { + if (dropdownRef.current && !dropdownRef.current.contains(e.target)) + setOpenDropdown(null); }; - - // Get conflicting times for display - const getConflictingTimes = (selectedDate, selectedTime, excludeScheduleId = null) => { - if (!selectedDate || !selectedTime) return []; - - const selectedDateTime = new Date(`${selectedDate}T${selectedTime}`); - - return schedules - .filter(schedule => { - if (schedule.id === excludeScheduleId) return false; - if (schedule.date !== selectedDate) return false; - - const scheduleDateTime = new Date(`${schedule.date}T${schedule.time}`); - const timeDiff = Math.abs(selectedDateTime - scheduleDateTime) / (1000 * 60 * 60); - return timeDiff < 1; - }) - .map(schedule => ({ - team: schedule.manager?.group_name || 'Unknown Team', - time: schedule.time - })); + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + useEffect(() => { + const fetchData = async () => { + const { data: accData, error: accError } = await supabase + .from("user_credentials") + .select("*"); + if (accError) { + console.error("Error fetching accounts:", accError); + return; + } + setAccounts(accData || []); + setAdvisersOnly((accData || []).filter((a) => a.user_roles === 3)); + + const { data: schedData, error: schedError } = await supabase + .from("user_final_sched") + .select(`*, manager:manager_id ( group_name )`); + if (schedError) { + console.error("Error fetching schedules:", schedError); + return; + } + setSchedules(schedData || []); }; + fetchData(); + }, []); + + const sortedSchedules = [...schedules].sort((a, b) => { + const priorityOrder = { 1: 1, 3: 2, 4: 3, 2: 4 }; // Pending -> Re-Oral -> Not-Accepted -> Accepted + return (priorityOrder[a.verdict] || 99) - (priorityOrder[b.verdict] || 99); + }); + + const filteredSchedules = sortedSchedules.filter((s) => + s.manager?.group_name?.toLowerCase().includes(search.toLowerCase()) + ); + + const isTeamAlreadyScheduled = (teamName) => + schedules.some((schedule) => schedule.manager?.group_name === teamName); + + const getScheduledTeams = () => + schedules.map((s) => s.manager?.group_name).filter(Boolean); + + const hasTimeConflict = ( + selectedDate, + selectedTime, + excludeScheduleId = null + ) => { + if (!selectedDate || !selectedTime) return false; + const selectedDateTime = new Date(`${selectedDate}T${selectedTime}`); + return schedules.some((schedule) => { + if (schedule.id === excludeScheduleId) return false; + if (schedule.date !== selectedDate) return false; + const scheduleDateTime = new Date(`${schedule.date}T${schedule.time}`); + const timeDiff = + Math.abs(selectedDateTime - scheduleDateTime) / (1000 * 60 * 60); + return timeDiff < 1; + }); + }; + + const getConflictingTimes = ( + selectedDate, + selectedTime, + excludeScheduleId = null + ) => { + if (!selectedDate || !selectedTime) return []; + const selectedDateTime = new Date(`${selectedDate}T${selectedTime}`); + return schedules + .filter((schedule) => { + if (schedule.id === excludeScheduleId) return false; + if (schedule.date !== selectedDate) return false; + const scheduleDateTime = new Date(`${schedule.date}T${schedule.time}`); + const timeDiff = + Math.abs(selectedDateTime - scheduleDateTime) / (1000 * 60 * 60); + return timeDiff < 1; + }) + .map((schedule) => ({ + team: schedule.manager?.group_name || "Unknown Team", + time: schedule.time, + })); + }; + + const exportFinalDefenseAsPDF = (data) => { + const today = new Date().toLocaleDateString(); + const fileName = `final-defense-schedule-${today.replace(/\//g, "-")}.pdf`; + const doc = new jsPDF(); + + doc.setFillColor(59, 3, 4); + doc.rect(0, 0, 210, 30, "F"); + doc.setTextColor(255, 255, 255); + doc.setFontSize(20); + doc.setFont("helvetica", "bold"); + doc.text("Final Defense Schedule Report", 105, 15, { align: "center" }); + doc.setFontSize(10); + doc.setFont("helvetica", "normal"); + doc.text(`Generated on: ${today}`, 105, 22, { align: "center" }); + + doc.setTextColor(255, 255, 255); + doc.setFillColor(59, 3, 4); + doc.rect(10, 35, 190, 10, "F"); + doc.setFont("helvetica", "bold"); + doc.setFontSize(8); + + const headers = [ + "NO", + "TEAM", + "TITLE", + "DATE", + "TIME", + "ADVISER", + "PANELISTS", + "VERDICT", + ]; + const columnWidths = [10, 30, 35, 20, 15, 25, 40, 20]; + let x = 10; + headers.forEach((h, idx) => { + doc.text(h, x + 2, 42); + x += columnWidths[idx]; + }); - const exportFinalDefenseAsPDF = (data) => { - const today = new Date().toLocaleDateString(); - const fileName = `final-defense-schedule-${today.replace(/\//g, '-')}.pdf`; - - // Create PDF using jsPDF - const doc = new jsPDF(); - - // Add header - doc.setFillColor(59, 3, 4); - doc.rect(0, 0, 210, 30, 'F'); - doc.setTextColor(255, 255, 255); - doc.setFontSize(20); - doc.setFont('helvetica', 'bold'); - doc.text('Final Defense Schedule Report', 105, 15, { align: 'center' }); - - doc.setFontSize(10); - doc.setFont('helvetica', 'normal'); - doc.text(`Generated on: ${today}`, 105, 22, { align: 'center' }); - - // Reset text color for content - doc.setTextColor(0, 0, 0); - - // Add table headers - doc.setFillColor(59, 3, 4); - doc.rect(10, 35, 190, 10, 'F'); - doc.setTextColor(255, 255, 255); - doc.setFontSize(8); - doc.setFont('helvetica', 'bold'); - - const headers = ['NO', 'TEAM', 'TITLE', 'DATE', 'TIME', 'ADVISER', 'PANELISTS', 'VERDICT']; - const columnWidths = [10, 30, 35, 20, 15, 25, 40, 20]; - let xPosition = 10; - - headers.forEach((header, index) => { - doc.text(header, xPosition + 2, 42); - xPosition += columnWidths[index]; - }); + doc.setTextColor(0, 0, 0); + doc.setFont("helvetica", "normal"); + doc.setFontSize(7); + + let y = 50; + data.forEach((row, idx) => { + if (y > 270) { + doc.addPage(); + y = 20; + } + if (idx % 2 === 0) { + doc.setFillColor(245, 245, 245); + doc.rect(10, y - 4, 190, 6, "F"); + } + x = 10; + const rowData = [ + String(row.no), + row.team, + row.title, + row.date, + row.time, + row.adviser, + row.panelists, + row.verdict, + ]; + rowData.forEach((cell, ci) => { + if (ci === 2 || ci === 6) { + const lines = doc.splitTextToSize(cell, columnWidths[ci] - 2); + doc.text(lines, x + 1, y); + } else { + doc.text(cell, x + 1, y); + } + x += columnWidths[ci]; + }); + y += 6; + }); - // Add table rows - doc.setTextColor(0, 0, 0); - doc.setFont('helvetica', 'normal'); - doc.setFontSize(7); + doc.setFontSize(8); + doc.text(`Total Records: ${data.length}`, 14, 285); + doc.save(fileName); + }; + + // -------- CREATE: panelists pulled from Oral Defense (locked) -------- + const handleCreateSchedule = () => { + let selectedPanelists = []; // filled from oral defense + let selectedAdviser = null; + + MySwal.fire({ + title: `
Create Schedule
`, + html: ` + +
+ + +
+
+ + + +
- let yPosition = 50; + +
+ +
+ Select a team to load panelists from Oral Defense… +
+
- data.forEach((item, index) => { - if (yPosition > 270) { - doc.addPage(); - yPosition = 20; - } +
+ + +
+
+ + + +
+ `, + showCancelButton: true, + confirmButtonText: "Create", + cancelButtonText: "Cancel", + confirmButtonColor: "#3B0304", + cancelButtonColor: "#999", + width: "600px", + didOpen: () => { + const adviserSelect = document.getElementById("adviserSelect"); + const teamSelect = document.getElementById("teamSelect"); + const panelList = document.getElementById("panelList"); + const oralNote = document.getElementById("oralNote"); + const scheduleDate = document.getElementById("scheduleDate"); + const scheduleTime = document.getElementById("scheduleTime"); + const timeConflictWarning = document.getElementById( + "timeConflictWarning" + ); - // Alternate row background - if (index % 2 === 0) { - doc.setFillColor(245, 245, 245); - doc.rect(10, yPosition - 4, 190, 6, 'F'); + // date/time guards like your other pages + if (scheduleDate) { + const today = new Date(); + today.setHours(0, 0, 0, 0); + scheduleDate.min = today.toISOString().slice(0, 10); + const openPicker = () => + scheduleDate.showPicker && scheduleDate.showPicker(); + scheduleDate.addEventListener("click", openPicker); + scheduleDate.addEventListener("focus", openPicker); + } + if (scheduleTime) { + const openTP = () => + scheduleTime.showPicker && scheduleTime.showPicker(); + scheduleTime.addEventListener("click", openTP); + scheduleTime.addEventListener("focus", openTP); + const getNowHHMM = () => { + const n = new Date(); + return `${String(n.getHours()).padStart(2, "0")}:${String( + n.getMinutes() + ).padStart(2, "0")}`; + }; + const setMinIfToday = () => { + const d = scheduleDate.value; + if (!d) { + scheduleTime.removeAttribute("min"); + return; } - - xPosition = 10; - const rowData = [ - item.no.toString(), - item.team, - item.title, - item.date, - item.time, - item.adviser, - item.panelists, - item.verdict - ]; - - rowData.forEach((cell, cellIndex) => { - // Wrap text for panelists and title columns - if (cellIndex === 2 || cellIndex === 6) { - const lines = doc.splitTextToSize(cell, columnWidths[cellIndex] - 2); - doc.text(lines, xPosition + 1, yPosition); - } else { - doc.text(cell, xPosition + 1, yPosition); - } - xPosition += columnWidths[cellIndex]; + const today = new Date(); + today.setHours(0, 0, 0, 0); + const picked = new Date(d); + picked.setHours(0, 0, 0, 0); + if (picked.getTime() === today.getTime()) { + const min = getNowHHMM(); + scheduleTime.min = min; + if (scheduleTime.value && scheduleTime.value < min) + scheduleTime.value = min; + } else scheduleTime.removeAttribute("min"); + }; + scheduleDate.addEventListener("change", setMinIfToday); + scheduleTime.addEventListener("input", () => { + const min = scheduleTime.getAttribute("min"); + if (min && scheduleTime.value && scheduleTime.value < min) + scheduleTime.value = min; + }); + setMinIfToday(); + } + const checkTimeConflict = () => { + const d = scheduleDate.value, + t = scheduleTime.value; + if (d && t) { + if (hasTimeConflict(d, t)) { + const conflicts = getConflictingTimes(d, t); + timeConflictWarning.style.display = "block"; + timeConflictWarning.innerHTML = `⚠️ Time conflict with: ${conflicts + .map((c) => `${c.team} at ${c.time}`) + .join( + ", " + )}. Please choose a different time with at least 1 hour gap.`; + } else timeConflictWarning.style.display = "none"; + } else timeConflictWarning.style.display = "none"; + }; + scheduleDate.addEventListener("change", checkTimeConflict); + scheduleTime.addEventListener("change", checkTimeConflict); + + // Enable team list after adviser is picked; show teams in same adviser_group and not already final-scheduled + adviserSelect.addEventListener("change", () => { + teamSelect.disabled = false; + const adviser = accounts.find((a) => a.id === adviserSelect.value); + const scheduledTeams = getScheduledTeams(); + teamSelect.innerHTML = + ''; + if (adviser) { + const teams = accounts.filter( + (a) => + a.adviser_group === adviser.adviser_group && a.user_roles === 1 + ); + teams.forEach((m) => { + if (!m.group_name) return; + const already = scheduledTeams.includes(m.group_name); + const opt = document.createElement("option"); + opt.value = m.group_name; + opt.textContent = + m.group_name + (already ? " (Already Scheduled)" : ""); + if (already) { + opt.disabled = true; + opt.className = "disabled-option"; + } + teamSelect.appendChild(opt); }); - - yPosition += 6; + } + // reset panel display + selectedPanelists = []; + panelList.innerHTML = `Select a team to load panelists from Oral Defense…`; + oralNote.style.display = "none"; + oralNote.innerHTML = ""; }); - // Add footer with total records - doc.setFontSize(8); - doc.text(`Total Records: ${data.length}`, 14, 285); - - // Save the PDF - doc.save(fileName); - }; - - const handleCreateSchedule = () => { - let selectedPanelists = []; - let selectedAdviser = null; - - MySwal.fire({ - title: `
- Create Schedule
`, - html: ` - -
- - -
-
- - -
-
- - -
-
- -
- No panelist selected -
-
-
- - -
-
- - - -
- `, - showCancelButton: true, - confirmButtonText: "Create", - cancelButtonText: "Cancel", - confirmButtonColor: "#3B0304", - cancelButtonColor: "#999", - width: "600px", - didOpen: () => { - const adviserSelect = document.getElementById("adviserSelect"); - const teamSelect = document.getElementById("teamSelect"); - const panelSelect = document.getElementById("panelSelect"); - const panelList = document.getElementById("panelList"); - const scheduleDate = document.getElementById("scheduleDate"); - const scheduleTime = document.getElementById("scheduleTime"); - const timeConflictWarning = document.getElementById("timeConflictWarning"); - - // Get already scheduled teams - const scheduledTeams = getScheduledTeams(); - - // update team options - const updateTeamOptions = (adviserId) => { - const adviser = accounts.find((a) => a.id === adviserId); - if (!adviser) return; - - const teams = accounts.filter( - (a) => a.adviser_group === adviser.adviser_group && a.user_roles === 1 - ); - - teamSelect.innerHTML = ''; - - if (teams.length > 0) { - teams.forEach((t) => { - if (t.group_name) { - const isAlreadyScheduled = scheduledTeams.includes(t.group_name); - const disabledAttr = isAlreadyScheduled ? 'disabled class="disabled-option"' : ''; - const scheduledText = isAlreadyScheduled ? ' (Already Scheduled)' : ''; - teamSelect.innerHTML += ``; - } - }); - } else { - const opt = document.createElement("option"); - opt.value = ""; - opt.disabled = true; - opt.textContent = "No manager available"; - teamSelect.appendChild(opt); - } - }; - - // update panel options - const updatePanelOptions = (adviserId) => { - panelSelect.innerHTML = ''; - advisers.forEach((a) => { - if (a.id !== adviserId) - panelSelect.innerHTML += ``; - }); - }; - - // Check for time conflicts - const checkTimeConflict = () => { - const date = scheduleDate.value; - const time = scheduleTime.value; - - if (date && time) { - if (hasTimeConflict(date, time)) { - const conflicts = getConflictingTimes(date, time); - const conflictText = conflicts.map(conflict => - `${conflict.team} at ${conflict.time}` - ).join(', '); - - timeConflictWarning.style.display = 'block'; - timeConflictWarning.innerHTML = `⚠️ Time conflict with: ${conflictText}. Please choose a different time with at least 1 hour gap.`; - } else { - timeConflictWarning.style.display = 'none'; - } - } else { - timeConflictWarning.style.display = 'none'; - } - }; - - // Event listeners for date and time changes - scheduleDate.addEventListener('change', checkTimeConflict); - scheduleTime.addEventListener('change', checkTimeConflict); - - adviserSelect.addEventListener("change", () => { - selectedAdviser = adviserSelect.value; - updateTeamOptions(selectedAdviser); - updatePanelOptions(selectedAdviser); - teamSelect.disabled = false; - panelSelect.disabled = false; - selectedPanelists = []; - panelList.innerHTML = 'No panelist selected'; - }); - - // add panelist - panelSelect.addEventListener("change", () => { - const id = panelSelect.value; - if (!selectedPanelists.includes(id)) { - if (selectedPanelists.length < 3) { - selectedPanelists.push(id); - const person = advisers.find((a) => a.id === id); - if (panelList.querySelector(".text-muted")) panelList.innerHTML = ""; - const tag = document.createElement("span"); - tag.className = "bg-gray-200 text-gray-800 rounded-full px-2 py-1 text-sm flex items-center gap-1"; - tag.innerHTML = `${person.last_name}, ${person.first_name} `; - panelList.appendChild(tag); - } else { - MySwal.showValidationMessage("Maximum of 3 panelists."); - } - } - panelSelect.value = ""; - }); - - // remove panelist - panelList.addEventListener("click", (e) => { - if (e.target.classList.contains("remove-panelist-btn")) { - const idToRemove = e.target.dataset.id; - selectedPanelists = selectedPanelists.filter((pid) => pid !== idToRemove); - e.target.parentElement.remove(); - if (selectedPanelists.length === 0) - panelList.innerHTML = 'No panelist selected'; - } - }); - }, - preConfirm: () => { - const team = document.getElementById("teamSelect").value; - const date = document.getElementById("scheduleDate").value; - const time = document.getElementById("scheduleTime").value; - const teamSelect = document.getElementById("teamSelect"); - const selectedOption = teamSelect.options[teamSelect.selectedIndex]; - - // Check if selected team is already scheduled - if (selectedOption.disabled) { - MySwal.showValidationMessage("This team already has a schedule. Please select a different team."); - return false; - } - - // Check for time conflicts - if (hasTimeConflict(date, time)) { - const conflicts = getConflictingTimes(date, time); - const conflictText = conflicts.map(conflict => - `${conflict.team} at ${conflict.time}` - ).join(', '); - MySwal.showValidationMessage(`Time conflict detected with: ${conflictText}. Please choose a different time with at least 1 hour gap.`); - return false; - } - - if (!selectedAdviser || !team || !date || !time || selectedPanelists.length === 0) { - MySwal.showValidationMessage("Please fill all fields and select panelists."); - return false; - } - - return { adviser: selectedAdviser, team, date, time, panelists: selectedPanelists }; - }, - }).then(async (result) => { - if (result.isConfirmed) { - const { adviser, team, date, time, panelists } = result.value; - - // Double-check if team is already scheduled (in case of race condition) - if (isTeamAlreadyScheduled(team)) { - MySwal.fire("Error", "This team already has a schedule. Please select a different team.", "error"); - return; - } - - // Double-check for time conflicts - if (hasTimeConflict(date, time)) { - const conflicts = getConflictingTimes(date, time); - const conflictText = conflicts.map(conflict => - `${conflict.team} at ${conflict.time}` - ).join(', '); - MySwal.fire("Error", `Time conflict detected with: ${conflictText}. Please choose a different time with at least 1 hour gap.`, "error"); - return; - } + // When team changes, fetch ORAL DEFENSE panelists (latest for that manager) + teamSelect.addEventListener("change", async () => { + const teamName = teamSelect.value; + selectedPanelists = []; + panelList.innerHTML = `Loading Oral Defense panelists…`; + oralNote.style.display = "none"; + oralNote.innerHTML = ""; + + // find team manager id + const manager = accounts.find( + (a) => a.user_roles === 1 && a.group_name === teamName + ); + if (!manager) { + panelList.innerHTML = `No manager found for this team.`; + return; + } + + // get latest oraldef of this manager + const { data: oralList, error } = await supabase + .from("user_oraldef") + .select("*") + .eq("manager_id", manager.id) + .order("date", { ascending: false }) + .order("time", { ascending: false }) + .limit(1); + + if (error) { + panelList.innerHTML = `Failed to fetch Oral Defense.`; + return; + } + const oral = oralList?.[0]; + if (!oral) { + panelList.innerHTML = `No Oral Defense found for this team. You must schedule Oral Defense first.`; + return; + } + + const ids = [ + oral.panelist1_id, + oral.panelist2_id, + oral.panelist3_id, + ].filter(Boolean); + if (ids.length === 0) { + panelList.innerHTML = `Oral Defense has no panelists set for this team.`; + return; + } + + selectedPanelists = ids.map(String); + + // render locked chips + const pills = ids + .map((id) => { + const p = accounts.find((a) => a.id === id); + if (!p) return null; + const isGuest = p.user_roles === 5; + return `${p.last_name}, ${p.first_name}${ + isGuest ? " β€’ Guest" : "" + }`; + }) + .filter(Boolean) + .join(" "); - const manager = accounts.find((a) => a.user_roles === 1 && a.group_name === team); - const [p1, p2, p3] = panelists; - - const { error, data } = await supabase - .from("user_final_sched") - .insert([ - { - manager_id: manager.id, - adviser_id: adviser, - date, - time, - panelist1_id: p1 || null, - panelist2_id: p2 || null, - panelist3_id: p3 || null, - verdict: 1, - title: null, - }, - ]) - .select( - ` - *, - manager:manager_id ( group_name ) - ` - ); - - if (!error) { - setSchedules((prev) => [...prev, data[0]]); - MySwal.fire({ - icon: "success", - title: "βœ“ Schedule Created", - showConfirmButton: false, - timer: 1500, - }); - } else MySwal.fire("Error", "Failed to create schedule", "error"); - } + panelList.innerHTML = pills; + oralNote.style.display = "block"; + oralNote.innerHTML = + "Panelists are locked to match the team's Oral Defense."; }); - }; + }, + preConfirm: () => { + const teamSel = document.getElementById("teamSelect"); + const team = teamSel?.value; + const opt = teamSel?.options[teamSel.selectedIndex]; + const date = document.getElementById("scheduleDate").value; + const time = document.getElementById("scheduleTime").value; + const adviser = document.getElementById("adviserSelect").value; + + if (opt && opt.disabled) { + MySwal.showValidationMessage( + "This team already has a Final Defense schedule." + ); + return false; + } - // Update schedule with SweetAlert2 modal - const handleUpdate = (id) => { - setOpenDropdown(null); - const schedule = schedules.find(s => s.id === id); - if (!schedule) return; + if (!adviser || !team || !date || !time) { + MySwal.showValidationMessage( + "Please complete Adviser, Team, Date, and Time." + ); + return false; + } - let selectedPanelists = [ - schedule.panelist1_id, - schedule.panelist2_id, - schedule.panelist3_id - ].filter(Boolean); + if (hasTimeConflict(date, time)) { + const conflicts = getConflictingTimes(date, time); + MySwal.showValidationMessage( + `Time conflict detected with: ${conflicts + .map((c) => `${c.team} at ${c.time}`) + .join(", ")}.` + ); + return false; + } - const currentAdviser = accounts.find(a => a.id === schedule.adviser_id); - const currentTeam = schedule.manager?.group_name; + // must have panelists from oral + if ( + !Array.isArray(selectedPanelists) || + selectedPanelists.length === 0 + ) { + MySwal.showValidationMessage( + "This team has no Oral Defense panelists to copy." + ); + return false; + } + return { adviser, team, date, time, panelists: selectedPanelists }; + }, + }).then(async (res) => { + if (!res.isConfirmed) return; + const { adviser, team, date, time, panelists } = res.value; + + if (isTeamAlreadyScheduled(team)) { + MySwal.fire( + "Error", + "This team already has a Final Defense schedule.", + "error" + ); + return; + } + if (hasTimeConflict(date, time)) { + const conflicts = getConflictingTimes(date, time); + MySwal.fire( + "Error", + `Time conflict detected with: ${conflicts + .map((c) => `${c.team} at ${c.time}`) + .join(", ")}.`, + "error" + ); + return; + } + + const manager = accounts.find( + (a) => a.user_roles === 1 && a.group_name === team + ); + const [p1, p2, p3] = panelists; + + const { error, data } = await supabase + .from("user_final_sched") + .insert([ + { + manager_id: manager?.id ?? null, + adviser_id: adviser, + date, + time, + panelist1_id: p1 || null, + panelist2_id: p2 || null, + panelist3_id: p3 || null, + verdict: 1, + title: null, + }, + ]) + .select(`*, manager:manager_id ( group_name )`); + + if (error) { + MySwal.fire("Error", "Failed to create schedule", "error"); + } else { + setSchedules((prev) => [...prev, data[0]]); MySwal.fire({ - title: `
- Update Schedule
`, - html: ` - -
-
Current Selection
-
Adviser: ${currentAdviser ? `${currentAdviser.last_name}, ${currentAdviser.first_name}` : 'N/A'}
-
Team: ${currentTeam || 'N/A'}
-
-
- - -
-
- -
- ${selectedPanelists.length > 0 ? - selectedPanelists.map(pid => { - const person = advisers.find(a => a.id === pid); - return person ? - ` - ${person.last_name}, ${person.first_name} - - ` : ''; - }).join('') - : 'No panelist selected' - } -
-
-
- - -
-
- - - -
- `, - showCancelButton: true, - confirmButtonText: "Update", - cancelButtonText: "Cancel", - confirmButtonColor: "#3B0304", - cancelButtonColor: "#999", - width: "600px", - didOpen: () => { - const panelSelect = document.getElementById("panelSelect"); - const panelList = document.getElementById("panelList"); - const scheduleDate = document.getElementById("scheduleDate"); - const scheduleTime = document.getElementById("scheduleTime"); - const timeConflictWarning = document.getElementById("timeConflictWarning"); - - // Function to update panelist dropdown options - const updatePanelistDropdown = () => { - panelSelect.innerHTML = ''; - - advisers.forEach((a) => { - const isCurrentAdviser = a.id === schedule.adviser_id; - const isAlreadyPanelist = selectedPanelists.includes(a.id); - const isDisabled = isCurrentAdviser || isAlreadyPanelist; - - let disabledReason = ''; - if (isCurrentAdviser) disabledReason = ' (Current Adviser)'; - if (isAlreadyPanelist) disabledReason = ' (Already Selected)'; - - const option = document.createElement("option"); - option.value = a.id; - option.textContent = `${a.last_name}, ${a.first_name}${disabledReason}`; - - if (isDisabled) { - option.disabled = true; - option.className = 'disabled-option'; - } - - panelSelect.appendChild(option); - }); - }; - - // Check for time conflicts (excluding current schedule) - const checkTimeConflict = () => { - const date = scheduleDate.value; - const time = scheduleTime.value; - - if (date && time) { - if (hasTimeConflict(date, time, schedule.id)) { - const conflicts = getConflictingTimes(date, time, schedule.id); - const conflictText = conflicts.map(conflict => - `${conflict.team} at ${conflict.time}` - ).join(', '); - - timeConflictWarning.style.display = 'block'; - timeConflictWarning.innerHTML = `⚠️ Time conflict with: ${conflictText}. Please choose a different time with at least 1 hour gap.`; - } else { - timeConflictWarning.style.display = 'none'; - } - } else { - timeConflictWarning.style.display = 'none'; - } - }; - - // Event listeners for date and time changes - scheduleDate.addEventListener('change', checkTimeConflict); - scheduleTime.addEventListener('change', checkTimeConflict); - - // add panelist - panelSelect.addEventListener("change", () => { - const id = panelSelect.value; - const selectedOption = panelSelect.options[panelSelect.selectedIndex]; - - // Check if the selected option is disabled - if (selectedOption.disabled) { - MySwal.showValidationMessage("This adviser cannot be selected as a panelist."); - panelSelect.value = ""; - return; - } - - if (!selectedPanelists.includes(id)) { - if (selectedPanelists.length < 3) { - selectedPanelists.push(id); - const person = advisers.find((a) => a.id === id); - if (panelList.querySelector(".text-muted")) panelList.innerHTML = ""; - const tag = document.createElement("span"); - tag.className = "bg-gray-200 text-gray-800 rounded-full px-2 py-1 text-sm flex items-center gap-1"; - tag.innerHTML = `${person.last_name}, ${person.first_name} `; - panelList.appendChild(tag); - - // Update dropdown to disable the newly selected panelist - updatePanelistDropdown(); - } else { - MySwal.showValidationMessage("Maximum of 3 panelists."); - } - } - panelSelect.value = ""; - }); - - // remove panelist - panelList.addEventListener("click", (e) => { - if (e.target.classList.contains("remove-panelist-btn")) { - const idToRemove = e.target.dataset.id; - selectedPanelists = selectedPanelists.filter((pid) => pid !== idToRemove); - e.target.parentElement.remove(); - - // Update dropdown to re-enable the removed panelist - updatePanelistDropdown(); - - if (selectedPanelists.length === 0) - panelList.innerHTML = 'No panelist selected'; - } - }); - }, - preConfirm: () => { - const date = document.getElementById("scheduleDate").value; - const time = document.getElementById("scheduleTime").value; - - // Check for time conflicts (excluding current schedule) - if (hasTimeConflict(date, time, schedule.id)) { - const conflicts = getConflictingTimes(date, time, schedule.id); - const conflictText = conflicts.map(conflict => - `${conflict.team} at ${conflict.time}` - ).join(', '); - MySwal.showValidationMessage(`Time conflict detected with: ${conflictText}. Please choose a different time with at least 1 hour gap.`); - return false; - } - - if (!date || !time || selectedPanelists.length === 0) { - MySwal.showValidationMessage("Please fill all fields and select panelists."); - return false; - } - - return { date, time, panelists: selectedPanelists }; - }, - }).then(async (result) => { - if (result.isConfirmed) { - const { date, time, panelists } = result.value; - const [p1, p2, p3] = panelists; - - const { error } = await supabase - .from("user_final_sched") - .update({ - date, - time, - panelist1_id: p1 || null, - panelist2_id: p2 || null, - panelist3_id: p3 || null - }) - .eq("id", id); - - if (!error) { - setSchedules((prev) => - prev.map((s) => (s.id === id ? { - ...s, - date, - time, - panelist1_id: p1 || null, - panelist2_id: p2 || null, - panelist3_id: p3 || null - } : s)) - ); - MySwal.fire({ - icon: "success", - title: "βœ“ Schedule Updated", - showConfirmButton: false, - timer: 1500, - }); - } else { - MySwal.fire("Error", "Failed to update schedule", "error"); - } - } + icon: "success", + title: "βœ“ Schedule Created", + showConfirmButton: false, + timer: 1500, }); - }; - - // Export functionality with filter options - const handleExport = () => { - MySwal.fire({ - title: "Export Final Defense Data", - html: ` -
-

Select which schedules to export:

- -
- `, - icon: "question", - showCancelButton: true, - confirmButtonColor: "#3B0304", - cancelButtonColor: "#999", - confirmButtonText: "Export", - cancelButtonText: "Cancel", - preConfirm: () => { - const exportFilter = document.getElementById('exportFilter').value; - return { exportFilter }; - } - }).then((result) => { - if (result.isConfirmed) { - const { exportFilter } = result.value; - - // Filter data based on selected option - let filteredExportData; - - if (exportFilter === 'all') { - filteredExportData = filteredSchedules; - } else { - const verdictValue = parseInt(exportFilter); - filteredExportData = filteredSchedules.filter(sched => sched.verdict === verdictValue); - } - - // Prepare data for export - const exportData = filteredExportData.map((sched, index) => { - const panelists = [sched.panelist1_id, sched.panelist2_id, sched.panelist3_id] - .map((pid) => { - const account = accounts.find((a) => a.id === pid); - return account ? `${account.last_name}, ${account.first_name}` : ""; - }) - .filter(Boolean) - .join("; "); - - const adviser = accounts.find((a) => a.id === sched.adviser_id); - const adviserName = adviser ? `${adviser.last_name}, ${adviser.first_name}` : "N/A"; - - return { - no: index + 1, - team: sched.manager?.group_name || "-", - title: sched.title || "-", - date: sched.date, - time: sched.time, - adviser: adviserName, - panelists: panelists, - verdict: verdictMap[sched.verdict] || "PENDING" - }; - }); - - if (exportData.length === 0) { - const filterText = exportFilter === 'all' ? 'schedules' : - exportFilter === '1' ? 'PENDING schedules' : - exportFilter === '2' ? 'Re-Oral schedules' : - exportFilter === '3' ? 'Not-Accepted schedules' : 'Accepted schedules'; - - MySwal.fire({ - title: "No Data to Export", - text: `There are no ${filterText} to export.`, - icon: "warning", - confirmButtonColor: "#3B0304" - }); - return; - } + } + }); + }; + + // -------- UPDATE: panelists locked to Oral Defense (no edits) -------- + const handleUpdate = (id) => { + setOpenDropdown(null); + const schedule = schedules.find((s) => s.id === id); + if (!schedule) return; + + let lockedPanelists = [ + schedule.panelist1_id, + schedule.panelist2_id, + schedule.panelist3_id, + ].filter(Boolean); + + const currentAdviser = accounts.find((a) => a.id === schedule.adviser_id); + const currentTeam = schedule.manager?.group_name; + + MySwal.fire({ + title: `
Update Schedule
`, + html: ` + +
+
Current Selection
+
Adviser: ${ + currentAdviser + ? `${currentAdviser.last_name}, ${currentAdviser.first_name}` + : "N/A" + }
+
Team: ${currentTeam || "N/A"}
+
- // Export as PDF - exportFinalDefenseAsPDF(exportData); - - const filterText = exportFilter === 'all' ? 'data' : - exportFilter === '1' ? 'PENDING schedules' : - exportFilter === '2' ? 'Re-Oral schedules' : - exportFilter === '3' ? 'Not-Accepted schedules' : 'Accepted schedules'; - - MySwal.fire({ - title: "Export Successful!", - text: `Final defense ${filterText} has been downloaded as PDF.`, - icon: "success", - confirmButtonColor: "#3B0304", - timer: 2000, - showConfirmButton: false - }); +
+ +
+ ${ + lockedPanelists.length + ? lockedPanelists + .map((pid) => { + const p = accounts.find((a) => a.id === pid); + if (!p) return ""; + const isGuest = p.user_roles === 5; + return `${p.last_name}, ${ + p.first_name + }${isGuest ? " β€’ Guest" : ""}`; + }) + .join(" ") + : 'No panelists' } - }); - }; +
+
Panelist assignments can’t be changed in Final Defense.
+
- const handleDelete = async (id) => { - const confirm = await MySwal.fire({ - title: "Delete Schedule?", - text: "This action cannot be undone.", - icon: "warning", - showCancelButton: true, - confirmButtonColor: "#3B0304", - cancelButtonColor: "#999", - confirmButtonText: "Yes, delete it!", - }); +
+ + +
+
+ + + +
+ `, + showCancelButton: true, + confirmButtonText: "Update", + cancelButtonText: "Cancel", + confirmButtonColor: "#3B0304", + cancelButtonColor: "#999", + width: "600px", + didOpen: () => { + const scheduleDate = document.getElementById("scheduleDate"); + const scheduleTime = document.getElementById("scheduleTime"); + const timeConflictWarning = document.getElementById( + "timeConflictWarning" + ); - if (confirm.isConfirmed) { - const { error } = await supabase - .from("user_final_sched") - .delete() - .eq("id", id); - - if (!error) { - setSchedules((prev) => prev.filter((s) => s.id !== id)); - MySwal.fire("Deleted!", "Schedule has been deleted.", "success"); - } else { - MySwal.fire("Error", "Failed to delete schedule.", "error"); - } + if (scheduleDate) { + const today = new Date(); + today.setHours(0, 0, 0, 0); + scheduleDate.min = today.toISOString().slice(0, 10); + const open = () => + scheduleDate.showPicker && scheduleDate.showPicker(); + scheduleDate.addEventListener("click", open); + scheduleDate.addEventListener("focus", open); } - }; - - const handleCheckboxChange = (id, isChecked) => { - setSelectedSchedules(prev => { - if (isChecked) { - return [...prev, id]; - } else { - return prev.filter(scheduleId => scheduleId !== id); + if (scheduleTime) { + const openTP = () => + scheduleTime.showPicker && scheduleTime.showPicker(); + scheduleTime.addEventListener("click", openTP); + scheduleTime.addEventListener("focus", openTP); + const getNowHHMM = () => { + const n = new Date(); + return `${String(n.getHours()).padStart(2, "0")}:${String( + n.getMinutes() + ).padStart(2, "0")}`; + }; + const setMinIfToday = () => { + const d = scheduleDate.value; + if (!d) { + scheduleTime.removeAttribute("min"); + return; } - }); - }; - - const handleDeleteSelected = async () => { - if (selectedSchedules.length === 0) { - MySwal.fire("No schedules selected", "Please select one or more schedules to delete.", "warning"); - return; + const today = new Date(); + today.setHours(0, 0, 0, 0); + const picked = new Date(d); + picked.setHours(0, 0, 0, 0); + if (picked.getTime() === today.getTime()) { + const min = getNowHHMM(); + scheduleTime.min = min; + if (scheduleTime.value && scheduleTime.value < min) + scheduleTime.value = min; + } else scheduleTime.removeAttribute("min"); + }; + scheduleDate.addEventListener("change", setMinIfToday); + scheduleTime.addEventListener("input", () => { + const min = scheduleTime.getAttribute("min"); + if (min && scheduleTime.value && scheduleTime.value < min) + scheduleTime.value = min; + }); + setMinIfToday(); } - const confirm = await MySwal.fire({ - title: "Delete Selected Schedules?", - text: "This action cannot be undone.", - icon: "warning", - showCancelButton: true, - confirmButtonColor: "#3B0304", - cancelButtonColor: "#999", - confirmButtonText: "Yes, delete them!", - }); - - if (confirm.isConfirmed) { - const { error } = await supabase - .from("user_final_sched") - .delete() - .in("id", selectedSchedules); - - if (!error) { - setSchedules(prev => prev.filter(s => !selectedSchedules.includes(s.id))); - setSelectedSchedules([]); - setIsDeleteMode(false); - MySwal.fire("Deleted!", "Selected schedules have been deleted.", "success"); - } else { - MySwal.fire("Error", "Failed to delete selected schedules.", "error"); - } + const checkTimeConflict = () => { + const d = scheduleDate.value, + t = scheduleTime.value; + if (d && t) { + if (hasTimeConflict(d, t, schedule.id)) { + const conflicts = getConflictingTimes(d, t, schedule.id); + timeConflictWarning.style.display = "block"; + timeConflictWarning.innerHTML = `⚠️ Time conflict with: ${conflicts + .map((c) => `${c.team} at ${c.time}`) + .join( + ", " + )}. Please choose a different time with at least 1 hour gap.`; + } else timeConflictWarning.style.display = "none"; + } else timeConflictWarning.style.display = "none"; + }; + scheduleDate.addEventListener("change", checkTimeConflict); + scheduleTime.addEventListener("change", checkTimeConflict); + }, + preConfirm: () => { + const date = document.getElementById("scheduleDate").value; + const time = document.getElementById("scheduleTime").value; + + if (!date || !time) { + MySwal.showValidationMessage("Please set Date and Time."); + return false; } - }; - - // change verdict - const handleVerdictChange = async (id, newVerdict) => { - const { error } = await supabase - .from("user_final_sched") - .update({ verdict: newVerdict }) - .eq("id", id); - - if (!error) { - setSchedules((prev) => - prev.map((s) => (s.id === id ? { ...s, verdict: newVerdict } : s)) - ); - - // Show success message for Re-Oral - if (newVerdict === 2) { - MySwal.fire({ - icon: "success", - title: "Marked as Re-Oral", - text: "The schedule has been marked for re-defense and will remain in the list.", - showConfirmButton: false, - timer: 2000, - }); - } - } else { - MySwal.fire("Error", "Failed to update verdict.", "error"); + if (hasTimeConflict(date, time, schedule.id)) { + const conflicts = getConflictingTimes(date, time, schedule.id); + MySwal.showValidationMessage( + `Time conflict detected with: ${conflicts + .map((c) => `${c.team} at ${c.time}`) + .join(", ")}.` + ); + return false; } - }; - - // Get adviser name for display - const getAdviserName = (adviserId) => { - const adviser = accounts.find(a => a.id === adviserId); - return adviser ? `${adviser.last_name}, ${adviser.first_name}` : "N/A"; - }; - // Get select styling based on verdict - Only style the selected value - const getSelectStyle = (verdict) => { - switch (verdict) { - case 1: // Pending - Default styling - return 'text-gray-700 bg-white border-gray-300'; - case 2: // Re-Defense - Red - return 'text-white bg-[#3B0304] border-[#3B0304] font-semibold'; - case 3: // Not-Accepted - Gray - return 'text-white bg-gray-600 border-gray-600 font-semibold'; - case 4: // Accepted - Green - return 'text-white bg-[#809D3C] border-[#809D3C] font-semibold'; - default: // Default - return 'text-gray-700 bg-white border-gray-300'; - } - }; + // keep the same panelists (locked) + return { date, time, panelists: lockedPanelists }; + }, + }).then(async (res) => { + if (!res.isConfirmed) return; + const { date, time, panelists } = res.value; + const [p1, p2, p3] = panelists; + + const { error } = await supabase + .from("user_final_sched") + .update({ + date, + time, + panelist1_id: p1 || null, + panelist2_id: p2 || null, + panelist3_id: p3 || null, + }) + .eq("id", id); + + if (error) { + MySwal.fire("Error", "Failed to update schedule", "error"); + } else { + setSchedules((prev) => + prev.map((s) => + s.id === id + ? { + ...s, + date, + time, + panelist1_id: p1 || null, + panelist2_id: p2 || null, + panelist3_id: p3 || null, + } + : s + ) + ); + MySwal.fire({ + icon: "success", + title: "βœ“ Schedule Updated", + showConfirmButton: false, + timer: 1500, + }); + } + }); + }; + + const handleExport = () => { + MySwal.fire({ + title: "Export Final Defense Data", + html: ` +
+

Select which schedules to export:

+ +
+ `, + icon: "question", + showCancelButton: true, + confirmButtonColor: "#3B0304", + cancelButtonColor: "#999", + confirmButtonText: "Export", + cancelButtonText: "Cancel", + preConfirm: () => ({ + exportFilter: document.getElementById("exportFilter").value, + }), + }).then((result) => { + if (!result.isConfirmed) return; + const { exportFilter } = result.value; + + let filteredExportData; + if (exportFilter === "all") filteredExportData = filteredSchedules; + else { + const verdictValue = parseInt(exportFilter, 10); + filteredExportData = filteredSchedules.filter( + (sched) => sched.verdict === verdictValue + ); + } + + const exportData = filteredExportData.map((sched, index) => { + const panelists = [ + sched.panelist1_id, + sched.panelist2_id, + sched.panelist3_id, + ] + .map((pid) => { + const account = accounts.find((a) => a.id === pid); + return account ? `${account.last_name}, ${account.first_name}` : ""; + }) + .filter(Boolean) + .join("; "); + + const adviser = accounts.find((a) => a.id === sched.adviser_id); + const adviserName = adviser + ? `${adviser.last_name}, ${adviser.first_name}` + : "N/A"; + + return { + no: index + 1, + team: sched.manager?.group_name || "-", + title: sched.title || "-", + date: sched.date, + time: sched.time, + adviser: adviserName, + panelists, + verdict: verdictMap[sched.verdict] || "PENDING", + }; + }); + + if (exportData.length === 0) { + const filterText = + exportFilter === "all" + ? "schedules" + : exportFilter === "1" + ? "PENDING schedules" + : exportFilter === "3" + ? "Re-Oral schedules" + : exportFilter === "4" + ? "Not-Accepted schedules" + : "Accepted schedules"; - return ( -
-

- Final Defense Β» Scheduled Teams -

-
- -
-
- - + +
+
+
+ setSearch(e.target.value)} + /> + +
+
+ {isDeleteMode && ( + + )} + +
+
+
+ +
+
- + + NOAdviser IDPasswordFirst NameLast NameMiddle InitialAction + NO + + Adviser ID + + Password + + First Name + + Last Name + + Middle Initial + + Action +
- handleToggleSelect(row.id)} /> + handleToggleSelect(row.id)} + /> {importedData.indexOf(row) + 1}{row.user_id}{row.password}{row.first_name}{row.last_name}{row.middle_name} + {importedData.indexOf(row) + 1} + + {row.user_id} + + {row.password} + + {row.first_name} + + {row.last_name} + + {row.middle_name} + {/* Action Button only visible when not in selection mode */} {!isSelectionMode && ( - )} - + {/* Detached Dropdown Menu */} {openDropdown === index && !isSelectionMode && ( -
    +
    • -
    • -
    • @@ -1106,7 +1365,12 @@ try { })} {filteredData.length === 0 && (
No advisers match your search. + No advisers match your search. +
+ + + {isDeleteMode && ( + + )} + + + + + + + + + + + + + {filteredSchedules.map((s, index) => ( + + {isDeleteMode && ( + + )} + + + + + + + + + + + ))} + {filteredSchedules.length === 0 && ( + + + + )} + +
+ + setSelectedSchedules( + e.target.checked + ? filteredSchedules.map((s) => s.id) + : [] + ) + } + checked={ + selectedSchedules.length === filteredSchedules.length && + filteredSchedules.length > 0 + } + /> + + NO + + TEAM + + TITLE + + DATE + + TIME + + ADVISER + + PANELISTS + + VERDICT + + ACTION +
+ + handleCheckboxChange(s.id, e.target.checked) + } + /> + + {index + 1} + + {s.manager?.group_name || "-"} + + {s.title || "-"} + + {s.date} + + {s.time} + + {getAdviserName(s.adviser_id)} + + {[s.panelist1_id, s.panelist2_id, s.panelist3_id] + .map((pid) => { + const account = accounts.find((a) => a.id === pid); + return account + ? `${account.last_name}, ${account.first_name}` + : ""; + }) + .filter(Boolean) + .join(", ")} + + + + + + {openDropdown === index && ( +
- Export - -
-
-
- setSearch(e.target.value)} - /> - -
-
- {isDeleteMode && ( - - )} +
+ +
-
- - -
- - - - {isDeleteMode && ( - - )} - - - - - - - - - - - - - {filteredSchedules.map((s, index) => ( - - {isDeleteMode && ( - - )} - - - - - - - - - - - ))} - {filteredSchedules.length === 0 && ( - - - - )} - -
- { - if (e.target.checked) { - setSelectedSchedules(filteredSchedules.map(s => s.id)); - } else { - setSelectedSchedules([]); - } - }} - checked={selectedSchedules.length === filteredSchedules.length && filteredSchedules.length > 0} - /> - - NO - - TEAM - - TITLE - - DATE - - TIME - - ADVISER - - PANELISTS - - VERDICT - - ACTION -
- handleCheckboxChange(s.id, e.target.checked)} - /> - - {index + 1} - - {s.manager?.group_name || "-"} - - {s.title || "-"} - - {s.date} - - {s.time} - - {getAdviserName(s.adviser_id)} - - {[s.panelist1_id, s.panelist2_id, s.panelist3_id] - .map((pid) => { - const account = accounts.find((a) => a.id === pid); - return account ? `${account.last_name}, ${account.first_name}` : ""; - }) - .filter(Boolean) - .join(", ")} - - - - - - {/* Dropdown positioned close to the button */} - {openDropdown === index && ( -
-
- - -
-
- )} -
- No schedules found. -
-
- - ); + )} +
+ No schedules found. +
+
+
+ ); }; -export default FinalDefense; \ No newline at end of file +export default FinalDefense; diff --git a/src/components/Instructor/InstructorDashboard.jsx b/src/components/Instructor/InstructorDashboard.jsx index b143977..1bb427f 100644 --- a/src/components/Instructor/InstructorDashboard.jsx +++ b/src/components/Instructor/InstructorDashboard.jsx @@ -30,12 +30,7 @@ import timeGridPlugin from "@fullcalendar/timegrid"; import interactionPlugin from "@fullcalendar/interaction"; // ADDED CHARTJS IMPORTS -import { - Chart as ChartJS, - Tooltip, - Legend, - ArcElement -} from "chart.js"; +import { Chart as ChartJS, Tooltip, Legend, ArcElement } from "chart.js"; import { Doughnut } from "react-chartjs-2"; ChartJS.register(Tooltip, Legend, ArcElement); @@ -48,18 +43,21 @@ const TASKSPHERE_PURPLE = "#805ad5"; // Enhanced Team Progress Component with detailed stats const AdviserTeamProgress = ({ teamsProgress }) => { const calculateCompletionPercentage = (counts) => { - const totalTasks = Object.values(counts).reduce((sum, count) => sum + count, 0); + const totalTasks = Object.values(counts).reduce( + (sum, count) => sum + count, + 0 + ); const completedTasks = counts["Completed"] || 0; return totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0; }; const getStatusColor = (status) => { const colors = { - "Completed": "#4BC0C0", + Completed: "#4BC0C0", "To Do": "#FABC3F", "In Progress": "#809D3C", "To Review": "#578FCA", - "Missed": "#FF6384" + Missed: "#FF6384", }; return colors[status] || "#999999"; }; @@ -69,20 +67,27 @@ const AdviserTeamProgress = ({ teamsProgress }) => { {teamsProgress.length > 0 ? (
{teamsProgress.map((team, index) => { - const completionPercentage = calculateCompletionPercentage(team.counts); - const totalTasks = Object.values(team.counts).reduce((sum, count) => sum + count, 0); - + const completionPercentage = calculateCompletionPercentage( + team.counts + ); + const totalTasks = Object.values(team.counts).reduce( + (sum, count) => sum + count, + 0 + ); + const chartData = { labels: ["Completed", "Remaining"], datasets: [ { label: "Tasks", - data: [team.counts["Completed"] || 0, - (totalTasks - (team.counts["Completed"] || 0))], + data: [ + team.counts["Completed"] || 0, + totalTasks - (team.counts["Completed"] || 0), + ], backgroundColor: ["#AA60C8", "#e9ecef"], borderColor: "#ffffff", borderWidth: 2, - cutout: '70%', + cutout: "70%", }, ], }; @@ -92,11 +97,11 @@ const AdviserTeamProgress = ({ teamsProgress }) => { maintainAspectRatio: false, plugins: { legend: { - display: false + display: false, }, tooltip: { - enabled: false - } + enabled: false, + }, }, }; @@ -107,10 +112,19 @@ const AdviserTeamProgress = ({ teamsProgress }) => { Total: {totalTasks} tasks
-
+
- {completionPercentage}% + + {completionPercentage}% + Completed
@@ -118,19 +132,27 @@ const AdviserTeamProgress = ({ teamsProgress }) => {
Completed: - {team.counts["Completed"] || 0} + + {team.counts["Completed"] || 0} +
In Progress: - {team.counts["In Progress"] || 0} + + {team.counts["In Progress"] || 0} +
To Do: - {team.counts["To Do"] || 0} + + {team.counts["To Do"] || 0} +
To Review: - {team.counts["To Review"] || 0} + + {team.counts["To Review"] || 0} +
{team.task_breakdown && (
@@ -149,7 +171,9 @@ const AdviserTeamProgress = ({ teamsProgress }) => { ) : (
-

No teams with tasks to display progress.

+

+ No teams with tasks to display progress. +

)}
@@ -160,10 +184,18 @@ const AdviserTeamProgress = ({ teamsProgress }) => { const AdviserGroupSection = ({ adviserGroup, teams, adviserName }) => { const calculateGroupStats = (teams) => { const totalTeams = teams.length; - const totalTasks = teams.reduce((sum, team) => sum + Object.values(team.counts).reduce((a, b) => a + b, 0), 0); - const completedTasks = teams.reduce((sum, team) => sum + (team.counts["Completed"] || 0), 0); - const completionRate = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0; - + const totalTasks = teams.reduce( + (sum, team) => + sum + Object.values(team.counts).reduce((a, b) => a + b, 0), + 0 + ); + const completedTasks = teams.reduce( + (sum, team) => sum + (team.counts["Completed"] || 0), + 0 + ); + const completionRate = + totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0; + return { totalTeams, totalTasks, completedTasks, completionRate }; }; @@ -177,7 +209,8 @@ const AdviserGroupSection = ({ adviserGroup, teams, adviserName }) => {

{adviserName || `Adviser Group: ${adviserGroup}`}

- {groupStats.totalTeams} team(s) β€’ {groupStats.totalTasks} total tasks β€’ {groupStats.completionRate}% overall completion + {groupStats.totalTeams} team(s) β€’ {groupStats.totalTasks} total + tasks β€’ {groupStats.completionRate}% overall completion
@@ -268,7 +301,9 @@ const InstructorDashboard = () => { const handlePageChange = (page) => { setActivePage(page); - navigate(`/Instructor/${page.replace(/\s+/g, "")}`, { state: { activePage: page } }); + navigate(`/Instructor/${page.replace(/\s+/g, "")}`, { + state: { activePage: page }, + }); }; useEffect(() => { @@ -281,7 +316,6 @@ const InstructorDashboard = () => { } }, [isSoloMode, subPage]); - // ==== NEW STATES FOR TASKS ==== const [upcomingTasks, setUpcomingTasks] = useState([]); const [recentTasks, setRecentTasks] = useState([]); @@ -289,14 +323,14 @@ const InstructorDashboard = () => { const [teamsProgress, setTeamsProgress] = useState([]); const [adviserGroups, setAdviserGroups] = useState([]); - const formatTime = (timeString) => { + const formatTime = (timeString) => { if (!timeString) return "N/A"; const [hour, minute] = timeString.split(":"); let h = parseInt(hour, 10); const ampm = h >= 12 ? "PM" : "AM"; h = h % 12 || 12; // Convert to 12-hour format return `${h}:${minute} ${ampm}`; - }; + }; // ==== FETCH ALL PROJECT MANAGERS PROGRESS AND GROUP BY ADVISER ==== useEffect(() => { @@ -308,15 +342,23 @@ const InstructorDashboard = () => { // Fetch all adviser tasks (oral and final defense) const [oralTasksResult, finalTasksResult] = await Promise.all([ supabase.from("adviser_oral_def").select("*"), - supabase.from("adviser_final_def").select("*") + supabase.from("adviser_final_def").select("*"), ]); - if (oralTasksResult.error) console.error("Error fetching oral tasks:", oralTasksResult.error); - if (finalTasksResult.error) console.error("Error fetching final tasks:", finalTasksResult.error); + if (oralTasksResult.error) + console.error("Error fetching oral tasks:", oralTasksResult.error); + if (finalTasksResult.error) + console.error("Error fetching final tasks:", finalTasksResult.error); const allAdviserTasks = [ - ...(oralTasksResult.data || []).map((t) => ({ ...t, type: "Oral Defense" })), - ...(finalTasksResult.data || []).map((t) => ({ ...t, type: "Final Defense" })), + ...(oralTasksResult.data || []).map((t) => ({ + ...t, + type: "Oral Defense", + })), + ...(finalTasksResult.data || []).map((t) => ({ + ...t, + type: "Final Defense", + })), ]; console.log("πŸ“Š All Adviser Tasks Found:", allAdviserTasks.length); @@ -334,36 +376,53 @@ const InstructorDashboard = () => { .sort((a, b) => new Date(a.due_date) - new Date(b.due_date)) .slice(0, 5); - const upcomingWithNames = await Promise.all(upcoming.map(async (task) => { - if (!task.manager_id) return { ...task, managerName: "N/A" }; - const { data: manager } = await supabase.from("user_credentials").select("first_name, last_name").eq("id", task.manager_id).single(); - return { ...task, managerName: manager ? `${manager.first_name} ${manager.last_name}` : "Unknown Manager" }; - })); + const upcomingWithNames = await Promise.all( + upcoming.map(async (task) => { + if (!task.manager_id) return { ...task, managerName: "N/A" }; + const { data: manager } = await supabase + .from("user_credentials") + .select("first_name, last_name") + .eq("id", task.manager_id) + .single(); + return { + ...task, + managerName: manager + ? `${manager.first_name} ${manager.last_name}` + : "Unknown Manager", + }; + }) + ); setUpcomingTasks(upcomingWithNames); // 2. Recent Tasks const recent = [...allAdviserTasks] - .sort((a, b) => new Date(b.date_created || b.created_at) - new Date(a.date_created || a.created_at)) - .slice(0, 5); + .sort( + (a, b) => + new Date(b.date_created || b.created_at) - + new Date(a.date_created || a.created_at) + ) + .slice(0, 4); const recentWithNames = await Promise.all( - recent.map(async (task) => { - if (!task.manager_id) return { ...task, managerName: "N/A" }; - const { data: manager } = await supabase - .from("user_credentials") - .select("first_name, last_name") - .eq("id", task.manager_id) - .single(); - return { - ...task, - managerName: manager ? `${manager.first_name} ${manager.last_name}` : "Unknown Manager" - }; - }) -); -setRecentTasks(recentWithNames); + recent.map(async (task) => { + if (!task.manager_id) return { ...task, managerName: "N/A" }; + const { data: manager } = await supabase + .from("user_credentials") + .select("first_name, last_name") + .eq("id", task.manager_id) + .single(); + return { + ...task, + managerName: manager + ? `${manager.first_name} ${manager.last_name}` + : "Unknown Manager", + }; + }) + ); + setRecentTasks(recentWithNames); // 3. Teams' Progress - Fetch ALL project managers with ALL their tasks console.log("πŸ‘₯ Fetching all project managers..."); - + const { data: teams, error: teamsError } = await supabase .from("user_credentials") .select("id, group_name, first_name, last_name, adviser_group") @@ -383,21 +442,36 @@ setRecentTasks(recentWithNames); setAdviserGroups([]); } else { setDebugInfo(`Found ${teams.length} project manager(s)`); - + // Fetch comprehensive task progress for each project manager const teamsProgressData = await Promise.all( teams.map(async (team) => { try { - console.log(`πŸ” Checking tasks for team: ${team.group_name} (ID: ${team.id})`); - + console.log( + `πŸ” Checking tasks for team: ${team.group_name} (ID: ${team.id})` + ); + // Fetch ALL task types for this project manager - const [managerTasksResult, adviserOralTasksResult, adviserFinalTasksResult] = await Promise.all([ + const [ + managerTasksResult, + adviserOralTasksResult, + adviserFinalTasksResult, + ] = await Promise.all([ // Manager tasks from manager_title_task - supabase.from("manager_title_task").select("status, task_name, due_date").eq("manager_id", team.id), + supabase + .from("manager_title_task") + .select("status, task_name, due_date") + .eq("manager_id", team.id), // Adviser oral defense tasks for this manager (from any adviser) - supabase.from("adviser_oral_def").select("status, task, due_date").eq("manager_id", team.id), + supabase + .from("adviser_oral_def") + .select("status, task, due_date") + .eq("manager_id", team.id), // Adviser final defense tasks for this manager (from any adviser) - supabase.from("adviser_final_def").select("status, task, due_date").eq("manager_id", team.id) + supabase + .from("adviser_final_def") + .select("status, task, due_date") + .eq("manager_id", team.id), ]); const managerTasks = managerTasksResult.data || []; @@ -406,34 +480,42 @@ setRecentTasks(recentWithNames); // Combine ALL tasks for this team const allTeamTasks = [ - ...managerTasks.map(t => ({ ...t, source: 'manager' })), - ...adviserOralTasks.map(t => ({ ...t, source: 'adviser_oral' })), - ...adviserFinalTasks.map(t => ({ ...t, source: 'adviser_final' })) + ...managerTasks.map((t) => ({ ...t, source: "manager" })), + ...adviserOralTasks.map((t) => ({ + ...t, + source: "adviser_oral", + })), + ...adviserFinalTasks.map((t) => ({ + ...t, + source: "adviser_final", + })), ]; console.log(`πŸ“‹ Team ${team.group_name} tasks:`, { managerTasks: managerTasks.length, adviserOralTasks: adviserOralTasks.length, adviserFinalTasks: adviserFinalTasks.length, - total: allTeamTasks.length + total: allTeamTasks.length, }); // Only include teams that actually have tasks if (allTeamTasks.length === 0) { - console.log(`ℹ️ Team ${team.group_name} has no tasks, skipping`); + console.log( + `ℹ️ Team ${team.group_name} has no tasks, skipping` + ); return null; } // Count tasks by status - COMBINE ALL TASK TYPES - const counts = { - "To Do": 0, - "In Progress": 0, - "To Review": 0, - "Completed": 0, - "Missed": 0 + const counts = { + "To Do": 0, + "In Progress": 0, + "To Review": 0, + Completed: 0, + Missed: 0, }; - - allTeamTasks.forEach(task => { + + allTeamTasks.forEach((task) => { if (counts[task.status] !== undefined) { counts[task.status]++; } else { @@ -452,73 +534,91 @@ setRecentTasks(recentWithNames); task_breakdown: { manager_tasks: managerTasks.length, adviser_oral_tasks: adviserOralTasks.length, - adviser_final_tasks: adviserFinalTasks.length - } + adviser_final_tasks: adviserFinalTasks.length, + }, }; - console.log(`βœ… Team ${team.group_name} progress:`, teamProgress); + console.log( + `βœ… Team ${team.group_name} progress:`, + teamProgress + ); return teamProgress; - } catch (error) { - console.error(`❌ Error processing team ${team.group_name}:`, error); + console.error( + `❌ Error processing team ${team.group_name}:`, + error + ); return null; } }) ); - + // Filter out null values (teams with no tasks or errors) const filteredTeamsProgress = teamsProgressData.filter(Boolean); console.log("🎯 Final teams progress data:", filteredTeamsProgress); setTeamsProgress(filteredTeamsProgress); - + // Group teams by adviser_group - const groupedByAdviser = filteredTeamsProgress.reduce((groups, team) => { - const adviserGroup = team.adviser_group || 'Ungrouped'; - if (!groups[adviserGroup]) { - groups[adviserGroup] = []; - } - groups[adviserGroup].push(team); - return groups; - }, {}); + const groupedByAdviser = filteredTeamsProgress.reduce( + (groups, team) => { + const adviserGroup = team.adviser_group || "Ungrouped"; + if (!groups[adviserGroup]) { + groups[adviserGroup] = []; + } + groups[adviserGroup].push(team); + return groups; + }, + {} + ); console.log("πŸ‘¨β€πŸ« Teams grouped by adviser:", groupedByAdviser); // Get adviser names for each group (user_roles = 3 for advisers) const adviserGroupsWithNames = await Promise.all( - Object.entries(groupedByAdviser).map(async ([adviserGroup, teams]) => { - // Try to get adviser name from user_credentials - let adviserName = `Adviser Group: ${adviserGroup}`; - - if (adviserGroup && adviserGroup !== 'Ungrouped') { - const { data: adviser } = await supabase - .from("user_credentials") - .select("first_name, last_name") - .eq("adviser_group", adviserGroup) - .eq("user_roles", 3) // FIXED: user_roles = 3 for advisers - .single(); - - if (adviser) { - adviserName = `${adviser.first_name} ${adviser.last_name}`; - console.log(`βœ… Found adviser: ${adviserName} for group: ${adviserGroup}`); - } else { - console.log(`❌ No adviser found for group: ${adviserGroup} with user_roles = 3`); + Object.entries(groupedByAdviser).map( + async ([adviserGroup, teams]) => { + // Try to get adviser name from user_credentials + let adviserName = `Adviser Group: ${adviserGroup}`; + + if (adviserGroup && adviserGroup !== "Ungrouped") { + const { data: adviser } = await supabase + .from("user_credentials") + .select("first_name, last_name") + .eq("adviser_group", adviserGroup) + .eq("user_roles", 3) // FIXED: user_roles = 3 for advisers + .single(); + + if (adviser) { + adviserName = `${adviser.first_name} ${adviser.last_name}`; + console.log( + `βœ… Found adviser: ${adviserName} for group: ${adviserGroup}` + ); + } else { + console.log( + `❌ No adviser found for group: ${adviserGroup} with user_roles = 3` + ); + } } - } - return { - adviserGroup, - adviserName, - teams - }; - }) + return { + adviserGroup, + adviserName, + teams, + }; + } + ) ); setAdviserGroups(adviserGroupsWithNames); - + if (filteredTeamsProgress.length === 0) { - setDebugInfo(prev => prev + " - But no teams have tasks yet"); + setDebugInfo((prev) => prev + " - But no teams have tasks yet"); } else { - setDebugInfo(prev => prev + ` - ${filteredTeamsProgress.length} team(s) across ${adviserGroupsWithNames.length} adviser group(s)`); + setDebugInfo( + (prev) => + prev + + ` - ${filteredTeamsProgress.length} team(s) across ${adviserGroupsWithNames.length} adviser group(s)` + ); } } @@ -540,7 +640,6 @@ setRecentTasks(recentWithNames); })); setCalendarEvents(events); - } catch (error) { console.error("❌ Error loading instructor dashboard data:", error); setDebugInfo(`Error: ${error.message}`); @@ -548,14 +647,12 @@ setRecentTasks(recentWithNames); setIsLoading(false); } }; - if (!isSoloMode) { - ; fetchInstructorData() + fetchInstructorData(); } }, [isSoloMode]); - // ==== MAIN CONTENT RENDER ==== const renderContent = () => { switch (activePage) { @@ -608,7 +705,9 @@ setRecentTasks(recentWithNames);

INSTRUCTOR UPCOMING ACTIVITY

{upcomingTasks.length === 0 ? ( -

No upcoming tasks.

+

+ No upcoming tasks. +

) : ( upcomingTasks.map((t, i) => (
@@ -617,11 +716,25 @@ setRecentTasks(recentWithNames); {t.managerName}
-
{t.task}
-

{new Date(t.due_date).toLocaleDateString()}

-

{formatTime(t.time) || "No Time"}

-

{t.type}

- +
+ {t.task} +
+

+ {" "} + {new Date(t.due_date).toLocaleDateString()} +

+

+ {" "} + {formatTime(t.time) || "No Time"} +

+

+ {t.type} +

+ {t.status}
@@ -636,10 +749,11 @@ setRecentTasks(recentWithNames);

TEAMS PROGRESS BY ADVISER

- {teamsProgress.length} team(s) across {adviserGroups.length} adviser group(s) + {teamsProgress.length} team(s) across{" "} + {adviserGroups.length} adviser group(s)
- + {adviserGroups.length > 0 ? (
{adviserGroups.map((group, index) => ( @@ -654,7 +768,9 @@ setRecentTasks(recentWithNames); ) : (
-

No teams with tasks to display progress.

+

+ No teams with tasks to display progress. +

)}
@@ -663,7 +779,9 @@ setRecentTasks(recentWithNames);

RECENT ACTIVITY CREATED

{recentTasks.length === 0 ? ( -

No recent activities.

+

+ No recent activities. +

) : (
@@ -684,11 +802,23 @@ setRecentTasks(recentWithNames); - - + + @@ -707,12 +837,16 @@ setRecentTasks(recentWithNames);

INSTRUCTOR CALENDAR

+
@@ -1080,7 +1216,9 @@ setRecentTasks(recentWithNames); }} id="main-content-wrapper" > -
{renderContent()}
+
+ {renderContent()} +
@@ -1088,4 +1226,4 @@ setRecentTasks(recentWithNames); ); }; -export default InstructorDashboard; \ No newline at end of file +export default InstructorDashboard; diff --git a/src/components/Instructor/ManuScript.jsx b/src/components/Instructor/ManuScript.jsx index d63abff..bc10ebf 100644 --- a/src/components/Instructor/ManuScript.jsx +++ b/src/components/Instructor/ManuScript.jsx @@ -1,95 +1,176 @@ import React, { useState, useEffect, useRef } from "react"; import Swal from "sweetalert2"; import withReactContent from "sweetalert2-react-content"; -import { FaFileAlt, FaEllipsisV, FaSearch, FaTrash, FaEdit } from "react-icons/fa"; +import { + FaFileAlt, + FaEllipsisV, + FaSearch, + FaTrash, + FaEdit, +} from "react-icons/fa"; import { supabase } from "../../supabaseClient"; - + +const FILES_BUCKET = "manuscripts"; // <- your bucket name + +const getFileName = (urlOrPath) => { + if (!urlOrPath) return ""; + const noQuery = urlOrPath.split("?")[0]; + return decodeURIComponent(noQuery.split("/").pop() || ""); +}; + +const isHttpUrl = (s) => /^https?:\/\//i.test(s); + +/** Build a downloadable URL from file_url: + * - If it's already an http(s) URL, use it. + * - Else, treat it as a Storage path inside FILES_BUCKET (no extra prefix). + */ +const getDownloadHref = (file_url) => { + if (!file_url) return null; + if (isHttpUrl(file_url)) return file_url; + + // ensure no leading slash to avoid // in key + const key = file_url.replace(/^\/+/, ""); + const { data } = supabase.storage.from(FILES_BUCKET).getPublicUrl(key); + return data?.publicUrl || null; +}; + +// If bucket is PRIVATE, use a signed URL instead: +// const getSignedDownloadHref = async (file_url) => { +// if (!file_url) return null; +// if (isHttpUrl(file_url)) return file_url; +// const key = file_url.replace(/^\/+/, ""); +// const { data } = await supabase.storage.from(FILES_BUCKET).createSignedUrl(key, 3600); +// return data?.signedUrl || null; +// }; + const MySwal = withReactContent(Swal); - + +; + // --- Component Start --- - + const ManuScript = () => { const [accounts, setAccounts] = useState([]); const [schedules, setSchedules] = useState([]); // openDropdown stores the calculated position and ID of the active menu - const [openDropdown, setOpenDropdown] = useState(null); + const [openDropdown, setOpenDropdown] = useState(null); const [search, setSearch] = useState(""); const [advisers, setAdvisers] = useState([]); - + // STATE for row selection/deletion - const [selectedSchedules, setSelectedSchedules] = useState([]); - const [isSelectionMode, setIsSelectionMode] = useState(false); - + const [selectedSchedules, setSelectedSchedules] = useState([]); + const [isSelectionMode, setIsSelectionMode] = useState(false); + // Ref for the outer table container (must be position: relative) - const tableWrapperRef = useRef(null); + const tableWrapperRef = useRef(null); // Ref to store button references for coordinate calculation - const dropdownButtonRefs = useRef({}); - - // Verdict mapping + const dropdownButtonRefs = useRef({}); + + // Verdict mapping const verdictMap = { 1: "Pending", 2: "Re-Def", 3: "Completed", }; - + // --- Dropdown/Utility Functions --- - + const handleToggleDropdown = (e, index, schedId) => { - e.stopPropagation(); - - if (openDropdown && openDropdown.index === index) { - setOpenDropdown(null); - } else { - // Use a timeout to ensure React has flushed any layout changes before measuring - setTimeout(() => { - const buttonRef = dropdownButtonRefs.current[index]; - const wrapperRef = tableWrapperRef.current; - - if (!buttonRef || !wrapperRef) return; - - // Get the coordinates of the button and the wrapper relative to the viewport - const buttonRect = buttonRef.getBoundingClientRect(); - const wrapperRect = wrapperRef.getBoundingClientRect(); - - // Calculate position relative to the wrapper's current scroll/viewport offset. - const top = buttonRect.top - wrapperRect.top + buttonRect.height + 5; - - // Align the dropdown's right edge with the button's right edge. - const dropdownWidth = 160; - const left = buttonRect.right - wrapperRect.left - dropdownWidth; - - setOpenDropdown({ - index: index, - schedId: schedId, - top: top, - left: left, - }); - }, 0); - } + e.stopPropagation(); + + if (openDropdown && openDropdown.index === index) { + setOpenDropdown(null); + } else { + // Use a timeout to ensure React has flushed any layout changes before measuring + setTimeout(() => { + const buttonRef = dropdownButtonRefs.current[index]; + const wrapperRef = tableWrapperRef.current; + + if (!buttonRef || !wrapperRef) return; + + // Get the coordinates of the button and the wrapper relative to the viewport + const buttonRect = buttonRef.getBoundingClientRect(); + const wrapperRect = wrapperRef.getBoundingClientRect(); + + // Calculate position relative to the wrapper's current scroll/viewport offset. + const top = buttonRect.top - wrapperRect.top + buttonRect.height + 5; + + // Align the dropdown's right edge with the button's right edge. + const dropdownWidth = 160; + const left = buttonRect.right - wrapperRect.left - dropdownWidth; + + setOpenDropdown({ + index: index, + schedId: schedId, + top: top, + left: left, + }); + }, 0); + } }; - + // Click handler to close the detached menu when clicking anywhere else useEffect(() => { const handleClickOutside = (event) => { if (openDropdown) { const buttonRef = dropdownButtonRefs.current[openDropdown.index]; - const dropdownElement = document.querySelector('.dropdown-menu-detached'); - + const dropdownElement = document.querySelector( + ".dropdown-menu-detached" + ); + if ( - (buttonRef && !buttonRef.contains(event.target)) && - (dropdownElement && !dropdownElement.contains(event.target)) + buttonRef && + !buttonRef.contains(event.target) && + dropdownElement && + !dropdownElement.contains(event.target) ) { setOpenDropdown(null); } } }; - + document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, [openDropdown]); - + // --- Data Fetching Hooks --- useEffect(() => { const fetchAdvisers = async () => { @@ -98,37 +179,36 @@ const ManuScript = () => { .select("*") .eq("user_roles", 3) .not("adviser_group", "is", null); - + if (!error) { setAdvisers(data); } }; fetchAdvisers(); - + const fetchData = async () => { - const { data: accData } = await supabase - .from("user_credentials") - .select("*"); - if (accData) setAccounts(accData); - - const { data: schedData } = await supabase - .from("user_manuscript_sched") - .select("*"); - if (schedData) setSchedules(schedData); + const { data: accData } = await supabase + .from("user_credentials") + .select("*"); + if (accData) setAccounts(accData); + + const { data: schedData } = await supabase + .from("user_manuscript_sched") + .select("*"); + if (schedData) setSchedules(schedData); }; fetchData(); }, []); - - + // --- Selection Functions --- const handleToggleSelect = (id) => { - setSelectedSchedules((prev) => - prev.includes(id) - ? prev.filter((schedId) => schedId !== id) + setSelectedSchedules((prev) => + prev.includes(id) + ? prev.filter((schedId) => schedId !== id) : [...prev, id] ); }; - + const handleSelectAll = (e) => { if (e.target.checked) { const allIds = filteredSchedules.map((sched) => sched.id); @@ -137,15 +217,15 @@ const ManuScript = () => { setSelectedSchedules([]); } }; - + const handleCancelSelection = () => { setIsSelectionMode(false); - setSelectedSchedules([]); + setSelectedSchedules([]); }; - + // --- CRUD/Action Functions --- - - const handleCreateSchedule = () => { + + const handleCreateSchedule = () => { MySwal.fire({ title: `
Create Manuscript Schedule
`, @@ -188,23 +268,23 @@ const ManuScript = () => { didOpen: () => { const adviserSelect = document.getElementById("adviserSelect"); const teamSelect = document.getElementById("teamSelect"); - + adviserSelect.addEventListener("change", async (e) => { const adviserId = e.target.value; - + const { data: adviser } = await supabase .from("user_credentials") .select("adviser_group") .eq("id", adviserId) .single(); - + if (adviser?.adviser_group) { const { data: managerTeams } = await supabase .from("user_credentials") .select("id, group_name") .eq("user_roles", 1) .eq("adviser_group", adviser.adviser_group); - + teamSelect.innerHTML = ``; managerTeams?.forEach((t) => { const opt = document.createElement("option"); @@ -221,7 +301,7 @@ const ManuScript = () => { const managerId = document.getElementById("teamSelect").value; const date = document.getElementById("scheduleDate").value; const time = document.getElementById("scheduleTime").value; - + if (!adviser || !managerId || !date || !time) { MySwal.showValidationMessage("Please fill all fields"); return false; @@ -231,7 +311,7 @@ const ManuScript = () => { }).then(async (result) => { if (result.isConfirmed) { const { adviser, managerId, date, time } = result.value; - + const { error, data } = await supabase .from("user_manuscript_sched") .insert([ @@ -243,11 +323,11 @@ const ManuScript = () => { plagiarism: 0, ai: 0, file_uploaded: null, - verdict: 1, + verdict: 1, }, ]) .select(); - + if (error) { console.error("Insert error:", error); MySwal.fire("Error", "Failed to create schedule", "error"); @@ -263,22 +343,23 @@ const ManuScript = () => { } }); }; - - const handleUpdate = async (schedId) => { - setOpenDropdown(null); - + + const handleUpdate = async (schedId) => { + setOpenDropdown(null); + // 1. Find the schedule to be updated - const scheduleToUpdate = schedules.find(s => s.id === schedId); - + const scheduleToUpdate = schedules.find((s) => s.id === schedId); + if (!scheduleToUpdate) { - MySwal.fire("Error", "Schedule not found.", "error"); - return; + MySwal.fire("Error", "Schedule not found.", "error"); + return; } - + // Get the team name for display purposes in the modal - const teamName = accounts.find((a) => a.id === scheduleToUpdate.manager_id)?.group_name || "Unknown Team"; - - + const teamName = + accounts.find((a) => a.id === scheduleToUpdate.manager_id)?.group_name || + "Unknown Team"; + MySwal.fire({ title: `
Update Schedule for ${teamName}
`, @@ -299,12 +380,12 @@ const ManuScript = () => { showCancelButton: true, confirmButtonText: "Update", cancelButtonText: "Cancel", - confirmButtonColor: "#3B0304", + confirmButtonColor: "#3B0304", width: "500px", preConfirm: () => { const newDate = document.getElementById("scheduleDate").value; const newTime = document.getElementById("scheduleTime").value; - + if (!newDate || !newTime) { MySwal.showValidationMessage("Please fill both Date and Time fields"); return false; @@ -314,7 +395,7 @@ const ManuScript = () => { }).then(async (result) => { if (result.isConfirmed) { const { newDate, newTime } = result.value; - + const { error, data } = await supabase .from("user_manuscript_sched") .update({ @@ -323,18 +404,18 @@ const ManuScript = () => { }) .eq("id", schedId) .select(); - + if (error) { console.error("Update error:", error); MySwal.fire("Error", "Failed to update schedule", "error"); } else { - // Update local state with the new data - if (data && data.length > 0) { - setSchedules(prev => prev.map(s => - s.id === schedId ? data[0] : s - )); - } - + // Update local state with the new data + if (data && data.length > 0) { + setSchedules((prev) => + prev.map((s) => (s.id === schedId ? data[0] : s)) + ); + } + MySwal.fire({ icon: "success", title: "βœ“ Schedule Updated", @@ -345,25 +426,25 @@ const ManuScript = () => { } }); }; - + const handleDelete = async (id) => { - setOpenDropdown(null); + setOpenDropdown(null); const confirm = await MySwal.fire({ title: "Delete Schedule?", text: "This action cannot be undone.", icon: "warning", showCancelButton: true, - confirmButtonColor: "#3B0304", - cancelButtonColor: "#999", + confirmButtonColor: "#3B0304", + cancelButtonColor: "#999", confirmButtonText: "Yes, delete it!", }); - + if (confirm.isConfirmed) { const { error } = await supabase .from("user_manuscript_sched") .delete() .eq("id", id); - + if (!error) { setSchedules((prev) => prev.filter((s) => s.id !== id)); MySwal.fire("Deleted!", "Schedule has been deleted.", "success"); @@ -372,7 +453,7 @@ const ManuScript = () => { } } }; - + const handleDeleteSelected = async () => { if (selectedSchedules.length === 0) { MySwal.fire({ @@ -382,7 +463,7 @@ const ManuScript = () => { }); return; } - + const confirm = await MySwal.fire({ title: `Delete ${selectedSchedules.length} Schedules?`, text: "This action cannot be undone.", @@ -392,31 +473,38 @@ const ManuScript = () => { cancelButtonColor: "#999", confirmButtonText: "Yes, delete them!", }); - + if (confirm.isConfirmed) { const { error } = await supabase .from("user_manuscript_sched") .delete() - .in("id", selectedSchedules); - + .in("id", selectedSchedules); + if (!error) { - setSchedules((prev) => prev.filter((s) => !selectedSchedules.includes(s.id))); + setSchedules((prev) => + prev.filter((s) => !selectedSchedules.includes(s.id)) + ); setSelectedSchedules([]); setIsSelectionMode(false); - MySwal.fire("Deleted!", `${selectedSchedules.length} schedules have been deleted.`, "success"); + MySwal.fire( + "Deleted!", + `${selectedSchedules.length} schedules have been deleted.`, + "success" + ); } else { MySwal.fire("Error", "Failed to delete selected schedules.", "error"); } } }; - + // --- Filtering and State Calculation --- - + const filteredSchedules = schedules.filter((sched) => { - const teamName = accounts.find((a) => a.id === sched.manager_id)?.group_name || ""; - const verdict = verdictMap[sched.verdict] || "Pending"; + const teamName = + accounts.find((a) => a.id === sched.manager_id)?.group_name || ""; + const verdict = verdictMap[sched.verdict] || "Pending"; const fileName = sched.file_uploaded || "No File"; - + const searchText = search.toLowerCase(); return ( teamName.toLowerCase().includes(searchText) || @@ -426,15 +514,15 @@ const ManuScript = () => { fileName.toLowerCase().includes(searchText) ); }); - - const isAllSelected = filteredSchedules.length > 0 && - filteredSchedules.every(sched => selectedSchedules.includes(sched.id)); - + + const isAllSelected = + filteredSchedules.length > 0 && + filteredSchedules.every((sched) => selectedSchedules.includes(sched.id)); + // --- Render --- - + return (
- {/* Scrollbar Fix for Webkit (Chrome/Safari) must be in a style tag */} - + {/* Header */}

Manuscript » Scheduled Teams

- + {/* Control Block */}
+ +
+ setSearch(e.target.value)} + /> + +
+
+
+ {!isSelectionMode ? ( -
- setSearch(e.target.value)} - /> - -
-
-
- {!isSelectionMode ? ( - - ) : ( - <> - - + - - )} + onClick={handleDeleteSelected} + disabled={selectedSchedules.length === 0} + > + Delete Selected + + + )}
- + {/* Table Container - THE RELATIVE PARENT FOR THE DETACHED DROPDOWN */} -
- {/* Inner div with Conditional Scrolling and Scrollbar Hiding */} -
-
{i + 1}. {t.task}{new Date(t.date_created || t.created_at).toLocaleDateString()}{t.due_date ? new Date(t.due_date).toLocaleDateString() : "β€”"} + {new Date( + t.date_created || t.created_at + ).toLocaleDateString()} + + {t.due_date + ? new Date(t.due_date).toLocaleDateString() + : "β€”"} + {formatTime(t.time) || "β€”"} - + {t.status}
+
+
+ + + + + + + + + + + + - - {isSelectionMode && ( - + + {isSelectionMode ? ( + + ) : ( + )} - - - - - - - - - - + + + + + + + + + + + - {filteredSchedules.map((sched, index) => { + {filteredSchedules.map((sched, index) => { const teamName = - accounts.find((a) => a.id === sched.manager_id)?.group_name || - "Unknown"; - + accounts.find((a) => a.id === sched.manager_id)?.group_name || + "Unknown"; + const isSelected = selectedSchedules.includes(sched.id); - + return ( - + {isSelectionMode && ( - + )} - - + - - - + - + + + + {/* ADVISER β€” wrap (no ellipsis) */} + + + {/* Verdict */} + + {/* Action Column - Just the button here */} - + ); - })} - {filteredSchedules.length === 0 && ( + })} + {filteredSchedules.length === 0 && ( - + - )} + )} -
- -
+ + + NO + NOTeamDateTimePlagiarismAIFile UploadedVerdictAction
+ TEAM + + DATE + + TIME + + PLAGIARISM + + AI + + FILE UPLOADED + + ADVISER + + VERDICT + + ACTION +
- handleToggleSelect(sched.id)} - /> - + handleToggleSelect(sched.id)} + /> + {index + 1}{teamName} + {index + 1} + - {new Date(sched.date).toLocaleDateString("en-US", { - month: "short", - day: "numeric", - year: "numeric", - })} + {teamName} {sched.time}{sched.plagiarism}%{sched.ai}% - {sched.file_uploaded || "No File"} + {new Date(sched.date).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + })} + + {sched.time} - Pending + {sched.plagiarism}% + {sched.ai}% + + {sched.file_url ? ( + + {getFileName(sched.file_url)} + + ) : ( + No File + )} + + Gerald Tolentino + + {verdictMap[sched.verdict] || "Pending"} + - {!isSelectionMode && ( - - )} + {!isSelectionMode && ( + + )}
+ No schedules found. -
+
- + {/* RENDER DETACHED DROPDOWN MENU HERE - ABSOLUTE POSITIONED RELATIVE TO THE WRAPPER */} {openDropdown && !isSelectionMode && ( -
e.stopPropagation()} - > -
- {/* Update Button - Locked to white background and black text on hover */} - - {/* Delete Button - Locked to white background and black text on hover */} - -
+
e.stopPropagation()} + > +
+ {/* Update Button - Locked to white background and black text on hover */} + + {/* Delete Button - Locked to white background and black text on hover */} +
+
)} -
); }; - -export default ManuScript; \ No newline at end of file + +export default ManuScript; diff --git a/src/components/Instructor/OralDefense.jsx b/src/components/Instructor/OralDefense.jsx index 4c11759..95d7176 100644 --- a/src/components/Instructor/OralDefense.jsx +++ b/src/components/Instructor/OralDefense.jsx @@ -2,1222 +2,1529 @@ import { useState, useEffect, useRef } from "react"; import Swal from "sweetalert2"; import withReactContent from "sweetalert2-react-content"; -import { FaCalendarAlt, FaEllipsisV, FaSearch, FaTrash, FaFileExport, FaPen, FaPlus } from "react-icons/fa"; +import { + FaCalendarAlt, + FaEllipsisV, + FaSearch, + FaTrash, + FaFileExport, + FaPen, + FaPlus, +} from "react-icons/fa"; import { supabase } from "../../supabaseClient"; import jsPDF from "jspdf"; - + const MySwal = withReactContent(Swal); - + const OralDefense = () => { - const [advisers, setAdvisers] = useState([]); - const [accounts, setAccounts] = useState([]); - const [schedules, setSchedules] = useState([]); - const [openDropdown, setOpenDropdown] = useState(null); - const [search, setSearch] = useState(""); - const [isDeleteMode, setIsDeleteMode] = useState(false); - const [selectedSchedules, setSelectedSchedules] = useState([]); - const dropdownRef = useRef(null); - - const verdictMap = { - 1: "Pending", - 2: "Approved", - 3: "Revisions", - 4: "Disapproved", + const [accounts, setAccounts] = useState([]); + const [advisers, setAdvisers] = useState([]); // panel pool (roles 3/4/5) + const [schedules, setSchedules] = useState([]); + const [openDropdown, setOpenDropdown] = useState(null); + const [search, setSearch] = useState(""); + const [isDeleteMode, setIsDeleteMode] = useState(false); + const [selectedSchedules, setSelectedSchedules] = useState([]); + const dropdownRef = useRef(null); + + const verdictMap = { + 1: "Pending", + 2: "Approved", + 3: "Revisions", + 4: "Disapproved", + }; + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + setOpenDropdown(null); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + + // fetch advisers, accounts, schedules + useEffect(() => { + const fetchData = async () => { + const { data: accData, error: accError } = await supabase + .from("user_credentials") + .select("*"); + if (accError) { + console.error("Error fetching accounts:", accError); + return; + } + + if (accData) { + setAccounts(accData); + // panel pool includes: advisers (3), chairs (4) if any, and guests (5) + const panelPool = accData.filter((a) => + [3, 4, 5].includes(a.user_roles) + ); + setAdvisers(panelPool); + } + + const { data: schedData, error: schedError } = await supabase + .from("user_oraldef") + .select( + ` + *, + manager:manager_id ( group_name ) + ` + ); + + if (schedError) { + console.error("Error fetching schedules:", schedError); + return; + } + if (schedData) setSchedules(schedData); }; - - // Close dropdown when clicking outside - useEffect(() => { - const handleClickOutside = (event) => { - if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { - setOpenDropdown(null); + + fetchData(); + }, []); + + // Sort schedules by verdict priority: Pending -> Revisions -> Approved -> Disapproved + const sortedSchedules = [...schedules].sort((a, b) => { + const priorityOrder = { 1: 1, 3: 2, 2: 3, 4: 4 }; + return priorityOrder[a.verdict] - priorityOrder[b.verdict]; + }); + + // search by team name + const filteredSchedules = sortedSchedules.filter((s) => + s.manager?.group_name?.toLowerCase().includes(search.toLowerCase()) + ); + + // team already scheduled? + const isTeamAlreadyScheduled = (teamName) => { + return schedules.some( + (schedule) => schedule.manager?.group_name === teamName + ); + }; + + // list of already scheduled team names + const getScheduledTeams = () => { + return schedules + .map((schedule) => schedule.manager?.group_name) + .filter(Boolean); + }; + + // conflicts: same date and within 1 hour + const hasTimeConflict = ( + selectedDate, + selectedTime, + excludeScheduleId = null + ) => { + if (!selectedDate || !selectedTime) return false; + + const selectedDateTime = new Date(`${selectedDate}T${selectedTime}`); + + return schedules.some((schedule) => { + if (schedule.id === excludeScheduleId) return false; + if (schedule.date !== selectedDate) return false; + + const scheduleDateTime = new Date(`${schedule.date}T${schedule.time}`); + const timeDiff = + Math.abs(selectedDateTime - scheduleDateTime) / (1000 * 60 * 60); + return timeDiff < 1; + }); + }; + + const getConflictingTimes = ( + selectedDate, + selectedTime, + excludeScheduleId = null + ) => { + if (!selectedDate || !selectedTime) return []; + + const selectedDateTime = new Date(`${selectedDate}T${selectedTime}`); + + return schedules + .filter((schedule) => { + if (schedule.id === excludeScheduleId) return false; + if (schedule.date !== selectedDate) return false; + const scheduleDateTime = new Date(`${schedule.date}T${schedule.time}`); + const timeDiff = + Math.abs(selectedDateTime - scheduleDateTime) / (1000 * 60 * 60); + return timeDiff < 1; + }) + .map((schedule) => ({ + team: schedule.manager?.group_name || "Unknown Team", + time: schedule.time, + })); + }; + + // PDF export + const exportOralDefenseAsPDF = (data) => { + const today = new Date().toLocaleDateString(); + const fileName = `oral-defense-schedule-${today.replace(/\//g, "-")}.pdf`; + + const doc = new jsPDF(); + + // Header + doc.setFillColor(59, 3, 4); + doc.rect(0, 0, 210, 30, "F"); + doc.setTextColor(255, 255, 255); + doc.setFontSize(20); + doc.setFont("helvetica", "bold"); + doc.text("Oral Defense Schedule Report", 105, 15, { align: "center" }); + doc.setFontSize(10); + doc.setFont("helvetica", "normal"); + doc.text(`Generated on: ${today}`, 105, 22, { align: "center" }); + + // Table header + doc.setTextColor(255, 255, 255); + doc.setFillColor(59, 3, 4); + doc.rect(10, 35, 190, 10, "F"); + doc.setFont("helvetica", "bold"); + doc.setFontSize(8); + + const headers = [ + "NO", + "TEAM", + "TITLE", + "DATE", + "TIME", + "ADVISER", + "PANELISTS", + "VERDICT", + ]; + const columnWidths = [10, 30, 35, 20, 15, 25, 40, 20]; + let x = 10; + headers.forEach((h, idx) => { + doc.text(h, x + 2, 42); + x += columnWidths[idx]; + }); + + // Rows + doc.setTextColor(0, 0, 0); + doc.setFont("helvetica", "normal"); + doc.setFontSize(7); + let y = 50; + + data.forEach((row, idx) => { + if (y > 270) { + doc.addPage(); + y = 20; + } + + if (idx % 2 === 0) { + doc.setFillColor(245, 245, 245); + doc.rect(10, y - 4, 190, 6, "F"); + } + + x = 10; + const rowData = [ + String(row.no), + row.team, + row.title, + row.date, + row.time, + row.adviser, + row.panelists, + row.verdict, + ]; + + rowData.forEach((cell, ci) => { + if (ci === 2 || ci === 6) { + const lines = doc.splitTextToSize(cell, columnWidths[ci] - 2); + doc.text(lines, x + 1, y); + } else { + doc.text(cell, x + 1, y); + } + x += columnWidths[ci]; + }); + + y += 6; + }); + + doc.setFontSize(8); + doc.text(`Total Records: ${data.length}`, 14, 285); + + doc.save(fileName); + }; + + const handleCreateSchedule = () => { + let selectedPanelists = []; + let selectedAdviser = null; + + MySwal.fire({ + title: `
+ Create Schedule
`, + html: ` + +
+ + +
+
+ + +
+
+ + +
+
+ +
+ No panelist selected +
+
+
+ + +
+
+ + + +
+ `, + showCancelButton: true, + confirmButtonText: "Create", + cancelButtonText: "Cancel", + confirmButtonColor: "#3B0304", + cancelButtonColor: "#999", + width: "600px", + didOpen: () => { + const adviserSelect = document.getElementById("adviserSelect"); + const teamSelect = document.getElementById("teamSelect"); + const panelSelect = document.getElementById("panelSelect"); + const panelList = document.getElementById("panelList"); + const scheduleDate = document.getElementById("scheduleDate"); + const scheduleTime = document.getElementById("scheduleTime"); + const timeConflictWarning = document.getElementById( + "timeConflictWarning" + ); + + // set min date to today + open picker + if (scheduleDate) { + const today = new Date(); + today.setHours(0, 0, 0, 0); + const yyyy = today.getFullYear(); + const mm = String(today.getMonth() + 1).padStart(2, "0"); + const dd = String(today.getDate()).padStart(2, "0"); + scheduleDate.setAttribute("min", `${yyyy}-${mm}-${dd}`); + const openPicker = () => + scheduleDate.showPicker && scheduleDate.showPicker(); + scheduleDate.addEventListener("click", openPicker); + scheduleDate.addEventListener("focus", openPicker); + } + + // time min enforcement when date is today + if (scheduleTime) { + const openTimePicker = () => + scheduleTime.showPicker && scheduleTime.showPicker(); + scheduleTime.addEventListener("click", openTimePicker); + scheduleTime.addEventListener("focus", openTimePicker); + + const getNowHHMM = () => { + const now = new Date(); + const hh = String(now.getHours()).padStart(2, "0"); + const mm = String(now.getMinutes()).padStart(2, "0"); + return `${hh}:${mm}`; + }; + + const setTimeMinIfToday = () => { + const dateVal = scheduleDate.value; + if (!dateVal) { + scheduleTime.removeAttribute("min"); + return; + } + const today = new Date(); + today.setHours(0, 0, 0, 0); + const picked = new Date(dateVal); + picked.setHours(0, 0, 0, 0); + if (picked.getTime() === today.getTime()) { + const min = getNowHHMM(); + scheduleTime.setAttribute("min", min); + if (scheduleTime.value && scheduleTime.value < min) { + scheduleTime.value = min; + } + } else { + scheduleTime.removeAttribute("min"); + } + }; + + scheduleDate.addEventListener("change", setTimeMinIfToday); + scheduleTime.addEventListener("input", () => { + const min = scheduleTime.getAttribute("min"); + if (min && scheduleTime.value && scheduleTime.value < min) { + scheduleTime.value = min; + } + }); + setTimeMinIfToday(); + } + + // Conflicts live warning + const checkTimeConflict = () => { + const date = scheduleDate.value; + const time = scheduleTime.value; + if (date && time) { + if (hasTimeConflict(date, time)) { + const conflicts = getConflictingTimes(date, time); + const text = conflicts + .map((c) => `${c.team} at ${c.time}`) + .join(", "); + timeConflictWarning.style.display = "block"; + timeConflictWarning.innerHTML = `⚠️ Time conflict with: ${text}. Please choose a different time with at least 1 hour gap.`; + } else { + timeConflictWarning.style.display = "none"; } + } else { + timeConflictWarning.style.display = "none"; + } }; - - document.addEventListener("mousedown", handleClickOutside); - return () => { - document.removeEventListener("mousedown", handleClickOutside); + scheduleDate.addEventListener("change", checkTimeConflict); + scheduleTime.addEventListener("change", checkTimeConflict); + + // get already scheduled teams to mark in UI + const scheduledTeams = getScheduledTeams(); + + // populate teams for chosen adviser.group + const updateTeamOptions = (adviserId) => { + const adviser = accounts.find((a) => a.id === adviserId); + teamSelect.innerHTML = + ''; + if (!adviser) return; + const teams = accounts.filter( + (a) => + a.adviser_group === adviser.adviser_group && a.user_roles === 1 + ); + if (teams.length === 0) { + const opt = document.createElement("option"); + opt.value = ""; + opt.disabled = true; + opt.textContent = "No manager available"; + teamSelect.appendChild(opt); + } else { + teams.forEach((t) => { + if (!t.group_name) return; + const isAlreadyScheduled = scheduledTeams.includes(t.group_name); + const opt = document.createElement("option"); + opt.value = t.group_name; + opt.textContent = + t.group_name + + (isAlreadyScheduled ? " (Already Scheduled)" : ""); + if (isAlreadyScheduled) { + opt.disabled = true; + opt.className = "disabled-option"; + } + teamSelect.appendChild(opt); + }); + } }; - }, []); - - // fetch advisers, accounts, schedules - useEffect(() => { - const fetchData = async () => { - const { data: accData, error: accError } = await supabase - .from("user_credentials") - .select("*"); - if (accError) return console.error("Error fetching accounts:", accError); - - if (accData) { - setAccounts(accData); - setAdvisers(accData.filter((a) => a.user_roles === 3)); // advisers + + // build panel options: label guests, disable same-group non-guests; exclude current adviser from panel pool + const rebuildPanelOptions = (teamAdviserGroup, excludeId) => { + panelSelect.innerHTML = + ''; + advisers.forEach((p) => { + // never list the selected adviser as panelist choice + if (p.id === excludeId) return; + + const isGuest = p.user_roles === 5; + const sameGroup = + teamAdviserGroup && + p.user_roles !== 5 && + p.adviser_group === teamAdviserGroup; + + const opt = document.createElement("option"); + opt.value = p.id; + opt.textContent = `${p.last_name}, ${p.first_name}${ + isGuest ? " (Guest Panelist)" : "" + }`; + + if (sameGroup) { + opt.disabled = true; + opt.className = "disabled-option"; + opt.textContent += " (Team Adviser)"; } - - const { data: schedData, error: schedError } = await supabase - .from("user_oraldef") - .select( - ` - *, - manager:manager_id ( group_name ) - ` - ); - - if (schedError) return console.error("Error fetching schedules:", schedError); - if (schedData) setSchedules(schedData); + + panelSelect.appendChild(opt); + }); }; - fetchData(); - }, []); - - // Sort schedules by verdict priority: Pending -> Revisions -> Approved -> Disapproved - const sortedSchedules = [...schedules].sort((a, b) => { - const priorityOrder = {1: 1, 3: 2, 2: 3, 4: 4}; // Pending -> Revisions -> Approved -> Disapproved - return priorityOrder[a.verdict] - priorityOrder[b.verdict]; - }); - - // search by team - const filteredSchedules = sortedSchedules.filter((s) => - s.manager?.group_name?.toLowerCase().includes(search.toLowerCase()) - ); - - // Check if team already has a schedule - const isTeamAlreadyScheduled = (teamName) => { - return schedules.some(schedule => - schedule.manager?.group_name === teamName - ); - }; - - // Get teams that are already scheduled - const getScheduledTeams = () => { - return schedules.map(schedule => schedule.manager?.group_name).filter(Boolean); - }; - - // Check if time conflict exists (same time or within 1 hour gap) - const hasTimeConflict = (selectedDate, selectedTime, excludeScheduleId = null) => { - if (!selectedDate || !selectedTime) return false; - - const selectedDateTime = new Date(`${selectedDate}T${selectedTime}`); - - return schedules.some(schedule => { - if (schedule.id === excludeScheduleId) return false; // Exclude current schedule when updating - if (schedule.date !== selectedDate) return false; // Different dates, no conflict - - const scheduleDateTime = new Date(`${schedule.date}T${schedule.time}`); - const timeDiff = Math.abs(selectedDateTime - scheduleDateTime) / (1000 * 60 * 60); // Difference in hours - - return timeDiff < 1; // Conflict if less than 1 hour gap + + adviserSelect.addEventListener("change", () => { + selectedAdviser = adviserSelect.value; + // enable team/panel after adviser is chosen + teamSelect.disabled = false; + panelSelect.disabled = false; + + // reset selected panelists + selectedPanelists = []; + panelList.innerHTML = + 'No panelist selected'; + + // populate team list + updateTeamOptions(selectedAdviser); + + // initial panel options (no team context yet: do not disable same-group, but still exclude adviser) + rebuildPanelOptions(null, selectedAdviser); }); - }; - - // Get conflicting times for display - const getConflictingTimes = (selectedDate, selectedTime, excludeScheduleId = null) => { - if (!selectedDate || !selectedTime) return []; - - const selectedDateTime = new Date(`${selectedDate}T${selectedTime}`); - - return schedules - .filter(schedule => { - if (schedule.id === excludeScheduleId) return false; - if (schedule.date !== selectedDate) return false; - - const scheduleDateTime = new Date(`${schedule.date}T${schedule.time}`); - const timeDiff = Math.abs(selectedDateTime - scheduleDateTime) / (1000 * 60 * 60); - return timeDiff < 1; - }) - .map(schedule => ({ - team: schedule.manager?.group_name || 'Unknown Team', - time: schedule.time - })); - }; - - const exportOralDefenseAsPDF = (data) => { - const today = new Date().toLocaleDateString(); - const fileName = `oral-defense-schedule-${today.replace(/\//g, '-')}.pdf`; - - // Create PDF using jsPDF - const doc = new jsPDF(); - - // Add header - doc.setFillColor(59, 3, 4); - doc.rect(0, 0, 210, 30, 'F'); - doc.setTextColor(255, 255, 255); - doc.setFontSize(20); - doc.setFont('helvetica', 'bold'); - doc.text('Oral Defense Schedule Report', 105, 15, { align: 'center' }); - - doc.setFontSize(10); - doc.setFont('helvetica', 'normal'); - doc.text(`Generated on: ${today}`, 105, 22, { align: 'center' }); - - // Reset text color for content - doc.setTextColor(0, 0, 0); - - // Add table headers - doc.setFillColor(59, 3, 4); - doc.rect(10, 35, 190, 10, 'F'); - doc.setTextColor(255, 255, 255); - doc.setFontSize(8); - doc.setFont('helvetica', 'bold'); - - const headers = ['NO', 'TEAM', 'TITLE', 'DATE', 'TIME', 'ADVISER', 'PANELISTS', 'VERDICT']; - const columnWidths = [10, 30, 35, 20, 15, 25, 40, 20]; - let xPosition = 10; - - headers.forEach((header, index) => { - doc.text(header, xPosition + 2, 42); - xPosition += columnWidths[index]; + + teamSelect.addEventListener("change", () => { + const teamName = teamSelect.value; + const teamMembers = accounts.filter((a) => a.group_name === teamName); + const teamAdviserGroup = teamMembers[0]?.adviser_group || null; + + // reset panelists when team changes + selectedPanelists = []; + panelList.innerHTML = + 'No panelist selected'; + + // now we know the team group β€” disable regular advisers from same group; guests are always allowed + rebuildPanelOptions(teamAdviserGroup, selectedAdviser); }); - - // Add table rows - doc.setTextColor(0, 0, 0); - doc.setFont('helvetica', 'normal'); - doc.setFontSize(7); - - let yPosition = 50; - - data.forEach((item, index) => { - if (yPosition > 270) { - doc.addPage(); - yPosition = 20; - } - - // Alternate row background - if (index % 2 === 0) { - doc.setFillColor(245, 245, 245); - doc.rect(10, yPosition - 4, 190, 6, 'F'); + + // add panelist + panelSelect.addEventListener("change", () => { + const id = panelSelect.value; + if (!selectedPanelists.includes(id)) { + if (selectedPanelists.length < 3) { + selectedPanelists.push(id); + const person = advisers.find((a) => a.id === id); + if (panelList.querySelector(".text-muted")) + panelList.innerHTML = ""; + const tag = document.createElement("span"); + tag.className = + "bg-gray-200 text-gray-800 rounded-full px-2 py-1 text-sm flex items-center gap-1"; + tag.innerHTML = `${person.last_name}, ${person.first_name} `; + panelList.appendChild(tag); + } else { + MySwal.showValidationMessage("Maximum of 3 panelists."); } - - xPosition = 10; - const rowData = [ - item.no.toString(), - item.team, - item.title, - item.date, - item.time, - item.adviser, - item.panelists, - item.verdict - ]; - - rowData.forEach((cell, cellIndex) => { - // Wrap text for panelists and title columns - if (cellIndex === 2 || cellIndex === 6) { - const lines = doc.splitTextToSize(cell, columnWidths[cellIndex] - 2); - doc.text(lines, xPosition + 1, yPosition); - } else { - doc.text(cell, xPosition + 1, yPosition); - } - xPosition += columnWidths[cellIndex]; - }); - - yPosition += 6; + } + panelSelect.value = ""; }); - - // Add footer with total records - doc.setFontSize(8); - doc.text(`Total Records: ${data.length}`, 14, 285); - - // Save the PDF - doc.save(fileName); - }; - - const handleCreateSchedule = () => { - let selectedPanelists = []; - let selectedAdviser = null; - - MySwal.fire({ - title: `
- Create Schedule
`, - html: ` - -
- - -
-
- - -
-
- - -
-
- -
- No panelist selected -
-
-
- - -
-
- - - -
- `, - showCancelButton: true, - confirmButtonText: "Create", - cancelButtonText: "Cancel", - confirmButtonColor: "#3B0304", - cancelButtonColor: "#999", - width: "600px", - didOpen: () => { - const adviserSelect = document.getElementById("adviserSelect"); - const teamSelect = document.getElementById("teamSelect"); - const panelSelect = document.getElementById("panelSelect"); - const panelList = document.getElementById("panelList"); - const scheduleDate = document.getElementById("scheduleDate"); - const scheduleTime = document.getElementById("scheduleTime"); - const timeConflictWarning = document.getElementById("timeConflictWarning"); - - // Get already scheduled teams - const scheduledTeams = getScheduledTeams(); - - // update team options - const updateTeamOptions = (adviserId) => { - const adviser = accounts.find((a) => a.id === adviserId); - if (!adviser) return; - - const teams = accounts.filter( - (a) => a.adviser_group === adviser.adviser_group && a.user_roles === 1 - ); - - teamSelect.innerHTML = ''; - - if (teams.length > 0) { - teams.forEach((t) => { - if (t.group_name) { - const isAlreadyScheduled = scheduledTeams.includes(t.group_name); - const disabledAttr = isAlreadyScheduled ? 'disabled class="disabled-option"' : ''; - const scheduledText = isAlreadyScheduled ? ' (Already Scheduled)' : ''; - teamSelect.innerHTML += ``; - } - }); - } else { - const opt = document.createElement("option"); - opt.value = ""; - opt.disabled = true; - opt.textContent = "No manager available"; - teamSelect.appendChild(opt); - } - }; - - // update panel options - const updatePanelOptions = (adviserId) => { - panelSelect.innerHTML = ''; - advisers.forEach((a) => { - if (a.id !== adviserId) - panelSelect.innerHTML += ``; - }); - }; - - // Check for time conflicts - const checkTimeConflict = () => { - const date = scheduleDate.value; - const time = scheduleTime.value; - - if (date && time) { - if (hasTimeConflict(date, time)) { - const conflicts = getConflictingTimes(date, time); - const conflictText = conflicts.map(conflict => - `${conflict.team} at ${conflict.time}` - ).join(', '); - - timeConflictWarning.style.display = 'block'; - timeConflictWarning.innerHTML = `⚠️ Time conflict with: ${conflictText}. Please choose a different time with at least 1 hour gap.`; - } else { - timeConflictWarning.style.display = 'none'; - } - } else { - timeConflictWarning.style.display = 'none'; - } - }; - - // Event listeners for date and time changes - scheduleDate.addEventListener('change', checkTimeConflict); - scheduleTime.addEventListener('change', checkTimeConflict); - - adviserSelect.addEventListener("change", () => { - selectedAdviser = adviserSelect.value; - updateTeamOptions(selectedAdviser); - updatePanelOptions(selectedAdviser); - teamSelect.disabled = false; - panelSelect.disabled = false; - selectedPanelists = []; - panelList.innerHTML = 'No panelist selected'; - }); - - // add panelist - panelSelect.addEventListener("change", () => { - const id = panelSelect.value; - if (!selectedPanelists.includes(id)) { - if (selectedPanelists.length < 3) { - selectedPanelists.push(id); - const person = advisers.find((a) => a.id === id); - if (panelList.querySelector(".text-muted")) panelList.innerHTML = ""; - const tag = document.createElement("span"); - tag.className = "bg-gray-200 text-gray-800 rounded-full px-2 py-1 text-sm flex items-center gap-1"; - tag.innerHTML = `${person.last_name}, ${person.first_name} `; - panelList.appendChild(tag); - } else { - MySwal.showValidationMessage("Maximum of 3 panelists."); - } - } - panelSelect.value = ""; - }); - - // remove panelist - panelList.addEventListener("click", (e) => { - if (e.target.classList.contains("remove-panelist-btn")) { - const idToRemove = e.target.dataset.id; - selectedPanelists = selectedPanelists.filter((pid) => pid !== idToRemove); - e.target.parentElement.remove(); - if (selectedPanelists.length === 0) - panelList.innerHTML = 'No panelist selected'; - } - }); - }, - preConfirm: () => { - const team = document.getElementById("teamSelect").value; - const date = document.getElementById("scheduleDate").value; - const time = document.getElementById("scheduleTime").value; - const teamSelect = document.getElementById("teamSelect"); - const selectedOption = teamSelect.options[teamSelect.selectedIndex]; - - // Check if selected team is already scheduled - if (selectedOption.disabled) { - MySwal.showValidationMessage("This team already has a schedule. Please select a different team."); - return false; - } - - // Check for time conflicts - if (hasTimeConflict(date, time)) { - const conflicts = getConflictingTimes(date, time); - const conflictText = conflicts.map(conflict => - `${conflict.team} at ${conflict.time}` - ).join(', '); - MySwal.showValidationMessage(`Time conflict detected with: ${conflictText}. Please choose a different time with at least 1 hour gap.`); - return false; - } - - if (!selectedAdviser || !team || !date || !time || selectedPanelists.length === 0) { - MySwal.showValidationMessage("Please fill all fields and select panelists."); - return false; - } - - return { adviser: selectedAdviser, team, date, time, panelists: selectedPanelists }; - }, - }).then(async (result) => { - if (result.isConfirmed) { - const { adviser, team, date, time, panelists } = result.value; - - // Double-check if team is already scheduled (in case of race condition) - if (isTeamAlreadyScheduled(team)) { - MySwal.fire("Error", "This team already has a schedule. Please select a different team.", "error"); - return; - } - - // Double-check for time conflicts - if (hasTimeConflict(date, time)) { - const conflicts = getConflictingTimes(date, time); - const conflictText = conflicts.map(conflict => - `${conflict.team} at ${conflict.time}` - ).join(', '); - MySwal.fire("Error", `Time conflict detected with: ${conflictText}. Please choose a different time with at least 1 hour gap.`, "error"); - return; - } - - const manager = accounts.find((a) => a.user_roles === 1 && a.group_name === team); - const [p1, p2, p3] = panelists; - - const { error, data } = await supabase - .from("user_oraldef") - .insert([ - { - manager_id: manager.id, - adviser_id: adviser, - date, - time, - panelist1_id: p1 || null, - panelist2_id: p2 || null, - panelist3_id: p3 || null, - verdict: 1, - title: null, - }, - ]) - .select( - ` - *, - manager:manager_id ( group_name ) - ` - ); - - if (!error) { - setSchedules((prev) => [...prev, data[0]]); - MySwal.fire({ - icon: "success", - title: "βœ“ Schedule Created", - showConfirmButton: false, - timer: 1500, - }); - } else MySwal.fire("Error", "Failed to create schedule", "error"); + + // remove panelist + panelList.addEventListener("click", (e) => { + if (e.target.classList.contains("remove-panelist-btn")) { + const idToRemove = e.target.dataset.id; + selectedPanelists = selectedPanelists.filter( + (pid) => pid !== idToRemove + ); + e.target.parentElement.remove(); + if (selectedPanelists.length === 0) { + panelList.innerHTML = + 'No panelist selected'; } + } }); - }; - - // Update schedule with SweetAlert2 modal - const handleUpdate = (id) => { - setOpenDropdown(null); - const schedule = schedules.find(s => s.id === id); - if (!schedule) return; - - let selectedPanelists = [ - schedule.panelist1_id, - schedule.panelist2_id, - schedule.panelist3_id - ].filter(Boolean); - - const currentAdviser = accounts.find(a => a.id === schedule.adviser_id); - const currentTeam = schedule.manager?.group_name; - - MySwal.fire({ - title: `
- Update Schedule
`, - html: ` - -
-
Current Selection
-
Adviser: ${currentAdviser ? `${currentAdviser.last_name}, ${currentAdviser.first_name}` : 'N/A'}
-
Team: ${currentTeam || 'N/A'}
-
-
- - -
-
- -
- ${selectedPanelists.length > 0 ? - selectedPanelists.map(pid => { - const person = advisers.find(a => a.id === pid); - return person ? - ` - ${person.last_name}, ${person.first_name} - - ` : ''; - }).join('') - : 'No panelist selected' - } -
-
-
- - -
-
- - - -
- `, - showCancelButton: true, - confirmButtonText: "Update", - cancelButtonText: "Cancel", - confirmButtonColor: "#3B0304", - cancelButtonColor: "#999", - width: "600px", - didOpen: () => { - const panelSelect = document.getElementById("panelSelect"); - const panelList = document.getElementById("panelList"); - const scheduleDate = document.getElementById("scheduleDate"); - const scheduleTime = document.getElementById("scheduleTime"); - const timeConflictWarning = document.getElementById("timeConflictWarning"); - - // Function to update panelist dropdown options - const updatePanelistDropdown = () => { - panelSelect.innerHTML = ''; - - advisers.forEach((a) => { - const isCurrentAdviser = a.id === schedule.adviser_id; - const isAlreadyPanelist = selectedPanelists.includes(a.id); - const isDisabled = isCurrentAdviser || isAlreadyPanelist; - - let disabledReason = ''; - if (isCurrentAdviser) disabledReason = ' (Current Adviser)'; - if (isAlreadyPanelist) disabledReason = ' (Already Selected)'; - - const option = document.createElement("option"); - option.value = a.id; - option.textContent = `${a.last_name}, ${a.first_name}${disabledReason}`; - - if (isDisabled) { - option.disabled = true; - option.className = 'disabled-option'; - } - - panelSelect.appendChild(option); - }); - }; - - // Check for time conflicts (excluding current schedule) - const checkTimeConflict = () => { - const date = scheduleDate.value; - const time = scheduleTime.value; - - if (date && time) { - if (hasTimeConflict(date, time, schedule.id)) { - const conflicts = getConflictingTimes(date, time, schedule.id); - const conflictText = conflicts.map(conflict => - `${conflict.team} at ${conflict.time}` - ).join(', '); - - timeConflictWarning.style.display = 'block'; - timeConflictWarning.innerHTML = `⚠️ Time conflict with: ${conflictText}. Please choose a different time with at least 1 hour gap.`; - } else { - timeConflictWarning.style.display = 'none'; - } - } else { - timeConflictWarning.style.display = 'none'; - } - }; - - // Event listeners for date and time changes - scheduleDate.addEventListener('change', checkTimeConflict); - scheduleTime.addEventListener('change', checkTimeConflict); - - // add panelist - panelSelect.addEventListener("change", () => { - const id = panelSelect.value; - const selectedOption = panelSelect.options[panelSelect.selectedIndex]; - - // Check if the selected option is disabled - if (selectedOption.disabled) { - MySwal.showValidationMessage("This adviser cannot be selected as a panelist."); - panelSelect.value = ""; - return; - } - - if (!selectedPanelists.includes(id)) { - if (selectedPanelists.length < 3) { - selectedPanelists.push(id); - const person = advisers.find((a) => a.id === id); - if (panelList.querySelector(".text-muted")) panelList.innerHTML = ""; - const tag = document.createElement("span"); - tag.className = "bg-gray-200 text-gray-800 rounded-full px-2 py-1 text-sm flex items-center gap-1"; - tag.innerHTML = `${person.last_name}, ${person.first_name} `; - panelList.appendChild(tag); - - // Update dropdown to disable the newly selected panelist - updatePanelistDropdown(); - } else { - MySwal.showValidationMessage("Maximum of 3 panelists."); - } - } - panelSelect.value = ""; - }); - - // remove panelist - panelList.addEventListener("click", (e) => { - if (e.target.classList.contains("remove-panelist-btn")) { - const idToRemove = e.target.dataset.id; - selectedPanelists = selectedPanelists.filter((pid) => pid !== idToRemove); - e.target.parentElement.remove(); - - // Update dropdown to re-enable the removed panelist - updatePanelistDropdown(); - - if (selectedPanelists.length === 0) - panelList.innerHTML = 'No panelist selected'; - } - }); - }, - preConfirm: () => { - const date = document.getElementById("scheduleDate").value; - const time = document.getElementById("scheduleTime").value; - - // Check for time conflicts (excluding current schedule) - if (hasTimeConflict(date, time, schedule.id)) { - const conflicts = getConflictingTimes(date, time, schedule.id); - const conflictText = conflicts.map(conflict => - `${conflict.team} at ${conflict.time}` - ).join(', '); - MySwal.showValidationMessage(`Time conflict detected with: ${conflictText}. Please choose a different time with at least 1 hour gap.`); - return false; - } - - if (!date || !time || selectedPanelists.length === 0) { - MySwal.showValidationMessage("Please fill all fields and select panelists."); - return false; - } - - return { date, time, panelists: selectedPanelists }; + }, + preConfirm: () => { + const teamSelect = document.getElementById("teamSelect"); + const selectedOption = teamSelect.options[teamSelect.selectedIndex]; + const team = teamSelect.value; + const date = document.getElementById("scheduleDate").value; + const time = document.getElementById("scheduleTime").value; + + if (selectedOption && selectedOption.disabled) { + MySwal.showValidationMessage( + "This team already has a schedule. Please select a different team." + ); + return false; + } + + if (hasTimeConflict(date, time)) { + const conflicts = getConflictingTimes(date, time); + const conflictText = conflicts + .map((c) => `${c.team} at ${c.time}`) + .join(", "); + MySwal.showValidationMessage( + `Time conflict detected with: ${conflictText}. Please choose a different time with at least 1 hour gap.` + ); + return false; + } + + if ( + !selectedAdviser || + !team || + !date || + !time || + selectedPanelists.length === 0 + ) { + MySwal.showValidationMessage( + "Please fill all fields and select panelists." + ); + return false; + } + + return { + adviser: selectedAdviser, + team, + date, + time, + panelists: selectedPanelists, + }; + }, + }).then(async (result) => { + if (result.isConfirmed) { + const { adviser, team, date, time, panelists } = result.value; + + // double-check at insert time + if (isTeamAlreadyScheduled(team)) { + MySwal.fire( + "Error", + "This team already has a schedule. Please select a different team.", + "error" + ); + return; + } + if (hasTimeConflict(date, time)) { + const conflicts = getConflictingTimes(date, time); + const conflictText = conflicts + .map((c) => `${c.team} at ${c.time}`) + .join(", "); + MySwal.fire( + "Error", + `Time conflict detected with: ${conflictText}. Please choose a different time with at least 1 hour gap.`, + "error" + ); + return; + } + + const manager = accounts.find( + (a) => a.user_roles === 1 && a.group_name === team + ); + const [p1, p2, p3] = panelists; + + const { error, data } = await supabase + .from("user_oraldef") + .insert([ + { + manager_id: manager?.id ?? null, + adviser_id: adviser, + date, + time, + panelist1_id: p1 || null, + panelist2_id: p2 || null, + panelist3_id: p3 || null, + verdict: 1, + title: null, }, - }).then(async (result) => { - if (result.isConfirmed) { - const { date, time, panelists } = result.value; - const [p1, p2, p3] = panelists; - - const { error } = await supabase - .from("user_oraldef") - .update({ - date, - time, - panelist1_id: p1 || null, - panelist2_id: p2 || null, - panelist3_id: p3 || null + ]) + .select( + ` + *, + manager:manager_id ( group_name ) + ` + ); + + if (!error) { + setSchedules((prev) => [...prev, data[0]]); + MySwal.fire({ + icon: "success", + title: "βœ“ Schedule Created", + showConfirmButton: false, + timer: 1500, + }); + } else { + MySwal.fire("Error", "Failed to create schedule", "error"); + } + } + }); + }; + + // Update schedule with SweetAlert2 modal + const handleUpdate = (id) => { + setOpenDropdown(null); + const schedule = schedules.find((s) => s.id === id); + if (!schedule) return; + + let selectedPanelists = [ + schedule.panelist1_id, + schedule.panelist2_id, + schedule.panelist3_id, + ].filter(Boolean); + + const currentAdviser = accounts.find((a) => a.id === schedule.adviser_id); + const currentTeam = schedule.manager?.group_name; + + MySwal.fire({ + title: `
+ Update Schedule
`, + html: ` + +
+
Current Selection
+
Adviser: ${ + currentAdviser + ? `${currentAdviser.last_name}, ${currentAdviser.first_name}` + : "N/A" + }
+
Team: ${currentTeam || "N/A"}
+
+ +
+ + +
+ +
+ +
+ ${ + selectedPanelists.length > 0 + ? selectedPanelists + .map((pid) => { + const person = advisers.find((a) => a.id === pid); + if (!person) return ""; + return ` + ${person.last_name}, ${person.first_name} + + `; }) - .eq("id", id); - - if (!error) { - setSchedules((prev) => - prev.map((s) => (s.id === id ? { - ...s, - date, - time, - panelist1_id: p1 || null, - panelist2_id: p2 || null, - panelist3_id: p3 || null - } : s)) - ); - MySwal.fire({ - icon: "success", - title: "βœ“ Schedule Updated", - showConfirmButton: false, - timer: 1500, - }); - } else { - MySwal.fire("Error", "Failed to update schedule", "error"); - } + .join("") + : 'No panelist selected' } - }); - }; - - // Export functionality with filter options - const handleExport = () => { - MySwal.fire({ - title: "Export Oral Defense Data", - html: ` -
-

Select which schedules to export:

- -
- `, - icon: "question", - showCancelButton: true, - confirmButtonColor: "#3B0304", - cancelButtonColor: "#999", - confirmButtonText: "Export", - cancelButtonText: "Cancel", - preConfirm: () => { - const exportFilter = document.getElementById('exportFilter').value; - return { exportFilter }; - } - }).then((result) => { - if (result.isConfirmed) { - const { exportFilter } = result.value; - - // Filter data based on selected option - let filteredExportData; - - if (exportFilter === 'all') { - filteredExportData = filteredSchedules; - } else { - const verdictValue = parseInt(exportFilter); - filteredExportData = filteredSchedules.filter(sched => sched.verdict === verdictValue); - } - - // Prepare data for export - const exportData = filteredExportData.map((sched, index) => { - const panelists = [sched.panelist1_id, sched.panelist2_id, sched.panelist3_id] - .map((pid) => { - const account = accounts.find((a) => a.id === pid); - return account ? `${account.last_name}, ${account.first_name}` : ""; - }) - .filter(Boolean) - .join("; "); - - const adviser = accounts.find((a) => a.id === sched.adviser_id); - const adviserName = adviser ? `${adviser.last_name}, ${adviser.first_name}` : "N/A"; - - return { - no: index + 1, - team: sched.manager?.group_name || "-", - title: sched.title || "-", - date: sched.date, - time: sched.time, - adviser: adviserName, - panelists: panelists, - verdict: verdictMap[sched.verdict] || "PENDING" - }; - }); - - if (exportData.length === 0) { - const filterText = exportFilter === 'all' ? 'schedules' : - exportFilter === '1' ? 'PENDING schedules' : - exportFilter === '3' ? 'REVISIONS schedules' : - exportFilter === '2' ? 'APPROVED schedules' : 'DISAPPROVED schedules'; - - MySwal.fire({ - title: "No Data to Export", - text: `There are no ${filterText} to export.`, - icon: "warning", - confirmButtonColor: "#3B0304" - }); - return; - } - - // Export as PDF - exportOralDefenseAsPDF(exportData); - - const filterText = exportFilter === 'all' ? 'data' : - exportFilter === '1' ? 'PENDING schedules' : - exportFilter === '3' ? 'REVISIONS schedules' : - exportFilter === '2' ? 'APPROVED schedules' : 'DISAPPROVED schedules'; - - MySwal.fire({ - title: "Export Successful!", - text: `Oral defense ${filterText} has been downloaded as PDF.`, - icon: "success", - confirmButtonColor: "#3B0304", - timer: 2000, - showConfirmButton: false - }); +
+
+ +
+ + +
+
+ + + +
+ `, + showCancelButton: true, + confirmButtonText: "Update", + cancelButtonText: "Cancel", + confirmButtonColor: "#3B0304", + cancelButtonColor: "#999", + width: "600px", + didOpen: () => { + const panelSelect = document.getElementById("panelSelect"); + const panelList = document.getElementById("panelList"); + const scheduleDate = document.getElementById("scheduleDate"); + const scheduleTime = document.getElementById("scheduleTime"); + const timeConflictWarning = document.getElementById( + "timeConflictWarning" + ); + + // date min today, time min when today + if (scheduleDate) { + const today = new Date(); + today.setHours(0, 0, 0, 0); + const yyyy = today.getFullYear(); + const mm = String(today.getMonth() + 1).padStart(2, "0"); + const dd = String(today.getDate()).padStart(2, "0"); + scheduleDate.setAttribute("min", `${yyyy}-${mm}-${dd}`); + const openPicker = () => + scheduleDate.showPicker && scheduleDate.showPicker(); + scheduleDate.addEventListener("click", openPicker); + scheduleDate.addEventListener("focus", openPicker); + } + if (scheduleTime) { + const openTimePicker = () => + scheduleTime.showPicker && scheduleTime.showPicker(); + scheduleTime.addEventListener("click", openTimePicker); + scheduleTime.addEventListener("focus", openTimePicker); + + const getNowHHMM = () => { + const now = new Date(); + const hh = String(now.getHours()).padStart(2, "0"); + const mm = String(now.getMinutes()).padStart(2, "0"); + return `${hh}:${mm}`; + }; + const setTimeMinIfToday = () => { + const dateVal = scheduleDate.value; + if (!dateVal) { + scheduleTime.removeAttribute("min"); + return; } - }); - }; - - const handleDelete = async (id) => { - const confirm = await MySwal.fire({ - title: "Delete Schedule?", - text: "This action cannot be undone.", - icon: "warning", - showCancelButton: true, - confirmButtonColor: "#3B0304", - cancelButtonColor: "#999", - confirmButtonText: "Yes, delete it!", - }); - - if (confirm.isConfirmed) { - const { error } = await supabase - .from("user_oraldef") - .delete() - .eq("id", id); - - if (!error) { - setSchedules((prev) => prev.filter((s) => s.id !== id)); - MySwal.fire("Deleted!", "Schedule has been deleted.", "success"); + const today = new Date(); + today.setHours(0, 0, 0, 0); + const picked = new Date(dateVal); + picked.setHours(0, 0, 0, 0); + if (picked.getTime() === today.getTime()) { + const min = getNowHHMM(); + scheduleTime.setAttribute("min", min); + if (scheduleTime.value && scheduleTime.value < min) { + scheduleTime.value = min; + } } else { - MySwal.fire("Error", "Failed to delete schedule.", "error"); + scheduleTime.removeAttribute("min"); + } + }; + scheduleDate.addEventListener("change", setTimeMinIfToday); + scheduleTime.addEventListener("input", () => { + const min = scheduleTime.getAttribute("min"); + if (min && scheduleTime.value && scheduleTime.value < min) { + scheduleTime.value = min; } + }); + setTimeMinIfToday(); } - }; - - const handleCheckboxChange = (id, isChecked) => { - setSelectedSchedules(prev => { - if (isChecked) { - return [...prev, id]; + + // conflict warning live update (excluding this schedule) + const checkTimeConflict = () => { + const date = scheduleDate.value; + const time = scheduleTime.value; + if (date && time) { + if (hasTimeConflict(date, time, schedule.id)) { + const conflicts = getConflictingTimes(date, time, schedule.id); + const txt = conflicts + .map((c) => `${c.team} at ${c.time}`) + .join(", "); + timeConflictWarning.style.display = "block"; + timeConflictWarning.innerHTML = `⚠️ Time conflict with: ${txt}. Please choose a different time with at least 1 hour gap.`; } else { - return prev.filter(scheduleId => scheduleId !== id); + timeConflictWarning.style.display = "none"; } - }); - }; - - const handleDeleteSelected = async () => { - if (selectedSchedules.length === 0) { - MySwal.fire("No schedules selected", "Please select one or more schedules to delete.", "warning"); + } else { + timeConflictWarning.style.display = "none"; + } + }; + scheduleDate.addEventListener("change", checkTimeConflict); + scheduleTime.addEventListener("change", checkTimeConflict); + + // figure team group to disable regular advisers from same group + const teamName = schedule.manager?.group_name || null; + const teamMembers = accounts.filter((a) => a.group_name === teamName); + const teamAdviserGroup = teamMembers[0]?.adviser_group || null; + + // build options (guests labeled, same-group non-guests disabled, current adviser disabled, already selected disabled) + const rebuildUpdateOptions = () => { + panelSelect.innerHTML = + ''; + advisers.forEach((p) => { + const isGuest = p.user_roles === 5; + const isCurrentAdviser = p.id === schedule.adviser_id; + const isAlreadyPanelist = selectedPanelists.includes(p.id); + const sameGroup = + teamAdviserGroup && + p.user_roles !== 5 && + p.adviser_group === teamAdviserGroup; + + const opt = document.createElement("option"); + opt.value = p.id; + + let note = ""; + if (isCurrentAdviser) note = " (Current Adviser)"; + else if (isAlreadyPanelist) note = " (Already Selected)"; + else if (sameGroup) note = " (Team Adviser)"; + + opt.textContent = `${p.last_name}, ${p.first_name}${ + isGuest ? " (Guest Panelist)" : "" + }${note}`; + + if (isCurrentAdviser || isAlreadyPanelist || sameGroup) { + opt.disabled = true; + opt.className = "disabled-option"; + } + + panelSelect.appendChild(opt); + }); + }; + rebuildUpdateOptions(); + + // add panelist + panelSelect.addEventListener("change", () => { + const id = panelSelect.value; + const selectedOption = panelSelect.options[panelSelect.selectedIndex]; + + if (selectedOption.disabled) { + MySwal.showValidationMessage( + "This adviser cannot be selected as a panelist." + ); + panelSelect.value = ""; return; - } - - const confirm = await MySwal.fire({ - title: "Delete Selected Schedules?", - text: "This action cannot be undone.", - icon: "warning", - showCancelButton: true, - confirmButtonColor: "#3B0304", - cancelButtonColor: "#999", - confirmButtonText: "Yes, delete them!", - }); - - if (confirm.isConfirmed) { - const { error } = await supabase - .from("user_oraldef") - .delete() - .in("id", selectedSchedules); - - if (!error) { - setSchedules(prev => prev.filter(s => !selectedSchedules.includes(s.id))); - setSelectedSchedules([]); - setIsDeleteMode(false); - MySwal.fire("Deleted!", "Selected schedules have been deleted.", "success"); + } + + if (!selectedPanelists.includes(id)) { + if (selectedPanelists.length < 3) { + selectedPanelists.push(id); + const person = advisers.find((a) => a.id === id); + if (panelList.querySelector(".text-muted")) + panelList.innerHTML = ""; + const tag = document.createElement("span"); + tag.className = + "bg-gray-200 text-gray-800 rounded-full px-2 py-1 text-sm flex items-center gap-1"; + tag.innerHTML = `${person.last_name}, ${person.first_name} `; + panelList.appendChild(tag); + + // refresh disabled/enabled states + rebuildUpdateOptions(); } else { - MySwal.fire("Error", "Failed to delete selected schedules.", "error"); + MySwal.showValidationMessage("Maximum of 3 panelists."); } + } + panelSelect.value = ""; + }); + + // remove panelist + panelList.addEventListener("click", (e) => { + if (e.target.classList.contains("remove-panelist-btn")) { + const idToRemove = e.target.dataset.id; + selectedPanelists = selectedPanelists.filter( + (pid) => pid !== idToRemove + ); + e.target.parentElement.remove(); + + // refresh options to re-enable removed person + rebuildUpdateOptions(); + + if (selectedPanelists.length === 0) + panelList.innerHTML = + 'No panelist selected'; + } + }); + }, + preConfirm: () => { + const date = document.getElementById("scheduleDate").value; + const time = document.getElementById("scheduleTime").value; + + if (hasTimeConflict(date, time, schedule.id)) { + const conflicts = getConflictingTimes(date, time, schedule.id); + const conflictText = conflicts + .map((c) => `${c.team} at ${c.time}`) + .join(", "); + MySwal.showValidationMessage( + `Time conflict detected with: ${conflictText}. Please choose a different time with at least 1 hour gap.` + ); + return false; } - }; - - // change verdict - const handleVerdictChange = async (id, newVerdict) => { + + if (!date || !time || selectedPanelists.length === 0) { + MySwal.showValidationMessage( + "Please fill all fields and select panelists." + ); + return false; + } + + return { date, time, panelists: selectedPanelists }; + }, + }).then(async (result) => { + if (result.isConfirmed) { + const { date, time, panelists } = result.value; + const [p1, p2, p3] = panelists; + const { error } = await supabase - .from("user_oraldef") - .update({ verdict: newVerdict }) - .eq("id", id); - + .from("user_oraldef") + .update({ + date, + time, + panelist1_id: p1 || null, + panelist2_id: p2 || null, + panelist3_id: p3 || null, + }) + .eq("id", id); + if (!error) { - setSchedules((prev) => - prev.map((s) => (s.id === id ? { ...s, verdict: newVerdict } : s)) - ); - - // Show success message for REVISIONS - if (newVerdict === 3) { - MySwal.fire({ - icon: "success", - title: "Marked as REVISIONS", - text: "The schedule has been marked for revisions and will remain in the list.", - showConfirmButton: false, - timer: 2000, - }); - } + setSchedules((prev) => + prev.map((s) => + s.id === id + ? { + ...s, + date, + time, + panelist1_id: p1 || null, + panelist2_id: p2 || null, + panelist3_id: p3 || null, + } + : s + ) + ); + MySwal.fire({ + icon: "success", + title: "βœ“ Schedule Updated", + showConfirmButton: false, + timer: 1500, + }); } else { - MySwal.fire("Error", "Failed to update verdict.", "error"); + MySwal.fire("Error", "Failed to update schedule", "error"); } - }; - - // Get adviser name for display - const getAdviserName = (adviserId) => { - const adviser = accounts.find(a => a.id === adviserId); - return adviser ? `${adviser.last_name}, ${adviser.first_name}` : "N/A"; - }; - - // Get select styling based on verdict - Only style the selected value - const getSelectStyle = (verdict) => { - switch (verdict) { - case 1: // Pending - Default styling - return 'text-gray-700 bg-white border-gray-300'; - case 2: // Approved - Green - return 'text-white bg-[#809D3C] border-[#809D3C] font-semibold'; - case 3: // Revisions - Red - return 'text-white bg-[#3B0304] border-[#3B0304] font-semibold'; - case 4: // Disapproved - Gray - return 'text-white bg-gray-600 border-gray-600 font-semibold'; - default: // Default - return 'text-gray-700 bg-white border-gray-300'; + } + }); + }; + + // Export dialog + action + const handleExport = () => { + MySwal.fire({ + title: "Export Oral Defense Data", + html: ` +
+

Select which schedules to export:

+ +
+ `, + icon: "question", + showCancelButton: true, + confirmButtonColor: "#3B0304", + cancelButtonColor: "#999", + confirmButtonText: "Export", + cancelButtonText: "Cancel", + preConfirm: () => { + const exportFilter = document.getElementById("exportFilter").value; + return { exportFilter }; + }, + }).then((result) => { + if (result.isConfirmed) { + const { exportFilter } = result.value; + + let filteredExportData; + if (exportFilter === "all") { + filteredExportData = filteredSchedules; + } else { + const verdictValue = parseInt(exportFilter, 10); + filteredExportData = filteredSchedules.filter( + (sched) => sched.verdict === verdictValue + ); } - }; - - return ( -
-

- Oral Defense Β» Scheduled Teams -

-
- -
-
- - + +
+
+
+ setSearch(e.target.value)} + /> + +
+
+ {isDeleteMode && ( + + )} + +
+
+
+ +
+ + + + {isDeleteMode && ( + + )} + + + + + + + + + + + + + {filteredSchedules.map((s, index) => ( + + {isDeleteMode && ( + + )} + + + + + + + + + + + ))} + {filteredSchedules.length === 0 && ( + + + + )} + +
+ { + if (e.target.checked) { + setSelectedSchedules( + filteredSchedules.map((s) => s.id) + ); + } else { + setSelectedSchedules([]); + } + }} + checked={ + selectedSchedules.length === filteredSchedules.length && + filteredSchedules.length > 0 + } + /> + + NO + + TEAM + + TITLE + + DATE + + TIME + + ADVISER + + PANELISTS + + VERDICT + + ACTION +
+ + handleCheckboxChange(s.id, e.target.checked) + } + /> + + {index + 1} + + {s.manager?.group_name || "-"} + + {s.title || "-"} + + {s.date} + + {s.time} + + {getAdviserName(s.adviser_id)} + + {[s.panelist1_id, s.panelist2_id, s.panelist3_id] + .map((pid) => { + const account = accounts.find((a) => a.id === pid); + return account + ? `${account.last_name}, ${account.first_name}` + : ""; + }) + .filter(Boolean) + .join(", ")} + + + + + + {openDropdown === index && ( +
- Export - -
-
-
- setSearch(e.target.value)} - /> - -
-
- {isDeleteMode && ( - - )} +
+ +
-
- - -
- - - - {isDeleteMode && ( - - )} - - - - - - - - - - - - - {filteredSchedules.map((s, index) => ( - - {isDeleteMode && ( - - )} - - - - - - - - - - - ))} - {filteredSchedules.length === 0 && ( - - - - )} - -
- { - if (e.target.checked) { - setSelectedSchedules(filteredSchedules.map(s => s.id)); - } else { - setSelectedSchedules([]); - } - }} - checked={selectedSchedules.length === filteredSchedules.length && filteredSchedules.length > 0} - /> - - NO - - TEAM - - TITLE - - DATE - - TIME - - ADVISER - - PANELISTS - - VERDICT - - ACTION -
- handleCheckboxChange(s.id, e.target.checked)} - /> - - {index + 1} - - {s.manager?.group_name || "-"} - - {s.title || "-"} - - {s.date} - - {s.time} - - {getAdviserName(s.adviser_id)} - - {[s.panelist1_id, s.panelist2_id, s.panelist3_id] - .map((pid) => { - const account = accounts.find((a) => a.id === pid); - return account ? `${account.last_name}, ${account.first_name}` : ""; - }) - .filter(Boolean) - .join(", ")} - - - - - - {/* Dropdown positioned close to the button */} - {openDropdown === index && ( -
-
- - -
-
- )} -
- No schedules found. -
-
- - ); + )} +
+ No schedules found. +
+
+
+ ); }; - -export default OralDefense; \ No newline at end of file + +export default OralDefense; diff --git a/src/components/Instructor/Schedule.jsx b/src/components/Instructor/Schedule.jsx index f9a87c2..853a916 100644 --- a/src/components/Instructor/Schedule.jsx +++ b/src/components/Instructor/Schedule.jsx @@ -9,7 +9,7 @@ const Schedule = ({ setActivePage }) => { { title: 'Title Defense', icon: , - onClick: () => setActivePage('Title Defense'), // βœ… Switches view in Dashboard + onClick: () => setActivePage('Title Defense'), }, { title: 'Manuscript Submission', diff --git a/src/components/Instructor/TitleDefense.jsx b/src/components/Instructor/TitleDefense.jsx index b567223..bb48bff 100644 --- a/src/components/Instructor/TitleDefense.jsx +++ b/src/components/Instructor/TitleDefense.jsx @@ -1,215 +1,230 @@ import { useState, useEffect } from "react"; import Swal from "sweetalert2"; import withReactContent from "sweetalert2-react-content"; -import { FaCalendarAlt, FaEllipsisV, FaSearch, FaTrash, FaFileExport, FaPen } from "react-icons/fa"; +import { + FaCalendarAlt, + FaEllipsisV, + FaSearch, + FaTrash, + FaFileExport, + FaPen, +} from "react-icons/fa"; import { supabase } from "../../supabaseClient"; // Import jsPDF import jsPDF from "jspdf"; - + const MySwal = withReactContent(Swal); - + const TitleDefense = () => { - const [teams, setTeams] = useState([]); - const [advisers, setAdvisers] = useState([]); - const [accounts, setAccounts] = useState([]); - const [schedules, setSchedules] = useState([]); - const [openDropdown, setOpenDropdown] = useState(null); - const [search, setSearch] = useState(""); - const [isDeleteMode, setIsDeleteMode] = useState(false); - const [selectedSchedules, setSelectedSchedules] = useState([]); - - const verdictMap = { - 1: "Pending", - 2: "Re-defense", - 3: "Approved", + const [teams, setTeams] = useState([]); + const [advisers, setAdvisers] = useState([]); + const [accounts, setAccounts] = useState([]); + const [schedules, setSchedules] = useState([]); + const [openDropdown, setOpenDropdown] = useState(null); + const [search, setSearch] = useState(""); + const [isDeleteMode, setIsDeleteMode] = useState(false); + const [selectedSchedules, setSelectedSchedules] = useState([]); + + const verdictMap = { + 1: "Pending", + 2: "Re-defense", + 3: "Approved", + }; + + useEffect(() => { + const fetchData = async () => { + const { data: accData, error: accError } = await supabase + .from("user_credentials") + .select("*"); + + if (accError) { + console.error("Error fetching accounts:", accError); + return; + } + + if (accData) { + setAccounts(accData); + const uniqueTeams = [ + ...new Set( + accData + .filter((d) => d.group_number !== null && d.group_name !== null) + .map((t) => t.group_name) + ), + ]; + setTeams(uniqueTeams); + setAdvisers( + accData.filter( + (a) => + a.user_roles === 3 || a.user_roles === 4 || a.user_roles === 5 + ) + ); + } + + const { data: schedData, error: schedError } = await supabase + .from("user_titledef") + .select("*"); + + if (schedError) { + console.error("Error fetching schedules:", schedError); + return; + } + + if (schedData) setSchedules(schedData); }; - - useEffect(() => { - const fetchData = async () => { - const { data: accData, error: accError } = await supabase - .from("user_credentials") - .select("*"); - - if (accError) { - console.error("Error fetching accounts:", accError); - return; - } - - if (accData) { - setAccounts(accData); - const uniqueTeams = [ - ...new Set( - accData - .filter((d) => d.group_number !== null && d.group_name !== null) - .map((t) => t.group_name) - ), - ]; - setTeams(uniqueTeams); - setAdvisers(accData.filter((a) => a.user_roles === 3 || a.user_roles === 4)); - - } - - const { data: schedData, error: schedError } = await supabase - .from("user_titledef") - .select("*"); - - if (schedError) { - console.error("Error fetching schedules:", schedError); - return; - } - - if (schedData) setSchedules(schedData); - }; - - fetchData(); - }, []); - const formatTime = (timeString) => { + + fetchData(); + }, []); + const formatTime = (timeString) => { if (!timeString) return "N/A"; const [hour, minute] = timeString.split(":"); let h = parseInt(hour, 10); const ampm = h >= 12 ? "PM" : "AM"; h = h % 12 || 12; // Convert to 12-hour format return `${h}:${minute} ${ampm}`; - }; - - // Move filteredSchedules calculation here, before handleExport function - const filteredSchedules = schedules - .filter((sched) => { - const teamName = accounts.find((a) => a.id === sched.manager_id)?.group_name || ""; - const panelists = [sched.panelist1_id, sched.panelist2_id, sched.panelist3_id] - .filter(Boolean) - .map((id) => { - const person = accounts.find((a) => a.id === id); - return person ? `${person.last_name}, ${person.first_name}` : "Unknown"; - }) - .join("; "); - - const verdict = verdictMap[sched.verdict] || "Pending"; - const searchText = search.toLowerCase(); - - return ( - teamName.toLowerCase().includes(searchText) || - (sched.date || "").toLowerCase().includes(searchText) || - (sched.time || "").toLowerCase().includes(searchText) || - panelists.toLowerCase().includes(searchText) || - verdict.toLowerCase().includes(searchText) - ); + }; + + // Move filteredSchedules calculation here, before handleExport function + const filteredSchedules = schedules.filter((sched) => { + const teamName = + accounts.find((a) => a.id === sched.manager_id)?.group_name || ""; + const panelists = [ + sched.panelist1_id, + sched.panelist2_id, + sched.panelist3_id, + ] + .filter(Boolean) + .map((id) => { + const person = accounts.find((a) => a.id === id); + return person ? `${person.last_name}, ${person.first_name}` : "Unknown"; + }) + .join("; "); + + const verdict = verdictMap[sched.verdict] || "Pending"; + const searchText = search.toLowerCase(); + + return ( + teamName.toLowerCase().includes(searchText) || + (sched.date || "").toLowerCase().includes(searchText) || + (sched.time || "").toLowerCase().includes(searchText) || + panelists.toLowerCase().includes(searchText) || + verdict.toLowerCase().includes(searchText) + ); + }); + + // Function to check time conflicts + const hasTimeConflict = (date, time, existingSchedules) => { + const newDateTime = new Date(`${date}T${time}`); + + for (const schedule of existingSchedules) { + if (schedule.date === date) { + const existingTime = new Date(`${schedule.date}T${schedule.time}`); + const timeDiff = Math.abs(newDateTime - existingTime) / (1000 * 60); // difference in minutes + + // Check if the time difference is less than 60 minutes (1 hour) + if (timeDiff < 60) { + return true; + } + } + } + return false; + }; + + const exportTitleDefenseAsPDF = (data) => { + const today = new Date().toLocaleDateString(); + const fileName = `title-defense-schedule-${today.replace(/\//g, "-")}.pdf`; + + // Create PDF using jsPDF + const doc = new jsPDF(); + + // Add header + doc.setFillColor(59, 3, 4); + doc.rect(0, 0, 210, 30, "F"); + doc.setTextColor(255, 255, 255); + doc.setFontSize(20); + doc.setFont("helvetica", "bold"); + doc.text("Title Defense Schedule Report", 105, 15, { align: "center" }); + + doc.setFontSize(10); + doc.setFont("helvetica", "normal"); + doc.text(`Generated on: ${today}`, 105, 22, { align: "center" }); + + // Reset text color for content + doc.setTextColor(0, 0, 0); + + // Add table headers + doc.setFillColor(59, 3, 4); + doc.rect(10, 35, 190, 10, "F"); + doc.setTextColor(255, 255, 255); + doc.setFontSize(8); + doc.setFont("helvetica", "bold"); + + const headers = ["NO", "TEAM", "DATE", "TIME", "PANELISTS", "VERDICT"]; + const columnWidths = [15, 35, 25, 25, 60, 30]; + let xPosition = 10; + + headers.forEach((header, index) => { + doc.text(header, xPosition + 2, 42); + xPosition += columnWidths[index]; }); - - // Function to check time conflicts - const hasTimeConflict = (date, time, existingSchedules) => { - const newDateTime = new Date(`${date}T${time}`); - - for (const schedule of existingSchedules) { - if (schedule.date === date) { - const existingTime = new Date(`${schedule.date}T${schedule.time}`); - const timeDiff = Math.abs(newDateTime - existingTime) / (1000 * 60); // difference in minutes - - // Check if the time difference is less than 60 minutes (1 hour) - if (timeDiff < 60) { - return true; - } - } + + // Add table rows + doc.setTextColor(0, 0, 0); + doc.setFont("helvetica", "normal"); + doc.setFontSize(7); + + let yPosition = 50; + + data.forEach((item, index) => { + if (yPosition > 270) { + doc.addPage(); + yPosition = 20; + } + + // Alternate row background + if (index % 2 === 0) { + doc.setFillColor(245, 245, 245); + doc.rect(10, yPosition - 4, 190, 6, "F"); + } + + xPosition = 10; + const rowData = [ + item.no.toString(), + item.team, + item.date, + item.time, + item.panelists, + item.verdict, + ]; + + rowData.forEach((cell, cellIndex) => { + // Wrap text for panelists column + if (cellIndex === 4) { + const lines = doc.splitTextToSize(cell, columnWidths[cellIndex] - 2); + doc.text(lines, xPosition + 1, yPosition); + } else { + doc.text(cell, xPosition + 1, yPosition); } - return false; - }; - - const exportTitleDefenseAsPDF = (data) => { - const today = new Date().toLocaleDateString(); - const fileName = `title-defense-schedule-${today.replace(/\//g, '-')}.pdf`; - - // Create PDF using jsPDF - const doc = new jsPDF(); - - // Add header - doc.setFillColor(59, 3, 4); - doc.rect(0, 0, 210, 30, 'F'); - doc.setTextColor(255, 255, 255); - doc.setFontSize(20); - doc.setFont('helvetica', 'bold'); - doc.text('Title Defense Schedule Report', 105, 15, { align: 'center' }); - - doc.setFontSize(10); - doc.setFont('helvetica', 'normal'); - doc.text(`Generated on: ${today}`, 105, 22, { align: 'center' }); - - // Reset text color for content - doc.setTextColor(0, 0, 0); - - // Add table headers - doc.setFillColor(59, 3, 4); - doc.rect(10, 35, 190, 10, 'F'); - doc.setTextColor(255, 255, 255); - doc.setFontSize(8); - doc.setFont('helvetica', 'bold'); - - const headers = ['NO', 'TEAM', 'DATE', 'TIME', 'PANELISTS', 'VERDICT']; - const columnWidths = [15, 35, 25, 25, 60, 30]; - let xPosition = 10; - - headers.forEach((header, index) => { - doc.text(header, xPosition + 2, 42); - xPosition += columnWidths[index]; - }); - - // Add table rows - doc.setTextColor(0, 0, 0); - doc.setFont('helvetica', 'normal'); - doc.setFontSize(7); - - let yPosition = 50; - - data.forEach((item, index) => { - if (yPosition > 270) { - doc.addPage(); - yPosition = 20; - } - - // Alternate row background - if (index % 2 === 0) { - doc.setFillColor(245, 245, 245); - doc.rect(10, yPosition - 4, 190, 6, 'F'); - } - - xPosition = 10; - const rowData = [ - item.no.toString(), - item.team, - item.date, - item.time, - item.panelists, - item.verdict - ]; - - rowData.forEach((cell, cellIndex) => { - // Wrap text for panelists column - if (cellIndex === 4) { - const lines = doc.splitTextToSize(cell, columnWidths[cellIndex] - 2); - doc.text(lines, xPosition + 1, yPosition); - } else { - doc.text(cell, xPosition + 1, yPosition); - } - xPosition += columnWidths[cellIndex]; - }); - - yPosition += 6; - }); - - // Add footer with total records - doc.setFontSize(8); - doc.text(`Total Records: ${data.length}`, 14, 285); - - // Save the PDF - doc.save(fileName); - }; - - const handleCreateSchedule = () => { - let selectedPanelists = []; - - MySwal.fire({ - title: `
+ xPosition += columnWidths[cellIndex]; + }); + + yPosition += 6; + }); + + // Add footer with total records + doc.setFontSize(8); + doc.text(`Total Records: ${data.length}`, 14, 285); + + // Save the PDF + doc.save(fileName); + }; + + const handleCreateSchedule = () => { + let selectedPanelists = []; + + MySwal.fire({ + title: `
Create Schedule
`, - html: ` + html: `