diff --git a/docs/notification-enhancement-scope.md b/docs/notification-enhancement-scope.md
new file mode 100644
index 000000000..016d5f09f
--- /dev/null
+++ b/docs/notification-enhancement-scope.md
@@ -0,0 +1,14 @@
+# Notification Enhancement Scope Decision
+
+## Decision
+Starred notifications and advanced filtering/sorting are treated as enhancement features beyond baseline UC coverage.
+
+## UC Catalog Handling
+- Baseline UC remains: view notifications, read/unread toggle, delete.
+- Enhancement feature status: retained as optional UX improvements.
+- No blocking dependency for Assignment 7 requirement-driven completion.
+
+## Rationale
+- Assignment 7 focuses on missing/partial/incorrect UC-BR-WF items.
+- Notification enhancement does not block core workflow correctness.
+- Keeping enhancements separate avoids scope creep while preserving future roadmap value.
diff --git a/docs/role-permission-matrix.md b/docs/role-permission-matrix.md
new file mode 100644
index 000000000..63cd33837
--- /dev/null
+++ b/docs/role-permission-matrix.md
@@ -0,0 +1,25 @@
+# Role Permission Matrix for Database Dashboard
+
+## Scope
+This matrix documents role-based access for Assignment 7 database sprint workflows.
+
+## Modules and Roles
+
+| Capability | Student | Faculty | Staff | Acadadmin |
+|---|---|---|---|---|
+| Access Database module | Yes (if module access enabled) | Yes (if module access enabled) | Yes (if module access enabled) | Yes |
+| View Issues list | Yes | Yes | Yes | Yes |
+| Create Issue | Yes | Yes | Yes | Yes |
+| Edit own open Issue | Yes | Yes | Yes | Yes |
+| Edit closed Issue | No | No | No | No |
+| Support Issue (non-owner) | Yes | Yes | Yes | Yes |
+| Support own Issue | No | No | No | No |
+| Submit/Update own Feedback | Yes | Yes | Yes | Yes |
+| View top feedback entries | Yes | Yes | Yes | Yes |
+| Search users (q length >= 3) | Yes | Yes | Yes | Yes |
+
+## Enforcement Points
+- Backend issue support owner-block is enforced in globals API and legacy endpoint.
+- Closed issue edit block is enforced in backend update handlers.
+- Search minimum length is validated in both frontend and backend.
+- Role and accessible module payload validation is enforced in frontend auth bootstrap.
diff --git a/src/Modules/Dashboard/Dashboard.module.css b/src/Modules/Dashboard/Dashboard.module.css
index 58e230783..fb630f17a 100644
--- a/src/Modules/Dashboard/Dashboard.module.css
+++ b/src/Modules/Dashboard/Dashboard.module.css
@@ -6,10 +6,9 @@
}
.fusionCaretCircleIcon {
- font-size: 2rem;
- @media (min-width: mantine-breakpoint-md) {
- font-size: 1.5rem;
- }
+ width: 24px;
+ height: 24px;
+ color: #228be6;
}
.fusionCaretIcon {
@@ -30,42 +29,169 @@
flex-shrink: 1;
max-width: 80vw;
overflow-x: auto;
- overflow-y: hidden;
- scroll-behavior: smooth;
+ -ms-overflow-style: none;
}
-/* Enhanced tab list scrolling styles */
-.fusionTabsList {
- display: flex;
- flex-wrap: nowrap;
+.fusionTabsContainer::-webkit-scrollbar {
+ display: none;
+}
+
+.selectinputs {
+ background-color: #15abff20;
+ border-color: #15abff4d;
+}
+
+.selectinputs::placeholder {
+ color: #15abff;
+ font-weight: 600;
+}
+
+.selectoptions:hover {
+ background-color: #15abff10;
+}
+
+.tabsContainer {
+ flex: 1;
+ overflow: hidden;
+ margin: 0 4px;
+}
+
+.activeTab {
+ color: #228be6 !important;
+ border-color: #228be6 !important;
+}
+
+
+.tabsWrapper {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ max-width: 100%;
+ position: relative;
+ padding: 0 4px;
+}
+
+.tabsScrollArea {
overflow-x: auto;
- overflow-y: hidden;
- scroll-behavior: smooth;
- gap: 4px;
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+ max-width: calc(100% - 70px);
+ white-space: nowrap;
+}
+
+.tabsScrollArea::-webkit-scrollbar {
+ display: none;
+}
+
+.navigationButton {
+ flex-shrink: 0;
+ background: transparent;
+ border: none;
+ width: 32px;
+ height: 32px;
padding: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ z-index: 1;
}
-.fusionTabItem {
+.tabsList {
+ display: inline-flex;
+ white-space: nowrap;
+ flex-wrap: nowrap;
min-width: fit-content;
+ border-bottom: 1px solid #e9ecef;
+ gap: 8px;
+ padding: 0 4px;
+}
+
+.tabsList [role="tab"] {
white-space: nowrap;
flex-shrink: 0;
+}
+
+.activeTab {
+ color: #228be6 !important;
+ border-color: #228be6 !important;
+}
+
+
+@media (max-width: 1200px) {
+ .tabsScrollArea {
+ max-width: calc(100% - 80px);
+ }
+}
+
+@media (max-width: 768px) {
+ .tabsWrapper {
+ gap: 4px;
+ padding: 0 2px;
+ }
+
+ .tabsScrollArea {
+ max-width: calc(100% - 70px);
+ min-width: 150px;
+ }
+
+ .navigationButton {
+ width: 28px;
+ height: 28px;
+ padding: 2px;
+ }
+
+ .fusionCaretCircleIcon {
+ width: 20px;
+ height: 20px;
+ }
+
+ .tabsList {
+ gap: 4px;
+ padding: 0 2px;
+ }
+
+ .notificationActions {
+ margin-top: 10px;
+ justify-content: flex-start;
+ }
+}
+
+@media (max-width: 480px) {
+ .tabsScrollArea {
+ max-width: calc(100% - 60px);
+ min-width: 120px;
+ }
+
+ .navigationButton {
+ width: 24px;
+ height: 24px;
+ }
+
+ .fusionCaretCircleIcon {
+ width: 18px;
+ height: 18px;
+ }
+}
+
+
+.notificationItem {
+ position: relative;
transition: all 0.2s ease;
}
-.fusionTabItem:hover {
- transform: translateY(-1px);
+.notificationItem:hover {
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
-.selectinputs {
- background-color: #15abff20;
- border-color: #15abff4d;
+.starButton {
+ transition: transform 0.2s ease;
}
-.selectinputs::placeholder {
- color: #15abff;
- font-weight: 600;
+.starButton:hover {
+ transform: scale(1.1);
}
-.selectoptions:hover {
- background-color: #15abff10; /* Transparent blue hover background */
+.starredNotification {
+ background-color: transparent;
}
diff --git a/src/Modules/Dashboard/StudentProfile/achievementsComponent.jsx b/src/Modules/Dashboard/StudentProfile/achievementsComponent.jsx
index eeddf91d5..743303d70 100644
--- a/src/Modules/Dashboard/StudentProfile/achievementsComponent.jsx
+++ b/src/Modules/Dashboard/StudentProfile/achievementsComponent.jsx
@@ -1,21 +1,37 @@
-import { useState } from "react";
+import { useEffect, useState } from "react";
import PropTypes from "prop-types";
-import {
- Flex,
- Input,
- Divider,
- Text,
- Button,
- Select,
- Textarea,
- Table,
-} from "@mantine/core";
-import axios from "axios";
+import { Flex, Divider, Text } from "@mantine/core";
import { notifications } from "@mantine/notifications";
-import { updateProfileDataRoute } from "../../../routes/dashboardRoutes";
+import { useFormState } from "../utils/formHelpers";
+import { updateProfileSection } from "../services/profileService";
+import AchievementForm from "../components/forms/AchievementForm";
+import AchievementsTable from "../components/tables/AchievementsTable";
+
+const getAchievementType = (value) => {
+ const normalized = String(value || "Other").trim().toUpperCase();
+ if (normalized === "EDUCATIONAL") return "EDUCATIONAL";
+ return "OTHER";
+};
+
+const formatApiError = (error) => {
+ const data = error?.response?.data;
+ if (typeof data === "string") return data;
+ if (data?.error) return data.error;
+ if (data && typeof data === "object") {
+ const firstFieldError = Object.values(data).flat()[0];
+ if (firstFieldError) return firstFieldError;
+ }
+ return "Error adding achievement";
+};
function AchievementsComponent({ achievements }) {
- const [achievement, setAchievement] = useState({
+ const [achievementsList, setAchievementsList] = useState(achievements || []);
+
+ useEffect(() => {
+ setAchievementsList(achievements || []);
+ }, [achievements]);
+
+ const { formData: achievement, handleFieldChange, resetForm } = useFormState({
skill: "",
type: "Educational",
date: "",
@@ -23,42 +39,52 @@ function AchievementsComponent({ achievements }) {
description: "",
});
- const handleChange = (field, value) => {
- setAchievement((prev) => ({ ...prev, [field]: value }));
- };
-
const handleSubmit = async () => {
try {
- const response = await axios.put(
- updateProfileDataRoute,
- {
- achievementsubmit: {
- skill: achievement.skill,
- type: achievement.type,
- date: achievement.date,
- issuer: achievement.issuer,
- description: achievement.description,
- },
- },
- {
- headers: {
- Authorization: `Token ${localStorage.getItem("authToken")}`,
- },
- },
- );
- console.log(response);
+ const achievementName = String(achievement.skill || "").trim();
+ if (!achievementName) {
+ notifications.show({
+ message: "Achievement name is required.",
+ color: "yellow",
+ });
+ return;
+ }
+
+ const payload = {
+ achievement: achievementName,
+ achievement_type: getAchievementType(achievement.type),
+ issuer: String(achievement.issuer || "").trim(),
+ description: String(achievement.description || "").trim(),
+ };
+
+ if (achievement.date) {
+ payload.date_earned = achievement.date;
+ }
+
+ const response = await updateProfileSection({
+ achievementsubmit: payload,
+ });
+
+ const createdAchievement = response?.data?.id
+ ? response.data
+ : payload;
+
+ setAchievementsList((prev) => [...prev, createdAchievement]);
+
+ resetForm();
notifications.show({
message: "Achievement added successfully!",
color: "green",
});
} catch (error) {
- alert("Error adding achievement");
+ notifications.show({
+ message: formatApiError(error),
+ color: "red",
+ });
}
};
- console.log(achievements);
-
return (
Add a new achievement
-
-
- handleChange("skill", e.target.value)}
- />
-
-
-
-
-
-
- handleChange("date", e.target.value)}
- />
-
-
- handleChange("issuer", e.target.value)}
- />
-
-
-
-
-
-
-
+
Your Achievements
- {achievements.length > 0 ? (
-
-
-
- Type
- Date
- Issuer
- Description
-
-
-
- {achievements.map((ach, index) => (
-
-
- {ach.achievement_type}
-
-
- {ach.date_earned}
-
-
- {ach.issuer}
-
-
- {ach.description}
-
-
- ))}
-
-
- ) : (
- No achievements added yet.
- )}
+
);
@@ -193,13 +128,17 @@ function AchievementsComponent({ achievements }) {
AchievementsComponent.propTypes = {
achievements: PropTypes.arrayOf(
PropTypes.shape({
- skill: PropTypes.string,
- type: PropTypes.string,
- date: PropTypes.string,
+ id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ achievement_type: PropTypes.string,
+ date_earned: PropTypes.string,
issuer: PropTypes.string,
description: PropTypes.string,
}),
),
};
+AchievementsComponent.defaultProps = {
+ achievements: [],
+};
+
export default AchievementsComponent;
diff --git a/src/Modules/Dashboard/StudentProfile/educationCoursesComponent.jsx b/src/Modules/Dashboard/StudentProfile/educationCoursesComponent.jsx
index 00aef867d..9840d5186 100644
--- a/src/Modules/Dashboard/StudentProfile/educationCoursesComponent.jsx
+++ b/src/Modules/Dashboard/StudentProfile/educationCoursesComponent.jsx
@@ -1,21 +1,16 @@
-import { useState } from "react";
+import { useEffect, useState } from "react";
import PropTypes from "prop-types";
-import {
- Flex,
- Input,
- Tabs,
- Text,
- Button,
- Textarea,
- Table,
- Divider,
-} from "@mantine/core";
-import { notifications, Notifications } from "@mantine/notifications";
-import axios from "axios";
-import { updateProfileDataRoute } from "../../../routes/dashboardRoutes";
+import { Flex, Tabs, Text, Divider } from "@mantine/core";
+import { notifications } from "@mantine/notifications";
+import { useFormState } from "../utils/formHelpers";
+import { updateProfileSection } from "../services/profileService";
+import EducationForm from "../components/forms/EducationForm";
+import CourseForm from "../components/forms/CourseForm";
+import EducationTable from "../components/tables/EducationTable";
+import CoursesTable from "../components/tables/CoursesTable";
-function EducationTab({ educationData }) {
- const [formData, setFormData] = useState({
+function EducationTab({ educationData, onAddEducation }) {
+ const { formData, handleInputChange, resetForm } = useFormState({
degree: "",
stream: "",
institute: "",
@@ -24,39 +19,42 @@ function EducationTab({ educationData }) {
end_date: "",
});
- const handleChange = (e) => {
- setFormData({ ...formData, [e.target.name]: e.target.value });
- };
-
const handleSubmit = async () => {
try {
- await axios.put(
- updateProfileDataRoute,
- { education: formData },
- {
- headers: {
- Authorization: `Token ${localStorage.getItem("authToken")}`,
- },
- },
- );
+ const payload = {
+ degree: formData.degree,
+ stream: formData.stream,
+ institute: formData.institute,
+ grade: formData.grade,
+ };
+
+ if (formData.start_date) {
+ payload.sdate = formData.start_date;
+ }
+ if (formData.end_date) {
+ payload.edate = formData.end_date;
+ }
+
+ const response = await updateProfileSection({ education: payload });
+ const createdEducation = response?.data?.id
+ ? response.data
+ : {
+ ...formData,
+ sdate: formData.start_date,
+ edate: formData.end_date,
+ };
+
+ onAddEducation(createdEducation);
notifications.show({
message: "Education Added Successfully!",
color: "green",
});
- setFormData({
- degree: "",
- stream: "",
- institute: "",
- grade: "",
- start_date: "",
- end_date: "",
- });
+ resetForm();
} catch (error) {
notifications.show({
message: "Failed! Please try later.",
color: "red",
});
- console.error("Error updating education:", error);
}
};
@@ -70,121 +68,22 @@ function EducationTab({ educationData }) {
Add a New Educational Qualification
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
Your Educations
- {educationData.length > 0 ? (
-
-
-
- Degree
- Stream
- Institute
- Grade
- Start Date
- End Date
-
-
-
- {educationData.map((edu, index) => (
-
-
- {edu.degree}
-
-
- {edu.stream}
-
-
- {edu.institute}
-
- {edu.grade}
-
- {edu.sdate}
-
-
- {edu.edate}
-
-
- ))}
-
-
- ) : (
-
- No data found!
-
- )}
+
);
}
-function CoursesTab({ coursesData }) {
- const [formData, setFormData] = useState({
+function CoursesTab({ coursesData, onAddCourse }) {
+ const { formData, handleInputChange, resetForm } = useFormState({
course_name: "",
license: "",
start_date: "",
@@ -192,38 +91,42 @@ function CoursesTab({ coursesData }) {
description: "",
});
- const handleChange = (e) => {
- setFormData({ ...formData, [e.target.name]: e.target.value });
- };
-
const handleSubmit = async () => {
try {
- await axios.put(
- updateProfileDataRoute,
- { coursesubmit: formData },
- {
- headers: {
- Authorization: `Token ${localStorage.getItem("authToken")}`,
- },
- },
- );
- Notifications.show({
+ const payload = {
+ course_name: formData.course_name,
+ description: formData.description,
+ license_no: formData.license,
+ };
+
+ if (formData.start_date) {
+ payload.sdate = formData.start_date;
+ }
+ if (formData.end_date) {
+ payload.edate = formData.end_date;
+ }
+
+ const response = await updateProfileSection({ coursesubmit: payload });
+ const createdCourse = response?.data?.id
+ ? response.data
+ : {
+ ...formData,
+ license_no: formData.license,
+ sdate: formData.start_date,
+ edate: formData.end_date,
+ };
+
+ onAddCourse(createdCourse);
+ notifications.show({
message: "Certificates added Successfully!",
color: "green",
});
- setFormData({
- course_name: "",
- license: "",
- start_date: "",
- end_date: "",
- description: "",
- });
+ resetForm();
} catch (error) {
- Notifications.show({
+ notifications.show({
message: "Failed! Please try later.",
color: "red",
});
- console.error("Error updating courses:", error);
}
};
@@ -237,97 +140,40 @@ function CoursesTab({ coursesData }) {
Add a New Certification Course
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
Your Certificates
- {coursesData.length > 0 ? (
-
-
-
- Course Name
- License No.
- Start Date
- Completion Date
-
-
-
- {coursesData.map((course, index) => (
-
- | {course.course_name} |
- {course.license_no} |
- {course.sdate} |
- {course.edate} |
-
- ))}
-
-
- ) : (
-
- No data found!
-
- )}
+
);
}
export default function EducationCoursesComponent({ education, courses }) {
+ const [educationList, setEducationList] = useState(education || []);
+ const [coursesList, setCoursesList] = useState(courses || []);
+
+ useEffect(() => {
+ setEducationList(education || []);
+ }, [education]);
+
+ useEffect(() => {
+ setCoursesList(courses || []);
+ }, [courses]);
+
+ const handleAddEducation = (newEducation) => {
+ setEducationList((prev) => [...prev, newEducation]);
+ };
+
+ const handleAddCourse = (newCourse) => {
+ setCoursesList((prev) => [...prev, newCourse]);
+ };
+
return (
-
+
-
+
@@ -372,7 +221,7 @@ EducationCoursesComponent.propTypes = {
start_date: PropTypes.string,
end_date: PropTypes.string,
}),
- ),
+ ).isRequired,
courses: PropTypes.arrayOf(
PropTypes.shape({
course_name: PropTypes.string,
@@ -381,7 +230,7 @@ EducationCoursesComponent.propTypes = {
end_date: PropTypes.string,
description: PropTypes.string,
}),
- ),
+ ).isRequired,
};
EducationTab.propTypes = {
@@ -394,7 +243,8 @@ EducationTab.propTypes = {
start_date: PropTypes.string,
end_date: PropTypes.string,
}),
- ),
+ ).isRequired,
+ onAddEducation: PropTypes.func.isRequired,
};
CoursesTab.propTypes = {
@@ -402,9 +252,13 @@ CoursesTab.propTypes = {
PropTypes.shape({
course_name: PropTypes.string,
license: PropTypes.string,
+ license_no: PropTypes.string,
start_date: PropTypes.string,
end_date: PropTypes.string,
+ sdate: PropTypes.string,
+ edate: PropTypes.string,
description: PropTypes.string,
}),
- ),
+ ).isRequired,
+ onAddCourse: PropTypes.func.isRequired,
};
diff --git a/src/Modules/Dashboard/StudentProfile/profileComponent.jsx b/src/Modules/Dashboard/StudentProfile/profileComponent.jsx
index c748284cf..8e8434f88 100644
--- a/src/Modules/Dashboard/StudentProfile/profileComponent.jsx
+++ b/src/Modules/Dashboard/StudentProfile/profileComponent.jsx
@@ -1,62 +1,141 @@
-import { useState } from "react";
+import { useEffect, useRef, useState } from "react";
import PropTypes from "prop-types";
import { Table, Text, Button, Flex, Divider, TextInput } from "@mantine/core";
import { notifications } from "@mantine/notifications";
-import axios from "axios";
-import { updateProfileDataRoute } from "../../../routes/dashboardRoutes";
+import { useFormState } from "../utils/formHelpers";
+import { updateProfileSection } from "../services/profileService";
-function ProfileComponent({ data }) {
+const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
+const ALT_DATE_PATTERN = /^(\d{2})-(\d{2})-(\d{4})$/;
+
+const normalizeDateForApi = (value) => {
+ const raw = String(value || "").trim();
+ if (!raw) return "";
+ if (DATE_PATTERN.test(raw)) return raw;
+
+ const altMatch = raw.match(ALT_DATE_PATTERN);
+ if (altMatch) {
+ const [, dd, mm, yyyy] = altMatch;
+ return `${yyyy}-${mm}-${dd}`;
+ }
+
+ return "";
+};
+
+const normalizeDateForInput = (value) => normalizeDateForApi(value);
+
+const calculateAgeFromDob = (value) => {
+ const normalizedDob = normalizeDateForApi(value);
+ if (!normalizedDob) return "-";
+
+ const birthDate = new Date(`${normalizedDob}T00:00:00`);
+ if (Number.isNaN(birthDate.getTime())) return "-";
+
+ const today = new Date();
+ let age = today.getFullYear() - birthDate.getFullYear();
+ const monthDiff = today.getMonth() - birthDate.getMonth();
+
+ if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
+ age -= 1;
+ }
+
+ return age >= 0 ? age : "-";
+};
+
+const formatApiError = (error) => {
+ const data = error?.response?.data;
+ if (typeof data === "string") return data;
+ if (data?.error) return data.error;
+ if (data && typeof data === "object") {
+ const firstFieldError = Object.values(data).flat()[0];
+ if (firstFieldError) return firstFieldError;
+ }
+ return "Error updating profile";
+};
+
+function ProfileComponent({ data, isEditable, externalEditTrigger }) {
const [isEditing, setIsEditing] = useState(false);
- const [profileData, setProfileData] = useState({
+ const lastHandledTriggerRef = useRef(0);
+ const { formData: profileData, handleFieldChange } = useFormState({
about: data.profile?.about_me || "N/A",
- dob: data.profile?.date_of_birth || "Jan 01, 2004",
+ dob: normalizeDateForInput(data.profile?.date_of_birth),
address: data.profile?.address || "XYZ",
- contactNumber: data.profile?.phone_no || "+91 99999 99999",
+ contactNumber: data.profile?.phone_no || "",
mailId: data.current[0]?.user.email || "abc@gmail.com",
});
+ const calculatedAge = calculateAgeFromDob(profileData.dob);
const handleEditClick = async () => {
- const token = localStorage.getItem("authToken");
- if (!token) return console.error("No authentication token found!");
if (isEditing) {
try {
+ const normalizedContact = String(profileData.contactNumber || "")
+ .replace(/\D/g, "")
+ .trim();
+ const normalizedDob = normalizeDateForApi(profileData.dob);
+
+ if (!normalizedDob) {
+ notifications.show({
+ title: "Validation Error",
+ message: "Date of Birth must be in YYYY-MM-DD or DD-MM-YYYY format.",
+ color: "yellow",
+ });
+ return;
+ }
+
+ if (!normalizedContact) {
+ notifications.show({
+ title: "Validation Error",
+ message: "Contact Number must contain digits.",
+ color: "yellow",
+ });
+ return;
+ }
+
const payload = {
profilesubmit: {
about_me: profileData.about,
- date_of_birth: profileData.dob,
+ date_of_birth: normalizedDob,
address: profileData.address,
- phone_no: profileData.contactNumber,
+ phone_no: Number(normalizedContact),
},
};
- const response = await axios.put(updateProfileDataRoute, payload, {
- headers: { Authorization: `Token ${token}` },
- });
+ const response = await updateProfileSection(payload);
if (response.status === 200) {
notifications.show({
+ title: "Success",
message: "Profile updated successfully!",
- type: "success",
+ color: "green",
});
+ setIsEditing(false);
} else {
notifications.show({
+ title: "Update Failed",
message: "Failed to update profile",
- type: "error",
+ color: "red",
});
}
} catch (error) {
notifications.show({
- message: "Error updating profile",
- type: "error",
+ title: "Update Failed",
+ message: formatApiError(error),
+ color: "red",
});
}
+ return;
}
- setIsEditing(!isEditing);
+ setIsEditing(true);
};
- const handleChange = (field, value) => {
- setProfileData((prev) => ({ ...prev, [field]: value }));
- };
+ useEffect(() => {
+ if (!isEditable) return;
+ if (externalEditTrigger <= 0) return;
+ if (externalEditTrigger === lastHandledTriggerRef.current) return;
+
+ lastHandledTriggerRef.current = externalEditTrigger;
+ setIsEditing(true);
+ }, [externalEditTrigger, isEditable]);
return (
handleChange("about", e.target.value)}
+ onChange={(e) => handleFieldChange("about", e.target.value)}
w="80%"
/>
) : (
{profileData.about}
)}
-
+ {isEditable && (
+
+ )}
@@ -112,21 +196,28 @@ function ProfileComponent({ data }) {
{isEditing ? (
handleChange("dob", e.target.value)}
+ onChange={(e) => handleFieldChange("dob", e.target.value)}
/>
) : (
profileData.dob
)}
+
+ Age
+ {calculatedAge === "-" ? "-" : `${calculatedAge} years`}
+
Address
{isEditing ? (
handleChange("address", e.target.value)}
+ onChange={(e) =>
+ handleFieldChange("address", e.target.value)
+ }
/>
) : (
profileData.address
@@ -155,9 +246,10 @@ function ProfileComponent({ data }) {
{isEditing ? (
- handleChange("contactNumber", e.target.value)
+ handleFieldChange("contactNumber", e.target.value)
}
/>
) : (
@@ -192,6 +284,11 @@ ProfileComponent.propTypes = {
}),
),
}),
+ isEditable: PropTypes.bool.isRequired, // Added this line
+ externalEditTrigger: PropTypes.number,
};
+ProfileComponent.defaultProps = {
+ externalEditTrigger: 0,
+};
export default ProfileComponent;
diff --git a/src/Modules/Dashboard/StudentProfile/profilePage.jsx b/src/Modules/Dashboard/StudentProfile/profilePage.jsx
index 3949a1d20..24d7d9b48 100644
--- a/src/Modules/Dashboard/StudentProfile/profilePage.jsx
+++ b/src/Modules/Dashboard/StudentProfile/profilePage.jsx
@@ -1,6 +1,6 @@
-import { Stack, Text, Card, Image, Flex, Box } from "@mantine/core";
+import { Stack, Text, Card, Image, Flex, Box, Button } from "@mantine/core";
+
import { useState, useEffect } from "react";
-import axios from "axios";
import PropTypes from "prop-types";
import CustomBreadcrumbs from "../../../components/Breadcrumbs";
import ModuleTabs from "../../../components/moduleTabs";
@@ -10,9 +10,11 @@ import SkillsTechComponent from "./skillsComponent";
import AchievementsComponent from "./achievementsComponent";
import WorkExperienceComponent from "./workExperienceComponent";
import EducationCoursesComponent from "./educationCoursesComponent";
-import { getProfileDataRoute } from "../../../routes/dashboardRoutes";
+import { fetchProfileData } from "../services/profileService";
+import LoadingSpinner from "../components/common/LoadingSpinner";
+import EmptyState from "../components/common/EmptyState";
-function InfoCard({ data }) {
+function InfoCard({ data, isEditable, onEditProfile }) {
return (
@@ -39,58 +41,72 @@ function InfoCard({ data }) {
Student
+
+ {/* Conditionally render the "Edit Profile" button */}
+ {isEditable && (
+
+ )}
);
}
-function Profile() {
+function Profile({ connectionRoute }) {
const [activeTab, setActiveTab] = useState("0");
+ const [profileEditTrigger, setProfileEditTrigger] = useState(0);
const [profileData, setProfileData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
+ const handleEditProfileClick = () => {
+ setActiveTab("0");
+ setProfileEditTrigger((prev) => prev + 1);
+ };
+
useEffect(() => {
async function fetchProfile() {
- const token = localStorage.getItem("authToken");
- if (!token) return console.error("No authentication token found!");
try {
- const response = await axios.get(getProfileDataRoute, {
- headers: { Authorization: `Token ${token}` },
- });
+ const response = await fetchProfileData(connectionRoute);
setProfileData(response.data);
} catch (err) {
setError("Error fetching profile data.");
- console.error(err);
} finally {
setLoading(false);
}
}
fetchProfile();
- }, []);
+ }, [connectionRoute]);
+
+ // Conditionally define tabItems and tabToDisplay
+ const tabItems = connectionRoute
+ ? [{ title: "Profile" }] // Only show the Profile tab if connectionRoute is provided
+ : [
+ { title: "Profile" },
+ { title: "Skills & Technologies" },
+ { title: "Education & Courses" },
+ { title: "Work Experience" },
+ { title: "Achievements" },
+ ];
- const tabItems = [
- { title: "Profile" },
- { title: "Skills & Technologies" },
- { title: "Education & Courses" },
- { title: "Work Experience" },
- { title: "Achievements" },
- ];
- const tabToDisplay = [
- ,
- ,
- ,
- ,
- ,
- ];
+ const tabToDisplay = connectionRoute
+ ? [] // Only display Profile if connectionRoute is provided
+ : [
+ ,
+ ,
+ ,
+ ,
+ ,
+ ];
- if (loading) return Loading profile...
;
- if (error) return {error}
;
+ if (loading) return ;
+ if (error) return ;
return (
@@ -109,7 +125,12 @@ function Profile() {
>
{tabToDisplay[activeTab]}
-
+ {/* Pass the isEditable prop based on the connectionRoute */}
+
@@ -133,6 +154,16 @@ InfoCard.propTypes = {
}),
}),
}).isRequired,
+ isEditable: PropTypes.bool.isRequired,
+ onEditProfile: PropTypes.func,
+};
+
+InfoCard.defaultProps = {
+ onEditProfile: () => {},
+};
+
+Profile.propTypes = {
+ connectionRoute: PropTypes.string,
};
export default Profile;
diff --git a/src/Modules/Dashboard/StudentProfile/skillsComponent.jsx b/src/Modules/Dashboard/StudentProfile/skillsComponent.jsx
index 770b0c24f..9b47e190b 100644
--- a/src/Modules/Dashboard/StudentProfile/skillsComponent.jsx
+++ b/src/Modules/Dashboard/StudentProfile/skillsComponent.jsx
@@ -1,17 +1,10 @@
-import { useState } from "react";
+import { useEffect, useState } from "react";
import PropTypes from "prop-types";
-import {
- Text,
- Button,
- Input,
- Flex,
- Divider,
- NumberInput,
- Table,
-} from "@mantine/core";
+import { Text, Flex, Divider } from "@mantine/core";
import { notifications } from "@mantine/notifications";
-import axios from "axios";
-import { updateProfileDataRoute } from "../../../routes/dashboardRoutes";
+import { updateProfileSection } from "../services/profileService";
+import SkillForm from "../components/forms/SkillForm";
+import SkillsTable from "../components/tables/SkillsTable";
function SkillsTechComponent({ data }) {
const [skills, setSkills] = useState(data || []);
@@ -28,10 +21,12 @@ function SkillsTechComponent({ data }) {
return;
}
- if (rating < 0 || rating > 5) {
+ const normalizedRating = Number(rating);
+
+ if (!Number.isInteger(normalizedRating) || normalizedRating < 1 || normalizedRating > 5) {
notifications.show({
title: "Error",
- message: "Rating must be between 0 and 5",
+ message: "Rating must be between 1 and 5",
color: "red",
});
return;
@@ -39,39 +34,41 @@ function SkillsTechComponent({ data }) {
const newSkillEntry = {
skillsubmit: {
- skill_id: {
- skill_name: newSkill,
- },
- skill_rating: rating,
+ skill_name: newSkill.trim(),
+ skill_rating: normalizedRating,
},
};
try {
- await axios.put(updateProfileDataRoute, newSkillEntry, {
- headers: {
- Authorization: `Token ${localStorage.getItem("authToken")}`,
- },
- });
+ await updateProfileSection(newSkillEntry);
- setSkills([...skills, { skill_name: newSkill, skill_rating: rating }]);
+ setSkills([
+ ...skills,
+ { skill_name: newSkill.trim(), skill_rating: normalizedRating },
+ ]);
setNewSkill("");
- setRating(0);
+ setRating(1);
notifications.show({
title: "Success",
message: "Skill added successfully!",
color: "green",
});
} catch (error) {
+ const backendError = error?.response?.data;
+ const errorMessage =
+ backendError?.skill_name?.[0]
+ || backendError?.skill_rating?.[0]
+ || backendError?.error
+ || "Failed to update skills. Please try again.";
+
notifications.show({
title: "Error",
- message: "Failed to update skills. Please try again.",
+ message: errorMessage,
color: "red",
});
}
};
- console.log(skills);
-
return (
Add New Skill/Technology
-
-
- setNewSkill(e.target.value)}
- />
-
-
-
-
-
-
+
@@ -140,30 +114,7 @@ function SkillsTechComponent({ data }) {
Your Skills
-
-
-
- Skill
- Rating
-
-
-
- {skills.length > 0 ? (
- skills.map((skill, index) => (
-
- {skill.skill_name}
- {skill.skill_rating}
-
- ))
- ) : (
-
-
- No skills added yet
-
-
- )}
-
-
+
);
diff --git a/src/Modules/Dashboard/StudentProfile/workExperienceComponent.jsx b/src/Modules/Dashboard/StudentProfile/workExperienceComponent.jsx
index 0712b8867..bb4e84de3 100644
--- a/src/Modules/Dashboard/StudentProfile/workExperienceComponent.jsx
+++ b/src/Modules/Dashboard/StudentProfile/workExperienceComponent.jsx
@@ -1,22 +1,17 @@
-import { useState } from "react";
+import { useEffect, useState } from "react";
import PropTypes from "prop-types";
-import {
- Flex,
- Input,
- Tabs,
- Text,
- Button,
- Select,
- Table,
- Textarea,
- Divider,
-} from "@mantine/core";
-import axios from "axios";
+import { Flex, Tabs, Text, Divider } from "@mantine/core";
import { notifications } from "@mantine/notifications";
-import { updateProfileDataRoute } from "../../../routes/dashboardRoutes";
+import { useFormState } from "../utils/formHelpers";
+import { updateProfileSection } from "../services/profileService";
+import InternshipForm from "../components/forms/InternshipForm";
+import ProjectForm from "../components/forms/ProjectForm";
+import InternshipsTable from "../components/tables/InternshipsTable";
+import ProjectsTable from "../components/tables/ProjectsTable";
-function InternshipsTab({ internshipsData }) {
- const [formData, setFormData] = useState({
+function InternshipsTab({ internshipsData, onAddInternship }) {
+ const { formData, handleInputChange, handleFieldChange, resetForm } =
+ useFormState({
organization: "",
location: "",
job_title: "",
@@ -26,40 +21,45 @@ function InternshipsTab({ internshipsData }) {
description: "",
});
- const handleChange = (e) => {
- setFormData({ ...formData, [e.target.name]: e.target.value });
- };
-
const handleSubmit = async () => {
try {
- await axios.put(
- updateProfileDataRoute,
- { experiencesubmit: formData },
- {
- headers: {
- Authorization: `Token ${localStorage.getItem("authToken")}`,
- },
- },
- );
+ const payload = {
+ company: formData.organization,
+ location: formData.location,
+ title: formData.job_title,
+ status: formData.status,
+ description: formData.description,
+ };
+
+ if (formData.start_date) {
+ payload.sdate = formData.start_date;
+ }
+ if (formData.end_date) {
+ payload.edate = formData.end_date;
+ }
+
+ const response = await updateProfileSection({ experiencesubmit: payload });
+ const createdInternship = response?.data?.id
+ ? response.data
+ : {
+ ...formData,
+ company: formData.organization,
+ title: formData.job_title,
+ sdate: formData.start_date,
+ edate: formData.end_date,
+ };
+
+ onAddInternship(createdInternship);
notifications.show({
message: "Internship Added Successfully!",
color: "green",
});
- setFormData({
- organization: "",
- location: "",
- job_title: "",
- status: "ONGOING",
- start_date: "",
- end_date: "",
- description: "",
- });
+ resetForm();
} catch (error) {
notifications.show({
message: "Failed! Please try later.",
color: "red",
});
- console.error("Error updating internships:", error);
}
};
@@ -73,136 +73,24 @@ function InternshipsTab({ internshipsData }) {
Add a New Internship
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ handleFieldChange("status", value || "ONGOING")}
+ onSubmit={handleSubmit}
+ />
Your Experience
-
- {internshipsData.length > 0 ? (
-
-
-
- Organization
- Location
- Job Title
- Status
- Start Date
- End Date
-
-
-
- {internshipsData.map((internship, index) => (
-
-
- {internship.organization}
-
-
- {internship.location}
-
-
- {internship.job_title}
-
-
- {internship.status}
-
-
- {internship.sdate}
-
-
- {internship.edate}
-
-
- ))}
-
-
- ) : (
-
- No data found!
-
- )}
+
);
}
-function ProjectsTab({ projectsData }) {
- const [formData, setFormData] = useState({
+function ProjectsTab({ projectsData, onAddProject }) {
+ const { formData, handleInputChange, handleFieldChange, resetForm } =
+ useFormState({
project_name: "",
status: "ONGOING",
project_link: "",
@@ -211,39 +99,44 @@ function ProjectsTab({ projectsData }) {
description: "",
});
- const handleChange = (e) => {
- setFormData({ ...formData, [e.target.name]: e.target.value });
- };
-
const handleSubmit = async () => {
try {
- await axios.put(
- updateProfileDataRoute,
- { projectsubmit: formData },
- {
- headers: {
- Authorization: `Token ${localStorage.getItem("authToken")}`,
- },
- },
- );
+ const payload = {
+ project_name: formData.project_name,
+ project_status: formData.status,
+ project_link: formData.project_link,
+ summary: formData.description,
+ };
+
+ if (formData.start_date) {
+ payload.sdate = formData.start_date;
+ }
+ if (formData.end_date) {
+ payload.edate = formData.end_date;
+ }
+
+ const response = await updateProfileSection({ projectsubmit: payload });
+ const createdProject = response?.data?.id
+ ? response.data
+ : {
+ ...formData,
+ project_status: formData.status,
+ summary: formData.description,
+ sdate: formData.start_date,
+ edate: formData.end_date,
+ };
+
+ onAddProject(createdProject);
notifications.show({
message: "Project Added Successfully!",
color: "green",
});
- setFormData({
- project_name: "",
- status: "ONGOING",
- project_link: "",
- start_date: "",
- end_date: "",
- description: "",
- });
+ resetForm();
} catch (error) {
notifications.show({
message: "Failed! Please try later.",
color: "red",
});
- console.error("Error updating projects:", error);
}
};
@@ -257,125 +150,41 @@ function ProjectsTab({ projectsData }) {
Add a New Project
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ handleFieldChange("status", value || "ONGOING")}
+ onSubmit={handleSubmit}
+ />
Your Projects
- {projectsData.length > 0 ? (
-
-
-
- Project Name
- Status
- Project Link
- Start Date
- End Date
-
-
-
- {projectsData.map((project, index) => (
-
-
- {project.project_name}
-
-
- {project.status}
-
-
-
- {project.project_link}
-
-
-
- {project.start_date}
-
-
- {project.end_date}
-
-
- ))}
-
-
- ) : (
-
- No data found!
-
- )}
+
);
}
export default function WorkExperienceComponent({ experience, project }) {
+ const [internships, setInternships] = useState(experience || []);
+ const [projects, setProjects] = useState(project || []);
+
+ useEffect(() => {
+ setInternships(experience || []);
+ }, [experience]);
+
+ useEffect(() => {
+ setProjects(project || []);
+ }, [project]);
+
+ const handleAddInternship = (newInternship) => {
+ setInternships((prev) => [...prev, newInternship]);
+ };
+
+ const handleAddProject = (newProject) => {
+ setProjects((prev) => [...prev, newProject]);
+ };
+
return (
-
+
-
+
@@ -446,6 +261,7 @@ InternshipsTab.propTypes = {
description: PropTypes.string,
}),
).isRequired,
+ onAddInternship: PropTypes.func.isRequired,
};
ProjectsTab.propTypes = {
@@ -459,4 +275,5 @@ ProjectsTab.propTypes = {
description: PropTypes.string,
}),
).isRequired,
+ onAddProject: PropTypes.func.isRequired,
};
diff --git a/src/Modules/Dashboard/components/common/DataTable.jsx b/src/Modules/Dashboard/components/common/DataTable.jsx
new file mode 100644
index 000000000..209bb7e54
--- /dev/null
+++ b/src/Modules/Dashboard/components/common/DataTable.jsx
@@ -0,0 +1,59 @@
+import PropTypes from "prop-types";
+import { Table } from "@mantine/core";
+import EmptyState from "./EmptyState";
+
+export default function DataTable({ columns, rows, emptyMessage }) {
+ if (!rows.length) {
+ return ;
+ }
+
+ return (
+
+
+
+ {columns.map((column) => (
+
+ {column.label}
+
+ ))}
+
+
+
+ {rows.map((row) => (
+
+ {row.cells.map((cell) => (
+
+ {cell.content}
+
+ ))}
+
+ ))}
+
+
+ );
+}
+
+DataTable.propTypes = {
+ columns: PropTypes.arrayOf(
+ PropTypes.shape({
+ key: PropTypes.string.isRequired,
+ label: PropTypes.string.isRequired,
+ }),
+ ).isRequired,
+ rows: PropTypes.arrayOf(
+ PropTypes.shape({
+ key: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
+ cells: PropTypes.arrayOf(
+ PropTypes.shape({
+ key: PropTypes.string.isRequired,
+ content: PropTypes.node,
+ }),
+ ).isRequired,
+ }),
+ ).isRequired,
+ emptyMessage: PropTypes.string,
+};
+
+DataTable.defaultProps = {
+ emptyMessage: "No data found!",
+};
diff --git a/src/Modules/Dashboard/components/common/EmptyState.jsx b/src/Modules/Dashboard/components/common/EmptyState.jsx
new file mode 100644
index 000000000..6de752715
--- /dev/null
+++ b/src/Modules/Dashboard/components/common/EmptyState.jsx
@@ -0,0 +1,18 @@
+import PropTypes from "prop-types";
+import { Text } from "@mantine/core";
+
+export default function EmptyState({ message }) {
+ return (
+
+ {message}
+
+ );
+}
+
+EmptyState.propTypes = {
+ message: PropTypes.string,
+};
+
+EmptyState.defaultProps = {
+ message: "No data found!",
+};
diff --git a/src/Modules/Dashboard/components/common/LoadingSpinner.jsx b/src/Modules/Dashboard/components/common/LoadingSpinner.jsx
new file mode 100644
index 000000000..79c67fa49
--- /dev/null
+++ b/src/Modules/Dashboard/components/common/LoadingSpinner.jsx
@@ -0,0 +1,9 @@
+import { Center, Loader } from "@mantine/core";
+
+export default function LoadingSpinner() {
+ return (
+
+
+
+ );
+}
diff --git a/src/Modules/Dashboard/components/forms/AchievementForm.jsx b/src/Modules/Dashboard/components/forms/AchievementForm.jsx
new file mode 100644
index 000000000..4b2bc2147
--- /dev/null
+++ b/src/Modules/Dashboard/components/forms/AchievementForm.jsx
@@ -0,0 +1,74 @@
+import PropTypes from "prop-types";
+import { Flex, Input, Button, Select, Textarea } from "@mantine/core";
+
+export default function AchievementForm({ formData, onChange, onSubmit }) {
+ return (
+
+
+
+ onChange("skill", e.target.value)}
+ />
+
+
+
+
+
+
+ onChange("date", e.target.value)}
+ />
+
+
+ onChange("issuer", e.target.value)}
+ />
+
+
+
+
+
+
+
+
+ );
+}
+
+AchievementForm.propTypes = {
+ formData: PropTypes.shape({
+ skill: PropTypes.string,
+ type: PropTypes.string,
+ date: PropTypes.string,
+ issuer: PropTypes.string,
+ description: PropTypes.string,
+ }).isRequired,
+ onChange: PropTypes.func.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+};
diff --git a/src/Modules/Dashboard/components/forms/CourseForm.jsx b/src/Modules/Dashboard/components/forms/CourseForm.jsx
new file mode 100644
index 000000000..abee438b8
--- /dev/null
+++ b/src/Modules/Dashboard/components/forms/CourseForm.jsx
@@ -0,0 +1,65 @@
+import PropTypes from "prop-types";
+import { Flex, Input, Button, Textarea } from "@mantine/core";
+
+export default function CourseForm({ formData, onChange, onSubmit }) {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+CourseForm.propTypes = {
+ formData: PropTypes.shape({
+ course_name: PropTypes.string,
+ license: PropTypes.string,
+ start_date: PropTypes.string,
+ end_date: PropTypes.string,
+ description: PropTypes.string,
+ }).isRequired,
+ onChange: PropTypes.func.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+};
diff --git a/src/Modules/Dashboard/components/forms/EducationForm.jsx b/src/Modules/Dashboard/components/forms/EducationForm.jsx
new file mode 100644
index 000000000..7456d49dc
--- /dev/null
+++ b/src/Modules/Dashboard/components/forms/EducationForm.jsx
@@ -0,0 +1,63 @@
+import PropTypes from "prop-types";
+import { Flex, Input, Button } from "@mantine/core";
+
+export default function EducationForm({ formData, onChange, onSubmit }) {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+EducationForm.propTypes = {
+ formData: PropTypes.shape({
+ degree: PropTypes.string,
+ stream: PropTypes.string,
+ institute: PropTypes.string,
+ grade: PropTypes.string,
+ start_date: PropTypes.string,
+ end_date: PropTypes.string,
+ }).isRequired,
+ onChange: PropTypes.func.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+};
diff --git a/src/Modules/Dashboard/components/forms/InternshipForm.jsx b/src/Modules/Dashboard/components/forms/InternshipForm.jsx
new file mode 100644
index 000000000..938873e2f
--- /dev/null
+++ b/src/Modules/Dashboard/components/forms/InternshipForm.jsx
@@ -0,0 +1,75 @@
+import PropTypes from "prop-types";
+import { Flex, Input, Button, Select, Textarea } from "@mantine/core";
+
+export default function InternshipForm({ formData, onChange, onStatusChange, onSubmit }) {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+InternshipForm.propTypes = {
+ formData: PropTypes.object.isRequired,
+ onChange: PropTypes.func.isRequired,
+ onStatusChange: PropTypes.func.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+};
diff --git a/src/Modules/Dashboard/components/forms/ProjectForm.jsx b/src/Modules/Dashboard/components/forms/ProjectForm.jsx
new file mode 100644
index 000000000..befc4e6c1
--- /dev/null
+++ b/src/Modules/Dashboard/components/forms/ProjectForm.jsx
@@ -0,0 +1,76 @@
+import PropTypes from "prop-types";
+import { Flex, Input, Button, Select, Textarea } from "@mantine/core";
+
+export default function ProjectForm({ formData, onChange, onStatusChange, onSubmit }) {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+ProjectForm.propTypes = {
+ formData: PropTypes.object.isRequired,
+ onChange: PropTypes.func.isRequired,
+ onStatusChange: PropTypes.func.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+};
diff --git a/src/Modules/Dashboard/components/forms/SkillForm.jsx b/src/Modules/Dashboard/components/forms/SkillForm.jsx
new file mode 100644
index 000000000..6ca9a3a48
--- /dev/null
+++ b/src/Modules/Dashboard/components/forms/SkillForm.jsx
@@ -0,0 +1,43 @@
+import PropTypes from "prop-types";
+import { Button, Input, Flex, NumberInput } from "@mantine/core";
+
+export default function SkillForm({ newSkill, rating, setNewSkill, setRating, onSubmit }) {
+ return (
+
+
+ setNewSkill(e.target.value)}
+ />
+
+
+
+
+
+
+ );
+}
+
+SkillForm.propTypes = {
+ newSkill: PropTypes.string.isRequired,
+ rating: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
+ setNewSkill: PropTypes.func.isRequired,
+ setRating: PropTypes.func.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+};
diff --git a/src/Modules/Dashboard/components/tables/AchievementsTable.jsx b/src/Modules/Dashboard/components/tables/AchievementsTable.jsx
new file mode 100644
index 000000000..5d80769f8
--- /dev/null
+++ b/src/Modules/Dashboard/components/tables/AchievementsTable.jsx
@@ -0,0 +1,41 @@
+import PropTypes from "prop-types";
+import DataTable from "../common/DataTable";
+
+export default function AchievementsTable({ achievements }) {
+ const columns = [
+ { key: "type", label: "Type" },
+ { key: "date", label: "Date" },
+ { key: "issuer", label: "Issuer" },
+ { key: "description", label: "Description" },
+ ];
+
+ const rows = (achievements || []).map((ach) => ({
+ key:
+ ach.id ||
+ `${ach.achievement_type || ach.type || "type"}-${ach.date_earned || ach.date || "date"}-${ach.issuer || "issuer"}`,
+ cells: [
+ { key: "type", content: ach.achievement_type || ach.type || "-" },
+ { key: "date", content: ach.date_earned || ach.date || "-" },
+ { key: "issuer", content: ach.issuer || "-" },
+ { key: "description", content: ach.description || "-" },
+ ],
+ }));
+
+ return ;
+}
+
+AchievementsTable.propTypes = {
+ achievements: PropTypes.arrayOf(
+ PropTypes.shape({
+ id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ achievement_type: PropTypes.string,
+ date_earned: PropTypes.string,
+ issuer: PropTypes.string,
+ description: PropTypes.string,
+ }),
+ ),
+};
+
+AchievementsTable.defaultProps = {
+ achievements: [],
+};
diff --git a/src/Modules/Dashboard/components/tables/CoursesTable.jsx b/src/Modules/Dashboard/components/tables/CoursesTable.jsx
new file mode 100644
index 000000000..63b996252
--- /dev/null
+++ b/src/Modules/Dashboard/components/tables/CoursesTable.jsx
@@ -0,0 +1,33 @@
+import PropTypes from "prop-types";
+import DataTable from "../common/DataTable";
+
+export default function CoursesTable({ coursesData }) {
+ const columns = [
+ { key: "course", label: "Course Name" },
+ { key: "license", label: "License No." },
+ { key: "start", label: "Start Date" },
+ { key: "end", label: "Completion Date" },
+ ];
+
+ const rows = (coursesData || []).map((course) => ({
+ key:
+ course.id ||
+ `${course.course_name || "course"}-${course.license_no || course.license || "license"}`,
+ cells: [
+ { key: "course", content: course.course_name || "-" },
+ { key: "license", content: course.license_no || course.license || "-" },
+ { key: "start", content: course.sdate || course.start_date || "-" },
+ { key: "end", content: course.edate || course.end_date || "-" },
+ ],
+ }));
+
+ return ;
+}
+
+CoursesTable.propTypes = {
+ coursesData: PropTypes.arrayOf(PropTypes.object),
+};
+
+CoursesTable.defaultProps = {
+ coursesData: [],
+};
diff --git a/src/Modules/Dashboard/components/tables/EducationTable.jsx b/src/Modules/Dashboard/components/tables/EducationTable.jsx
new file mode 100644
index 000000000..58d27209e
--- /dev/null
+++ b/src/Modules/Dashboard/components/tables/EducationTable.jsx
@@ -0,0 +1,35 @@
+import PropTypes from "prop-types";
+import DataTable from "../common/DataTable";
+
+export default function EducationTable({ educationData }) {
+ const columns = [
+ { key: "degree", label: "Degree" },
+ { key: "stream", label: "Stream" },
+ { key: "institute", label: "Institute" },
+ { key: "grade", label: "Grade" },
+ { key: "start", label: "Start Date" },
+ { key: "end", label: "End Date" },
+ ];
+
+ const rows = (educationData || []).map((edu) => ({
+ key: edu.id || `${edu.degree || "degree"}-${edu.institute || "inst"}-${edu.sdate || "start"}`,
+ cells: [
+ { key: "degree", content: edu.degree || "-" },
+ { key: "stream", content: edu.stream || "-" },
+ { key: "institute", content: edu.institute || "-" },
+ { key: "grade", content: edu.grade || "-" },
+ { key: "start", content: edu.sdate || edu.start_date || "-" },
+ { key: "end", content: edu.edate || edu.end_date || "-" },
+ ],
+ }));
+
+ return ;
+}
+
+EducationTable.propTypes = {
+ educationData: PropTypes.arrayOf(PropTypes.object),
+};
+
+EducationTable.defaultProps = {
+ educationData: [],
+};
diff --git a/src/Modules/Dashboard/components/tables/InternshipsTable.jsx b/src/Modules/Dashboard/components/tables/InternshipsTable.jsx
new file mode 100644
index 000000000..b382f77ec
--- /dev/null
+++ b/src/Modules/Dashboard/components/tables/InternshipsTable.jsx
@@ -0,0 +1,37 @@
+import PropTypes from "prop-types";
+import DataTable from "../common/DataTable";
+
+export default function InternshipsTable({ internshipsData }) {
+ const columns = [
+ { key: "organization", label: "Organization" },
+ { key: "location", label: "Location" },
+ { key: "job", label: "Job Title" },
+ { key: "status", label: "Status" },
+ { key: "start", label: "Start Date" },
+ { key: "end", label: "End Date" },
+ ];
+
+ const rows = (internshipsData || []).map((internship) => ({
+ key:
+ internship.id ||
+ `${internship.organization || internship.company || "org"}-${internship.job_title || internship.title || "role"}-${internship.sdate || internship.start_date || "start"}`,
+ cells: [
+ { key: "organization", content: internship.organization || internship.company || "-" },
+ { key: "location", content: internship.location || "-" },
+ { key: "job", content: internship.job_title || internship.title || "-" },
+ { key: "status", content: internship.status || "-" },
+ { key: "start", content: internship.sdate || internship.start_date || "-" },
+ { key: "end", content: internship.edate || internship.end_date || "-" },
+ ],
+ }));
+
+ return ;
+}
+
+InternshipsTable.propTypes = {
+ internshipsData: PropTypes.arrayOf(PropTypes.object),
+};
+
+InternshipsTable.defaultProps = {
+ internshipsData: [],
+};
diff --git a/src/Modules/Dashboard/components/tables/ProjectsTable.jsx b/src/Modules/Dashboard/components/tables/ProjectsTable.jsx
new file mode 100644
index 000000000..28f5c9fa6
--- /dev/null
+++ b/src/Modules/Dashboard/components/tables/ProjectsTable.jsx
@@ -0,0 +1,44 @@
+import PropTypes from "prop-types";
+import DataTable from "../common/DataTable";
+
+export default function ProjectsTable({ projectsData }) {
+ const columns = [
+ { key: "name", label: "Project Name" },
+ { key: "status", label: "Status" },
+ { key: "link", label: "Project Link" },
+ { key: "start", label: "Start Date" },
+ { key: "end", label: "End Date" },
+ ];
+
+ const rows = (projectsData || []).map((project) => ({
+ key:
+ project.id ||
+ `${project.project_name || "project"}-${project.project_link || "link"}`,
+ cells: [
+ { key: "name", content: project.project_name || "-" },
+ { key: "status", content: project.status || project.project_status || "-" },
+ {
+ key: "link",
+ content: project.project_link ? (
+
+ {project.project_link}
+
+ ) : (
+ "-"
+ ),
+ },
+ { key: "start", content: project.start_date || project.sdate || "-" },
+ { key: "end", content: project.end_date || project.edate || "-" },
+ ],
+ }));
+
+ return ;
+}
+
+ProjectsTable.propTypes = {
+ projectsData: PropTypes.arrayOf(PropTypes.object),
+};
+
+ProjectsTable.defaultProps = {
+ projectsData: [],
+};
diff --git a/src/Modules/Dashboard/components/tables/SkillsTable.jsx b/src/Modules/Dashboard/components/tables/SkillsTable.jsx
new file mode 100644
index 000000000..35bfd37bf
--- /dev/null
+++ b/src/Modules/Dashboard/components/tables/SkillsTable.jsx
@@ -0,0 +1,35 @@
+import PropTypes from "prop-types";
+import DataTable from "../common/DataTable";
+
+export default function SkillsTable({ skills }) {
+ const columns = [
+ { key: "skill", label: "Skill" },
+ { key: "rating", label: "Rating" },
+ ];
+
+ const rows = (skills || []).map((skill) => ({
+ key: skill.id || skill.skill_id || skill.skill_name || skill.skill_id__skill,
+ cells: [
+ { key: "skill", content: skill.skill_name || skill.skill_id__skill || "-" },
+ { key: "rating", content: skill.skill_rating },
+ ],
+ }));
+
+ return ;
+}
+
+SkillsTable.propTypes = {
+ skills: PropTypes.arrayOf(
+ PropTypes.shape({
+ id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ skill_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ skill_name: PropTypes.string,
+ skill_id__skill: PropTypes.string,
+ skill_rating: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ }),
+ ),
+};
+
+SkillsTable.defaultProps = {
+ skills: [],
+};
diff --git a/src/Modules/Dashboard/dashboardNotifications.jsx b/src/Modules/Dashboard/dashboardNotifications.jsx
index ca8565bda..c26a609ef 100644
--- a/src/Modules/Dashboard/dashboardNotifications.jsx
+++ b/src/Modules/Dashboard/dashboardNotifications.jsx
@@ -1,91 +1,190 @@
-import axios from "axios";
import PropTypes from "prop-types";
-import { SortAscending } from "@phosphor-icons/react";
+import {
+ SortAscending,
+ Trash,
+ Star,
+ FunnelSimple,
+} from "@phosphor-icons/react";
import { useEffect, useMemo, useState } from "react";
import {
Container,
Loader,
Badge,
Button,
- Divider,
Flex,
Grid,
Paper,
Select,
Text,
- CloseButton,
+ ActionIcon,
+ Group,
+ Box,
+ Tooltip,
+ SegmentedControl,
} from "@mantine/core";
import { useDispatch } from "react-redux";
+import { setTotalNotifications } from "../../redux/userslice.jsx";
import classes from "./Dashboard.module.css";
import { Empty } from "../../components/empty";
import CustomBreadcrumbs from "../../components/Breadcrumbs.jsx";
import {
- notificationReadRoute,
- notificationDeleteRoute,
- notificationUnreadRoute,
- getNotificationsRoute,
-} from "../../routes/dashboardRoutes";
+ fetchNotifications,
+ markNotificationRead,
+ markNotificationUnread,
+ removeNotification,
+} from "./services/notificationService";
import ModuleTabs from "../../components/moduleTabs.jsx";
-const categories = ["Most Recent", "Tags", "Title"];
+const sortCategories = ["Most Recent", "Tags", "Title"];
+const filterCategories = ["All", "Starred"];
function NotificationItem({
notification,
markAsRead,
deleteNotification,
markAsUnread,
+ toggleStar,
loading,
+ starLoading,
}) {
const { module } = notification.data;
+ const isUnread = notification.unread;
+ const isStarred = notification.starred;
+ const formattedDate = new Date(notification.timestamp).toLocaleDateString(
+ undefined,
+ {
+ year: "numeric",
+ month: "short",
+ day: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ },
+ );
return (
-
+
-
-
-
-
- {notification.verb}
+
+
+
+ toggleStar(notification.id)}
+ loading={starLoading === notification.id}
+ title={isStarred ? "Unstar notification" : "Star notification"}
+ radius="xl"
+ size="md"
+ className={classes.starButton}
+ style={{
+ transition: "all 0.2s ease",
+ }}
+ >
+
+
+
+
+
+
+
+
+
+ {notification.verb}
+
+
+ {module || "N/A"}
+
+
+
+ {formattedDate}
- {module || "N/A"}
-
- {new Date(notification.timestamp).toLocaleDateString()}
+
+
+ {notification.description || "No description available."}
-
-
- deleteNotification(notification.id)}
- />
-
-
- {notification.description || "No description available."}
-
+
+
+ deleteNotification(notification.id)}
+ title="Delete notification"
+ radius="xl"
+ >
+
+
+
@@ -97,10 +196,11 @@ function Dashboard() {
const [announcementsList, setAnnouncementsList] = useState([]);
const [activeTab, setActiveTab] = useState("0");
const [sortedBy, setSortedBy] = useState("Most Recent");
+ const [filterBy, setFilterBy] = useState("All");
const [loading, setLoading] = useState(false);
const [read_Loading, setRead_Loading] = useState(-1);
+ const [star_Loading, setStar_Loading] = useState(-1);
const dispatch = useDispatch();
- // const tabsListRef = useRef(null);
const tabItems = [{ title: "Notifications" }, { title: "Announcements" }];
const notificationBadgeCount = notificationsList.filter(
@@ -110,32 +210,79 @@ function Dashboard() {
(n) => !n.deleted && n.unread,
).length;
const badges = [notificationBadgeCount, announcementBadgeCount];
+ dispatch(
+ setTotalNotifications(notificationBadgeCount + announcementBadgeCount),
+ );
useEffect(() => {
- const fetchDashboardData = async () => {
- const token = localStorage.getItem("authToken");
- if (!token) return console.error("No authentication token found!");
+ try {
+ const starredNotifications = JSON.parse(
+ localStorage.getItem("starredNotifications") || "{}",
+ );
+
+ setNotificationsList((prev) =>
+ prev.map((notification) => ({
+ ...notification,
+ starred: !!starredNotifications[notification.id],
+ })),
+ );
+
+ setAnnouncementsList((prev) =>
+ prev.map((notification) => ({
+ ...notification,
+ starred: !!starredNotifications[notification.id],
+ })),
+ );
+ } catch (err) {
+ console.error(
+ "Error loading starred notifications from localStorage:",
+ err,
+ );
+ }
+ }, []);
+ useEffect(() => {
+ const fetchDashboardData = async () => {
try {
setLoading(true);
- const { data } = await axios.get(getNotificationsRoute, {
- headers: { Authorization: `Token ${token}` },
- });
+ const { data } = await fetchNotifications();
const { notifications } = data;
- const notificationsData = notifications.map((item) => ({
- ...item,
- data: JSON.parse(item.data.replace(/'/g, '"')),
- }));
+ const notificationsData = notifications.map((item) => {
+ let parsedData;
+ try {
+ parsedData = JSON.parse(item.data.replace(/'/g, '"'));
+ } catch (err) {
+ console.error("Error parsing notification data:", err);
+ parsedData = {};
+ }
+
+ return {
+ ...item,
+ data: parsedData,
+ starred: false,
+ };
+ });
+
+ const starredNotifications = JSON.parse(
+ localStorage.getItem("starredNotifications") || "{}",
+ );
setNotificationsList(
- notificationsData.filter(
- (item) => item.data?.flag !== "announcement",
- ),
+ notificationsData
+ .filter((item) => item.data?.flag !== "announcement")
+ .map((notification) => ({
+ ...notification,
+ starred: !!starredNotifications[notification.id],
+ })),
);
+
setAnnouncementsList(
- notificationsData.filter(
- (item) => item.data?.flag === "announcement",
- ),
+ notificationsData
+ .filter((item) => item.data?.flag === "announcement")
+ .map((notification) => ({
+ ...notification,
+ starred: !!starredNotifications[notification.id],
+ })),
);
} catch (error) {
console.error("Error fetching dashboard data:", error);
@@ -147,47 +294,33 @@ function Dashboard() {
fetchDashboardData();
}, [dispatch]);
- // const handleTabChange = (direction) => {
- // const newIndex =
- // direction === "next"
- // ? Math.min(+activeTab + 1, tabItems.length - 1)
- // : Math.max(+activeTab - 1, 0);
- // setActiveTab(String(newIndex));
- // tabsListRef.current.scrollBy({
- // left: direction === "next" ? 50 : -50,
- // behavior: "smooth",
- // });
- // };
-
const notificationsToDisplay =
activeTab === "1" ? announcementsList : notificationsList;
- // const notification_for_badge_count =
- // activeTab === "0" ? announcementsList : notificationsList;
+ const filteredAndSortedNotifications = useMemo(() => {
+ let filteredNotifications = notificationsToDisplay;
- // const notification_count = notification_for_badge_count.filter(
- // (n) => !n.deleted && n.unread,
- // ).length;
+ if (filterBy === "Starred") {
+ filteredNotifications = notificationsToDisplay.filter(
+ (notification) => notification.starred,
+ );
+ }
- // sortMap is an object that maps sorting categories to sorting functions.
- const sortedNotifications = useMemo(() => {
const sortMap = {
"Most Recent": (a, b) => new Date(b.timestamp) - new Date(a.timestamp),
Tags: (a, b) => a.data.module.localeCompare(b.data.module),
Title: (a, b) => a.verb.localeCompare(b.verb),
};
- return [...notificationsToDisplay].sort(sortMap[sortedBy]);
- }, [sortedBy, notificationsToDisplay]);
+
+ return [...filteredNotifications]
+ .filter((notification) => !notification.deleted)
+ .sort(sortMap[sortedBy]);
+ }, [sortedBy, filterBy, notificationsToDisplay]);
const markAsRead = async (notifId) => {
- const token = localStorage.getItem("authToken");
try {
setRead_Loading(notifId);
- const response = await axios.post(
- notificationReadRoute,
- { id: notifId },
- { headers: { Authorization: `Token ${token}` } },
- );
+ const response = await markNotificationRead(notifId);
if (response.status === 200) {
setNotificationsList((prev) =>
prev.map((notif) =>
@@ -208,14 +341,9 @@ function Dashboard() {
};
const markAsUnread = async (notifId) => {
- const token = localStorage.getItem("authToken");
try {
setRead_Loading(notifId);
- const response = await axios.post(
- notificationUnreadRoute,
- { id: notifId },
- { headers: { Authorization: `Token ${token}` } },
- );
+ const response = await markNotificationUnread(notifId);
if (response.status === 200) {
setNotificationsList((prev) =>
prev.map((notif) =>
@@ -235,20 +363,53 @@ function Dashboard() {
}
};
- const deleteNotification = async (notifId) => {
- const token = localStorage.getItem("authToken");
-
+ const toggleStar = (notifId) => {
try {
- const response = await axios.post(
- notificationDeleteRoute,
- { id: notifId },
- {
- headers: {
- Authorization: `Token ${token}`,
- },
- },
+ setStar_Loading(notifId);
+
+ const notification = [...notificationsList, ...announcementsList].find(
+ (notif) => notif.id === notifId,
+ );
+
+ if (!notification) return;
+
+ const isCurrentlyStarred = notification.starred;
+
+ const starredNotifications = JSON.parse(
+ localStorage.getItem("starredNotifications") || "{}",
+ );
+
+ if (isCurrentlyStarred) {
+ delete starredNotifications[notifId];
+ } else {
+ starredNotifications[notifId] = true;
+ }
+
+ localStorage.setItem(
+ "starredNotifications",
+ JSON.stringify(starredNotifications),
);
+ const updateStarStatus = (items) =>
+ items.map((notif) =>
+ notif.id === notifId
+ ? { ...notif, starred: !isCurrentlyStarred }
+ : notif,
+ );
+
+ setNotificationsList((prev) => updateStarStatus(prev));
+ setAnnouncementsList((prev) => updateStarStatus(prev));
+ } catch (err) {
+ console.error("Error toggling star:", err);
+ } finally {
+ setStar_Loading(-1);
+ }
+ };
+
+ const deleteNotification = async (notifId) => {
+ try {
+ const response = await removeNotification(notifId);
+
if (response.status === 200) {
setNotificationsList((prev) =>
prev.filter((notif) => notif.id !== notifId),
@@ -256,6 +417,18 @@ function Dashboard() {
setAnnouncementsList((prev) =>
prev.filter((notif) => notif.id !== notifId),
);
+
+ const starredNotifications = JSON.parse(
+ localStorage.getItem("starredNotifications") || "{}",
+ );
+ if (starredNotifications[notifId]) {
+ delete starredNotifications[notifId];
+ localStorage.setItem(
+ "starredNotifications",
+ JSON.stringify(starredNotifications),
+ );
+ }
+
}
} catch (err) {
console.error("Error deleting notification:", err);
@@ -271,69 +444,6 @@ function Dashboard() {
mt="lg"
direction={{ base: "column", sm: "row" }}
>
- {/*
-
-
-
-
-
- {tabItems.map((item, index) => (
-
-
- {item.title}
- {activeTab !== index.toString() && (
-
- {notification_count}
-
- )}
-
-
- ))}
-
-
-
-
-
- */}
-
-
- }
- data={categories}
- value={sortedBy}
- onChange={setSortedBy}
- placeholder="Sort By"
- />
+
+
+
+
+
+
+
+ }
+ data={sortCategories}
+ value={sortedBy}
+ onChange={setSortedBy}
+ placeholder="Sort By"
+ size="sm"
+ />
+
@@ -368,22 +484,34 @@ function Dashboard() {
- ) : sortedNotifications.filter((notification) => !notification.deleted)
- .length === 0 ? (
-
+ ) : filteredAndSortedNotifications.length === 0 ? (
+
+ {filterBy === "Starred" ? (
+
+
+ No starred notifications
+
+
+ Star important notifications to see them here
+
+
+ ) : (
+
+ )}
+
) : (
- sortedNotifications
- .filter((notification) => !notification.deleted)
- .map((notification) => (
-
- ))
+ filteredAndSortedNotifications.map((notification) => (
+
+ ))
)}
>
@@ -403,9 +531,12 @@ NotificationItem.propTypes = {
flag: PropTypes.string,
}),
unread: PropTypes.bool.isRequired,
+ starred: PropTypes.bool,
}).isRequired,
markAsRead: PropTypes.func.isRequired,
markAsUnread: PropTypes.func.isRequired,
deleteNotification: PropTypes.func.isRequired,
+ toggleStar: PropTypes.func.isRequired,
loading: PropTypes.number.isRequired,
-};
\ No newline at end of file
+ starLoading: PropTypes.number.isRequired,
+};
diff --git a/src/Modules/Dashboard/index.js b/src/Modules/Dashboard/index.js
new file mode 100644
index 000000000..04df89db2
--- /dev/null
+++ b/src/Modules/Dashboard/index.js
@@ -0,0 +1,2 @@
+export { default as DashboardNotifications } from "./dashboardNotifications";
+export { default as ProfilePage } from "./pages/ProfilePage";
diff --git a/src/Modules/Dashboard/pages/ProfilePage.jsx b/src/Modules/Dashboard/pages/ProfilePage.jsx
new file mode 100644
index 000000000..49c0f0665
--- /dev/null
+++ b/src/Modules/Dashboard/pages/ProfilePage.jsx
@@ -0,0 +1 @@
+export { default } from "../StudentProfile/profilePage";
diff --git a/src/Modules/Dashboard/services/notificationService.js b/src/Modules/Dashboard/services/notificationService.js
new file mode 100644
index 000000000..1a02a83ff
--- /dev/null
+++ b/src/Modules/Dashboard/services/notificationService.js
@@ -0,0 +1,28 @@
+import axios from "axios";
+import {
+ notificationReadRoute,
+ notificationDeleteRoute,
+ notificationUnreadRoute,
+ getNotificationsRoute,
+} from "../../../routes/dashboardRoutes";
+import { getAuthHeader } from "../utils/authHelpers";
+
+const getRequestConfig = () => ({
+ headers: getAuthHeader(),
+});
+
+export const fetchNotifications = () => {
+ return axios.get(getNotificationsRoute, getRequestConfig());
+};
+
+export const markNotificationRead = (id) => {
+ return axios.post(notificationReadRoute, { id }, getRequestConfig());
+};
+
+export const markNotificationUnread = (id) => {
+ return axios.post(notificationUnreadRoute, { id }, getRequestConfig());
+};
+
+export const removeNotification = (id) => {
+ return axios.post(notificationDeleteRoute, { id }, getRequestConfig());
+};
diff --git a/src/Modules/Dashboard/services/profileService.js b/src/Modules/Dashboard/services/profileService.js
new file mode 100644
index 000000000..444bd07a1
--- /dev/null
+++ b/src/Modules/Dashboard/services/profileService.js
@@ -0,0 +1,19 @@
+import axios from "axios";
+import {
+ getProfileDataRoute,
+ updateProfileDataRoute,
+} from "../../../routes/dashboardRoutes";
+import { getAuthHeader } from "../utils/authHelpers";
+
+const getRequestConfig = () => ({
+ headers: getAuthHeader(),
+});
+
+export const fetchProfileData = (connectionRoute) => {
+ const endpoint = connectionRoute || getProfileDataRoute;
+ return axios.get(endpoint, getRequestConfig());
+};
+
+export const updateProfileSection = (payload) => {
+ return axios.put(updateProfileDataRoute, payload, getRequestConfig());
+};
diff --git a/src/Modules/Dashboard/styles/module.css b/src/Modules/Dashboard/styles/module.css
new file mode 100644
index 000000000..db0258253
--- /dev/null
+++ b/src/Modules/Dashboard/styles/module.css
@@ -0,0 +1 @@
+@import "../Dashboard.module.css";
diff --git a/src/Modules/Dashboard/utils/authHelpers.js b/src/Modules/Dashboard/utils/authHelpers.js
new file mode 100644
index 000000000..2daf69b76
--- /dev/null
+++ b/src/Modules/Dashboard/utils/authHelpers.js
@@ -0,0 +1,4 @@
+export const getAuthHeader = () => {
+ const token = localStorage.getItem("authToken");
+ return token ? { Authorization: `Token ${token}` } : {};
+};
diff --git a/src/Modules/Dashboard/utils/formHelpers.js b/src/Modules/Dashboard/utils/formHelpers.js
new file mode 100644
index 000000000..371527ada
--- /dev/null
+++ b/src/Modules/Dashboard/utils/formHelpers.js
@@ -0,0 +1,20 @@
+import { useState } from "react";
+
+export const useFormState = (initialState) => {
+ const [formData, setFormData] = useState(initialState);
+
+ const handleFieldChange = (field, value) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
+
+ const handleInputChange = (event) => {
+ const { name, value } = event.target;
+ handleFieldChange(name, value);
+ };
+
+ const resetForm = () => {
+ setFormData(initialState);
+ };
+
+ return { formData, setFormData, handleFieldChange, handleInputChange, resetForm };
+};
diff --git a/src/Modules/Database/FeedbackPage.jsx b/src/Modules/Database/FeedbackPage.jsx
new file mode 100644
index 000000000..cd10e901c
--- /dev/null
+++ b/src/Modules/Database/FeedbackPage.jsx
@@ -0,0 +1,92 @@
+import { useEffect, useState } from "react";
+import { Button, Card, Group, Rating, Stack, Text, Textarea, Title } from "@mantine/core";
+import { notifications } from "@mantine/notifications";
+import axios from "axios";
+import { dbFeedbackRoute } from "../../routes/dashboardRoutes";
+
+export default function FeedbackPage() {
+ const [myFeedback, setMyFeedback] = useState(null);
+ const [topFeedbacks, setTopFeedbacks] = useState([]);
+ const [average, setAverage] = useState(0);
+ const [rating, setRating] = useState(0);
+ const [feedback, setFeedback] = useState("");
+
+ const token = localStorage.getItem("authToken");
+ const authHeaders = { Authorization: `Token ${token}` };
+
+ const fetchFeedback = async () => {
+ try {
+ const { data } = await axios.get(dbFeedbackRoute, { headers: authHeaders });
+ setMyFeedback(data.my_feedback);
+ setTopFeedbacks(data.top_feedbacks || []);
+ setAverage(data.average_rating || 0);
+
+ if (data.my_feedback) {
+ setRating(data.my_feedback.rating || 0);
+ setFeedback(data.my_feedback.feedback || "");
+ }
+ } catch (error) {
+ notifications.show({ title: "Feedback", message: "Unable to load feedback", color: "red" });
+ }
+ };
+
+ useEffect(() => {
+ fetchFeedback();
+ }, []);
+
+ const submit = async () => {
+ if (rating < 1 || rating > 5) {
+ notifications.show({ title: "Validation", message: "Rating must be between 1 and 5", color: "yellow" });
+ return;
+ }
+
+ try {
+ await axios.post(
+ dbFeedbackRoute,
+ { rating, feedback },
+ { headers: { ...authHeaders, "Content-Type": "application/json" } },
+ );
+ notifications.show({ title: "Feedback", message: "Feedback saved", color: "green" });
+ fetchFeedback();
+ } catch (error) {
+ notifications.show({
+ title: "Feedback",
+ message: error?.response?.data?.error || "Unable to save feedback",
+ color: "red",
+ });
+ }
+ };
+
+ return (
+
+ Feedback
+
+
+ Average rating: {average}
+
+ Your rating
+
+
+
+
+
+ Top Feedbacks
+
+ {topFeedbacks.map((item) => (
+
+ {item.username}
+ Rating: {item.rating}
+ {item.feedback || "No comment"}
+
+ ))}
+
+
+ );
+}
diff --git a/src/Modules/Database/IssuesPage.jsx b/src/Modules/Database/IssuesPage.jsx
new file mode 100644
index 000000000..92ac5a267
--- /dev/null
+++ b/src/Modules/Database/IssuesPage.jsx
@@ -0,0 +1,269 @@
+import { useEffect, useState } from "react";
+import {
+ Badge,
+ Button,
+ Card,
+ Image,
+ FileInput,
+ Group,
+ Select,
+ Stack,
+ Text,
+ TextInput,
+ Textarea,
+ Title,
+} from "@mantine/core";
+import { notifications } from "@mantine/notifications";
+import axios from "axios";
+import {
+ dbIssuesRoute,
+ dbIssueSupportRoute,
+ dbIssueUpdateRoute,
+} from "../../routes/dashboardRoutes";
+import { mediaRoute } from "../../routes/globalRoutes";
+
+const moduleOptions = [
+ { value: "academic_information", label: "Academic" },
+ { value: "central_mess", label: "Central Mess" },
+ { value: "complaint_system", label: "Complaint System" },
+ { value: "eis", label: "Employee Information System" },
+ { value: "file_tracking", label: "File Tracking" },
+ { value: "health_center", label: "Health Center" },
+ { value: "leave", label: "Leave" },
+ { value: "online_cms", label: "Online CMS" },
+ { value: "placement_cell", label: "Placement Cell" },
+ { value: "scholarships", label: "Scholarships" },
+ { value: "visitor_hostel", label: "Visitor Hostel" },
+ { value: "other", label: "Other" },
+];
+
+const typeOptions = [
+ { value: "feature_request", label: "Feature Request" },
+ { value: "bug_report", label: "Bug Report" },
+ { value: "security_issue", label: "Security Issue" },
+ { value: "ui_issue", label: "UI Issue" },
+ { value: "other", label: "Other" },
+];
+
+export default function IssuesPage() {
+ const [issues, setIssues] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [editingId, setEditingId] = useState(null);
+
+ const [formData, setFormData] = useState({
+ module: "academic_information",
+ report_type: "bug_report",
+ title: "",
+ text: "",
+ images: [],
+ });
+
+ const token = localStorage.getItem("authToken");
+
+ const authHeaders = {
+ Authorization: `Token ${token}`,
+ };
+
+ const fetchIssues = async () => {
+ setLoading(true);
+ try {
+ const { data } = await axios.get(dbIssuesRoute, { headers: authHeaders });
+ setIssues(data.issues || []);
+ } catch (error) {
+ notifications.show({
+ title: "Issues",
+ message: "Failed to fetch issues",
+ color: "red",
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchIssues();
+ }, []);
+
+ const submitIssue = async () => {
+ if (!formData.title.trim() || !formData.text.trim()) {
+ notifications.show({ title: "Validation", message: "Title and text are required", color: "yellow" });
+ return;
+ }
+
+ const body = new FormData();
+ body.append("module", formData.module);
+ body.append("report_type", formData.report_type);
+ body.append("title", formData.title);
+ body.append("text", formData.text);
+ formData.images.forEach((img) => body.append("images", img));
+
+ try {
+ if (editingId) {
+ await axios.put(dbIssueUpdateRoute(editingId), body, { headers: authHeaders });
+ notifications.show({ title: "Issue", message: "Issue updated", color: "green" });
+ } else {
+ await axios.post(dbIssuesRoute, body, { headers: authHeaders });
+ notifications.show({ title: "Issue", message: "Issue created", color: "green" });
+ }
+
+ setFormData({
+ module: "academic_information",
+ report_type: "bug_report",
+ title: "",
+ text: "",
+ images: [],
+ });
+ setEditingId(null);
+ fetchIssues();
+ } catch (error) {
+ notifications.show({
+ title: "Issue",
+ message: error?.response?.data?.error || "Operation failed",
+ color: "red",
+ });
+ }
+ };
+
+ const toggleSupport = async (issueId) => {
+ try {
+ await axios.post(dbIssueSupportRoute(issueId), {}, { headers: authHeaders });
+ fetchIssues();
+ } catch (error) {
+ notifications.show({
+ title: "Support",
+ message: error?.response?.data?.error || "Unable to toggle support",
+ color: "red",
+ });
+ }
+ };
+
+ const getIssueImageSrc = (imageValue) => {
+ if (!imageValue) return "";
+
+ const imagePath = typeof imageValue === "string" ? imageValue : imageValue.image;
+ if (!imagePath) return "";
+
+ return imagePath.startsWith("http") || imagePath.startsWith("/")
+ ? imagePath
+ : `${mediaRoute}${imagePath}`;
+ };
+
+ const startEdit = (issue) => {
+ setEditingId(issue.id);
+ setFormData({
+ module: issue.module,
+ report_type: issue.report_type,
+ title: issue.title,
+ text: issue.text,
+ images: [],
+ });
+ };
+
+ return (
+
+ Issue Reporting
+
+
+
+
+ setFormData((p) => ({ ...p, title: e.currentTarget.value }))}
+ />
+
+
+
+ Open and Closed Issues
+
+ {issues.map((issue) => (
+
+
+
+ {issue.title}
+
+ {issue.closed ? "Closed" : "Open"}
+ {issue.report_type}
+
+
+ By {issue.username}
+ {issue.text}
+ {Array.isArray(issue.images) && issue.images.length > 0 && (
+
+ Attached images
+
+ {issue.images.map((image) => {
+ const imageSrc = getIssueImageSrc(image);
+
+ return imageSrc ? (
+
+ ) : null;
+ })}
+
+
+ )}
+
+
+
+
+
+
+ ))}
+
+ {!loading && issues.length === 0 && No issues found.}
+
+ );
+}
diff --git a/src/Modules/Database/SearchPage.jsx b/src/Modules/Database/SearchPage.jsx
new file mode 100644
index 000000000..3a8ff5125
--- /dev/null
+++ b/src/Modules/Database/SearchPage.jsx
@@ -0,0 +1,75 @@
+import { useState } from "react";
+import { Button, Card, Group, Stack, Table, Text, TextInput, Title } from "@mantine/core";
+import { notifications } from "@mantine/notifications";
+import axios from "axios";
+import { dbSearchRoute } from "../../routes/dashboardRoutes";
+
+export default function SearchPage() {
+ const [query, setQuery] = useState("");
+ const [results, setResults] = useState([]);
+
+ const token = localStorage.getItem("authToken");
+ const authHeaders = { Authorization: `Token ${token}` };
+
+ const searchUsers = async () => {
+ if (query.trim().length < 3) {
+ notifications.show({ title: "Search", message: "Enter at least 3 characters", color: "yellow" });
+ return;
+ }
+
+ try {
+ const { data } = await axios.get(dbSearchRoute, {
+ headers: authHeaders,
+ params: { q: query.trim() },
+ });
+ setResults(data.results || []);
+ } catch (error) {
+ notifications.show({
+ title: "Search",
+ message: error?.response?.data?.error || "Search failed",
+ color: "red",
+ });
+ setResults([]);
+ }
+ };
+
+ return (
+
+ User Search
+
+
+ setQuery(e.currentTarget.value)}
+ placeholder="Search by name or username"
+ style={{ flex: 1 }}
+ />
+
+
+
+
+ {results.length > 0 ? (
+
+
+
+ Username
+ First Name
+ Last Name
+
+
+
+ {results.map((user) => (
+
+ {user.username}
+ {user.first_name}
+ {user.last_name}
+
+ ))}
+
+
+ ) : (
+ No results yet.
+ )}
+
+ );
+}
diff --git a/src/Modules/Database/ViewDatabase.jsx b/src/Modules/Database/ViewDatabase.jsx
index 56d0ca0b5..dbd3d9b65 100644
--- a/src/Modules/Database/ViewDatabase.jsx
+++ b/src/Modules/Database/ViewDatabase.jsx
@@ -1,16 +1,163 @@
-import { Container, Title, Text, Paper } from "@mantine/core";
+import { useEffect, useMemo, useState } from "react";
+import { Link } from "react-router-dom";
+import axios from "axios";
+import {
+ Badge,
+ Button,
+ Card,
+ Container,
+ Group,
+ Loader,
+ Paper,
+ SimpleGrid,
+ Stack,
+ Text,
+ Title,
+} from "@mantine/core";
+import { notifications } from "@mantine/notifications";
+import { dbFeedbackRoute, dbIssuesRoute } from "../../routes/dashboardRoutes";
export default function ViewDatabase() {
+ const [loading, setLoading] = useState(true);
+ const [issues, setIssues] = useState([]);
+ const [feedbackSummary, setFeedbackSummary] = useState({
+ average_rating: 0,
+ my_feedback: null,
+ top_feedbacks: [],
+ });
+
+ const token = localStorage.getItem("authToken");
+ const authHeaders = { Authorization: `Token ${token}` };
+
+ useEffect(() => {
+ const fetchDashboardSummary = async () => {
+ setLoading(true);
+ try {
+ const [issuesRes, feedbackRes] = await Promise.all([
+ axios.get(dbIssuesRoute, { headers: authHeaders }),
+ axios.get(dbFeedbackRoute, { headers: authHeaders }),
+ ]);
+
+ setIssues(issuesRes.data?.issues || []);
+ setFeedbackSummary({
+ average_rating: feedbackRes.data?.average_rating || 0,
+ my_feedback: feedbackRes.data?.my_feedback || null,
+ top_feedbacks: feedbackRes.data?.top_feedbacks || [],
+ });
+ } catch (error) {
+ notifications.show({
+ title: "Database",
+ message: "Unable to load dashboard summary",
+ color: "red",
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchDashboardSummary();
+ }, []);
+
+ const metrics = useMemo(() => {
+ const openCount = issues.filter((issue) => !issue.closed).length;
+ const closedCount = issues.filter((issue) => issue.closed).length;
+ const myReported = issues.filter((issue) => issue.is_owner).length;
+ const mySupported = issues.filter((issue) => issue.is_supported).length;
+
+ return {
+ openCount,
+ closedCount,
+ myReported,
+ mySupported,
+ avgRating: Number(feedbackSummary.average_rating || 0).toFixed(1),
+ totalTopFeedbacks: feedbackSummary.top_feedbacks.length,
+ hasMyFeedback: Boolean(feedbackSummary.my_feedback),
+ };
+ }, [issues, feedbackSummary]);
+
+ const recentIssues = useMemo(() => issues.slice(0, 5), [issues]);
+
return (
-
-
- View Database
-
-
- Database viewing interface will be implemented here.
-
-
+
+
+
+ Database Dashboard
+
+ Monitor issues, feedback, and activity from one place.
+
+
+
+
+
+
+
+
+
+
+ {loading ? (
+
+
+
+ ) : (
+ <>
+
+
+ Open Issues
+ {metrics.openCount}
+
+
+ Closed Issues
+ {metrics.closedCount}
+
+
+ Your Reported Issues
+ {metrics.myReported}
+
+
+ Your Supported Issues
+ {metrics.mySupported}
+
+
+ Average Feedback Rating
+ {metrics.avgRating}
+
+
+ Top Feedback Entries
+ {metrics.totalTopFeedbacks}
+
+
+ Your Feedback
+ {metrics.hasMyFeedback ? "Submitted" : "Not Submitted"}
+
+
+
+
+
+ Recent Issues
+ {recentIssues.length === 0 ? (
+ No issues found yet.
+ ) : (
+ recentIssues.map((issue) => (
+
+
+
+ {issue.title}
+ {issue.text || "No description"}
+ By {issue.username}
+
+
+ {issue.closed ? "Closed" : "Open"}
+
+
+
+ ))
+ )}
+
+
+ >
+ )}
+
);
}
diff --git a/src/Modules/Database/components/nav.jsx b/src/Modules/Database/components/nav.jsx
index 7e080a2a4..74d1a2bbb 100644
--- a/src/Modules/Database/components/nav.jsx
+++ b/src/Modules/Database/components/nav.jsx
@@ -49,6 +49,21 @@ export default function Nav() {
path: "/database/view",
roles: ["acadadmin"],
},
+ {
+ title: "Issues",
+ path: "/database/issues",
+ roles: ["acadadmin", "student", "faculty", "staff"],
+ },
+ {
+ title: "Feedback",
+ path: "/database/feedback",
+ roles: ["acadadmin", "student", "faculty", "staff"],
+ },
+ {
+ title: "Search",
+ path: "/database/search",
+ roles: ["acadadmin", "student", "faculty", "staff"],
+ },
];
const filteredTabs = tabItems.filter((tab) => tab.roles.includes(userRole));
diff --git a/src/Modules/Database/database.jsx b/src/Modules/Database/database.jsx
index e26b859ee..8f422d31a 100644
--- a/src/Modules/Database/database.jsx
+++ b/src/Modules/Database/database.jsx
@@ -5,6 +5,9 @@ import CustomBreadDatabase from "./components/customBreadCrumbs.jsx";
import { useSelector } from "react-redux";
import { useState, useEffect } from "react";
import ViewDatabase from "./ViewDatabase.jsx";
+import IssuesPage from "./IssuesPage.jsx";
+import FeedbackPage from "./FeedbackPage.jsx";
+import SearchPage from "./SearchPage.jsx";
export default function Database() {
const userRole = useSelector((state) => state.user.role);
@@ -38,6 +41,9 @@ export default function Database() {
element={}
/>
} />
+ } />
+ } />
+ } />
diff --git a/src/components/header.jsx b/src/components/header.jsx
index b2ed336ce..74fdb54bd 100644
--- a/src/components/header.jsx
+++ b/src/components/header.jsx
@@ -183,13 +183,7 @@ function Header({ opened, toggleSidebar }) {
variant="light"
color="blue"
size="xs"
- onClick={() =>
- navigate(
- role === "student"
- ? "/profile"
- : "/facultyprofessionalprofile",
- )
- }
+ onClick={() => navigate("/profile")}
>
Profile
diff --git a/src/components/sidebarContent.jsx b/src/components/sidebarContent.jsx
index b77581b39..d803888f0 100644
--- a/src/components/sidebarContent.jsx
+++ b/src/components/sidebarContent.jsx
@@ -194,7 +194,10 @@ function SidebarContent({ isCollapsed, toggleSidebar }) {
useEffect(() => {
const filterModules = Modules.filter(
- (module) => accessibleModules[module.id] || module.id === "home",
+ (module) =>
+ accessibleModules[module.id] ||
+ module.id === "home" ||
+ module.id === "database",
);
setFilteredModules(filterModules);
}, [accessibleModules]);
diff --git a/src/helper/validateauth.jsx b/src/helper/validateauth.jsx
index 314d6513c..63023e0d1 100644
--- a/src/helper/validateauth.jsx
+++ b/src/helper/validateauth.jsx
@@ -46,6 +46,14 @@ function ValidateAuth() {
roll_no,
} = data;
+ if (!Array.isArray(designation_info) || designation_info.length === 0) {
+ throw new Error("ROLE_RESOLUTION_FAILED: Missing designation_info");
+ }
+
+ if (typeof accessible_modules !== "object" || accessible_modules === null) {
+ throw new Error("ROLE_RESOLUTION_FAILED: Invalid accessible_modules payload");
+ }
+
// console.log("User Data:", data);
dispatch(setUserName(name));
@@ -58,10 +66,21 @@ function ValidateAuth() {
dispatch(setAccessibleModules(accessible_modules));
dispatch(setCurrentAccessibleModules());
} catch (error) {
- console.error("User validation failed:", error);
+ const statusCode = error?.response?.status;
+ const isRoleError = String(error?.message || "").startsWith("ROLE_RESOLUTION_FAILED");
+
+ console.error("User validation failed", {
+ statusCode,
+ message: error?.message,
+ responseData: error?.response?.data,
+ path: window.location.pathname,
+ });
+
notifications.show({
- title: "Session Expired",
- message: "Your session has expired. Please log in again.",
+ title: isRoleError ? "Role Resolution Failed" : "Session Expired",
+ message: isRoleError
+ ? "Unable to resolve your role and module access. Please log in again."
+ : "Your session has expired. Please log in again.",
color: "red",
});
localStorage.removeItem("authToken");
diff --git a/src/redux/userslice.jsx b/src/redux/userslice.jsx
index a61699cc8..38d4b00e1 100644
--- a/src/redux/userslice.jsx
+++ b/src/redux/userslice.jsx
@@ -7,6 +7,7 @@ const userSlice = createSlice({
roll_no: "",
roles: ["Guest-User"],
role: "Guest-User",
+ totalNotifications: 0,
accessibleModules: {}, // Format---> {role: {module: true}}
currentAccessibleModules: {}, // Format---> {module: true}
},
@@ -23,6 +24,9 @@ const userSlice = createSlice({
setRole: (state, action) => {
state.role = action.payload;
},
+ setTotalNotifications: (state, action) => {
+ state.totalNotifications = action.payload;
+ },
setAccessibleModules: (state, action) => {
state.accessibleModules = action.payload;
},
@@ -44,6 +48,7 @@ export const {
setRollNo,
setRoles,
setRole,
+ setTotalNotifications,
setAccessibleModules,
setCurrentAccessibleModules,
clearUserName,
diff --git a/src/routes/dashboardRoutes/index.jsx b/src/routes/dashboardRoutes/index.jsx
index 007c8f013..ff014f61a 100644
--- a/src/routes/dashboardRoutes/index.jsx
+++ b/src/routes/dashboardRoutes/index.jsx
@@ -8,3 +8,8 @@ export const notificationDeleteRoute = `${host}/api/notificationdelete`;
export const notificationUnreadRoute = `${host}/api/notificationunread`;
export const getProfileDataRoute = `${host}/api/profile/`;
export const updateProfileDataRoute = `${host}/api/profile_update/`;
+export const dbIssuesRoute = `${host}/api/db/issues/`;
+export const dbIssueSupportRoute = (issueId) => `${host}/api/db/issues/${issueId}/support/`;
+export const dbIssueUpdateRoute = (issueId) => `${host}/api/db/issues/${issueId}/`;
+export const dbFeedbackRoute = `${host}/api/db/feedback/`;
+export const dbSearchRoute = `${host}/api/db/search/`;