diff --git a/client/src/api/Timetable.js b/client/src/api/Timetable.js new file mode 100644 index 00000000..259b3a4b --- /dev/null +++ b/client/src/api/Timetable.js @@ -0,0 +1,17 @@ +import axios from 'axios'; +import serverRoot from './server'; + +const API = axios.create({ + baseURL: `${serverRoot}/api/examslots`, + withCredentials: true, +}); + +export const fetchExamSlot = async (course, branch, semester) => { + const { data } = await API.get(`/${course}/${branch}/${semester}`); + return data; +} + +export const createOrUpdateExamSlot = async (examSlotData) => { + const { data } = await API.post('/update', examSlotData); + return data; +} \ No newline at end of file diff --git a/client/src/screens/contributions/index.jsx b/client/src/screens/contributions/index.jsx index c38969aa..3fca2808 100644 --- a/client/src/screens/contributions/index.jsx +++ b/client/src/screens/contributions/index.jsx @@ -108,7 +108,7 @@ const Contributions = () => { ? "Upload files to this folder" : "Your files will be added to this folder"} -
+
{ Files require approval from a Branch Representative before becoming visible to other users.
-
- ) : ( + + ):( <> - )} + )} +
{isUploading ? "UPLOADING..." : "SUBMIT"} diff --git a/client/src/screens/contributions/styles.scss b/client/src/screens/contributions/styles.scss index 8468a9fc..b52b989f 100644 --- a/client/src/screens/contributions/styles.scss +++ b/client/src/screens/contributions/styles.scss @@ -174,4 +174,30 @@ .filepond--root { max-height: 20vh; +} + +.file_pond { + margin-top: 1rem; + + .large { + width: 100%; + height: 100%; + } + + &.large { + .filepond--root { + height: 00px; // make it taller + max-width: 100%; // stretch width + font-size: 1.1rem; // slightly bigger text + } + + .filepond--drop-label { + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2rem; + padding: 2rem; + } + } } \ No newline at end of file diff --git a/client/src/screens/dashboard/components/timetable/timetable.jsx b/client/src/screens/dashboard/components/timetable/timetable.jsx new file mode 100644 index 00000000..75b12bfd --- /dev/null +++ b/client/src/screens/dashboard/components/timetable/timetable.jsx @@ -0,0 +1,682 @@ +import React, { useState } from 'react'; +import './timetable.scss'; +import { createOrUpdateExamSlot, fetchExamSlot } from '../../../../api/Timetable'; // Adjust path as needed +import { useEffect } from 'react'; +import { toast } from 'react-toastify'; + +const Timetable = ({ user }) => { + const [showForm, setShowForm] = useState(false); + const [examSlots, setExamSlots] = useState({}); + const [formData, setFormData] = useState({ + A: '', B: '', C: '', D: '', E: '', F: '', G: '', + A1: '', B1: '', C1: '', D1: '', E1: '', F1: '', G1: '', + ML1: '', ML2: '', ML3: '', ML4: '', ML5: '', + AL1: '', AL2: '', AL3: '', AL4: '', AL5: '' + }); + const [overlaps, setOverlaps] = useState([]); + + console.log(examSlots); + + const timeSlots = [ + '8:00 AM', + '9:00 AM', + '10:00 AM', + '11:00 AM', + '12:00 PM', + '1:00 PM', + '2:00 PM', + '3:00 PM', + '4:00 PM', + '5:00 PM' + ]; + + const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']; + + // Define slot schedules for overlap detection + const slotSchedules = { + 'Monday': { + '8:00 AM': ['A'], + '9:00 AM': ['B', 'ML1'], + '10:00 AM': ['C', 'ML1'], + '11:00 AM': ['D', 'ML1'], + '12:00 PM': ['F'], + '1:00 PM': ['F1'], + '2:00 PM': ['D1', 'AL1'], + '3:00 PM': ['C1', 'AL1'], + '4:00 PM': ['B1', 'AL1'], + '5:00 PM': ['A1'] + }, + 'Tuesday': { + '8:00 AM': ['E'], + '9:00 AM': ['A', 'ML2'], + '10:00 AM': ['B', 'ML2'], + '11:00 AM': ['C', 'ML2'], + '12:00 PM': ['F'], + '1:00 PM': ['F1'], + '2:00 PM': ['C1', 'AL2'], + '3:00 PM': ['B1', 'AL2'], + '4:00 PM': ['A1', 'AL2'], + '5:00 PM': ['E1'] + }, + 'Wednesday': { + '8:00 AM': ['D'], + '9:00 AM': ['E', 'ML3'], + '10:00 AM': ['A', 'ML3'], + '11:00 AM': ['B', 'ML3'], + '12:00 PM': ['G'], + '1:00 PM': ['G1'], + '2:00 PM': ['B1', 'AL3'], + '3:00 PM': ['A1', 'AL3'], + '4:00 PM': ['E1', 'AL3'], + '5:00 PM': ['D1'] + }, + 'Thursday': { + '8:00 AM': ['C'], + '9:00 AM': ['D', 'ML4'], + '10:00 AM': ['E', 'ML4'], + '11:00 AM': ['A', 'ML4'], + '12:00 PM': ['G'], + '1:00 PM': ['G1'], + '2:00 PM': ['A1', 'AL4'], + '3:00 PM': ['E1', 'AL4'], + '4:00 PM': ['D1', 'AL4'], + '5:00 PM': ['C1'] + }, + 'Friday': { + '8:00 AM': ['B'], + '9:00 AM': ['C', 'ML5'], + '10:00 AM': ['D', 'ML5'], + '11:00 AM': ['F', 'ML5'], + '12:00 PM': ['G'], + '1:00 PM': ['G1'], + '2:00 PM': ['F1', 'AL5'], + '3:00 PM': ['D1', 'AL5'], + '4:00 PM': ['C1', 'AL5'], + '5:00 PM': ['B1'] + } + }; + + // Function to detect overlaps + const detectOverlaps = (currentFormData) => { + const foundOverlaps = []; + + Object.entries(slotSchedules).forEach(([day, daySchedule]) => { + Object.entries(daySchedule).forEach(([time, slots]) => { + // Check if multiple slots have courses assigned for the same time + const activeCourses = slots.filter(slot => { + const course = currentFormData[slot]; + return course && course.trim() !== ''; + }); + + if (activeCourses.length > 1) { + const courseDetails = activeCourses.map(slot => ({ + slot, + course: currentFormData[slot] + })); + + foundOverlaps.push({ + day, + time, + conflicts: courseDetails + }); + } + }); + }); + + return foundOverlaps; + }; + + // Generate timetable data based on examSlots + const generateTimetableData = () => { + const daySchedule = { + 'Monday': { + '8:00 AM': 'A', + '9:00 AM': examSlots.ML1 ? 'ML1' : 'B', + '10:00 AM': examSlots.ML1 ? 'ML1' : 'C', + '11:00 AM': examSlots.ML1 ? 'ML1' : 'D', + '12:00 PM': 'F', + '1:00 PM': 'F1', + '2:00 PM': examSlots.AL1 ? 'AL1' : 'D1', + '3:00 PM': examSlots.AL1 ? 'AL1' : 'C1', + '4:00 PM': examSlots.AL1 ? 'AL1' : 'B1', + '5:00 PM': 'A1' + }, + 'Tuesday': { + '8:00 AM': 'E', + '9:00 AM': examSlots.ML2 ? 'ML2' : 'A', + '10:00 AM': examSlots.ML2 ? 'ML2' : 'B', + '11:00 AM': examSlots.ML2 ? 'ML2' : 'C', + '12:00 PM': 'F', + '1:00 PM': 'F1', + '2:00 PM': examSlots.AL2 ? 'AL2' : 'C1', + '3:00 PM': examSlots.AL2 ? 'AL2' : 'B1', + '4:00 PM': examSlots.AL2 ? 'AL2' : 'A1', + '5:00 PM': 'E1' + }, + 'Wednesday': { + '8:00 AM': 'D', + '9:00 AM': examSlots.ML3 ? 'ML3' : 'E', + '10:00 AM': examSlots.ML3 ? 'ML3' : 'A', + '11:00 AM': examSlots.ML3 ? 'ML3' : 'B', + '12:00 PM': 'G', + '1:00 PM': 'G1', + '2:00 PM': examSlots.AL3 ? 'AL3' : 'B1', + '3:00 PM': examSlots.AL3 ? 'AL3' : 'A1', + '4:00 PM': examSlots.AL3 ? 'AL3' : 'E1', + '5:00 PM': 'D1' + }, + 'Thursday': { + '8:00 AM': 'C', + '9:00 AM': examSlots.ML4 ? 'ML4' : 'D', + '10:00 AM': examSlots.ML4 ? 'ML4' : 'E', + '11:00 AM': examSlots.ML4 ? 'ML4' : 'A', + '12:00 PM': 'G', + '1:00 PM': 'G1', + '2:00 PM': examSlots.AL4 ? 'AL4' : 'A1', + '3:00 PM': examSlots.AL4 ? 'AL4' : 'E1', + '4:00 PM': examSlots.AL4 ? 'AL4' : 'D1', + '5:00 PM': 'C1' + }, + 'Friday': { + '8:00 AM': 'B', + '9:00 AM': examSlots.ML5 ? 'ML5' : 'C', + '10:00 AM': examSlots.ML5 ? 'ML5' : 'D', + '11:00 AM': examSlots.ML5 ? 'ML5' : 'F', + '12:00 PM': 'G', + '1:00 PM': 'G1', + '2:00 PM': examSlots.AL5 ? 'AL5' : 'F1', + '3:00 PM': examSlots.AL5 ? 'AL5' : 'D1', + '4:00 PM': examSlots.AL5 ? 'AL5' : 'C1', + '5:00 PM': 'B1' + } + }; + + const timetableData = {}; + + days.forEach(day => { + timetableData[day] = {}; + Object.entries(daySchedule[day]).forEach(([time, slot]) => { + const courseCode = examSlots[slot]; + if (courseCode && courseCode.trim() !== '') { + timetableData[day][time] = { + subject: courseCode, + room: `Slot ${slot}`, + type: slot.includes('L') ? 'lab' : 'lecture' + }; + } + }); + }); + + return timetableData; + }; + + const timetableData = generateTimetableData(); + + const handleInputChange = (slot, value) => { + const newFormData = { + ...formData, + [slot]: value + }; + + setFormData(newFormData); + + // Detect overlaps in real-time + const currentOverlaps = detectOverlaps(newFormData); + setOverlaps(currentOverlaps); + }; + + const handleFormSubmit = async () => { + // Check for overlaps before submitting + const currentOverlaps = detectOverlaps(formData); + + if (currentOverlaps.length > 0) { + toast.error('Please resolve all schedule conflicts before saving!'); + return; + } + + try { + // Prepare the data for API request + const examSlotData = { + branch: user?.user?.department || user?.department, + semester: user?.user?.semester || user?.semester, + course: user?.user?.degree || user?.degree, + + // Regular slots + A: formData.A || '', + B: formData.B || '', + C: formData.C || '', + D: formData.D || '', + E: formData.E || '', + F: formData.F || '', + G: formData.G || '', + + A1: formData.A1 || '', + B1: formData.B1 || '', + C1: formData.C1 || '', + D1: formData.D1 || '', + E1: formData.E1 || '', + F1: formData.F1 || '', + G1: formData.G1 || '', + + // AL slots + AL1: formData.AL1 || '', + AL2: formData.AL2 || '', + AL3: formData.AL3 || '', + AL4: formData.AL4 || '', + AL5: formData.AL5 || '', + + // ML slots + ML1: formData.ML1 || '', + ML2: formData.ML2 || '', + ML3: formData.ML3 || '', + ML4: formData.ML4 || '', + ML5: formData.ML5 || '' + }; + + console.log('Submitting timetable data:', examSlotData); + + // Make API request + const response = await createOrUpdateExamSlot(examSlotData); + + console.log('Timetable updated successfully:', response); + toast.success('Timetable updated successfully!'); + + // Update examSlots with new data + setExamSlots(formData); + + // Clear overlaps and close form + setOverlaps([]); + setShowForm(false); + + } catch (error) { + console.error('Error updating timetable:', error); + toast.error('Failed to update timetable. Please try again.'); + } + }; + + const handleFormCancel = () => { + setShowForm(false); + setOverlaps([]); + // Reset form data to current examSlots values + setFormData({ + A: examSlots.A || '', + B: examSlots.B || '', + C: examSlots.C || '', + D: examSlots.D || '', + E: examSlots.E || '', + F: examSlots.F || '', + G: examSlots.G || '', + A1: examSlots.A1 || '', + B1: examSlots.B1 || '', + C1: examSlots.C1 || '', + D1: examSlots.D1 || '', + E1: examSlots.E1 || '', + F1: examSlots.F1 || '', + G1: examSlots.G1 || '', + ML1: examSlots.ML1 || '', + ML2: examSlots.ML2 || '', + ML3: examSlots.ML3 || '', + ML4: examSlots.ML4 || '', + ML5: examSlots.ML5 || '', + AL1: examSlots.AL1 || '', + AL2: examSlots.AL2 || '', + AL3: examSlots.AL3 || '', + AL4: examSlots.AL4 || '', + AL5: examSlots.AL5 || '' + }); + }; + + const getSlotColor = (slot) => { + const slotColors = { + // Regular slots - vibrant colors matching the course cards + 'A': '#C8A8E9', // light purple + 'B': '#FFB3D1', // light pink + 'C': '#87CEEB', // light blue + 'D': '#DDA0DD', // plum + 'E': '#F0E68C', // khaki + 'F': '#FFE4B5', // moccasin + 'G': '#98FB98', // pale green + + // Lab slots - slightly darker versions + 'A1': '#B19CD9', // darker purple + 'B1': '#FF9AC1', // darker pink + 'C1': '#6BB6DB', // darker blue + 'D1': '#CD88CD', // darker plum + 'E1': '#E6D87C', // darker khaki + 'F1': '#FFDAB9', // darker moccasin + 'G1': '#90EE90', // darker pale green + + // Morning lab slots - rich colors + 'ML1': '#9370DB', // medium slate blue + 'ML2': '#FF69B4', // hot pink + 'ML3': '#4682B4', // steel blue + 'ML4': '#DA70D6', // orchid + 'ML5': '#FFD700', // gold + + // Afternoon lab slots - warm colors + 'AL1': '#87CEEB', // light blue + 'AL2': '#DDA0DD', // plum + 'AL3': '#F0E68C', // khaki + 'AL4': '#FFE4B5', // moccasin + 'AL5': '#98FB98', // pale green + }; + return slotColors[slot] || '#f8f9fa'; + }; + + // Check if user is BR (you can adjust this condition based on your user object structure) + const isBR = user?.user?.isBR || user?.isBR; + + useEffect(() => { + const fetchData = async () => { + console.log(user); + console.log(user?.user?.degree, user?.user?.department,user?.user?.semester); + + var branch = user?.user?.department; + // replace spaces of branch with - + branch = branch.replace(/ /g, '-'); + + console.log(branch); + + const response = await fetchExamSlot(user?.user?.degree, branch,user?.user?.semester); + + setExamSlots(response); + + // Update formData with fetched exam slots + const newFormData = { + A: response.A || '', + B: response.B || '', + C: response.C || '', + D: response.D || '', + E: response.E || '', + F: response.F || '', + G: response.G || '', + A1: response.A1 || '', + B1: response.B1 || '', + C1: response.C1 || '', + D1: response.D1 || '', + E1: response.E1 || '', + F1: response.F1 || '', + G1: response.G1 || '', + ML1: response.ML1 || '', + ML2: response.ML2 || '', + ML3: response.ML3 || '', + ML4: response.ML4 || '', + ML5: response.ML5 || '', + AL1: response.AL1 || '', + AL2: response.AL2 || '', + AL3: response.AL3 || '', + AL4: response.AL4 || '', + AL5: response.AL5 || '' + }; + + setFormData(newFormData); + + // Check for existing overlaps + const initialOverlaps = detectOverlaps(newFormData); + setOverlaps(initialOverlaps); + + console.log(response); + }; + + fetchData(); + }, [user]); + + const getInputStyle = (slot) => { + // Check if this slot is part of any overlap + const isInOverlap = overlaps.some(overlap => + overlap.conflicts.some(conflict => conflict.slot === slot) + ); + + return { + width: '100%', + padding: '8px', + border: isInOverlap ? '2px solid #dc3545' : '1px solid #000000ff', + borderRadius: '4px', + fontSize: '14px', + backgroundColor: isInOverlap ? '#fff5f5' : 'white' + }; + }; + + return ( +
+ {/* BR Form Button and Modal */} + {isBR && ( +
+ +
+ )} + + {/* Form Modal */} + {showForm && ( +
+
+

Update Timetable Slots

+ + {/* Overlap Warning */} + {overlaps.length > 0 && ( +
+

⚠️ Schedule Conflicts Detected:

+ {overlaps.map((overlap, index) => ( +
+ {overlap.day} at {overlap.time}: +
    + {overlap.conflicts.map((conflict, i) => ( +
  • Slot {conflict.slot}: {conflict.course}
  • + ))} +
+
+ ))} +

+ Please remove courses from conflicting slots or clear one of the overlapping entries. +

+
+ )} + +
+
+ {/* Regular slots A-G */} +
+

Regular Slots

+ {['A', 'B', 'C', 'D', 'E', 'F', 'G'].map(slot => ( +
+
+ Slot {slot}: +
+ handleInputChange(slot, e.target.value)} + placeholder={`Enter course for slot ${slot}`} + style={getInputStyle(slot)} + /> +
+ ))} +
+ + {/* Lab slots A1-G1 */} +
+

Lab Slots

+ {['A1', 'B1', 'C1', 'D1', 'E1', 'F1', 'G1'].map(slot => ( +
+
+ Slot {slot}: +
+ handleInputChange(slot, e.target.value)} + placeholder={`Enter course for slot ${slot}`} + style={getInputStyle(slot)} + /> +
+ ))} +
+ + {/* ML slots ML1-ML5 */} +
+

Morning Lab Slots

+ {['ML1', 'ML2', 'ML3', 'ML4', 'ML5'].map(slot => ( +
+
+ {slot} (9-12): +
+ handleInputChange(slot, e.target.value)} + placeholder={`Enter course for ${slot}`} + style={getInputStyle(slot)} + /> +
+ ))} +
+ + {/* AL slots AL1-AL5 */} +
+

Afternoon Lab Slots

+ {['AL1', 'AL2', 'AL3', 'AL4', 'AL5'].map(slot => ( +
+
+ {slot} (2-5): +
+ handleInputChange(slot, e.target.value)} + placeholder={`Enter course for ${slot}`} + style={getInputStyle(slot)} + /> +
+ ))} +
+
+ +
+ + +
+
+
+
+ )} + + {/* Original Timetable */} +
+ {/* Header row with time slots */} +
+ {timeSlots.map((time) => ( +
+ {time} +
+ ))} + + {/* Day rows */} + {days.map((day) => ( + +
{day}
+ {timeSlots.map((time) => { + const classData = timetableData[day]?.[time]; + return ( +
+ {classData ? ( +
+
+ {classData.subject} +
+
+ {classData.room} +
+
+ ) : null} +
+ ); + })} +
+ ))} +
+
+ ); +}; + +export default Timetable; \ No newline at end of file diff --git a/client/src/screens/dashboard/components/timetable/timetable.scss b/client/src/screens/dashboard/components/timetable/timetable.scss new file mode 100644 index 00000000..7b3cdaa2 --- /dev/null +++ b/client/src/screens/dashboard/components/timetable/timetable.scss @@ -0,0 +1,154 @@ + + +// timetable.scss +.timetable-container { + width: 100%; + overflow-x: auto; + padding: 20px 0; + + .timetable-grid { + display: grid; + grid-template-columns: 120px repeat(10, 1fr); + // grid-gap: 1px; + background-color: #e0e0e0; + border: 1px solid #000000; + border-radius: 8px; + overflow: hidden; + min-width: 800px; + + .time-header { + background-color: #f5f5f5; + padding: 12px 8px; + font-weight: bold; + text-align: center; + // border-right: 1px solid #000000; + } + + .time-slot-header { + background-color: #f5f5f5; + padding: 12px 8px; + font-weight: bold; + text-align: center; + font-size: 0.9rem; + color: #333; + } + + .day-header { + background-color: #f0f0f0; + padding: 12px 8px; + font-weight: bold; + text-align: center; + color: #333; + // border-right: 1px solid #000000; + } + + .timetable-cell { + background-color: white; + border: 0.02rem solid #000000 !important; + padding: 8px; + min-height: 60px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + + &.empty { + background-color: #fafafa; + + &:hover { + background-color: #f0f0f0; + } + } + + &.occupied { + cursor: pointer; + + &:hover { + transform: scale(1.02); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + } + + .class-info { + text-align: center; + width: 100%; + + .subject-name { + font-weight: bold; + font-size: 0.85rem; + margin-bottom: 4px; + color: rgb(0, 0, 0); + } + + .room-info { + font-size: 0.7rem; + opacity: 0.9; + color: rgb(0, 0, 0); + } + } + + // Different colors for different class types + &.lecture { + background: linear-gradient(135deg, #4CAF50, #45a049); + } + + &.lab { + background: linear-gradient(135deg, #2196F3, #1976D2); + } + + &.practical { + background: linear-gradient(135deg, #FF9800, #F57C00); + } + + &.tutorial { + background: linear-gradient(135deg, #9C27B0, #7B1FA2); + } + + &.project { + background: linear-gradient(135deg, #E91E63, #C2185B); + } + + &.seminar { + background: linear-gradient(135deg, #607D8B, #455A64); + } + + &.study { + background: linear-gradient(135deg, #795548, #5D4037); + } + + &.default { + background: linear-gradient(135deg, #757575, #616161); + } + } + } + } +} + +// Mobile responsiveness +@media (max-width: 768px) { + .timetable-container { + .timetable-grid { + grid-template-columns: 80px repeat(10, minmax(80px, 1fr)); + + .time-slot-header, + .day-header { + font-size: 0.8rem; + padding: 8px 4px; + } + + .timetable-cell { + min-height: 50px; + padding: 4px; + + &.occupied .class-info { + .subject-name { + font-size: 0.75rem; + } + + .room-info { + font-size: 0.65rem; + } + } + } + } + } +} \ No newline at end of file diff --git a/client/src/screens/dashboard/index.jsx b/client/src/screens/dashboard/index.jsx index b9b8f431..ab5ee7a0 100644 --- a/client/src/screens/dashboard/index.jsx +++ b/client/src/screens/dashboard/index.jsx @@ -9,7 +9,7 @@ import CourseCard from "./components/coursecard"; import ContributionBanner from "./components/contributionbanner"; import Footer from "../../components/footer"; import FavouriteCard from "./components/favouritecard"; - +import Timetable from "./components/timetable/timetable.jsx"; import { ChangeCurrentCourse, ResetFileBrowserState } from "../../actions/filebrowser_actions"; import { useDispatch, useSelector } from "react-redux"; import { useNavigate } from "react-router-dom"; @@ -238,12 +238,13 @@ const Dashboard = () => { text={ showPrevious ? "▼ HIDE PREVIOUS COURSES" - : "▶ SHOW PREVIOUS COURSES" + : "▶️ SHOW PREVIOUS COURSES" } color={"light"} type={"bold"} />
+ {showPrevious && ( <> @@ -268,12 +269,19 @@ const Dashboard = () => { )} + )} + + + + + +
@@ -306,4 +314,4 @@ const Dashboard = () => { ); }; -export default Dashboard; +export default Dashboard; \ No newline at end of file diff --git a/client/yarn.lock b/client/yarn.lock index bbc033e4..a4ff28eb 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -492,6 +492,11 @@ electron-to-chromium@^1.4.251: resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz" integrity sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA== +esbuild-darwin-arm64@0.15.16: + version "0.15.16" + resolved "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.16.tgz" + integrity sha512-fMXaUr5ou0M4WnewBKsspMtX++C1yIa3nJ5R2LSbLCfJT3uFdcRoU/NZjoM4kOMKyOD9Sa/2vlgN8G07K3SJnw== + esbuild@^0.15.9: version "0.15.16" resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.15.16.tgz" @@ -561,6 +566,11 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" diff --git a/server/index.js b/server/index.js index e0d8c9eb..cdf978b4 100644 --- a/server/index.js +++ b/server/index.js @@ -31,6 +31,7 @@ import brRoutes from "./modules/br/br.routes.js"; import fileRoutes from "./modules/file/file.routes.js"; import folderRoutes from "./modules/folder/folder.routes.js"; import yearRoutes from "./modules/year/year.routes.js"; +import examSlotsRoutes from "./modules/exam-slots/exam-slots.routes.js"; import links from "./links.js"; const app = express(); @@ -75,6 +76,7 @@ app.use("/api/br", brRoutes); app.use("/api/files", fileRoutes); app.use("/api/folder", folderRoutes); app.use("/api/year", yearRoutes); +app.use("/api/examslots", examSlotsRoutes); app.use( "/homepage", diff --git a/server/modules/exam-slots/exam-slots.controller.js b/server/modules/exam-slots/exam-slots.controller.js new file mode 100644 index 00000000..44550b8b --- /dev/null +++ b/server/modules/exam-slots/exam-slots.controller.js @@ -0,0 +1,48 @@ +import express from 'express'; +import ExamSlotModel from './exam-slots.model.js'; + +//check if exam slot exists, if it does edit it, if not create it +export const createOrUpdateExamSlot = async (req, res) => { + try { + const {course} = req.body; + const { branch } = req.body; + const {semester} = req.body; + let examSlot = await ExamSlotModel.findOne({course, branch, semester }); + if (examSlot) { + //update + examSlot = await ExamSlotModel.findOneAndUpdate( + {course, branch, semester }, + { $set: req.body }); + return res.status(200).json({ message: 'Exam slot updated successfully', examSlot }); + } else { + //create + examSlot = new ExamSlotModel(req.body); + await examSlot.save(); + return res.status(201).json({ message: 'Exam slot created successfully', examSlot }); + } + } catch (error) { + console.error('Error in createOrUpdateExamSlot:', error); + return res.status(500).json({ message: 'Server error', error: error.message }); + } +}; + +export const getExamSlot = async (req, res) => { + try { + const {course, branch, semester } = req.params; + + const branchFixed = branch.replace(/-/g, ' '); + const examSlot = await ExamSlotModel.findOne({course, branchFixed, semester }); + if (!examSlot) { + return res.status(404).json({ message: 'Exam slot not found' }); + } + return res.status(200).json(examSlot); + } catch (error) { + console.error('Error in getExamSlot:', error); + return res.status(500).json({ message: 'Server error', error: error.message }); + } +}; + +export default { + createOrUpdateExamSlot, + getExamSlot +} \ No newline at end of file diff --git a/server/modules/exam-slots/exam-slots.model.js b/server/modules/exam-slots/exam-slots.model.js new file mode 100644 index 00000000..f8654cca --- /dev/null +++ b/server/modules/exam-slots/exam-slots.model.js @@ -0,0 +1,52 @@ +import mongoose from 'mongoose'; +const { Schema, model } = mongoose; + +const courseSlotSchema = { + course: { + type: String, + required: true, // still required (probably needed for uniqueness) + }, + branch: { + type: String, + required: true, // still required + }, + semester: { + type: Number, + required: true, // still required + }, + + // Regular slots + A: { type: String, required: false }, + A1: { type: String, required: false }, + B: { type: String, required: false }, + B1: { type: String, required: false }, + C: { type: String, required: false }, + C1: { type: String, required: false }, + D: { type: String, required: false }, + D1: { type: String, required: false }, + E: { type: String, required: false }, + E1: { type: String, required: false }, + F: { type: String, required: false }, + F1: { type: String, required: false }, + G: { type: String, required: false }, + G1: { type: String, required: false }, + + // ML slots + ML1: { type: String, required: false }, + ML2: { type: String, required: false }, + ML3: { type: String, required: false }, + ML4: { type: String, required: false }, + ML5: { type: String, required: false }, + + // AL slots + AL1: { type: String, required: false }, + AL2: { type: String, required: false }, + AL3: { type: String, required: false }, + AL4: { type: String, required: false }, + AL5: { type: String, required: false }, +}; + +const courseSlotModel = new Schema(courseSlotSchema, { timestamps: true }); + +const ExamSlotModel = model('ExamSlot', courseSlotModel); +export default ExamSlotModel; \ No newline at end of file diff --git a/server/modules/exam-slots/exam-slots.routes.js b/server/modules/exam-slots/exam-slots.routes.js new file mode 100644 index 00000000..ff3fc3d2 --- /dev/null +++ b/server/modules/exam-slots/exam-slots.routes.js @@ -0,0 +1,9 @@ +import express from 'express'; +import { createOrUpdateExamSlot, getExamSlot } from './exam-slots.controller.js'; + +const router = express.Router(); + +router.post('/update', createOrUpdateExamSlot); +router.get('/:course/:branch/:semester', getExamSlot); + +export default router; diff --git a/server/yarn.lock b/server/yarn.lock index 586fa22f..4f24dbbb 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -2040,6 +2040,11 @@ fs.realpath@^1.0.0: resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + function-bind@^1.1.1, function-bind@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"