From 9f97c7d4feab08e96bd75c842215b1dfc9cf9c35 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Oct 2025 18:37:37 +0000 Subject: [PATCH 1/2] Initial plan From 40e3a641c1a8e03af16e6112e377d666fd949265 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Oct 2025 18:47:26 +0000 Subject: [PATCH 2/2] Add SubjectTypes CRUD UI with Tailwind CSS Co-authored-by: Uttam-Mahata <92252205+Uttam-Mahata@users.noreply.github.com> --- client/src/App.tsx | 2 + client/src/components/Layout/MainLayout.tsx | 1 + .../SubjectTypes/SubjectTypeFormDialog.tsx | 203 ++++++++++++++++++ .../pages/SubjectTypes/SubjectTypeList.tsx | 197 +++++++++++++++++ 4 files changed, 403 insertions(+) create mode 100644 client/src/pages/SubjectTypes/SubjectTypeFormDialog.tsx create mode 100644 client/src/pages/SubjectTypes/SubjectTypeList.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index e23d698..4c6c81c 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -5,6 +5,7 @@ import ProgrammeList from './pages/Programmes/ProgrammeList'; import DepartmentList from './pages/Departments/DepartmentList'; import TeacherList from './pages/Teachers/TeacherList'; import SubjectList from './pages/Subjects/SubjectList'; +import SubjectTypeList from './pages/SubjectTypes/SubjectTypeList'; import RoomList from './pages/Rooms/RoomList'; import SessionList from './pages/Sessions/SessionList'; import SemesterOfferingList from './pages/SemesterOfferings/SemesterOfferingList'; @@ -20,6 +21,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/client/src/components/Layout/MainLayout.tsx b/client/src/components/Layout/MainLayout.tsx index 53a46f3..186d1e5 100644 --- a/client/src/components/Layout/MainLayout.tsx +++ b/client/src/components/Layout/MainLayout.tsx @@ -13,6 +13,7 @@ const menuItems: MenuItem[] = [ { text: 'Departments', icon: '🏢', path: '/departments' }, { text: 'Teachers', icon: '👨‍🏫', path: '/teachers' }, { text: 'Subjects', icon: '📚', path: '/subjects' }, + { text: 'Subject Types', icon: '🏷️', path: '/subject-types' }, { text: 'Rooms', icon: '🚪', path: '/rooms' }, { text: 'Sessions', icon: '📅', path: '/sessions' }, { text: 'Semester Offerings', icon: '📋', path: '/semester-offerings' }, diff --git a/client/src/pages/SubjectTypes/SubjectTypeFormDialog.tsx b/client/src/pages/SubjectTypes/SubjectTypeFormDialog.tsx new file mode 100644 index 0000000..730c4cd --- /dev/null +++ b/client/src/pages/SubjectTypes/SubjectTypeFormDialog.tsx @@ -0,0 +1,203 @@ +import React, { useState, useEffect } from 'react'; +import { Dialog, DialogTitle, DialogContent, DialogActions } from '@mui/material'; +import { type SubjectType } from '../../types/models'; +import { subjectService, type CreateSubjectTypeRequest, type UpdateSubjectTypeRequest } from '../../services/subjectService'; + +interface SubjectTypeFormDialogProps { + open: boolean; + subjectType: SubjectType | null; + onClose: () => void; + onSubmit: () => void; +} + +const SubjectTypeFormDialog: React.FC = ({ + open, + subjectType, + onClose, + onSubmit, +}) => { + const [formData, setFormData] = useState({ + name: '', + is_lab: false, + default_consecutive_preferred: true, + }); + const [errors, setErrors] = useState>({}); + const [submitting, setSubmitting] = useState(false); + const [submitError, setSubmitError] = useState(null); + + useEffect(() => { + if (subjectType) { + setFormData({ + name: subjectType.name, + is_lab: subjectType.is_lab, + default_consecutive_preferred: subjectType.default_consecutive_preferred, + }); + } else { + setFormData({ + name: '', + is_lab: false, + default_consecutive_preferred: true, + }); + } + setErrors({}); + setSubmitError(null); + }, [subjectType, open]); + + const validateForm = (): boolean => { + const newErrors: Record = {}; + + if (!formData.name.trim()) { + newErrors.name = 'Subject type name is required'; + } else if (formData.name.length > 100) { + newErrors.name = 'Subject type name must be 100 characters or less'; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!validateForm()) return; + + setSubmitting(true); + setSubmitError(null); + + try { + if (subjectType) { + const updateData: UpdateSubjectTypeRequest = formData; + await subjectService.updateSubjectType(subjectType.id, updateData); + } else { + const createData: CreateSubjectTypeRequest = formData; + await subjectService.createSubjectType(createData); + } + onSubmit(); + } catch (err) { + setSubmitError(err instanceof Error ? err.message : 'Failed to save subject type'); + } finally { + setSubmitting(false); + } + }; + + const handleChange = (field: string, value: any) => { + setFormData(prev => ({ ...prev, [field]: value })); + if (errors[field]) { + setErrors(prev => ({ ...prev, [field]: '' })); + } + }; + + return ( + + + {subjectType ? 'Edit Subject Type' : 'Add New Subject Type'} + + +
+ {submitError && ( +
+
+
+ + + +
+
+

{submitError}

+
+
+ +
+
+
+ )} + +
+ + handleChange('name', e.target.value)} + className={`mt-1 block w-full rounded-md shadow-sm sm:text-sm ${ + errors.name + ? 'border-red-300 focus:border-red-500 focus:ring-red-500' + : 'border-gray-300 focus:border-blue-500 focus:ring-blue-500' + }`} + placeholder="e.g., Theory, Lab, Practical" + maxLength={100} + required + /> + {errors.name && ( +

{errors.name}

+ )} +
+ +
+
+ handleChange('is_lab', e.target.checked)} + className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" + /> + +
+

+ Check this if this subject type represents laboratory courses (requires 3 consecutive slots) +

+ +
+ handleChange('default_consecutive_preferred', e.target.checked)} + className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" + /> + +
+

+ When scheduling, prefer consecutive time slots for this subject type +

+
+
+
+ + + + +
+ ); +}; + +export default SubjectTypeFormDialog; diff --git a/client/src/pages/SubjectTypes/SubjectTypeList.tsx b/client/src/pages/SubjectTypes/SubjectTypeList.tsx new file mode 100644 index 0000000..0ccd6d6 --- /dev/null +++ b/client/src/pages/SubjectTypes/SubjectTypeList.tsx @@ -0,0 +1,197 @@ +import React, { useState, useEffect } from 'react'; +import { subjectService } from '../../services/subjectService'; +import { type SubjectType } from '../../types/models'; +import SubjectTypeFormDialog from './SubjectTypeFormDialog'; +import LoadingSpinner from '../../components/Common/LoadingSpinner'; +import ErrorAlert from '../../components/Common/ErrorAlert'; +import ConfirmDialog from '../../components/Common/ConfirmDialog'; +import { Add as Plus, Edit as Edit2, Delete as Trash2, Book as BookOpen, Science as Flask } from '@mui/icons-material'; + +const SubjectTypeList: React.FC = () => { + const [subjectTypes, setSubjectTypes] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [openForm, setOpenForm] = useState(false); + const [selectedType, setSelectedType] = useState(null); + const [deleteDialog, setDeleteDialog] = useState<{ + open: boolean; + type: SubjectType | null; + }>({ open: false, type: null }); + + useEffect(() => { + fetchData(); + }, []); + + const fetchData = async () => { + try { + setLoading(true); + setError(null); + const data = await subjectService.getSubjectTypes(); + setSubjectTypes(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch subject types'); + } finally { + setLoading(false); + } + }; + + const handleAdd = () => { + setSelectedType(null); + setOpenForm(true); + }; + + const handleEdit = (type: SubjectType) => { + setSelectedType(type); + setOpenForm(true); + }; + + const handleDelete = async () => { + if (!deleteDialog.type) return; + + try { + await subjectService.deleteSubjectType(deleteDialog.type.id); + await fetchData(); + setDeleteDialog({ open: false, type: null }); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to delete subject type'); + setDeleteDialog({ open: false, type: null }); + } + }; + + const handleFormClose = () => { + setOpenForm(false); + setSelectedType(null); + }; + + const handleFormSubmit = async () => { + await fetchData(); + handleFormClose(); + }; + + if (loading) { + return ; + } + + return ( +
+ {/* Header */} +
+
+

Subject Types

+

+ Manage subject types such as Theory, Lab, and other course categories +

+
+ +
+ + {/* Error Alert */} + {error && setError(null)} />} + + {/* Subject Types Grid */} +
+ {subjectTypes.map((type) => ( +
+
+
+
+ {type.is_lab ? ( + + ) : ( + + )} +
+
+ + +
+
+ +

{type.name}

+ +
+
+ Type: + + {type.is_lab ? 'Lab' : 'Theory'} + +
+ +
+ Consecutive Preferred: + + {type.default_consecutive_preferred ? 'Yes' : 'No'} + +
+
+
+
+ ))} +
+ + {/* Empty State */} + {subjectTypes.length === 0 && !loading && ( +
+ +

No subject types

+

Get started by creating a new subject type.

+
+ +
+
+ )} + + {/* Form Dialog */} + + + {/* Delete Confirmation Dialog */} + setDeleteDialog({ open: false, type: null })} + confirmText="Delete" + cancelText="Cancel" + /> +
+ ); +}; + +export default SubjectTypeList;